404 lines
13 KiB
Swift
404 lines
13 KiB
Swift
//
|
|
// 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<Calendar.Component> = [.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<Calendar.Component> = [.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<Calendar.Component> = [.year, .month, .day, .hour, .minute, .second]
|
|
var components = Calendar.current.dateComponents(sets, from: self)
|
|
components.timeZone = TimeZone(abbreviation: timeZone)
|
|
if let hour = hour {
|
|
components.hour = hour
|
|
}
|
|
if let min = min {
|
|
components.minute = min
|
|
}
|
|
if let sec = sec {
|
|
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<Calendar.Component> = [.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 filePointer = fopen(path, "r") else {
|
|
throw GeneralError.cannotReadFile
|
|
}
|
|
|
|
var cLineBytes: UnsafeMutablePointer<CChar>? = nil
|
|
defer {
|
|
fclose(filePointer)
|
|
cLineBytes?.deallocate()
|
|
}
|
|
|
|
var lineCap: Int = 0
|
|
let bytesRead = getline(&cLineBytes, &lineCap, filePointer)
|
|
|
|
guard bytesRead > 0, let cLineBytes = cLineBytes else {
|
|
throw GeneralError.cannotReadFileLine
|
|
}
|
|
guard let str = String(cString: cLineBytes, encoding: .utf8) else {
|
|
throw GeneralError.cannotReadFileToConvertString
|
|
}
|
|
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[..<range.lowerBound]
|
|
break
|
|
}
|
|
else {
|
|
headerString += part
|
|
}
|
|
}
|
|
|
|
self = headerString
|
|
}
|
|
*/
|
|
|
|
static func readCsvHeader(fromFile: URL) throws -> [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
|
|
}
|
|
|
|
/// period: If nil, all period of csv collected.
|
|
/// candleDate: If nil, all date of csv collected.
|
|
///
|
|
static func collectCsv(period: KissConsole.CandleFilePeriod?, candleDate: Date?) throws -> [URL] {
|
|
guard let enumerator = FileManager.subPathFiles("data") else {
|
|
throw GeneralError.noCsvFile
|
|
}
|
|
|
|
var csvUrls = [URL]()
|
|
for case let fileUrl as URL in enumerator {
|
|
guard fileUrl.pathExtension == "csv" else {
|
|
continue
|
|
}
|
|
|
|
let fileName = fileUrl.lastPathComponent
|
|
let periodDir = fileUrl.deletingLastPathComponent()
|
|
let periodAtPath = periodDir.lastPathComponent
|
|
let productNoDir = periodDir.deletingLastPathComponent()
|
|
let productNo = productNoDir.lastPathComponent
|
|
|
|
guard let _ = Int(productNo) else { continue }
|
|
|
|
if let period = period {
|
|
guard periodAtPath == period.rawValue else { continue }
|
|
}
|
|
if let candleDate = candleDate {
|
|
guard candleDate.yyyyMMdd == fileName.parseCandleDate() else { continue }
|
|
}
|
|
csvUrls.append(fileUrl)
|
|
}
|
|
return csvUrls
|
|
}
|
|
}
|