From b3666a3cbc34ccfabf65a7cace325e8cc6d745bd Mon Sep 17 00:00:00 2001 From: Rudi Farkas Date: Tue, 27 Dec 2022 15:49:03 +0100 Subject: [PATCH 01/11] update doc comments in DebugExt.swift --- Sources/RudifaUtilPkg/DebugExt.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/RudifaUtilPkg/DebugExt.swift b/Sources/RudifaUtilPkg/DebugExt.swift index 2f55f91..cf07a0d 100644 --- a/Sources/RudifaUtilPkg/DebugExt.swift +++ b/Sources/RudifaUtilPkg/DebugExt.swift @@ -30,9 +30,9 @@ import Foundation /// /// - Add to app's info.plist 2 keys: /// -/// `UIFileSharingEnabled` +/// | Application supports iTunes file sharing | YES | UIFileSharingEnabled | /// -/// `LSSupportsOpeningDocumentsInPlace` +/// | Supports opening documents in place | YES | LSSupportsOpeningDocumentsInPlace | /// /// - Connect iOS device to the Mac via cable or WiFi /// From a525e0b85df9a1927551232adf577b7c6cca30bd Mon Sep 17 00:00:00 2001 From: Rudi Farkas Date: Tue, 27 Dec 2022 16:11:18 +0100 Subject: [PATCH 02/11] update README: badge url --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6636d0b..3dce3ae 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # RudifaUtilPkg     ![XCtests](https://github.com/rudifa/RudifaUtilPkg/workflows/build_and_test/badge.svg) -![XCtests](https://github.com/rudifa/RudifaUtilPkg/workflows/jazzy_docs/badge.svg) +![XCtests](https://github.com/rudifa/RudifaUtilPkg/workflows/build_jazzy_docs/badge.svg) [RudifaUtilPkg](https://rudifa.github.io/RudifaUtilPkg/) contains swift extensions and utility methods. From 07aa906d59cbe89203807c826e0fc6816334cbba Mon Sep 17 00:00:00 2001 From: Rudi Farkas Date: Tue, 27 Dec 2022 17:54:04 +0100 Subject: [PATCH 03/11] package.swift: swift-tools-version:5.7; platforms: .iOS(.v11), macOS(.v13); and ... NSAttributedStringExt: build for os(iOS) only add FileManagerExt swiftformat tidy up comments --- .../xcschemes/RudifaUtilPkg.xcscheme | 2 +- Package.swift | 13 +- Sources/RudifaUtilPkg/CGExt.swift | 38 ++--- Sources/RudifaUtilPkg/ClassExt.swift | 2 +- Sources/RudifaUtilPkg/CodableExt.swift | 2 +- Sources/RudifaUtilPkg/CollectionExt.swift | 6 +- Sources/RudifaUtilPkg/DataExt.swift | 40 ++++- Sources/RudifaUtilPkg/DateExt.swift | 2 +- Sources/RudifaUtilPkg/DateIntervalExt.swift | 6 +- Sources/RudifaUtilPkg/DebugExt.swift | 18 +-- Sources/RudifaUtilPkg/EnumExt.swift | 2 +- Sources/RudifaUtilPkg/FileManagerExt.swift | 30 ++++ .../RudifaUtilPkg/NSAttributedStringExt.swift | 143 +++++++++--------- Sources/RudifaUtilPkg/OptionalExt.swift | 4 +- Sources/RudifaUtilPkg/StringExt.swift | 14 +- Sources/RudifaUtilPkg/TimeIntervalExt.swift | 2 +- Sources/RudifaUtilPkg/UserDefaultsExt.swift | 2 +- Tests/RudifaUtilPkgTests/CGExtTests.swift | 2 +- Tests/RudifaUtilPkgTests/ClassExtTests.swift | 7 +- .../RudifaUtilPkgTests/CodableExtTests.swift | 2 +- .../CollectionExtTests.swift | 4 +- Tests/RudifaUtilPkgTests/DataExtTests.swift | 2 +- Tests/RudifaUtilPkgTests/DateExtTests.swift | 50 +++++- .../DateIntervalExtTests.swift | 4 +- Tests/RudifaUtilPkgTests/DebugExtTests.swift | 2 +- Tests/RudifaUtilPkgTests/EnumExtTests.swift | 2 +- .../FileManagerExtTests.swift | 38 +++++ .../NSAttributedStringExtTests.swift | 5 +- Tests/RudifaUtilPkgTests/StringExtTests.swift | 4 +- .../TimeIntervalExtTests.swift | 2 +- .../UserDefaultsExtTests.swift | 2 +- Tests/RudifaUtilPkgTests/XCTExtTests.swift | 2 +- 32 files changed, 308 insertions(+), 146 deletions(-) create mode 100644 Sources/RudifaUtilPkg/FileManagerExt.swift create mode 100644 Tests/RudifaUtilPkgTests/FileManagerExtTests.swift diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/RudifaUtilPkg.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/RudifaUtilPkg.xcscheme index 98e5f8f..cb991a1 100644 --- a/.swiftpm/xcode/xcshareddata/xcschemes/RudifaUtilPkg.xcscheme +++ b/.swiftpm/xcode/xcshareddata/xcschemes/RudifaUtilPkg.xcscheme @@ -1,6 +1,6 @@ CGSize { + static func * (size: CGSize, scalar: CGFloat) -> CGSize { return CGSize(width: size.width * scalar, height: size.height * scalar) } /// Multiplies a CGSize by a Double - public static func * (size: CGSize, scalar: Double) -> CGSize { + static func * (size: CGSize, scalar: Double) -> CGSize { return size * CGFloat(scalar) } /// Divides a CGSize by a CGFloat - public static func / (size: CGSize, scalar: CGFloat) -> CGSize { + static func / (size: CGSize, scalar: CGFloat) -> CGSize { return CGSize(width: size.width / scalar, height: size.height / scalar) } /// Divides a CGSize by a Double - public static func / (size: CGSize, scalar: Double) -> CGSize { + static func / (size: CGSize, scalar: Double) -> CGSize { return size / CGFloat(scalar) } } -extension CGPoint { +public extension CGPoint { /// Multiplies a CGPoint by a CGFloat - public static func * (size: CGPoint, scalar: CGFloat) -> CGPoint { + static func * (size: CGPoint, scalar: CGFloat) -> CGPoint { return CGPoint(x: size.x * scalar, y: size.y * scalar) } /// Multiplies a CGPoint by a Double - public static func * (size: CGPoint, scalar: Double) -> CGPoint { + static func * (size: CGPoint, scalar: Double) -> CGPoint { return size * CGFloat(scalar) } /// Divides a CGPoint by a CGFloat - public static func / (size: CGPoint, scalar: CGFloat) -> CGPoint { + static func / (size: CGPoint, scalar: CGFloat) -> CGPoint { return CGPoint(x: size.x / scalar, y: size.y / scalar) } /// Divides a CGPoint by a Double - public static func / (size: CGPoint, scalar: Double) -> CGPoint { + static func / (size: CGPoint, scalar: Double) -> CGPoint { return size / CGFloat(scalar) } } diff --git a/Sources/RudifaUtilPkg/ClassExt.swift b/Sources/RudifaUtilPkg/ClassExt.swift index 222b213..0fc86f4 100644 --- a/Sources/RudifaUtilPkg/ClassExt.swift +++ b/Sources/RudifaUtilPkg/ClassExt.swift @@ -1,5 +1,5 @@ // -// ClassExt.swift v.0.1.0 +// ClassExt.swift // RudifaUtilPkg // // Created by Rudolf Farkas on 29.09.19. diff --git a/Sources/RudifaUtilPkg/CodableExt.swift b/Sources/RudifaUtilPkg/CodableExt.swift index 2b19edc..a27f1a7 100644 --- a/Sources/RudifaUtilPkg/CodableExt.swift +++ b/Sources/RudifaUtilPkg/CodableExt.swift @@ -1,5 +1,5 @@ // -// CodableExt.swift v.0.2.1 +// CodableExt.swift // RudifaUtilPkg // // Created by Rudolf Farkas on 23.06.19. diff --git a/Sources/RudifaUtilPkg/CollectionExt.swift b/Sources/RudifaUtilPkg/CollectionExt.swift index da6b0e5..fe47dd7 100644 --- a/Sources/RudifaUtilPkg/CollectionExt.swift +++ b/Sources/RudifaUtilPkg/CollectionExt.swift @@ -1,5 +1,5 @@ // -// CollectionExt.swift v.0.6.0 +// CollectionExt.swift // RudifaUtilPkg // // Created by Rudolf Farkas on 24.12.19. @@ -32,8 +32,8 @@ public extension Array { /// - predicate: returns true if a pair of elements, one from each array, satisfies it /// - Returns: updated array func updatedPreservingOrder(from other: Array, predicate: (Element, Element) -> Bool) -> [Element] { - var updated: [Element] = filter { elt1 in other.contains { (elt2) -> Bool in predicate(elt1, elt2) } } - updated += other.filter { elt1 in !self.contains { (elt2) -> Bool in predicate(elt1, elt2) } } + var updated: [Element] = filter { elt1 in other.contains { elt2 -> Bool in predicate(elt1, elt2) } } + updated += other.filter { elt1 in !self.contains { elt2 -> Bool in predicate(elt1, elt2) } } return updated } diff --git a/Sources/RudifaUtilPkg/DataExt.swift b/Sources/RudifaUtilPkg/DataExt.swift index 3b2cf88..983ab63 100644 --- a/Sources/RudifaUtilPkg/DataExt.swift +++ b/Sources/RudifaUtilPkg/DataExt.swift @@ -1,6 +1,6 @@ // // DataExt.swift -// +// RudifaUtilPkg // // Created by Rudolf Farkas on 07.10.22. // @@ -22,3 +22,41 @@ public extension Data { } } } + +// MARK: read / write to a local cache file + +public extension Data { + /// Write to a cache file + /// - Parameters: + /// - name: file name + /// - ext: file extension + /// - Returns: URL of nil + func writeToCacheFile(name: String, ext: String) -> URL? { + guard let cacheFileUrl = FileManager.cacheFilePath(fileName: name, fileExt: ext) else { return nil } + do { + try write(to: cacheFileUrl, options: [.atomicWrite]) + } catch { return nil } + return cacheFileUrl + } + + /// Initialize self from a local file + /// - Parameter url: cache file url + init?(fromFileAt url: URL) { + guard let data = try? Data(contentsOf: url) else { return nil } + self = data + } +} + +public extension Data { + /// Return string from data + var string: String { + String(decoding: self, as: UTF8.self) + } + + /// Initialize self from string + /// - Parameter string: input string + init?(from string: String) { + guard let data = string.data(using: .utf8) else { return nil } + self = data + } +} diff --git a/Sources/RudifaUtilPkg/DateExt.swift b/Sources/RudifaUtilPkg/DateExt.swift index 0118abb..e22e82b 100644 --- a/Sources/RudifaUtilPkg/DateExt.swift +++ b/Sources/RudifaUtilPkg/DateExt.swift @@ -1,5 +1,5 @@ // -// DateExt.swift v.0.4.0 +// DateExt.swift // RudifaUtilPkg // // Created by Rudolf Farkas on 18.06.18. diff --git a/Sources/RudifaUtilPkg/DateIntervalExt.swift b/Sources/RudifaUtilPkg/DateIntervalExt.swift index 7ad3930..a78d5f7 100644 --- a/Sources/RudifaUtilPkg/DateIntervalExt.swift +++ b/Sources/RudifaUtilPkg/DateIntervalExt.swift @@ -1,5 +1,5 @@ // -// DateIntervalExt.swift v.0.1.3 +// DateIntervalExt.swift // RudifaUtilPkg // // Created by Rudolf Farkas on 28.08.20. @@ -14,7 +14,7 @@ public extension DateInterval { /// Returns true if self fully overlaps with interval /// - Parameter interval: interval to compare with func fullyOverlaps(with interval: DateInterval) -> Bool { - if let intersection = self.intersection(with: interval) { + if let intersection = intersection(with: interval) { if intersection.duration >= min(duration, interval.duration) { return true } @@ -25,7 +25,7 @@ public extension DateInterval { /// Returns true if self partially overlaps with interval /// - Parameter interval: interval to compare with func partiallyOverlaps(with interval: DateInterval) -> Bool { - if let intersection = self.intersection(with: interval) { + if let intersection = intersection(with: interval) { if intersection.duration > 0.0 { return true } diff --git a/Sources/RudifaUtilPkg/DebugExt.swift b/Sources/RudifaUtilPkg/DebugExt.swift index cf07a0d..9ae919e 100644 --- a/Sources/RudifaUtilPkg/DebugExt.swift +++ b/Sources/RudifaUtilPkg/DebugExt.swift @@ -1,5 +1,5 @@ // -// DebugExt.swift v.0.3.2 +// DebugExt.swift // RudifaUtilPkg // // Created by Rudolf Farkas on 23.07.19. @@ -48,7 +48,7 @@ import Foundation /// /// - Open the app's directory as above, right-click on the file and click `Delete` -extension NSObject { +public extension NSObject { /// Print to stdout current class and function names and optional info /// /// - Note: Printing is enabled by DEBUG constant which is normally absent from release builds. @@ -59,7 +59,7 @@ extension NSObject { /// - info: information string; a leading "@" will be replaced by the call date /// - fnc: current function (default value is the caller) @available(*, deprecated, message: "use printClassAndFunc(\"...\" instead") - public func printClassAndFunc(info inf_: String = "", fnc fnc_: String = #function) { + func printClassAndFunc(info inf_: String = "", fnc fnc_: String = #function) { #if DEBUG print(formatClassAndFunc(info: inf_, fnc: fnc_)) #endif @@ -74,7 +74,7 @@ extension NSObject { /// - Parameters: /// - _: information string; a leading "@" will be replaced by the call date /// - fnc: current function (default value is the caller) - public func printClassAndFunc(_ info: String = "", fnc fnc_: String = #function) { + func printClassAndFunc(_ info: String = "", fnc fnc_: String = #function) { #if DEBUG print(formatClassAndFunc(info: info, fnc: fnc_)) #endif @@ -87,7 +87,7 @@ extension NSObject { /// /// - TODO: remove when the above deprecated form is removed /// - public func printClassAndFunc(_fnc fnc_: String = #function) { + func printClassAndFunc(_fnc fnc_: String = #function) { #if DEBUG print(formatClassAndFunc(info: "", fnc: fnc_)) #endif @@ -100,7 +100,7 @@ extension NSObject { /// - Parameters: /// - info: information string; a leading "@" will be replaced by the call date /// - fnc: current function (default value is the caller) - public func logClassAndFunc(info inf_: String = "", fnc fnc_: String = #function) { + func logClassAndFunc(info inf_: String = "", fnc fnc_: String = #function) { Logger.shared.print(formatClassAndFunc(info: inf_, fnc: fnc_)) } @@ -109,7 +109,7 @@ extension NSObject { /// - Parameters: /// - info: information string; a leading "@" will be replaced by the call date /// - fnc: current function (default value is the caller) - public func formatClassAndFunc(info inf_: String = "", fnc fnc_: String = #function) -> String { + func formatClassAndFunc(info inf_: String = "", fnc fnc_: String = #function) -> String { var dateTime = "" var info = inf_ if inf_.first == "@" { @@ -120,7 +120,7 @@ extension NSObject { } /// Return dateTimeString with microsecond resolution - func dateTimeString() -> String { + internal func dateTimeString() -> String { let cal = Calendar.current let comps = cal.dateComponents([.year, .month, .day, .hour, .minute, .second, .nanosecond], from: Date()) @@ -157,7 +157,7 @@ class Logger: TextOutputStream { handle.write(string.data(using: .utf8)!) handle.closeFile() } else { - ((try? string.data(using: .utf8)?.write(to: logUrl)) as ()??) + (try? string.data(using: .utf8)?.write(to: logUrl)) as ()?? } } } diff --git a/Sources/RudifaUtilPkg/EnumExt.swift b/Sources/RudifaUtilPkg/EnumExt.swift index cb6b05b..715f544 100644 --- a/Sources/RudifaUtilPkg/EnumExt.swift +++ b/Sources/RudifaUtilPkg/EnumExt.swift @@ -1,5 +1,5 @@ // -// EnumExt.swift v.0.3.0 +// EnumExt.swift // RudifaUtilPkg // // Created by Rudolf Farkas on 18.03.20. diff --git a/Sources/RudifaUtilPkg/FileManagerExt.swift b/Sources/RudifaUtilPkg/FileManagerExt.swift new file mode 100644 index 0000000..8f9e2e5 --- /dev/null +++ b/Sources/RudifaUtilPkg/FileManagerExt.swift @@ -0,0 +1,30 @@ +// +// FileManagerExt.swift +// RudifaUtilPkg +// +// Created by Rudolf Farkas on 05.09.20. +// Copyright © 2020 Rudolf Farkas. All rights reserved. +// + +import Foundation + +public extension FileManager { + /// Create path to a cache file + /// - Parameters: + /// - fileName: filenamne + /// - fileExt: extension + /// - Returns: file URL + static func cacheFilePath(fileName: String, fileExt: String) -> URL? { + guard let directoryURL = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first else { + return nil + } + + let filePath = directoryURL.appendingPathComponent(fileName).appendingPathExtension(fileExt) + print("==== cacheFilePath = ", filePath.absoluteString) + return filePath + } +} + +// try data.write(to: fileURL, options: [.atomicWrite]) + +// guard let data = try? Data(contentsOf: url) else { return nil } diff --git a/Sources/RudifaUtilPkg/NSAttributedStringExt.swift b/Sources/RudifaUtilPkg/NSAttributedStringExt.swift index c62fb78..8c841f8 100644 --- a/Sources/RudifaUtilPkg/NSAttributedStringExt.swift +++ b/Sources/RudifaUtilPkg/NSAttributedStringExt.swift @@ -6,85 +6,88 @@ // Copyright © 2020 Rudolf Farkas. All rights reserved. // -import UIKit +#if os(iOS) -public extension NSAttributedString { - // MARK: API initializers from array of tuples (string, attr, ...) + import UIKit - /// Initialize from an array of strings with color, joining with the separator - /// - Parameters: - /// - stringsWithStyle: array of tuples (string, color) - /// - separator: string - convenience init(stringsWithColor: [(String, UIColor)], separator: String = " ") { - let nsaStrings = stringsWithColor.map { NSAttributedString(string: $0.0, fgColor: $0.1) } - self.init(from: nsaStrings, separator: separator) - } + public extension NSAttributedString { + // MARK: API initializers from array of tuples (string, attr, ...) - /// Initialize from an array of strings with styles, joining with the separator - /// - Parameters: - /// - stringsWithStyle: array of tuples (string, style) - /// - separator: string - convenience init(stringsWithStyle: [(String, UIFont.TextStyle)], separator: String) { - let nsaStrings = stringsWithStyle.map { NSAttributedString(string: $0.0, textStyle: $0.1) } - self.init(from: nsaStrings, separator: separator) - } + /// Initialize from an array of strings with color, joining with the separator + /// - Parameters: + /// - stringsWithStyle: array of tuples (string, color) + /// - separator: string + convenience init(stringsWithColor: [(String, UIColor)], separator: String = " ") { + let nsaStrings = stringsWithColor.map { NSAttributedString(string: $0.0, fgColor: $0.1) } + self.init(from: nsaStrings, separator: separator) + } - /// Initialize from an array of strings with styles and weights, joining with the separator - /// - Parameters: - /// - stringsWithStyle: array of tuples (string, style) - /// - separator: string - /// - weight: font weight, e.g. .thin - convenience init(stringsWithStyleAndWeight: [(String, UIFont.TextStyle, weight: UIFont.Weight)], separator: String) { - let nsaStrings = stringsWithStyleAndWeight.map { NSAttributedString(string: $0.0, textStyle: $0.1, weight: $0.2) } - self.init(from: nsaStrings, separator: separator) - } + /// Initialize from an array of strings with styles, joining with the separator + /// - Parameters: + /// - stringsWithStyle: array of tuples (string, style) + /// - separator: string + convenience init(stringsWithStyle: [(String, UIFont.TextStyle)], separator: String) { + let nsaStrings = stringsWithStyle.map { NSAttributedString(string: $0.0, textStyle: $0.1) } + self.init(from: nsaStrings, separator: separator) + } - // MARK: INTERNAL simple initializers from string and specific attributes + /// Initialize from an array of strings with styles and weights, joining with the separator + /// - Parameters: + /// - stringsWithStyle: array of tuples (string, style) + /// - separator: string + /// - weight: font weight, e.g. .thin + convenience init(stringsWithStyleAndWeight: [(String, UIFont.TextStyle, weight: UIFont.Weight)], separator: String) { + let nsaStrings = stringsWithStyleAndWeight.map { NSAttributedString(string: $0.0, textStyle: $0.1, weight: $0.2) } + self.init(from: nsaStrings, separator: separator) + } - /// Initialize from a string using the fgColor - /// - Parameters: - /// - string: input string - /// - color: foreground color - convenience init(string: String, fgColor: UIColor) { - let attributes = [NSAttributedString.Key.foregroundColor: fgColor] - self.init(string: string, attributes: attributes) - } + // MARK: INTERNAL simple initializers from string and specific attributes - /// Initialize from a string using the textStyle - /// - Parameters: - /// - string: input string - /// - textStyle: input font text style - convenience init(string: String, textStyle: UIFont.TextStyle) { - let attributes = [NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: textStyle)] - self.init(string: string, attributes: attributes) - } + /// Initialize from a string using the fgColor + /// - Parameters: + /// - string: input string + /// - color: foreground color + convenience init(string: String, fgColor: UIColor) { + let attributes = [NSAttributedString.Key.foregroundColor: fgColor] + self.init(string: string, attributes: attributes) + } - /// Initialize from a string using the textStyle and weight - /// - Parameters: - /// - string: input string - /// - textStyle: input font text style - /// - weight: font weight, e.g. .thin - convenience init(string: String, textStyle: UIFont.TextStyle, weight: UIFont.Weight) { - let font = UIFont.preferredFont(forTextStyle: textStyle) - let size = font.pointSize - let font2 = UIFont.systemFont(ofSize: size, weight: weight) - let attributes = [NSAttributedString.Key.font: font2] - self.init(string: string, attributes: attributes) - } + /// Initialize from a string using the textStyle + /// - Parameters: + /// - string: input string + /// - textStyle: input font text style + convenience init(string: String, textStyle: UIFont.TextStyle) { + let attributes = [NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: textStyle)] + self.init(string: string, attributes: attributes) + } + + /// Initialize from a string using the textStyle and weight + /// - Parameters: + /// - string: input string + /// - textStyle: input font text style + /// - weight: font weight, e.g. .thin + convenience init(string: String, textStyle: UIFont.TextStyle, weight: UIFont.Weight) { + let font = UIFont.preferredFont(forTextStyle: textStyle) + let size = font.pointSize + let font2 = UIFont.systemFont(ofSize: size, weight: weight) + let attributes = [NSAttributedString.Key.font: font2] + self.init(string: string, attributes: attributes) + } - // MARK: INTERNAL initializer with arrays of NSAttributedString + // MARK: INTERNAL initializer with arrays of NSAttributedString - /// Initialize from an array of NSAttributedString, joining with the separator - /// - Parameters: - /// - nsaStrings: array of attributes strings - /// - separator: input separator - convenience init(from nsaStrings: [NSAttributedString], separator: String) { - let nsaSeparator = NSAttributedString(string: separator) - let nsmasJoined = NSMutableAttributedString() - for (index, maString) in nsaStrings.enumerated() { - if index > 0 { nsmasJoined.append(nsaSeparator) } - nsmasJoined.append(maString) + /// Initialize from an array of NSAttributedString, joining with the separator + /// - Parameters: + /// - nsaStrings: array of attributes strings + /// - separator: input separator + convenience init(from nsaStrings: [NSAttributedString], separator: String) { + let nsaSeparator = NSAttributedString(string: separator) + let nsmasJoined = NSMutableAttributedString() + for (index, maString) in nsaStrings.enumerated() { + if index > 0 { nsmasJoined.append(nsaSeparator) } + nsmasJoined.append(maString) + } + self.init(attributedString: nsmasJoined) } - self.init(attributedString: nsmasJoined) } -} +#endif diff --git a/Sources/RudifaUtilPkg/OptionalExt.swift b/Sources/RudifaUtilPkg/OptionalExt.swift index 2dc7fd4..8203c78 100644 --- a/Sources/RudifaUtilPkg/OptionalExt.swift +++ b/Sources/RudifaUtilPkg/OptionalExt.swift @@ -8,11 +8,11 @@ import Foundation -extension Optional where Wrapped == Int { +public extension Optional where Wrapped == Int { /// Increment an optional Int (also from nil to val) /// /// - Parameter val: to increment by - public mutating func increment(by val: Int = 1) { + mutating func increment(by val: Int = 1) { self = (self ?? 0) + val } } diff --git a/Sources/RudifaUtilPkg/StringExt.swift b/Sources/RudifaUtilPkg/StringExt.swift index 2932b0b..4d67cdb 100644 --- a/Sources/RudifaUtilPkg/StringExt.swift +++ b/Sources/RudifaUtilPkg/StringExt.swift @@ -1,5 +1,5 @@ // -// StringExt.swift v.0.3.0 +// StringExt.swift // RudifaUtilPkg // // Created by Rudolf Farkas on 22.07.18. @@ -8,19 +8,19 @@ import Foundation -extension String { +public extension String { /// Returns a copy of self with 1st letter capitalized - public func capitalizingFirstLetter() -> String { + func capitalizingFirstLetter() -> String { return prefix(1).capitalized + dropFirst() } /// Capitalizes 1st letter - public mutating func capitalizeFirstLetter() { + mutating func capitalizeFirstLetter() { self = capitalizingFirstLetter() } /// Capitalizes 1st letter and inserts a space before any other capital letter - public var camelCaseSplit: String { + var camelCaseSplit: String { var newString: String = prefix(1).capitalized for char in dropFirst() { if "A" ... "Z" ~= char, newString != "" { @@ -32,13 +32,13 @@ extension String { } } -extension String { +public extension String { /// Localizes a text string /// - Parameters: /// - _: bundle /// - tableName: table /// - Returns: localized version of self (if self found in localization tables as a key) - public func localized(bundle _: Bundle = .main, tableName: String = "Localizable") -> String { + func localized(bundle _: Bundle = .main, tableName: String = "Localizable") -> String { return NSLocalizedString(self, tableName: tableName, value: "**\(self)**", comment: "") } } diff --git a/Sources/RudifaUtilPkg/TimeIntervalExt.swift b/Sources/RudifaUtilPkg/TimeIntervalExt.swift index 633b332..fcbb38c 100644 --- a/Sources/RudifaUtilPkg/TimeIntervalExt.swift +++ b/Sources/RudifaUtilPkg/TimeIntervalExt.swift @@ -1,5 +1,5 @@ // -// TimeIntervalExt.swift v.0.1.0 +// TimeIntervalExt.swift // RudifaUtilPkg // // Created by Rudolf Farkas on 22.06.19. diff --git a/Sources/RudifaUtilPkg/UserDefaultsExt.swift b/Sources/RudifaUtilPkg/UserDefaultsExt.swift index 034ea92..455ca2b 100644 --- a/Sources/RudifaUtilPkg/UserDefaultsExt.swift +++ b/Sources/RudifaUtilPkg/UserDefaultsExt.swift @@ -1,5 +1,5 @@ // -// UserDefaultsExt.swift v.0.3.0 +// UserDefaultsExt.swift // RudifaUtilPkg // // Created by Rudolf Farkas on 13.03.20. diff --git a/Tests/RudifaUtilPkgTests/CGExtTests.swift b/Tests/RudifaUtilPkgTests/CGExtTests.swift index 5a7be63..86ec5bc 100644 --- a/Tests/RudifaUtilPkgTests/CGExtTests.swift +++ b/Tests/RudifaUtilPkgTests/CGExtTests.swift @@ -1,5 +1,5 @@ // -// CGExtTests.swift v.0.1.2 +// CGExtTests.swift // RudifaUtilPkgTests // // Created by Rudolf Farkas on 31.07.18. diff --git a/Tests/RudifaUtilPkgTests/ClassExtTests.swift b/Tests/RudifaUtilPkgTests/ClassExtTests.swift index 894a2b2..f1f5371 100644 --- a/Tests/RudifaUtilPkgTests/ClassExtTests.swift +++ b/Tests/RudifaUtilPkgTests/ClassExtTests.swift @@ -1,6 +1,6 @@ // -// ClassExtTests.swift v.0.1.0 -// RudifaUtilPkg +// ClassExtTests.swift +// RudifaUtilPkgTests // // Created by Rudolf Farkas on 29.09.19. // Copyright © 2019 Rudolf Farkas. All rights reserved. @@ -12,7 +12,6 @@ import XCTest - class ClassExtTests: XCTestCase { override func setUp() {} @@ -27,6 +26,6 @@ class ClassExtTests: XCTestCase { XCTAssertEqual("MyClass", MyClass().className) XCTAssertEqual("UIViewController", UIViewController.className) XCTAssertEqual("UIViewController", UIViewController().className) - #endif + #endif } } diff --git a/Tests/RudifaUtilPkgTests/CodableExtTests.swift b/Tests/RudifaUtilPkgTests/CodableExtTests.swift index 599297a..4cc715f 100644 --- a/Tests/RudifaUtilPkgTests/CodableExtTests.swift +++ b/Tests/RudifaUtilPkgTests/CodableExtTests.swift @@ -1,5 +1,5 @@ // -// CodableExtTests.swift v.0.2.0 +// CodableExtTests.swift // RudifaUtilPkgTests // // Created by Rudolf Farkas on 23.06.19. diff --git a/Tests/RudifaUtilPkgTests/CollectionExtTests.swift b/Tests/RudifaUtilPkgTests/CollectionExtTests.swift index 0f223af..75ba1bd 100644 --- a/Tests/RudifaUtilPkgTests/CollectionExtTests.swift +++ b/Tests/RudifaUtilPkgTests/CollectionExtTests.swift @@ -1,6 +1,6 @@ // -// CollectionExtTests.swift v.0.6.0 -// RudifaUtilPkg +// CollectionExtTests.swift +// RudifaUtilPkgTests // // Created by Rudolf Farkas on 24.12.19. // Copyright © 2019 Rudolf Farkas. All rights reserved. diff --git a/Tests/RudifaUtilPkgTests/DataExtTests.swift b/Tests/RudifaUtilPkgTests/DataExtTests.swift index 85b3f9c..ee8c9a2 100644 --- a/Tests/RudifaUtilPkgTests/DataExtTests.swift +++ b/Tests/RudifaUtilPkgTests/DataExtTests.swift @@ -1,6 +1,6 @@ // // DataExtTests.swift -// +// RudifaUtilPkgTests // // Created by Rudolf Farkas on 07.10.22. // diff --git a/Tests/RudifaUtilPkgTests/DateExtTests.swift b/Tests/RudifaUtilPkgTests/DateExtTests.swift index 990acbd..e5d5dd4 100644 --- a/Tests/RudifaUtilPkgTests/DateExtTests.swift +++ b/Tests/RudifaUtilPkgTests/DateExtTests.swift @@ -1,5 +1,5 @@ // -// DateExtTests.swift.swift v.0.4.0 +// DateExtTests.swift.swift // RudifaUtilPkgTests // // Created by Rudolf Farkas on 18.06.18. @@ -436,4 +436,52 @@ class DateExtTests: XCTestCase { XCTAssertEqual("février 1938", testDateUTC.formatted(fmt: "LLLL yyyy", locale: Locale(identifier: "fr_CH"))) } } + + func test_DataToFromString() { + do { + let string = "qwertzuiop" + let data = Data(from: string) + let string2 = data?.string + XCTAssertEqual(string, string2) + XCTAssertEqual(data?.count, 10) + } + do { + let string = "" + let data = Data(from: string) + let string2 = data?.string + XCTAssertEqual(string, string2) + XCTAssertEqual(data?.count, 0) + } + } + + func test_WriteReadCacheFile() { + do { + let string = "qwertzuiop" + let data = Data(from: string) + guard let url = data?.writeToCacheFile(name: "zap" + string, ext: "txt") else { + XCTFail("url == nil") + return + } + guard let data2 = Data(fromFileAt: url) else { + XCTFail("url == nil") + return + } + let string2 = data2.string + XCTAssertEqual(string, string2) + } + do { + let string = "" + let data = Data(from: string) + guard let url = data?.writeToCacheFile(name: "zap" + string, ext: "txt") else { + XCTFail("url == nil") + return + } + guard let data2 = Data(fromFileAt: url) else { + XCTFail("url == nil") + return + } + let string2 = data2.string + XCTAssertEqual(string, string2) + } + } } diff --git a/Tests/RudifaUtilPkgTests/DateIntervalExtTests.swift b/Tests/RudifaUtilPkgTests/DateIntervalExtTests.swift index 7fe21ad..9e7e6ea 100644 --- a/Tests/RudifaUtilPkgTests/DateIntervalExtTests.swift +++ b/Tests/RudifaUtilPkgTests/DateIntervalExtTests.swift @@ -1,6 +1,6 @@ // -// DateIntervalExtTests.swift v.0.1.3 -// RudifaUtilPkg +// DateIntervalExtTests.swift +// RudifaUtilPkgTests // // Created by Rudolf Farkas on 28.08.20. // Copyright © 2018 Rudolf Farkas. All rights reserved. diff --git a/Tests/RudifaUtilPkgTests/DebugExtTests.swift b/Tests/RudifaUtilPkgTests/DebugExtTests.swift index d3261a0..1aa9e48 100644 --- a/Tests/RudifaUtilPkgTests/DebugExtTests.swift +++ b/Tests/RudifaUtilPkgTests/DebugExtTests.swift @@ -1,5 +1,5 @@ // -// DebugExtTests.swift v.0.3.2 +// DebugExtTests.swift // RudifaUtilPkgTests // // Created by Rudolf Farkas on 23.07.19. diff --git a/Tests/RudifaUtilPkgTests/EnumExtTests.swift b/Tests/RudifaUtilPkgTests/EnumExtTests.swift index 082a48c..a29d121 100644 --- a/Tests/RudifaUtilPkgTests/EnumExtTests.swift +++ b/Tests/RudifaUtilPkgTests/EnumExtTests.swift @@ -1,5 +1,5 @@ // -// EnumExtTests.swift v.0.3.0 +// EnumExtTests.swift // RudifaUtilPkgTests // // Created by Rudolf Farkas on 18.03.20. diff --git a/Tests/RudifaUtilPkgTests/FileManagerExtTests.swift b/Tests/RudifaUtilPkgTests/FileManagerExtTests.swift new file mode 100644 index 0000000..2db6bfb --- /dev/null +++ b/Tests/RudifaUtilPkgTests/FileManagerExtTests.swift @@ -0,0 +1,38 @@ +// +// FileManagerExtTests.swift +// RudifaUtilPkgTests +// +// Created by Rudolf Farkas on 05.09.20. +// Copyright © 2020 Rudolf Farkas. All rights reserved. +// + +import XCTest + +class FileManagerExtTests: XCTestCase { + override func setUpWithError() throws {} + + override func tearDownWithError() throws {} + + func test_FileManager_cacheFilePath() { + let cacheFileUrl = FileManager.cacheFilePath(fileName: "testfile", fileExt: "txt")! + + // validate cacheFileUrl by using it in file operations + + let testString = "hello FileManager cache" + let testData = testString.data(using: .utf8)! + + do { + try testData.write(to: cacheFileUrl, options: [.atomicWrite]) + } catch { + XCTFail("testData.write failed") + } + + if let data = try? Data(contentsOf: cacheFileUrl) { + XCTAssertEqual(data, testData) + XCTAssertEqual(String(data: data, encoding: .utf8), testString) + + } else { + XCTFail("testData read failed") + } + } +} diff --git a/Tests/RudifaUtilPkgTests/NSAttributedStringExtTests.swift b/Tests/RudifaUtilPkgTests/NSAttributedStringExtTests.swift index 60e5c97..bee88b7 100644 --- a/Tests/RudifaUtilPkgTests/NSAttributedStringExtTests.swift +++ b/Tests/RudifaUtilPkgTests/NSAttributedStringExtTests.swift @@ -1,11 +1,13 @@ // // NSAttributedStringExtTests.swift -// RudifaUtilPkgDemoTests +// RudifaUtilPkgTests // // Created by Rudolf Farkas on 18.10.20. // Copyright © 2020 Rudolf Farkas. All rights reserved. // +#if os(iOS) + import XCTest class NSAttributedStringExtTests: XCTestCase { @@ -53,3 +55,4 @@ class NSAttributedStringExtTests: XCTestCase { } } } +#endif diff --git a/Tests/RudifaUtilPkgTests/StringExtTests.swift b/Tests/RudifaUtilPkgTests/StringExtTests.swift index 5138d59..e6bcc1e 100644 --- a/Tests/RudifaUtilPkgTests/StringExtTests.swift +++ b/Tests/RudifaUtilPkgTests/StringExtTests.swift @@ -1,6 +1,6 @@ // -// StringExtTests.swift v.0.3.0 -// RudifaUtilPkg +// StringExtTests.swift +// RudifaUtilPkgTests // // Created by Rudolf Farkas on 22.07.18. // Copyright © 2018 Rudolf Farkas. All rights reserved. diff --git a/Tests/RudifaUtilPkgTests/TimeIntervalExtTests.swift b/Tests/RudifaUtilPkgTests/TimeIntervalExtTests.swift index 7b61931..d62c485 100644 --- a/Tests/RudifaUtilPkgTests/TimeIntervalExtTests.swift +++ b/Tests/RudifaUtilPkgTests/TimeIntervalExtTests.swift @@ -1,6 +1,6 @@ // // TimeIntervalExtTests.swift -// RudifaUtilPkg +// RudifaUtilPkgTests // // Created by Rudolf Farkas on 31.08.21. // Copyright © 2021 Rudolf Farkas. All rights reserved. diff --git a/Tests/RudifaUtilPkgTests/UserDefaultsExtTests.swift b/Tests/RudifaUtilPkgTests/UserDefaultsExtTests.swift index 9a137c3..acb5344 100644 --- a/Tests/RudifaUtilPkgTests/UserDefaultsExtTests.swift +++ b/Tests/RudifaUtilPkgTests/UserDefaultsExtTests.swift @@ -1,5 +1,5 @@ // -// UserDefaultsExtTests.swift v.0.3.0 +// UserDefaultsExtTests.swift // RudifaUtilPkgTests // // Created by Rudolf Farkas on 13.03.20. diff --git a/Tests/RudifaUtilPkgTests/XCTExtTests.swift b/Tests/RudifaUtilPkgTests/XCTExtTests.swift index 63cf254..df3a731 100644 --- a/Tests/RudifaUtilPkgTests/XCTExtTests.swift +++ b/Tests/RudifaUtilPkgTests/XCTExtTests.swift @@ -1,5 +1,5 @@ // -// XCTExtTests.swift v.0.1.0 +// XCTExtTests.swift // RudifaUtilPkgTests // // Created by Rudolf Farkas on 21.03.20. From 5c435ef1a7ae04a8e9d03c844d120e57a4f25d9e Mon Sep 17 00:00:00 2001 From: Rudi Farkas Date: Mon, 2 Jan 2023 20:38:26 +0100 Subject: [PATCH 04/11] add FileBackedDictionary + tests + doc comments --- .jazzy.yaml | 2 +- Sources/RudifaUtilPkg/CodableExt.swift | 74 ++++--- .../RudifaUtilPkg/FileBackedDictionary.swift | 200 ++++++++++++++++++ .../FileBackedDictionaryTests.swift | 103 +++++++++ 4 files changed, 345 insertions(+), 34 deletions(-) create mode 100644 Sources/RudifaUtilPkg/FileBackedDictionary.swift create mode 100644 Tests/RudifaUtilPkgTests/FileBackedDictionaryTests.swift diff --git a/.jazzy.yaml b/.jazzy.yaml index 4bf84aa..5c5b7c5 100644 --- a/.jazzy.yaml +++ b/.jazzy.yaml @@ -8,7 +8,7 @@ xcodebuild_arguments: - -sdk - iphonesimulator - -destination - - 'platform=iOS Simulator,name=iPhone 11 Pro,OS=latest' + - 'platform=iOS Simulator,name=iPhone 14,OS=latest' author: Rudi Farkas copyright: Copyright © 2019 Rudi Farkas. All rights reserved. min_acl: internal diff --git a/Sources/RudifaUtilPkg/CodableExt.swift b/Sources/RudifaUtilPkg/CodableExt.swift index a27f1a7..d8173f4 100644 --- a/Sources/RudifaUtilPkg/CodableExt.swift +++ b/Sources/RudifaUtilPkg/CodableExt.swift @@ -9,46 +9,54 @@ import Foundation /** - Extensions nspired by https://gist.github.com/StanislavK/e763cdc9fbe92f62f3c9dbd648e7e7ad - - Usage examples - - struct Language: Codable { - var name: String - var version: String - } - - // create an instance - let language = Language(name: "Swift", version: "4") - - // encode to Data? - if let data: Data = language.encode() { - // use data here - - // decode from Data - if let lang = Language.decode(from: data) { - // use lang here - } else { - // handle decode error - } + ###Usage examples:### + + ```` + struct Language: Codable { + var name: String + var version: String + } + ```` + + Create an instance + ```` + let language = Language(name: "Swift", version: "5.7") + ```` + Encode to Data? and decode to a new Language instance + + ```` + if let data: Data = language.encode() { + // use data here... + + // decode from Data + if let lang = Language.decode(from: data) { + // use lang here } else { - // handle encode error + // handle decode error } + } else { + // handle encode error + } + ```` - // encode to String? - if let string: String = language.encode() { - // use string here + Encode instance to String? and decode to a new Language instance - // decode from String - if let lang = Language.decode(from: string) { - // use lang here - } else { - // handle decode error - } + ```` + if let string: String = language.encode() { + // use string here... + + // decode from String + if let lang = Language.decode(from: string) { + // use lang here } else { - // handle encode error + // handle decode error } + } else { + // handle encode error + } + ```` */ + public extension Encodable { /// Encode self into Data /// THROWING VERSION REMOVED diff --git a/Sources/RudifaUtilPkg/FileBackedDictionary.swift b/Sources/RudifaUtilPkg/FileBackedDictionary.swift new file mode 100644 index 0000000..5dc79f9 --- /dev/null +++ b/Sources/RudifaUtilPkg/FileBackedDictionary.swift @@ -0,0 +1,200 @@ +// +// FileBackedDictionary.swift +// RudifaUtilPkg +// +// Created by Rudolf Farkas on 27.12.22. +// + +import Foundation +/** + + Encapsulates a `[String:T]` dictionary whose values are automatically persisted (each in its own file). + + Exposes methods and properties similar to those of the Swift Dictionary. + + ###Usage examples:### + + Create a `FileBackedDictionary` instance, using Documents subdirectory named `Resources` and `struct Resource` as the value type. + + ```` + let directoryName = "Resources" + var fbDict = FileBackedDictionary(directoryName: directoryName) + ```` + + Add resource instances + + ```` + fbDict["apples"] = Resource(name: "apples", value: 1, quantity: 1) + fbDict["oranges"] = Resource(name: "oranges", value: 2, quantity: 2) + fbDict["mangos"] = Resource(name: "mangos", value: 3, quantity: 3) + ```` + + Look up the resource info + + ```` + let count = bDict.count // 3 + let keys = fbDict.keys // ["apples", "oranges", "mangos"]) + let values = fbDict.values // ... + let myOranges = fbDict["oranges"] + ```` + + Remove resources + + ```` + fbDict["oranges"] = nil + fbDict.removeAll() + ```` + + At its initialization (typically at the application start), an instance of `FileBackedDictionary` recovers the keys and values from the file storage. + + */ +public struct FileBackedDictionary { + private(set) var dictionary: [String: T] = [:] + + private let fileManager = FileManager.default + private let directoryURL: URL + private func fileURL(key: String) -> URL { + return directoryURL.appendingPathComponent(key) + } + + /// Initialize the backed-up storage + /// - Parameter directoryName: names the backup directory + /// - Remark: if the directory for the backup files does not exist, creates it. Initiallizes the local dict from files in the directory (if any) + public init(directoryName: String) { + print("--- init directoryName= \(directoryName)") + directoryURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!.appendingPathComponent(directoryName) + ensureDirectoryExists(directoryURL: directoryURL) + recoverDict() + } + + /// Ensure that the directory at the URL exists + /// - Parameter directoryURL: + private func ensureDirectoryExists(directoryURL: URL) { + let fileManager = FileManager.default + if fileManager.fileExists(atPath: directoryURL.path) { + // Check if file is a directory + var isDirectory: ObjCBool = false + if fileManager.fileExists(atPath: directoryURL.path, isDirectory: &isDirectory) { + if isDirectory.boolValue { + // File exists and is a directory + return + } + } + } + // File does not exist or is not a directory so create it + do { + try fileManager.createDirectory(at: directoryURL, withIntermediateDirectories: true, attributes: nil) + } catch { + print("Error creating directory: \(error.localizedDescription)") + } + } + + /// Support for dictionary style setting and getting of a value by key + public subscript(key: String) -> T? { + get { + return dictionary[key] + } + set { + if let value = newValue { + saveValue(for: key, value: value) + } else { + dictionary.removeValue(forKey: key) + removeValue(forKey: key) + } + } + } + + /// Return sorted keys + public var keys: [String] { + dictionary.keys.sorted().map { String($0) } + } + + /// Return values sorted by keys + public var values: [T] { + keys.map { dictionary[$0]! } + } + + /// Save the value in the dictionary and in a file + /// - Parameters: + /// - key: key + /// - value: to be added or updated + private mutating func saveValue(for key: String, value: T) { + dictionary[key] = value + // Back up the dictionary value to a file named after the key + let fileURL = self.fileURL(key: key) + let data = try? JSONEncoder().encode(value) + _ = fileManager.createFile(atPath: fileURL.path, contents: data, attributes: nil) + } + + /// Recover dictionary values from backing files + private mutating func recoverDict() { + guard let fileURLs = try? fileManager.contentsOfDirectory(at: directoryURL, includingPropertiesForKeys: [], options: [.skipsHiddenFiles]) else { + print("*** recoverDict fileURLs not found") + return + } + for fileURL in fileURLs { + if let fileName = fileURL.lastPathComponent.components(separatedBy: ".").first { + do { + let data = try Data(contentsOf: fileURL) + let dictValue = try JSONDecoder().decode(T.self, from: data) + dictionary[fileName] = dictValue + } catch { + print("*** Error decoding data from file: \(error)") + } + } + } + } + + /// Remove the value for key from the dictionary and from the file + /// - Parameter forKey: key + private mutating func removeValue(forKey key: String) { + dictionary[key] = nil + let fileURL = self.fileURL(key: key) + do { + try FileManager.default.removeItem(at: fileURL) + } catch { + print("*** removeValue \(error)") + } + } + + /// Remove all values from dictionary and from files + public mutating func removeAll() { + do { + for key in try fileManager.contentsOfDirectory(atPath: directoryURL.path) { + removeValue(forKey: key) + } + } catch { + print("*** removeAll error= \(error)") + } + dictionary = [:] + } + + /// Print names of backing files + private func enumerateFiles() { + let fileManager = FileManager.default + guard let enumerator = fileManager.enumerator(at: directoryURL, includingPropertiesForKeys: [.nameKey], options: [.skipsHiddenFiles]) else { + print("enumerateFiles: no files found") + return + } + for case let fileURL as URL in enumerator { + let fileName = fileURL.lastPathComponent + print("enumerateFiles: \(fileName)") + } + } + + /// Return the dictionary item count + public var count: Int { + // do { // for debugging only + // // check the number of files in the directory + // let fileNames = try fileManager.contentsOfDirectory(atPath: directoryURL.path) + // if fileNames.count != dictionary.count { + // print("*** count directoryURL= \(directoryURL)") + // print("*** count fileNames= \(fileNames)") + // print("*** count keys= \(dictionary.keys)") + // } + // } catch { + // print("Error while enumerating files \(directoryURL.path): \(error.localizedDescription)") + // } + return dictionary.count + } +} diff --git a/Tests/RudifaUtilPkgTests/FileBackedDictionaryTests.swift b/Tests/RudifaUtilPkgTests/FileBackedDictionaryTests.swift new file mode 100644 index 0000000..1cfcdfc --- /dev/null +++ b/Tests/RudifaUtilPkgTests/FileBackedDictionaryTests.swift @@ -0,0 +1,103 @@ +// +// FileBackedDictionaryTests.swift +// RudifaUtilPkgTests +// +// Created by Rudolf Farkas on 24.12.22. +// + +import RudifaUtilPkg +import XCTest + +class FileBackedDictionaryTests: XCTestCase { + func test_SubscriptSetterAndGetter() { + let directoryName = "test_directory" + var fbd = FileBackedDictionary(directoryName: directoryName) + + // fbd.removeAll() + + let expectedValue = "test_value" + + fbd["test_key"] = expectedValue + + XCTAssertEqual(fbd["test_key"], expectedValue) + + fbd["test_key_2"] = expectedValue + expectedValue + + do { + let fbd2 = FileBackedDictionary(directoryName: directoryName) + XCTAssertEqual(fbd2.count, 2) + printClassAndFunc("fbd2.keys= \(fbd2.keys)") + } + + fbd["test_key_2"] = nil + + do { + let fbd2 = FileBackedDictionary(directoryName: directoryName) + XCTAssertEqual(fbd2.count, 1) + } + } + + // create a struct Resource for testing + struct Resource: Codable, Equatable { + let name: String + let value: Int + let quantity: Int + } + + func test_FileBackedDictionary1() { + // create a FileBackedDictionary under test with directory named + // "test_directory" using struct Resource as the value type + let directoryName = "test_directory1" + var fbDict = FileBackedDictionary(directoryName: directoryName) + + let resource1 = Resource(name: "resource_1", value: 1, quantity: 1) + let resource2 = Resource(name: "resource_2", value: 2, quantity: 2) + let resource3 = Resource(name: "resource_3", value: 3, quantity: 3) + + fbDict["resource_1"] = resource1 + fbDict["resource_2"] = resource2 + fbDict["resource_3"] = resource3 + + XCTAssertEqual(fbDict.count, 3) + // keys + XCTAssertEqual(fbDict.keys, ["resource_1", "resource_2", "resource_3"]) + // values + XCTAssertEqual(fbDict.values, [resource1, resource2, resource3]) + + // remove resource_2 by setting it to nil + fbDict["resource_2"] = nil + XCTAssertEqual(fbDict.count, 2) + XCTAssertEqual(fbDict.keys, ["resource_1", "resource_3"]) + XCTAssertEqual(fbDict.values, [resource1, resource3]) + + // remove all + fbDict.removeAll() + XCTAssertEqual(fbDict.count, 0) + XCTAssertEqual(fbDict.keys, []) + XCTAssertEqual(fbDict.values, []) + } + + func test_FileBackedDictionary2() { + // create a FileBackedDictionary under test with directory named + // "test_directory" using struct Resource as the value type + let directoryName = "test_directory2" + var fbDict = FileBackedDictionary(directoryName: directoryName) + + // create an array of resources + let resource1 = Resource(name: "resource_1", value: 1, quantity: 1) + let resource2 = Resource(name: "resource_2", value: 2, quantity: 2) + let resource3 = Resource(name: "resource_3", value: 3, quantity: 3) + let resources = [resource1, resource2, resource3] + + // add the resources to the dictionary from array, using the name as key + for resource in resources { + fbDict[resource.name] = resource + } + + XCTAssertEqual(fbDict.count, 3) + // keys + XCTAssertEqual(fbDict.keys, ["resource_1", "resource_2", "resource_3"]) + // values + XCTAssertEqual(fbDict.values, [resource1, resource2, resource3]) + } +} From bc2a696c7f99bffd428a7038d5e14d23823c97ac Mon Sep 17 00:00:00 2001 From: Rudi Farkas Date: Mon, 2 Jan 2023 21:13:29 +0100 Subject: [PATCH 05/11] .jazzy.yam: update copyright notice --- .jazzy.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.jazzy.yaml b/.jazzy.yaml index 5c5b7c5..136cf24 100644 --- a/.jazzy.yaml +++ b/.jazzy.yaml @@ -9,8 +9,8 @@ xcodebuild_arguments: - iphonesimulator - -destination - 'platform=iOS Simulator,name=iPhone 14,OS=latest' -author: Rudi Farkas -copyright: Copyright © 2019 Rudi Farkas. All rights reserved. +author: Rudolf Farkas @rudifa +copyright: Copyright © 2019-2023 Rudolf Farkas. All rights reserved. min_acl: internal # from build_and_test.xml From 3e7880adc5be4ff265de4002a4eeeb8f13eeb39b Mon Sep 17 00:00:00 2001 From: Rudi Farkas Date: Tue, 3 Jan 2023 12:43:50 +0100 Subject: [PATCH 06/11] FileBackedDictionary.removeValue: make public --- Sources/RudifaUtilPkg/FileBackedDictionary.swift | 2 +- Tests/RudifaUtilPkgTests/FileBackedDictionaryTests.swift | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/Sources/RudifaUtilPkg/FileBackedDictionary.swift b/Sources/RudifaUtilPkg/FileBackedDictionary.swift index 5dc79f9..51b68a7 100644 --- a/Sources/RudifaUtilPkg/FileBackedDictionary.swift +++ b/Sources/RudifaUtilPkg/FileBackedDictionary.swift @@ -147,7 +147,7 @@ public struct FileBackedDictionary { /// Remove the value for key from the dictionary and from the file /// - Parameter forKey: key - private mutating func removeValue(forKey key: String) { + public mutating func removeValue(forKey key: String) { dictionary[key] = nil let fileURL = self.fileURL(key: key) do { diff --git a/Tests/RudifaUtilPkgTests/FileBackedDictionaryTests.swift b/Tests/RudifaUtilPkgTests/FileBackedDictionaryTests.swift index 1cfcdfc..9df0f4e 100644 --- a/Tests/RudifaUtilPkgTests/FileBackedDictionaryTests.swift +++ b/Tests/RudifaUtilPkgTests/FileBackedDictionaryTests.swift @@ -95,9 +95,12 @@ class FileBackedDictionaryTests: XCTestCase { } XCTAssertEqual(fbDict.count, 3) - // keys XCTAssertEqual(fbDict.keys, ["resource_1", "resource_2", "resource_3"]) - // values XCTAssertEqual(fbDict.values, [resource1, resource2, resource3]) - } + + fbDict.removeValue(forKey: "resource_2") + XCTAssertEqual(fbDict.count, 2) + XCTAssertEqual(fbDict.keys, ["resource_1", "resource_3"]) + XCTAssertEqual(fbDict.values, [resource1, resource3]) + } } From 59f164cf6d2a00faa731518d2bd34f243e23461f Mon Sep 17 00:00:00 2001 From: Rudi Farkas Date: Thu, 5 Jan 2023 16:51:03 +0100 Subject: [PATCH 07/11] add IntExt.swift --- Sources/RudifaUtilPkg/IntExt.swift | 40 +++++++++++++ Tests/RudifaUtilPkgTests/IntExtTest.swift | 68 +++++++++++++++++++++++ 2 files changed, 108 insertions(+) create mode 100644 Sources/RudifaUtilPkg/IntExt.swift create mode 100644 Tests/RudifaUtilPkgTests/IntExtTest.swift diff --git a/Sources/RudifaUtilPkg/IntExt.swift b/Sources/RudifaUtilPkg/IntExt.swift new file mode 100644 index 0000000..1544744 --- /dev/null +++ b/Sources/RudifaUtilPkg/IntExt.swift @@ -0,0 +1,40 @@ +// +// IntExt.swift +// +// +// Created by Rudolf Farkas on 05.01.23. +// Copyright © 2023 Rudolf Farkas. All rights reserved. +// + +import Foundation + +public extension Int { + /// Return the value incremented or decremented, wrapped around the range 0.. + /// - Parameters: + /// - up: + /// - count: + /// - Returns: modified falue + /// - Remark: noop if self < 0 + private func incrementedWrapping(up: Bool, count: Int) -> Int { + guard self >= 0, count > 0 else { return self } + return (self + (up ? 1 : -1) + count) % count + } + + /// Return incremented self, constrained to range 0.. + /// - Parameter count: + /// - Returns: self + 1 wrapped around on count + func next(count: Int) -> Int { + return incrementedWrapping(up: true, count: count) + } + + /// Return decremented self, constrained to range 0.. + /// - Parameter count: + /// - Returns: self - 1 wrapped around on count + func prev(count: Int) -> Int { + return incrementedWrapping(up: false, count: count) + } + + mutating func toNext(next: Bool, count: Int) { + self = incrementedWrapping(up: next, count: count) + } +} diff --git a/Tests/RudifaUtilPkgTests/IntExtTest.swift b/Tests/RudifaUtilPkgTests/IntExtTest.swift new file mode 100644 index 0000000..37ee15b --- /dev/null +++ b/Tests/RudifaUtilPkgTests/IntExtTest.swift @@ -0,0 +1,68 @@ +// +// IntExtTest.swift +// +// +// Created by Rudolf Farkas on 05.01.23. +// Copyright © 2023 Rudolf Farkas. All rights reserved. +// + +import XCTest + +final class IntExtTest: XCTestCase { + override func setUpWithError() throws {} + + override func tearDownWithError() throws {} + + func test_IntIncrementWrapping() throws { + do { + var value: Int = 1 + let count: Int = 4 + value.toNext(next: true, count: count) + XCTAssertEqual(value, 2) + value.toNext(next: true, count: count) + XCTAssertEqual(value, 3) + value.toNext(next: true, count: count) + XCTAssertEqual(value, 0) + value.toNext(next: true, count: count) + XCTAssertEqual(value, 1) + } + do { + var value: Int = 1 + let count: Int = 4 + value.toNext(next: false, count: count) + XCTAssertEqual(value, 0) + value.toNext(next: false, count: count) + XCTAssertEqual(value, 3) + value.toNext(next: false, count: count) + XCTAssertEqual(value, 2) + value.toNext(next: false, count: count) + XCTAssertEqual(value, 1) + } + do { + var value: Int = -1 + let count: Int = 4 + value.toNext(next: false, count: count) + XCTAssertEqual(value, -1) // unchanged + } + do { + let value: Int = 1 + let count: Int = 4 + let valuea = value.next(count: count) + XCTAssertEqual(valuea, 2) + let valueb = valuea.next(count: count) + XCTAssertEqual(valueb, 3) + let valuec = valueb.next(count: count) + XCTAssertEqual(valuec, 0) + } + do { + let value: Int = 1 + let count: Int = 4 + let valuea = value.prev(count: count) + XCTAssertEqual(valuea, 0) + let valueb = valuea.prev(count: count) + XCTAssertEqual(valueb, 3) + let valuec = valueb.prev(count: count) + XCTAssertEqual(valuec, 2) + } + } +} From 4f2cafcfbbbfdd2322e3b81af0128ddbad9a0fcc Mon Sep 17 00:00:00 2001 From: Rudi Farkas Date: Thu, 5 Jan 2023 20:46:26 +0100 Subject: [PATCH 08/11] revise logClassAndFunc --- Sources/RudifaUtilPkg/DebugExt.swift | 33 +++++++++++++++++++- Tests/RudifaUtilPkgTests/DebugExtTests.swift | 15 +++++---- 2 files changed, 39 insertions(+), 9 deletions(-) diff --git a/Sources/RudifaUtilPkg/DebugExt.swift b/Sources/RudifaUtilPkg/DebugExt.swift index 9ae919e..c21391b 100644 --- a/Sources/RudifaUtilPkg/DebugExt.swift +++ b/Sources/RudifaUtilPkg/DebugExt.swift @@ -49,6 +49,8 @@ import Foundation /// - Open the app's directory as above, right-click on the file and click `Delete` public extension NSObject { + // MARK: print to console (DEBUG only) + /// Print to stdout current class and function names and optional info /// /// - Note: Printing is enabled by DEBUG constant which is normally absent from release builds. @@ -93,6 +95,8 @@ public extension NSObject { #endif } + // MARK: log to file log.txt + /// Print to log file current class and function names and optional info /// /// - Requires: to be called from a subclass of NSObject @@ -100,16 +104,43 @@ public extension NSObject { /// - Parameters: /// - info: information string; a leading "@" will be replaced by the call date /// - fnc: current function (default value is the caller) + @available(*, deprecated, message: "use logClassAndFunc(\"...\" instead") func logClassAndFunc(info inf_: String = "", fnc fnc_: String = #function) { Logger.shared.print(formatClassAndFunc(info: inf_, fnc: fnc_)) } + /// Print to log file current class and function names and optional info + /// + /// - Note: Printing is enabled by DEBUG constant which is normally absent from release builds. + /// + /// - Requires: to be called from a subclass of NSObject + /// + /// - Parameters: + /// - _: information string; a leading "@" will be replaced by the call date + /// - fnc: current function (default value is the caller) + func logClassAndFunc(_ info: String = "", fnc fnc_: String = #function) { + Logger.shared.print(formatClassAndFunc(info: info, fnc: fnc_)) + } + + /// Print to log file current class and function names and optional info + /// + /// - Note: This third form is only needed to make the call logClassAndFunc() unambigous + /// when both above forms are present in the code. + /// + /// - TODO: remove when the above deprecated form is removed + /// + func logClassAndFunc(_fnc fnc_: String = #function) { + Logger.shared.print(formatClassAndFunc(info: "", fnc: fnc_)) + } + + // MARK: supporting functions + /// Return a string containing current class and function names and optional info /// - Requires: to be called from a subclass of NSObject /// - Parameters: /// - info: information string; a leading "@" will be replaced by the call date /// - fnc: current function (default value is the caller) - func formatClassAndFunc(info inf_: String = "", fnc fnc_: String = #function) -> String { + internal func formatClassAndFunc(info inf_: String = "", fnc fnc_: String = #function) -> String { var dateTime = "" var info = inf_ if inf_.first == "@" { diff --git a/Tests/RudifaUtilPkgTests/DebugExtTests.swift b/Tests/RudifaUtilPkgTests/DebugExtTests.swift index 1aa9e48..5ba713e 100644 --- a/Tests/RudifaUtilPkgTests/DebugExtTests.swift +++ b/Tests/RudifaUtilPkgTests/DebugExtTests.swift @@ -11,23 +11,22 @@ import XCTest class DebugExtTests: XCTestCase { @available(*, deprecated) // silence warnings func test_printClassAndFunc() { + // deprecated style printClassAndFunc(info: "more info") printClassAndFunc(info: "@ even more info at this time") printClassAndFunc(info: "@ even more info a tad later") + // current style printClassAndFunc() printClassAndFunc("") printClassAndFunc("more info") printClassAndFunc("@ even more info at this time") printClassAndFunc("@ even more info a tad later") - XCTAssertEqual(formatClassAndFunc(info: "more info"), - "---- DebugExtTests.test_printClassAndFunc() more info") - - XCTAssertMatchesRegex(formatClassAndFunc(info: "@"), - #"^---- \d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{6} DebugExtTests.test_printClassAndFunc\(\) $"#) - - XCTAssertMatchesRegex(formatClassAndFunc(info: "@ even more info at this time"), - #"^---- \d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{6} DebugExtTests.test_printClassAndFunc\(\) even more info at this time$"#) + logClassAndFunc() + logClassAndFunc("") + logClassAndFunc("more info") + logClassAndFunc("@ even more info at this time") + logClassAndFunc("@ even more info a tad later") } } From 4dbde3d17fd5a1f8a894afdc8f1e680540c5fed6 Mon Sep 17 00:00:00 2001 From: Rudi Farkas Date: Sat, 28 Jan 2023 16:45:20 +0100 Subject: [PATCH 09/11] add extension Array where Element == String {func sortedBySuffixAndPrefix()...} --- Sources/RudifaUtilPkg/CollectionExt.swift | 26 +++++++++++++++++++ .../CollectionExtTests.swift | 17 ++++++++++-- 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/Sources/RudifaUtilPkg/CollectionExt.swift b/Sources/RudifaUtilPkg/CollectionExt.swift index fe47dd7..9c8747a 100644 --- a/Sources/RudifaUtilPkg/CollectionExt.swift +++ b/Sources/RudifaUtilPkg/CollectionExt.swift @@ -118,3 +118,29 @@ public extension Array { return temp } } + +public extension Array where Element == String { + /// Given an array of strings, each optionally sepatated into prefix and suffix by the separator, + /// sort strings aplhabeticall, but grouped by suffix + /// - Parameter separator: defaults to "_" + /// - Returns: sorted array + func sortedBySuffixAndPrefix(separator: String = "_") -> [Element] { + return sorted { a, b -> Bool in + let aComponents = a.components(separatedBy: separator) + let bComponents = b.components(separatedBy: separator) + if aComponents.count == 1, bComponents.count == 1 { + return a < b + } else if aComponents.count == 1 { + return true + } else if bComponents.count == 1 { + return false + } else { + if aComponents[1] == bComponents[1] { + return aComponents[0] < bComponents[0] + } else { + return aComponents[1] < bComponents[1] + } + } + } + } +} diff --git a/Tests/RudifaUtilPkgTests/CollectionExtTests.swift b/Tests/RudifaUtilPkgTests/CollectionExtTests.swift index 75ba1bd..11de874 100644 --- a/Tests/RudifaUtilPkgTests/CollectionExtTests.swift +++ b/Tests/RudifaUtilPkgTests/CollectionExtTests.swift @@ -164,14 +164,14 @@ class CollectionExtTests: XCTestCase { // using the inline predicate let updatedCalendarDataArray = calendarDataArray.updatedPreservingOrder(from: incomingCalendarDataArray, - predicate: { (elt1, elt2) -> Bool in elt1.title == elt2.title }) + predicate: { elt1, elt2 -> Bool in elt1.title == elt2.title }) printClassAndFunc("updatedCalendarDataArray= \(updatedCalendarDataArray.map { $0.string })") XCTAssertEqual(updatedCalendarDataArray.map { $0.string }, expectedResultStringArray) var localCopy = calendarDataArray localCopy.updatePreservingOrder(from: incomingCalendarDataArray, - predicate: { (elt1, elt2) -> Bool in elt1.title == elt2.title }) + predicate: { elt1, elt2 -> Bool in elt1.title == elt2.title }) XCTAssertEqual(localCopy.map { $0.string }, expectedResultStringArray) } @@ -324,4 +324,17 @@ class CollectionExtTests: XCTestCase { XCTAssertEqual(array.moved(where: MockCalendarData(title: "Orchid").sameTitle, to: 3).map { $0.title }, ["Anemone", "Begonia", "Clematis", "Dahlia"]) } + + func test_Array_sortedBySuffixAndPrefix() { + do { + let array = ["1.A", "9.B", "5.A", "7.C", "3.B", "7.A", "4.A", "3", "6", "1", "5.C"] + let sortedArray = array.sortedBySuffixAndPrefix(separator: ".") + XCTAssertEqual(sortedArray, ["1", "3", "6", "1.A", "4.A", "5.A", "7.A", "3.B", "9.B", "5.C", "7.C"]) + } + do { + let array = ["1_A", "9_B", "5_A", "7_C", "3_B", "7_A", "4_A", "3", "6", "1", "5_C"] + let sortedArray = array.sortedBySuffixAndPrefix() + XCTAssertEqual(sortedArray, ["1", "3", "6", "1_A", "4_A", "5_A", "7_A", "3_B", "9_B", "5_C", "7_C"]) + } + } } From 4d56d25548caa6ae159324d9d8fdf344ff851779 Mon Sep 17 00:00:00 2001 From: Rudi Farkas Date: Fri, 6 Jan 2023 15:39:59 +0100 Subject: [PATCH 10/11] add TimerDeltaSigma --- Sources/RudifaUtilPkg/TimerDeltaSigma.swift | 105 ++++++++++++++++++ .../TimerDeltaSigmaTests.swift | 31 ++++++ 2 files changed, 136 insertions(+) create mode 100644 Sources/RudifaUtilPkg/TimerDeltaSigma.swift create mode 100644 Tests/RudifaUtilPkgTests/TimerDeltaSigmaTests.swift diff --git a/Sources/RudifaUtilPkg/TimerDeltaSigma.swift b/Sources/RudifaUtilPkg/TimerDeltaSigma.swift new file mode 100644 index 0000000..89f5a09 --- /dev/null +++ b/Sources/RudifaUtilPkg/TimerDeltaSigma.swift @@ -0,0 +1,105 @@ +// +// TimerDeltaSigma.swift +// RudifaUtilPkg +// +// Created by Rudolf Farkas on 06.01.23. +// Copyright © 2023 Rudolf Farkas. All rights reserved. +// + +import Foundation + +/** + ###Usage examples:### + + Sample usage: + + ```` + func demo_TimerDeltaSigma() { + var tds = TimerDeltaSigma() + for _ in 0 ..< 10 { + tds.printElapsedTimes() + } + print() + for _ in 0 ..< 5 { + tds.printElapsedTimes(.ms) + } + print() + for _ in 0 ..< 5 { + tds.printElapsedTimes(.s) + } + print() + } + ``` + Sample output: + + ``` + Δ 0.000000, Σ 0.000000 s + Δ 0.000082, Σ 0.000082 s + ... + Δ 0.000022, Σ 0.000260 s + Δ 0.000021, Σ 0.000281 s + + Δ 0.000, Σ 0.000 s + ... + Δ 0.000, Σ 0.000 s + + Δ 0, Σ 0 s + ... + Δ 0, Σ 0 s + ``` + */ + +/// TimerDeltaSigma methods print elapsed time (sum since the instance creation and delta since the previous print) +public struct TimerDeltaSigma { + public enum Format: String { + case s + case ms + case μs + } + + typealias Elapsed = (delta: TimeInterval, sigma: TimeInterval) + + /// Create an instance and start time measurement + public init() {} + + private var initialTime: Date? + private var previousTime: Date? + + /// Update times variables and return the result tuple + /// - Returns: (delta, sigma) + private mutating func elapsedTimes() -> Elapsed { + let currentTime = Date() + if let initialTime = initialTime, let previousTime = previousTime { + let sigmaTime = currentTime.timeIntervalSince(initialTime) + let deltaTime = currentTime.timeIntervalSince(previousTime) + self.previousTime = currentTime + return (delta: deltaTime, sigma: sigmaTime) + } else { + initialTime = currentTime + previousTime = currentTime + return (delta: 0, sigma: 0) + } + } + + /// Format the tuple (delta, sigma) per format `fmt` + /// - Parameters: + /// - times: (delta, sigma) + /// - fmt: .s, .ms, .μs + /// - Returns: formatted string, like "Δ 0.000018, Σ 0.000291 s" + private func format(_ times: Elapsed, fmt: Format) -> String { + switch fmt { + case .s: + return String(format: "Δ %.0f, Σ %.0f s", times.delta, times.sigma) + case .ms: + return String(format: "Δ %.3f, Σ %.3f s", times.delta, times.sigma) + case .μs: + return String(format: "Δ %.6f, Σ %.6f s", times.delta, times.sigma) + } + } + + /// Print the times: Δ since the last print, Σ since the instance creation + /// - Parameter fmt: resolution .s, .ms, .μs + public mutating func printElapsedTimes(_ fmt: Format = .μs) { + print(format(elapsedTimes(), fmt: fmt)) + } +} diff --git a/Tests/RudifaUtilPkgTests/TimerDeltaSigmaTests.swift b/Tests/RudifaUtilPkgTests/TimerDeltaSigmaTests.swift new file mode 100644 index 0000000..250646b --- /dev/null +++ b/Tests/RudifaUtilPkgTests/TimerDeltaSigmaTests.swift @@ -0,0 +1,31 @@ +// +// TimerDeltaSigmaTests.swift +// +// +// Created by Rudolf Farkas on 06.01.23. +// + +import XCTest +import RudifaUtilPkg + +final class TimerDeltaSigmaTests: XCTestCase { + override func setUpWithError() throws {} + + override func tearDownWithError() throws {} + + func test_TimerDeltaSigma() { + var tds = TimerDeltaSigma() + for _ in 0 ..< 10 { + tds.printElapsedTimes() + } + print() + for _ in 0 ..< 5 { + tds.printElapsedTimes(.ms) + } + print() + for _ in 0 ..< 5 { + tds.printElapsedTimes(.s) + } + print() + } +} From 23c913fcf14a34b0dcb86e7842ddfe0f22402ba6 Mon Sep 17 00:00:00 2001 From: Rudi Farkas Date: Sat, 7 Jan 2023 22:01:19 +0100 Subject: [PATCH 11/11] add UIImage summary --- Sources/RudifaUtilPkg/UIKItExt.swift | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 Sources/RudifaUtilPkg/UIKItExt.swift diff --git a/Sources/RudifaUtilPkg/UIKItExt.swift b/Sources/RudifaUtilPkg/UIKItExt.swift new file mode 100644 index 0000000..b08657d --- /dev/null +++ b/Sources/RudifaUtilPkg/UIKItExt.swift @@ -0,0 +1,21 @@ +// +// UIKItExt.swift +// +// +// Created by Rudolf Farkas on 07.01.23. +// Copyright © 2023 Rudolf Farkas. All rights reserved. +// + +#if os(iOS) + import UIKit + + extension UIImage { + /// Return a summary of inmage size data + /// - Example: (700.0, 700.0) x 1.0, 609693 bytes + var summary: String { + let data = jpegData(compressionQuality: 1.0) ?? pngData() + let bytes = "\(data?.count ?? 0)" + return "\(size) x \(scale), \(bytes) bytes" + } + } +#endif