目標: 做出台灣加權指數 K 線圖
之前做出來的台股申購是獨立的功能,為了不影響前面已經完成的功能,所以開一個新的 VC 進行 K 線實作。架構一樣,遵照 Apple 的 MVC 規範。
先開 Model
import Foundation
class TwStockMarketKLineModel {
}
然後再開 VC
import UIKit
class TwStockMarketKLineViewController: UIViewController {
private lazy var model: TwStockMarketKLineModel = {
let model = TwStockMarketKLineModel()
return model
}()
override func viewDidLoad() {
super.viewDidLoad()
}
}
如果你不想讓 storyboard 上的 VC 太多,那你可以讓一系列相關的 VC 在一個 storyboard 上。所以我們另外開一個 storyboard 來放這個 VC。
K 線的拿取網址如下
加權指數-公開資訊觀測站 https://www.twse.com.tw/zh/page/trading/indices/MI_5MINS_HIST.html
加權指數的 K 線的 csv 檔位址
https://www.twse.com.tw/en/indicesReport/MI_5MINS_HIST?response=csv&date=20210907
因為加權指數的欄位和申購資訊相比少很多,而且我只需要[日期] [開] [高] [低] [收] 而已,所以就直接拿英文版的資料,減少轉換 Big5 的步驟。
因為需要 date=20210907 這一種日期格式,所以需要擴充 DateUtility 功能。
// 擴充在 DateUtility 裡面,在拿取 K 線的時候要輸入這個值
func getString(date: Date, format: String = "yyyy-MM-dd") -> String {
DateUtility.dateFormatter.calendar = isoCalendar
DateUtility.dateFormatter.dateFormat = format
return DateUtility.dateFormatter.string(from: date)
}
然後再新建一個 TwStockKLineManager
import Foundation
//加權指數-公開資訊觀測站 https://www.twse.com.tw/zh/page/trading/indices/MI_5MINS_HIST.html
// https://www.twse.com.tw/en/indicesReport/MI_5MINS_HIST?response=csv&date=20210907
class TwStockKLineManager {
private var dateUtility: DateUtility {
return DateUtility()
}
private lazy var alamofireAdapter: AlamofireAdapter = {
return AlamofireAdapter()
}()
func requestTwStockKLine(date: Date, completion: @escaping ((Result<String, Error>) -> Void)) {
let format = "yyyyMMdd"
let string = dateUtility.getString(date: date, format: format)
let urlString = "https://www.twse.com.tw/en/indicesReport/MI_5MINS_HIST?response=csv&date=\(string)"
alamofireAdapter.requestForString(urlString, method: .get) { result in
switch result {
case .success(let string):
completion(.success(string))
case .failure(let error):
completion(.failure(error))
}
}
}
}
這樣,專案中的任何物件,只要呼叫 requestTwStockKLine ,就可以拿到加權指數的 csv 檔。
我們先看一下載的 URL 和拿到的資料,可以看到後面的 date 需要輸入的格式是 yyyyMMdd
https://www.twse.com.tw/en/indicesReport/MI_5MINS_HIST?response=csv&date=20210907
所得到的 response CSV 檔,用 Number 或 Excel 打開會長這樣。
分析:
如同之前 [台股申購實作.4] 所提到的。千萬不要自己手動轉換曆法。千萬不要自己手動轉換曆法!!千萬不要自己手動轉換曆法!!!!
「正常來說」,每個月的月份天數,有 28 天,29 天,30 天,31 天。所以當下的日期減去 31 天,就絕對不會在這個月,也不能保證就會是「上個月」,如果真的這樣實作,會有可能出現意料之外的日期。所以,請用 Foundation 中的功能,去找上個月的日期,這比自己寫日期轉換的錯誤率低非常非常多。
在 DateUtility 擴充 func,輸入 Date,可以得到輸入 Date 當月的第一天 Date
//得到這個月第一天的 Date
func getStartOfMonth(date: Date = Date()) -> Date {
let calendar = isoCalendar
let startDate = isoCalendar.startOfDay(for: date)
return calendar.date(from: calendar.dateComponents([.year, .month], from: startDate)) ?? Date()
}
而 Foundation 的 Date 相關操作中,就有能對某個 DateComponent 進行加減操作的 API。
Apple 說明文件 https://developer.apple.com/documentation/foundation/calendar/2293453-date
程式碼
/// Returns a new `Date` representing the date calculated by adding an amount of a specific component to a given date.
///
/// - parameter component: A single component to add.
/// - parameter value: The value of the specified component to add.
/// - parameter date: The starting date.
/// - parameter wrappingComponents: If `true`, the component should be incremented and wrap around to zero/one on overflow, and should not cause higher components to be incremented. The default value is `false`.
/// - returns: A new date, or nil if a date could not be calculated with the given input.
@available(iOS 8.0, *)
public func date(byAdding component: Calendar.Component, value: Int, to date: Date, wrappingComponents: Bool = false) -> Date?
所以,得到上個月第一天的 Date,就是先呼叫 getStartOfMonth(),取得當月第一天後,再用 dateComponent 的計算,拿取上個月第一天。
//取得輸入 Date 的前一個月的第一天 Date
func getLastMonthStartDate(date: Date = Date()) -> Date {
let calendar = isoCalendar
let startOfMonth = getStartOfMonth(date: date)
return calendar.date(byAdding: DateComponents(month: -1), to: startOfMonth) ?? Date()
}
這樣,完成了 Model 在拿取台股加權指數資料的時候,只要使用 DateUtility 的 func 就可以得到要輸入的 Date。