Files
KissMe/KissMeMatrix/Sources/KissMatrix.swift
2023-07-04 22:53:54 +09:00

250 lines
8.0 KiB
Swift

//
// KissMatrix.swift
// KissMeMatrix
//
// Created by ened-book-m1 on 2023/06/20.
//
import Foundation
import KissMe
class KissMatrix {
func run() {
func printUsage() {
let appName = (CommandLine.arguments[0] as NSString).lastPathComponent
printError("\(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 {
printError("Invalid [yyyyMMdd] argument")
printUsage()
return
}
guard let (hour, min, sec) = CommandLine.arguments[4].HHmmss else {
printError("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 }
printError("Loaded \(model.indexSets.count) index set from \(modelJson)")
let semaphore = DispatchSemaphore(value: 0)
Task {
let results = 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 {
printError("Cannot get command from \(indexSet.name)")
continue
}
taskGroup.addTask {
do {
let result = try await self.runIndex(runUrl, args: args, date: runDate)
return result
} catch {
printError(error)
return nil
}
}
}
var taskResult = [KissIndexResult]()
for try await result in taskGroup.compactMap( { $0 }) {
taskResult.append(result)
}
return taskResult
}
mergeResult(results, date: runDate)
semaphore.signal()
}
semaphore.wait()
}
private func runIndex(_ runUrl: URL, args: [String], date: Date) async throws -> KissIndexResult {
assert(args.count >= 3)
return try await withUnsafeThrowingContinuation { continuation in
let kmi = args[0]
let outputUrl = KissMatrix.indexLogFile(kmi, date: date)
FileManager.default.createFile(atPath: outputUrl.path, contents: nil)
let output = FileHandle(forWritingAtPath: outputUrl.path)
let task = Process()
task.currentDirectoryURL = URL.currentDirectory()
task.executableURL = runUrl
task.arguments = args
task.standardOutput = output
task.standardError = FileHandle.standardError
task.qualityOfService = .default
printError("curPath: \(task.currentDirectoryPath)")
printError("runPath: \(runUrl.path)")
printError("args: \(args)")
do {
try task.run()
task.waitUntilExit()
} catch {
printError("run error \(error)")
continuation.resume(throwing: error)
}
try? output?.close()
guard let data = try? Data(contentsOf: outputUrl) else {
continuation.resume(throwing: GeneralError.emptyData(kmi))
return
}
if data.isEmpty {
continuation.resume(throwing: GeneralError.emptyData(kmi))
return
}
do {
let indexResult = try JSONDecoder().decode(KissIndexResult.self, from: data)
continuation.resume(returning: indexResult)
} catch {
printError("jsonError \(kmi)")
continuation.resume(throwing: error)
}
}
}
private func mergeResult(_ results: [KissIndexResult], date: Date) {
let indexCount = results.count
var mergedOutput = [String: [KissIndexResult.Output]]()
for result in results {
for item in result.output {
if let _ = mergedOutput[item.shortCode] {
mergedOutput[item.shortCode]!.append(item)
}
else {
mergedOutput[item.shortCode] = [item]
}
}
}
var normalized = [KissIndexResult.Output]()
for (productNo, output) in mergedOutput {
let weight = output.reduce(0.0, { $0 + $1.weight }) / Double(indexCount)
let output = KissIndexResult.Output(shortCode: productNo, productName: output.first?.productName, weight: weight)
normalized.append(output)
}
normalized.sort(by: { $0.weight > $1.weight })
let kmis = results.map { $0.kmi }
let matrixResult = KissMatrixResult(code: 200, kmis: kmis, day: date.yyyyMMdd, time: date.HHmmss, output: normalized)
do {
let jsonData = try JSONEncoder().encode(matrixResult)
try FileHandle.standardOutput.write(contentsOf: jsonData)
} catch {
printError(error)
}
}
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 {
printError(error)
return nil
}
}
static func indexLogFile(_ kmi: String, date: Date) -> URL {
let subPath = "log/index"
let subFile = "\(subPath)/\(date.yyyyMMdd_HHmmssSSSS_forFile)-\(kmi).log"
let fileUrl = URL.currentDirectory().appending(path: subFile)
createSubpath(subPath)
return fileUrl
}
static func createSubpath(_ name: String) {
let subPath = URL.currentDirectory().appending(path: name)
try? FileManager.default.createDirectory(at: subPath, withIntermediateDirectories: true)
}
}
enum RunMode: String {
case simulator = "sim"
case runner = "run"
}
struct Model: Decodable {
let indexSets: [IndexSet]
struct IndexSet: Codable {
/// Index Set
let name: String
/// Index Set
let memo: String
/// config.json
let config: String?
/// Index Runner app
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)
}
}
}