From a289e1852abf21f8ee4b0d63313146979d21cfad Mon Sep 17 00:00:00 2001 From: ened Date: Tue, 27 Jun 2023 17:30:10 +0900 Subject: [PATCH] Implement matrix model with given index set --- .../Common/Foundation+Extensions.swift | 11 + KissMe/Sources/Common/Request.swift | 2 +- .../Domestic/Shop/DomesticShopProduct.swift | 2 +- KissMeIndex/Sources/KissIndex.swift | 17 +- KissMeMatrix/Sources/KissMatrix.swift | 194 ++++++++++++++++++ .../KissMeBatch.xcodeproj/project.pbxproj | 3 +- .../xcschemes/KissMeIndex.xcscheme | 2 +- .../xcschemes/KissMeMatrix.xcscheme} | 39 ++-- scripts/build.sh | 1 + 9 files changed, 233 insertions(+), 38 deletions(-) rename projects/macos/{KissMeConsole.xcodeproj/xcshareddata/xcschemes/KissMeConsole on iTerm2.xcscheme => KissMeMatrix.xcodeproj/xcshareddata/xcschemes/KissMeMatrix.xcscheme} (69%) diff --git a/KissMe/Sources/Common/Foundation+Extensions.swift b/KissMe/Sources/Common/Foundation+Extensions.swift index 29ae663..ee4b06f 100644 --- a/KissMe/Sources/Common/Foundation+Extensions.swift +++ b/KissMe/Sources/Common/Foundation+Extensions.swift @@ -166,6 +166,17 @@ extension String { public var hasComma: Bool { return nil != rangeOfCharacter(from: commaCharSet) } + + public var kmiIndex: String? { + guard utf8.count == 8, String(prefix(4)).uppercased() == "KMI-" else { + return nil + } + let index = String(suffix(4)) + guard let _ = Int(index) else { + return nil + } + return "KMI-\(index)" + } } diff --git a/KissMe/Sources/Common/Request.swift b/KissMe/Sources/Common/Request.swift index b5f5114..cf183bd 100644 --- a/KissMe/Sources/Common/Request.swift +++ b/KissMe/Sources/Common/Request.swift @@ -32,7 +32,7 @@ public enum GeneralError: Error { case invalidAccessToken case invalidAccountNo case unsupportedQueryAtMockServer - case emptyData + case emptyData(String) case cannotReadFile case cannotWriteFile case cannotReadFileLine diff --git a/KissMe/Sources/Domestic/Shop/DomesticShopProduct.swift b/KissMe/Sources/Domestic/Shop/DomesticShopProduct.swift index 7552860..636281a 100644 --- a/KissMe/Sources/Domestic/Shop/DomesticShopProduct.swift +++ b/KissMe/Sources/Domestic/Shop/DomesticShopProduct.swift @@ -159,7 +159,7 @@ extension KissShop { continuation.resume(returning: (result.response.body.totalCount, items.item)) } else { - continuation.resume(throwing: GeneralError.emptyData) + continuation.resume(throwing: GeneralError.emptyData("getProduct(\(baseDate),\(pageNo))")) } case .failure(let error): continuation.resume(throwing: error) diff --git a/KissMeIndex/Sources/KissIndex.swift b/KissMeIndex/Sources/KissIndex.swift index 35c58ea..de7dc6a 100644 --- a/KissMeIndex/Sources/KissIndex.swift +++ b/KissMeIndex/Sources/KissIndex.swift @@ -330,7 +330,11 @@ extension KissIndex { var outputs = [KissIndexResult.Output]() for array in scoreArray { - let weight = Double(array.1) / (array.1 > 0 ? Double(positiveTotalScores): Double(negativeTotalScores)) + let weight = Double(array.1) / (array.1 >= 0 ? Double(positiveTotalScores): Double(negativeTotalScores)) + if weight.isNaN { + print("Ignored NaN: \(array.1) \(positiveTotalScores) \(negativeTotalScores) from \(array.0)") + continue + } let name: String? = (includeName ? getProduct(shortCode: array.0)?.itemName: nil) let output = KissIndexResult.Output(shortCode: array.0, productName: name, weight: weight) outputs.append(output) @@ -342,17 +346,6 @@ extension KissIndex { extension String { - var kmiIndex: String? { - guard utf8.count == 8, String(prefix(4)).uppercased() == "KMI-" else { - return nil - } - let index = String(suffix(4)) - guard let _ = Int(index) else { - return nil - } - return "KMI-\(index)" - } - func diffSecondsTwoHHmmss(_ another: String) -> TimeInterval { guard let (hour, min, sec) = self.HHmmss else { return Double.greatestFiniteMagnitude diff --git a/KissMeMatrix/Sources/KissMatrix.swift b/KissMeMatrix/Sources/KissMatrix.swift index 691a6f4..dfd103d 100644 --- a/KissMeMatrix/Sources/KissMatrix.swift +++ b/KissMeMatrix/Sources/KissMatrix.swift @@ -6,11 +6,205 @@ // import Foundation +import KissMe + + +enum RunMode: String { + case simulator = "sim" + case runner = "run" +} + + +struct Param: Codable { + + /// simulator 일 경우에 참고하는 날짜 정보 (begin ~ end) + /// + let beginDate: String // yyyyMMdd HHmmss + let endDate: String // yyyyMMdd HHmmss +} + + +struct Model: Codable { + let indexSets: [IndexSet] + + struct IndexSet: Codable { + let name: String + let memo: String + let config: String? + let runner: String + let weight: Double + + func build(date: Date) -> (URL, [String])? { + guard let _ = name.kmiIndex else { + return nil + } + + let command = URL.currentDirectory().appending(path: runner) + + let day = date.yyyyMMdd + let time = date.HHmmss + var args: [String] = [name, day, time] + + if let config = config { + args.append(config) + } + return (command, args) + } + } +} class KissMatrix { func run() { + func printUsage() { + let appName = (CommandLine.arguments[0] as NSString).lastPathComponent + print("\(appName) [sim|run] model.json [yyyyMMdd] [HHmmss]") + } + + guard CommandLine.argc >= 3 else { + printUsage() + return + } + + + let runDate: Date + let mode = RunMode(rawValue: CommandLine.arguments[1]) + switch mode { + case .runner: + runDate = Date() + case .simulator: + guard CommandLine.argc == 5 else { + printUsage() + return + } + guard let (year, month, day) = CommandLine.arguments[3].yyyyMMdd else { + print("Invalid [yyyyMMdd] argument") + printUsage() + return + } + guard let (hour, min, sec) = CommandLine.arguments[4].HHmmss else { + print("Invalid [HHmmss] argument") + printUsage() + return + } + var date = Date(timeIntervalSince1970: 0) + date.change(year: year, month: month, day: day) + date.change(hour: hour, min: min, sec: sec) + runDate = date + default: + printUsage() + return + } + + + let modelJson = CommandLine.arguments[2] + guard let model = loadModel(modelJson) else { return } + print("Loaded \(model.indexSets.count) index set from \(modelJson)") + + let semaphore = DispatchSemaphore(value: 0) + Task { + let indexResult = try await withThrowingTaskGroup(of: KissIndexResult?.self, returning: [KissIndexResult].self) { taskGroup in + for indexSet in model.indexSets { + guard let (runUrl, args) = indexSet.build(date: runDate) else { + print("Cannot get command from \(indexSet.name)") + continue + } + + taskGroup.addTask { + do { + let result = try await self.runIndex(runUrl, args: args) + return result + } catch { + print(error) + return nil + } + } + } + + var taskResult = [KissIndexResult]() + for try await result in taskGroup.compactMap( { $0 }) { + taskResult.append(result) + } + return taskResult + } + + for result in indexResult { + print(result) + } + semaphore.signal() + } + semaphore.wait() + } + + + private func runIndex(_ runUrl: URL, args: [String]) async throws -> KissIndexResult { + assert(args.count >= 3) + return try await withUnsafeThrowingContinuation { continuation in + let kmi = args[0] + + let output = Pipe() + //let output = FileHandle(forWritingTo: subPath.appending(path: fileName)) + + let task = Process() + task.currentDirectoryURL = URL.currentDirectory() + task.executableURL = runUrl + task.arguments = args + task.standardOutput = output + //task.standardError = error + //task.qualityOfService = .userInitiated + + print("curPath: \(task.currentDirectoryPath)") + print("runPath: \(runUrl)") + print("args: \(args)") + + do { + try task.run() + task.waitUntilExit() + } catch { + print("run error \(error)") + continuation.resume(throwing: error) + } + + let data = output.fileHandleForReading.readDataToEndOfFile() + if data.isEmpty { + continuation.resume(throwing: GeneralError.emptyData(kmi)) + return + } + + do { + try data.write(to: indexLogFile(kmi)) + + let indexResult = try JSONDecoder().decode(KissIndexResult.self, from: data) + continuation.resume(returning: indexResult) + } catch { + print("jsonError \(kmi)") + continuation.resume(throwing: error) + } + } + } + + + private func indexLogFile(_ kmi: String) -> URL { + let subPath = URL.currentDirectory().appending(path: "log/index") + try? FileManager.default.createDirectory(at: subPath, withIntermediateDirectories: true) + + let date = Date() + let fileName = "\(date.yyyyMMdd_HHmmssSSSS_forFile)-\(kmi).log" + return subPath.appending(path: fileName) + } + + + private func loadModel(_ jsonFile: String) -> Model? { + do { + let configUrl = URL.currentDirectory().appending(path: jsonFile) + let data = try Data(contentsOf: configUrl, options: .uncached) + let model = try JSONDecoder().decode(Model.self, from: data) + return model + } catch { + print(error) + return nil + } } } diff --git a/projects/macos/KissMeBatch.xcodeproj/project.pbxproj b/projects/macos/KissMeBatch.xcodeproj/project.pbxproj index a63679a..0c86622 100644 --- a/projects/macos/KissMeBatch.xcodeproj/project.pbxproj +++ b/projects/macos/KissMeBatch.xcodeproj/project.pbxproj @@ -91,7 +91,8 @@ children = ( 34F18FD62A3CCB5D0068C697 /* KissMeBatchTests.swift */, ); - path = KissMeBatchTests; + name = KissMeBatchTests; + path = ../../KissMeBatch/Tests; sourceTree = ""; }; /* End PBXGroup section */ diff --git a/projects/macos/KissMeIndex.xcodeproj/xcshareddata/xcschemes/KissMeIndex.xcscheme b/projects/macos/KissMeIndex.xcodeproj/xcshareddata/xcschemes/KissMeIndex.xcscheme index 3a243fc..5f52093 100644 --- a/projects/macos/KissMeIndex.xcodeproj/xcshareddata/xcschemes/KissMeIndex.xcscheme +++ b/projects/macos/KissMeIndex.xcodeproj/xcshareddata/xcschemes/KissMeIndex.xcscheme @@ -53,7 +53,7 @@ diff --git a/projects/macos/KissMeConsole.xcodeproj/xcshareddata/xcschemes/KissMeConsole on iTerm2.xcscheme b/projects/macos/KissMeMatrix.xcodeproj/xcshareddata/xcschemes/KissMeMatrix.xcscheme similarity index 69% rename from projects/macos/KissMeConsole.xcodeproj/xcshareddata/xcschemes/KissMeConsole on iTerm2.xcscheme rename to projects/macos/KissMeMatrix.xcodeproj/xcshareddata/xcschemes/KissMeMatrix.xcscheme index 33dad02..9f591e8 100644 --- a/projects/macos/KissMeConsole.xcodeproj/xcshareddata/xcschemes/KissMeConsole on iTerm2.xcscheme +++ b/projects/macos/KissMeMatrix.xcodeproj/xcshareddata/xcschemes/KissMeMatrix.xcscheme @@ -14,10 +14,10 @@ buildForAnalyzing = "YES"> + BlueprintIdentifier = "349843442A24DBF700E85B08" + BuildableName = "KissMeMatrix" + BlueprintName = "KissMeMatrix" + ReferencedContainer = "container:KissMeMatrix.xcodeproj"> @@ -40,25 +40,20 @@ debugDocumentVersioning = "YES" debugServiceExtension = "internal" allowLocationSimulation = "YES" - viewDebuggingEnabled = "No" - consoleMode = "1"> - - - + viewDebuggingEnabled = "No"> + + BlueprintIdentifier = "349843442A24DBF700E85B08" + BuildableName = "KissMeMatrix" + BlueprintName = "KissMeMatrix" + ReferencedContainer = "container:KissMeMatrix.xcodeproj"> - + @@ -73,10 +68,10 @@ runnableDebuggingMode = "0"> + BlueprintIdentifier = "349843442A24DBF700E85B08" + BuildableName = "KissMeMatrix" + BlueprintName = "KissMeMatrix" + ReferencedContainer = "container:KissMeMatrix.xcodeproj"> diff --git a/scripts/build.sh b/scripts/build.sh index 2463441..c1b7c16 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -8,6 +8,7 @@ THIS_PATH=`dirname "$0"` ${THIS_PATH}/build_any.sh KissMeConsole +${THIS_PATH}/build_any.sh KissMeIndex #${THIS_PATH}/build_any.sh KissMeBatch #${THIS_PATH}/build_any.sh KissGram