// // Foundation+Extensions.swift // KissMeConsole // // Created by ened-book-m1 on 2023/05/26. // import Foundation import KissMe extension String { func maxSpace(_ length: Int) -> String { let count = unicodeScalars.reduce(0) { $0 + ($1.value >= 0x80 ? 2: 1) } if count < length { return appending(String(repeating: " ", count: length - count)) } return self } func maxSpace(_ length: Int, digitBy: Int) -> String { guard let number = Int(self) else { return self } // Add comma let formatter = NumberFormatter() formatter.numberStyle = .decimal guard let str = formatter.string(from: NSNumber(value: number)) else { return self } // Add space let count = str.count if (count + count / 3) <= length { return str.appending(String(repeating: " ", count: length - count)) } return self } } extension String { func parseCandleDate() -> String? { let fileNameFrag = split(separator: ".") guard fileNameFrag.count == 2 else { return nil } let candlePrefix = "candle-" guard fileNameFrag[0].prefix(candlePrefix.count) == candlePrefix, fileNameFrag[1] == "csv" else { return nil } let fileDateFrag = fileNameFrag[0].suffix(fileNameFrag[0].count - candlePrefix.count) return String(fileDateFrag) } } extension Date { public var yyyyMMdd_split: (year: Int, month: Int, day: Int)? { let sets: Set = [.year, .month, .day, .hour, .minute, .second] let components = Calendar.current.dateComponents(sets, from: self) guard let year = components.year, let month = components.month, let day = components.day else { return nil } return (year, month, day) } public var HHmmss_split: (hour: Int, minute: Int, second: Int)? { let sets: Set = [.year, .month, .day, .hour, .minute, .second] let components = Calendar.current.dateComponents(sets, from: self) guard let hour = components.hour, let minute = components.minute, let second = components.second else { return nil } return (hour, minute, second) } public func changing(hour: Int, min: Int, sec: Int, timeZone: String = "KST") -> Date? { let sets: Set = [.year, .month, .day, .hour, .minute, .second] var components = Calendar.current.dateComponents(sets, from: self) components.timeZone = TimeZone(abbreviation: timeZone) components.hour = hour components.minute = min components.second = sec components.nanosecond = 0 return Calendar.current.date(from: components) } public mutating func change(hour: Int, min: Int, sec: Int, timeZone: String = "KST") { if let newDate = changing(hour: hour, min: min, sec: sec, timeZone: timeZone) { self = newDate } } public func changing(year: Int, month: Int, day: Int, timeZone: String = "KST") -> Date? { let sets: Set = [.year, .month, .day, .hour, .minute, .second] var components = Calendar.current.dateComponents(sets, from: self) components.timeZone = TimeZone(abbreviation: timeZone) components.year = year components.month = month components.day = day return Calendar.current.date(from: components) } public mutating func change(year: Int, month: Int, day: Int, timeZone: String = "KST") { if let newDate = changing(year: year, month: month, day: day, timeZone: timeZone) { self = newDate } } } func valueToString(_ any: Any) -> String { switch any { case let s as String: return s case let i as Int8: return String(i) case let i as UInt8: return String(i) case let i as Int16: return String(i) case let i as UInt16: return String(i) case let i as Int32: return String(i) case let i as UInt32: return String(i) case let i as Int: return String(i) case let i as UInt: return String(i) case let i as Int64: return String(i) case let i as UInt64: return String(i) case let f as Float16: return String(f) case let f as Float32: return String(f) case let f as Float: return String(f) case let f as Float64: return String(f) case let d as Double: return String(d) case let raw as any RawRepresentable: switch raw.rawValue { case let s as String: return s case let i as Int8: return String(i) case let i as UInt8: return String(i) case let i as Int16: return String(i) case let i as UInt16: return String(i) case let i as Int32: return String(i) case let i as UInt32: return String(i) case let i as Int: return String(i) case let i as UInt: return String(i) case let i as Int64: return String(i) case let i as UInt64: return String(i) default: return "" } case let c as CustomStringConvertible: return c.description default: return "" } } extension Array where Element: PropertyIterable { func writeCsv(toFile file: URL, appendable: Bool = false, localized: Bool) throws { if appendable, file.isFileExists == true { try appendAtEnd(ofCsv: file) } else { try overwrite(toCsv: file, localized: localized) } return // Nested function func appendAtEnd(ofCsv file: URL) throws { let oldHeader = try String.readCsvHeader(fromFile: file) var stringCsv = "" for item in self { let all = try item.allProperties() if stringCsv.isEmpty { let header = all.map{ $0.0 } if oldHeader != header { let (_, field) = oldHeader.getDiff(from: header) ?? (-1, "") throw GeneralError.incorrectCsvHeaderField(field) } } let values = all.map{ valueToString($0.1) }.joined(separator: ",").appending("\n") stringCsv.append(values) } try stringCsv.writeAppending(toFile: file.path) } // Nested function func overwrite(toCsv file: URL, localized: Bool) throws { var stringCsv = "" for item in self { let all = try item.allProperties() if stringCsv.isEmpty { let header = all.map { prop in localized ? localizeString(prop.0): prop.0 }.joined(separator: ",").appending("\n") stringCsv.append(header) } let values = all.map{ valueToString($0.1) }.joined(separator: ",").appending("\n") stringCsv.append(values) } try stringCsv.write(toFile: file.path, atomically: true, encoding: .utf8) } } static func readCsv(fromFile: URL, verifyHeader: Bool = true) throws -> [Element] where Element: ArrayDecodable { let stringCsv = try String(contentsOfFile: fromFile.path, encoding: .utf8) let items = stringCsv.split(separator: "\n") guard items.count > 0 else { return [] } var headerItems = [String]() var elements = [Element]() for (index, item) in items.enumerated() { if index == 0 { headerItems = item.split(separator: ",").map { String($0) } continue } let array = item.split(separator: ",").map { String($0) } let element = try Element(array: array) if index == 1, verifyHeader { // Validate property with header let properties = try element.allProperties() for (label, _) in properties { if false == headerItems.contains(where: { $0 == label }) { throw GeneralError.headerNoFiendName(label) } } } elements.append(element) } return elements } } extension Array where Element == String { func getDiff(from: [Element]) -> (Int, String)? { for (index, s) in enumerated() { guard index < from.count else { return (index, s) } if s != from[index] { return (index, s) } } return nil } } extension String { init(firstLineOfFile path: String) throws { guard let handle = FileHandle(forReadingAtPath: path) else { throw GeneralError.cannotReadFile } defer { try? handle.close() } var headerString = "" while (true) { guard let data = try handle.read(upToCount: 512) else { break } guard let part = String(data: data, encoding: .utf8) else { throw GeneralError.cannotReadFile } if let range = part.range(of: "\n") { headerString += part[.. [String] { let header = try String(firstLineOfFile: fromFile.path) return header.split(separator: ",").map { String($0) } } func writeAppending(toFile path: String) throws { guard let handle = FileHandle(forWritingAtPath: path) else { throw GeneralError.cannotReadFile } defer { try? handle.close() } _ = handle.seekToEndOfFile() handle.write(Data(utf8)) } } extension URL { var isDirectoryExists: Bool? { var isDir: ObjCBool = false if FileManager.default.fileExists(atPath: path, isDirectory: &isDir) { return isDir.boolValue } return false } var isFileExists: Bool? { var isDir: ObjCBool = false if FileManager.default.fileExists(atPath: path, isDirectory: &isDir) { return isDir.boolValue == false } return false } } extension FileManager { static func subPathFiles(_ subpath: String) -> FileManager.DirectoryEnumerator? { let baseUrl = URL.currentDirectory().appending(path: subpath) let manager = FileManager.default let resourceKeys : [URLResourceKey] = [] let enumerator = manager.enumerator(at: baseUrl, includingPropertiesForKeys: resourceKeys, options: [.skipsHiddenFiles]) { (url, error) -> Bool in print("directoryEnumerator error at \(url): ", error) return true } return enumerator } }