Files
KissMe/KissMeConsole/Sources/Foundation+Extensions.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
}
}