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