diff --git a/KissMe/Sources/Common/Foundation+Extensions.swift b/KissMe/Sources/Common/Foundation+Extensions.swift index 3711ca3..6533b34 100644 --- a/KissMe/Sources/Common/Foundation+Extensions.swift +++ b/KissMe/Sources/Common/Foundation+Extensions.swift @@ -11,24 +11,28 @@ import Foundation extension Date { public var yyyyMMdd: String { let dateFormatter = DateFormatter() + dateFormatter.timeZone = TimeZone(abbreviation: "KST") dateFormatter.dateFormat = "yyyyMMdd" return dateFormatter.string(from: self) } public var yyyyMMdd_HHmmss_forTime: String { let dateFormatter = DateFormatter() + dateFormatter.timeZone = TimeZone(abbreviation: "KST") dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss" return dateFormatter.string(from: self) } public var yyyyMMdd_HHmmssSSSS_forFile: String { let dateFormatter = DateFormatter() + dateFormatter.timeZone = TimeZone(abbreviation: "KST") dateFormatter.dateFormat = "yyyyMMdd_HHmmss_SSSS" return dateFormatter.string(from: self) } public var HHmmss: String { let dateFormatter = DateFormatter() + dateFormatter.timeZone = TimeZone(abbreviation: "KST") dateFormatter.dateFormat = "HHmmss" return dateFormatter.string(from: self) } @@ -46,12 +50,15 @@ extension String { } let mmStartIndex = index(startIndex, offsetBy: 4) let mmEndIndex = index(mmStartIndex, offsetBy: 2) - guard let hh = Int(String(prefix(4))), + guard let yyyy = Int(String(prefix(4))), let mm = Int(self[mmStartIndex..= 0, mm >= 1, mm <= 12, dd >= 1, dd <= 31 else { + return nil + } + return (yyyy, mm, dd) } public var HHmmss: (Int, Int, Int)? { @@ -65,6 +72,9 @@ extension String { let ss = Int(String(suffix(2))) else { return nil } + guard hh >= 0, hh <= 12, mm >= 0, mm <= 59, ss >= 0, ss <= 59 else { + return nil + } return (hh, mm, ss) } } diff --git a/KissMe/Sources/Common/Request.swift b/KissMe/Sources/Common/Request.swift index 2e35a64..73df03e 100644 --- a/KissMe/Sources/Common/Request.swift +++ b/KissMe/Sources/Common/Request.swift @@ -42,6 +42,7 @@ public enum GeneralError: Error { case noCsvFile case invalidCandleCsvFile(String) case incorrectCsvHeaderField(String) + case noData } diff --git a/KissMe/Sources/Domestic/DomesticStockSearch.swift b/KissMe/Sources/Domestic/DomesticStockSearch.swift index 0107015..6a84cf0 100644 --- a/KissMe/Sources/Domestic/DomesticStockSearch.swift +++ b/KissMe/Sources/Domestic/DomesticStockSearch.swift @@ -347,14 +347,14 @@ extension KissAccount { /// 휴장일 확인하기 /// - public func getHolyday(baseDate: Date) async throws -> HolidyResult { + public func getHolyday(baseDate: String) async throws -> HolidyResult { return try await withUnsafeThrowingContinuation { continuation in guard let accessToken = accessToken else { continuation.resume(throwing: GeneralError.invalidAccessToken) return } - let request = Domestic.HolidayRequest(credential: credential, accessToken: accessToken, baseDate: baseDate.yyyyMMdd) + let request = Domestic.HolidayRequest(credential: credential, accessToken: accessToken, baseDate: baseDate) request.query { result in switch result { case .success(let result): diff --git a/KissMe/Sources/Domestic/DomesticStockSearchResult.swift b/KissMe/Sources/Domestic/DomesticStockSearchResult.swift index 212766a..baaeaa8 100644 --- a/KissMe/Sources/Domestic/DomesticStockSearchResult.swift +++ b/KissMe/Sources/Domestic/DomesticStockSearchResult.swift @@ -153,15 +153,14 @@ public struct HolidyResult: Codable { case output = "output" } - public func isHoliday(_ date: Date) -> Bool { - let dateString = Date().yyyyMMdd - guard let target = output?.first(where: { $0.baseDate == dateString }) else { - return false + public func isHoliday(_ day: String) throws -> Bool { + guard let target = output?.first(where: { $0.baseDate == day }) else { + throw GeneralError.noData } return target.businessDayOpened != .yes } - public struct OutputDetail: Codable { + public struct OutputDetail: Codable, PropertyIterable, ArrayDecodable { /// 기준일자 public let baseDate: String @@ -188,6 +187,37 @@ public struct HolidyResult: Codable { case peningDay = "opnd_yn" case settlementDay = "sttl_day_yn" } + + public init(array: [String]) throws { + guard array.count == 6 else { + throw GeneralError.incorrectArrayItems + } + self.baseDate = array[0] + self.weekday = WeekdayDivision(rawValue: array[1])! + self.businessDayOpened = YesNo(rawValue: array[2])! + self.tradingDayOpened = YesNo(rawValue: array[3])! + self.peningDay = YesNo(rawValue: array[4])! + self.settlementDay = YesNo(rawValue: array[5])! + } + + public static func symbols() -> [String] { + let i = try! OutputDetail(array: Array(repeating: "", count: 22)) + return Mirror(reflecting: i).children.compactMap { $0.label } + } + + public static func localizedSymbols() -> [String: String] { + [:] + } + } +} + + +extension Array where Element == HolidyResult.OutputDetail { + public func isHoliday(_ dayString: String) throws -> Bool { + guard let target = first(where: { $0.baseDate == dayString }) else { + throw GeneralError.noData + } + return target.businessDayOpened != .yes } } diff --git a/KissMeConsole/Sources/Foundation+Extensions.swift b/KissMeConsole/Sources/Foundation+Extensions.swift index c7e0c01..2b8188c 100644 --- a/KissMeConsole/Sources/Foundation+Extensions.swift +++ b/KissMeConsole/Sources/Foundation+Extensions.swift @@ -285,39 +285,6 @@ extension String { self = str.trimmingCharacters(in: .whitespacesAndNewlines) } - /* - init(firstLineOfFile path: String) throws { - guard let handle = FileHandle(forReadingAtPath: path) else { - throw GeneralError.cannotReadFile - } - defer { - try? handle.close() - } - - var readData = Data() - var headerString = "" - while (true) { - guard let data = try handle.read(upToCount: 512) else { - break - } - readData.append(data) - guard let part = String(data: readData, encoding: .utf8) else { - continue - } - - if let range = part.range(of: "\n") { - headerString += part[.. [String] { let header = try String(firstLineOfFile: fromFile.path) return header.split(separator: ",").map { String($0) } diff --git a/KissMeConsole/Sources/KissConsole+CSV.swift b/KissMeConsole/Sources/KissConsole+CSV.swift index bf78d67..15013a7 100644 --- a/KissMeConsole/Sources/KissConsole+CSV.swift +++ b/KissMeConsole/Sources/KissConsole+CSV.swift @@ -36,6 +36,10 @@ extension KissConsole { URL.currentDirectory().appending(path: "data/localized-names.csv") } + static var holidayUrl: URL { + URL.currentDirectory().appending(path: "data/holiday.csv") + } + static func productPriceUrl(productNo: String) -> URL { let subPath = "data/\(productNo)/price" let subFile = "\(subPath)/prices.csv" diff --git a/KissMeConsole/Sources/KissConsole+Candle.swift b/KissMeConsole/Sources/KissConsole+Candle.swift index 3fb0b70..06ef8f9 100644 --- a/KissMeConsole/Sources/KissConsole+Candle.swift +++ b/KissMeConsole/Sources/KissConsole+Candle.swift @@ -169,11 +169,29 @@ extension KissConsole { func checkHoliday(_ date: Date) async throws -> Bool { - guard await KissContext.shared.targetDate.yyyyMMdd != date.yyyyMMdd else { + let day = date.yyyyMMdd + guard await KissContext.shared.targetDay != day else { return await KissContext.shared.isHoliday } - let isHoliday = try await account!.getHolyday(baseDate: date).isHoliday(date) - await KissContext.shared.updateHoliday(isHoliday, targetDate: date) + + do { + let holidays = try [HolidyResult.OutputDetail].readCsv(fromFile: KissConsole.holidayUrl) + let isHoliday = try holidays.isHoliday(day) + await KissContext.shared.updateHoliday(isHoliday, targetDay: day) + return isHoliday + } catch { + print(error) + } + + let result = try await account!.getHolyday(baseDate: day) + do { + try result.output?.writeCsv(toFile: KissConsole.holidayUrl, localized: localized) + } catch { + print(error) + } + + let isHoliday = try result.isHoliday(day) + await KissContext.shared.updateHoliday(isHoliday, targetDay: day) return isHoliday } diff --git a/KissMeConsole/Sources/KissConsole.swift b/KissMeConsole/Sources/KissConsole.swift index 2d1e33a..dd0dd37 100644 --- a/KissMeConsole/Sources/KissConsole.swift +++ b/KissMeConsole/Sources/KissConsole.swift @@ -71,6 +71,9 @@ class KissConsole { case updateShop = "update shop" case look = "look" + // 휴장일 + case holiday = "holiday" + // 진열 종목 (시스템에서 제공하는 추천 리스트) case showcase = "showcase" @@ -100,7 +103,7 @@ class KissConsole { return true case .shorts, .shortsAll: return false - case .loadShop, .updateShop, .look: + case .loadShop, .updateShop, .look, .holiday: return false case .showcase: return false @@ -229,6 +232,7 @@ class KissConsole { case .loadShop: await onLoadShop() case .updateShop: await onUpdateShop() case .look: await onLook(args) + case .holiday: await onHoliday(args) case .showcase: await onShowcase() case .loves: await onLoves() @@ -940,6 +944,28 @@ extension KissConsole { } + private func onHoliday(_ args: [String]) async { + var date = Date() + if args.count == 1, args[0].utf8.count == 8, let day = args[0].yyyyMMdd { + date.change(year: day.0, month: day.1, day: day.2) + } + + let targetDay = (Date().yyyyMMdd == date.yyyyMMdd ? "today": date.yyyyMMdd) + + do { + let holiday = try await checkHoliday(date) + if holiday { + print("DONE \(targetDay) is holiday") + } + else { + print("DONE \(targetDay) is business day") + } + } catch { + print(error) + } + } + + private func onShowcase() async { // TODO: write } diff --git a/KissMeConsole/Sources/KissContext.swift b/KissMeConsole/Sources/KissContext.swift index 9928b51..16f56ed 100644 --- a/KissMeConsole/Sources/KissContext.swift +++ b/KissMeConsole/Sources/KissContext.swift @@ -11,15 +11,15 @@ import Foundation actor KissContext { static let shared = KissContext() - private(set) var targetDate: Date = Date(timeIntervalSince1970: 0) + private(set) var targetDay: String = "00010101" // yyyyMMdd private(set) var isHoliday: Bool = false private(set) var isResuming: Bool = false private init() { } - func updateHoliday(_ isHolyday: Bool, targetDate: Date) { + func updateHoliday(_ isHolyday: Bool, targetDay: String) { self.isHoliday = isHolyday - self.targetDate = targetDate + self.targetDay = targetDay } func update(resuming: Bool) { diff --git a/README.md b/README.md index f4647f9..543530c 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,7 @@ WIP `modify (PNO) (ONO) (가격) (수량)` | 주문 내역을 변경. (수량) `load shop` | data/shop-products.csv 로부터 전체 상품을 로딩. `update shop` | **금융위원회_KRX상장종목정보** 로부터 전체 상품을 얻어서 **data/shop-products.csv** 로 저장. `look (상품명)` | (상품명) 에 해당되는 PNO 를 표시함. +`holiday [yyyyMMdd]` | 휴장일 여부를 판단함. yyyymmDD 를 생략하면 오늘 날짜로 확인. **data/holiday.csv** 로 저장. WIP `showcase` | 추천 상품을 제안함. `loves` | 관심 종목 전체를 열람. profile.json 에 저장된 관심 종목을 표시함. `love (탭).(번호) (PNO)` | 관심 종목에 추가함. (번호) 를 지정하지 않으면 (탭) 마지막에 추가함. diff --git a/bin/data b/bin/data index c4f8c97..0c26334 160000 --- a/bin/data +++ b/bin/data @@ -1 +1 @@ -Subproject commit c4f8c97d267f6d79b9c1fe8d571d7fdfea0d386e +Subproject commit 0c263342c64beb3d95c4b2b24f025a5584cc2fd2