398 lines
13 KiB
Swift
398 lines
13 KiB
Swift
//
|
|
// Foundation+Extensions.swift
|
|
// KissMe
|
|
//
|
|
// Created by ened-book-m1 on 2023/05/17.
|
|
//
|
|
|
|
import Foundation
|
|
|
|
|
|
extension Date {
|
|
public var yyyyMM: String {
|
|
let dateFormatter = DateFormatter()
|
|
dateFormatter.timeZone = TimeZone(abbreviation: "KST")
|
|
dateFormatter.dateFormat = "yyyyMM"
|
|
return dateFormatter.string(from: self)
|
|
}
|
|
|
|
public var yyyyMM01: String {
|
|
yyyyMM + "01"
|
|
}
|
|
|
|
public var yyyyMMdd: String {
|
|
let dateFormatter = DateFormatter()
|
|
dateFormatter.timeZone = TimeZone(abbreviation: "KST")
|
|
dateFormatter.dateFormat = "yyyyMMdd"
|
|
return dateFormatter.string(from: self)
|
|
}
|
|
|
|
public var yyyyMMdd_HHmmss_forTime: String {
|
|
let dateFormatter = DateFormatter()
|
|
dateFormatter.timeZone = TimeZone(abbreviation: "KST")
|
|
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
|
|
return dateFormatter.string(from: self)
|
|
}
|
|
|
|
public var yyyyMMdd_HHmmssSSSS_forFile: String {
|
|
let dateFormatter = DateFormatter()
|
|
dateFormatter.timeZone = TimeZone(abbreviation: "KST")
|
|
dateFormatter.dateFormat = "yyyyMMdd_HHmmss_SSSS"
|
|
return dateFormatter.string(from: self)
|
|
}
|
|
|
|
public var HHmmss: String {
|
|
let dateFormatter = DateFormatter()
|
|
dateFormatter.timeZone = TimeZone(abbreviation: "KST")
|
|
dateFormatter.dateFormat = "HHmmss"
|
|
return dateFormatter.string(from: self)
|
|
}
|
|
|
|
public static var appTime: TimeInterval {
|
|
ProcessInfo.processInfo.systemUptime
|
|
}
|
|
|
|
public static func date(yyyyMMdd: String, HHmmss: String) -> Date? {
|
|
let dateFormatter = DateFormatter()
|
|
dateFormatter.timeZone = TimeZone(abbreviation: "KST")
|
|
dateFormatter.dateFormat = "yyyyMMddHHmmss"
|
|
let fullDate = yyyyMMdd + HHmmss
|
|
return dateFormatter.date(from: fullDate)
|
|
}
|
|
}
|
|
|
|
|
|
extension String {
|
|
public var yyyyMMdd: (Int, Int, Int)? {
|
|
guard utf8.count == 8 else {
|
|
return nil
|
|
}
|
|
let mmStartIndex = index(startIndex, offsetBy: 4)
|
|
let mmEndIndex = index(mmStartIndex, offsetBy: 2)
|
|
guard let yyyy = Int(String(prefix(4))),
|
|
let mm = Int(self[mmStartIndex..<mmEndIndex]),
|
|
let dd = Int(String(suffix(2))) else {
|
|
return nil
|
|
}
|
|
guard yyyy >= 0, mm >= 1, mm <= 12, dd >= 1, dd <= 31 else {
|
|
return nil
|
|
}
|
|
return (yyyy, mm, dd)
|
|
}
|
|
|
|
public var HHmmss: (Int, Int, Int)? {
|
|
guard utf8.count == 6 else {
|
|
return nil
|
|
}
|
|
let mmStartIndex = index(startIndex, offsetBy: 2)
|
|
let mmEndIndex = index(mmStartIndex, offsetBy: 2)
|
|
guard let hh = Int(String(prefix(2))),
|
|
let mm = Int(self[mmStartIndex..<mmEndIndex]),
|
|
let ss = Int(String(suffix(2))) else {
|
|
return nil
|
|
}
|
|
guard hh >= 0, hh <= 24, mm >= 0, mm <= 59, ss >= 0, ss <= 59 else {
|
|
return nil
|
|
}
|
|
return (hh, mm, ss)
|
|
}
|
|
|
|
public var hasComma: Bool {
|
|
return nil != rangeOfCharacter(from: commaCharSet)
|
|
}
|
|
}
|
|
|
|
|
|
#if os(Linux) || os(Windows) || os(FreeBSD)
|
|
extension URL {
|
|
public static func currentDirectory() -> URL {
|
|
URL(fileURLWithPath: FileManager.default.currentDirectoryPath)
|
|
}
|
|
|
|
public mutating func append(path: String) {
|
|
appendPathComponent(path)
|
|
}
|
|
|
|
public func appending(path: String) -> URL {
|
|
appendingPathComponent(path)
|
|
}
|
|
|
|
public mutating func append(queryItems: [URLQueryItem]) {
|
|
var components = URLComponents(url: self, resolvingAgainstBaseURL: true)
|
|
components?.queryItems = queryItems
|
|
if let url = components?.url {
|
|
self = url
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
|
|
|
|
extension URL {
|
|
public var isDirectoryExists: Bool? {
|
|
var isDir: ObjCBool = false
|
|
if FileManager.default.fileExists(atPath: path, isDirectory: &isDir) {
|
|
return isDir.boolValue
|
|
}
|
|
return false
|
|
}
|
|
|
|
public var isFileExists: Bool? {
|
|
var isDir: ObjCBool = false
|
|
if FileManager.default.fileExists(atPath: path, isDirectory: &isDir) {
|
|
return isDir.boolValue == false
|
|
}
|
|
return false
|
|
}
|
|
}
|
|
|
|
|
|
@_silgen_name("swift_EnumCaseName")
|
|
func _getEnumCaseName<T>(_ value: T) -> UnsafePointer<CChar>?
|
|
|
|
public func getEnumCaseName<T>(for value: T) -> String? {
|
|
if let stringPtr = _getEnumCaseName(value) {
|
|
return String(validatingUTF8: stringPtr)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
|
|
extension FileManager {
|
|
public 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
|
|
}
|
|
}
|
|
|
|
|
|
private var assertCommaCsvData: Bool = {
|
|
let val = ProcessInfo.processInfo.environment["KISS_ASSERT_COMMA_CSV_DATA"]
|
|
switch val {
|
|
case "true", "1": return true
|
|
case "false", "0": return false
|
|
default: return false
|
|
}
|
|
}()
|
|
|
|
private let commaCharSet = CharacterSet([","])
|
|
|
|
public func valueToString(_ any: Any) -> String {
|
|
|
|
func validateComma(_ s: String) {
|
|
if s.hasComma {
|
|
print("There are comma in: \(s)")
|
|
if assertCommaCsvData {
|
|
assertionFailure("There are comma in: \(s)")
|
|
}
|
|
}
|
|
}
|
|
|
|
switch any {
|
|
case let s as String:
|
|
validateComma(s)
|
|
return s.trimmingCharacters(in: commaCharSet)
|
|
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:
|
|
validateComma(s)
|
|
return s.trimmingCharacters(in: commaCharSet)
|
|
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:
|
|
let s = c.description
|
|
validateComma(s)
|
|
return s.trimmingCharacters(in: commaCharSet)
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
|
|
extension Array where Element: PropertyIterable {
|
|
|
|
public func mergeCsv(toFile file: URL, merging: (_ this: [Element], _ file: [Element]) -> [Element], localized: Bool) throws where Element: ArrayDecodable {
|
|
guard file.isFileExists == true else {
|
|
try writeCsv(toFile: file, localized: localized)
|
|
return
|
|
}
|
|
let oldData = try Self.readCsv(fromFile: file, verifyHeader: true)
|
|
let finalData = merging(self, oldData)
|
|
try finalData.writeCsv(toFile: file, localized: localized)
|
|
}
|
|
|
|
public 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)
|
|
}
|
|
}
|
|
|
|
public 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: ",", omittingEmptySubsequences: false).map { String($0) }
|
|
continue
|
|
}
|
|
let array = item.split(separator: ",", omittingEmptySubsequences: false).map { String($0) }
|
|
|
|
//print("index: \(index), \(fromFile.path)")
|
|
let element = try Element(array: array, source: item)
|
|
|
|
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 {
|
|
public 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)
|
|
}
|
|
|
|
public static func readCsvHeader(fromFile: URL) throws -> [String] {
|
|
let header = try String(firstLineOfFile: fromFile.path)
|
|
return header.split(separator: ",", omittingEmptySubsequences: false).map { String($0) }
|
|
}
|
|
|
|
public 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))
|
|
}
|
|
}
|