[XRAY-2] Part 2a - Move foundation extensions package to its own repository (#1)

Works on xiiagency/Xray#2
This commit is contained in:
Gennadiy Shafranovich 2021-10-25 16:49:37 -04:00 committed by GitHub
parent 4f5866bbe3
commit 3560bdd3bb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 452 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
# Swift Package Manager
.swiftpm

29
Package.swift Normal file
View File

@ -0,0 +1,29 @@
// swift-tools-version:5.5
import PackageDescription
let package =
Package(
name: "SwiftFoundationExtensions",
platforms: [
.iOS(.v15),
.watchOS(.v8),
],
products: [
.library(
name: "SwiftFoundationExtensions",
targets: ["SwiftFoundationExtensions"]
),
],
dependencies: [],
targets: [
.target(
name: "SwiftFoundationExtensions",
dependencies: []
),
// NOTE: Re-enable when tests are added.
// .testTarget(
// name: "SwiftFoundationExtensionsTests",
// dependencies: ["SwiftFoundationExtensions"]
// ),
]
)

3
README.md Normal file
View File

@ -0,0 +1,3 @@
# SwiftFoundationExtensions
Provides extensions and utilities for the core `Foundation` Swift libraries.

View File

@ -0,0 +1,79 @@
import Foundation
extension Array {
/**
Returns a `Dictionary` containing unique key/value pairs, extracted by the provided closure for each element in the `Array`.
*/
public func mapped<Key : Hashable, MappedValue>(
by pairExtractor: @escaping (Element) -> (Key, MappedValue)
) -> [Key: MappedValue] {
Dictionary(uniqueKeysWithValues: map(pairExtractor))
}
/**
Returns a `Dictionary` containing unique key/value pairs, extracted by the provided closure for each element in the `Array`.
*/
public func mapped<Key : Hashable, MappedValue>(
by pairExtractor: @escaping (Element) throws -> (Key, MappedValue)
) throws -> [Key: MappedValue] {
Dictionary(uniqueKeysWithValues: try map(pairExtractor))
}
/**
Returns a `Dictionary` containing the elements of this array, uniquely keyed by the results of the provided key extractor.
*/
public func mapped<Key : Hashable>(
by keyExtractor: @escaping (Element) -> Key
) -> [Key: Element] {
mapped(by: { element in (keyExtractor(element), element) })
}
/**
Returns a `Dictionary` containing the elements of this array, uniquely keyed by the results of the provided key extractor.
*/
public func mapped<Key : Hashable>(
by keyExtractor: @escaping (Element) throws -> Key
) throws -> [Key: Element] {
try mapped(by: { element in (try keyExtractor(element), element) })
}
/**
Returns `Dictionary` containing the elements of this array, grouped by a key returned from the provided closure.
*/
public func grouped<Key : Hashable>(
by keyExtractor: @escaping (Element) -> Key
) -> [Key: [Element]] {
Dictionary(
grouping: self,
by: { element in keyExtractor(element) }
)
}
/**
Returns `Dictionary` containing the elements of this array, grouped by a key returned from the provided closure.
*/
public func grouped<Key : Hashable>(
by keyExtractor: @escaping (Element) throws -> Key
) throws -> [Key: [Element]] {
try Dictionary(
grouping: self,
by: { element in try keyExtractor(element) }
)
}
}
extension Array where Element: Identifiable {
/**
Returns a `Dictionary` containing the elements of this array, uniquely keyed by their IDs.
*/
public func mappedById() -> [Element.ID: Element] {
mapped(by: { element in element.id })
}
/**
Returns a `Dictionary` containing the elements of this array, grouped by their IDs.
*/
public func groupedById() -> [Element.ID: [Element]] {
grouped(by: { element in element.id })
}
}

View File

@ -0,0 +1,59 @@
import Foundation
extension Dictionary {
/**
Allows for incrementing the value stored under the requested `Key`.
If there is no such value it is assumed to be `0`.
*/
public mutating func increment(key: Key, by value: Value) where Value == Int {
self[key] = (self[key] ?? 0) + value
}
/**
Allows for incrementing by one the value stored under the requested `Key`.
If there is no such value it is assumed to be `0`.
*/
public mutating func increment(key: Key) where Value == Int {
increment(key: key, by: 1)
}
/**
Allows for decrementing by one the value stored under the requested `Key`.
If there is no such value it is assumed to be `0`.
*/
public mutating func decrement(key: Key) where Value == Int {
increment(key: key, by: -1)
}
/**
Allows for incrementing the value stored under the requested `Key`.
If there is no such value it is assumed to be `0.0`.
*/
public mutating func increment(key: Key, by value: Value) where Value == Double {
self[key] = (self[key] ?? 0.0) + value
}
/**
Allows for incrementing by one the value stored under the requested `Key`.
If there is no such value it is assumed to be `0.0`.
*/
public mutating func increment(key: Key) where Value == Double {
increment(key: key, by: 1.0)
}
/**
Allows for decrementing by one the value stored under the requested `Key`.
If there is no such value it is assumed to be `0.0`.
*/
public mutating func decrement(key: Key) where Value == Double {
increment(key: key, by: -1)
}
/**
Inverts the key->value mapping to one of value->key. Assumes that all values are unique and will raise a fatal error otherwise.
*/
public func inverted() -> [Value: Key] where Value : Hashable {
let invertedPairs: [(Value, Key)] = map { (key, value) in (value, key) }
return Dictionary<Value, Key>(uniqueKeysWithValues: invertedPairs)
}
}

View File

@ -0,0 +1,47 @@
import Foundation
extension Date {
/**
Returns the start of the week (dependent on device locale) for the given date.
*/
public var startOfWeek: Date {
let calendar = Calendar.current
let components = calendar.dateComponents([.yearForWeekOfYear, .weekOfYear], from: self)
return calendar.date(from: components)!
}
/**
Returns the number of days remaining this week as an `Int`.
*/
public var daysRemainingThisWeek: Int {
let calendar = Calendar.current
let numberOfDays = calendar.dateComponents([.day], from: self.startOfWeek, to: self)
return numberOfDays.day!
}
/**
Returns the start of the month for the given date.
*/
public var startOfMonth: Date {
let calendar = Calendar.current
let components = calendar.dateComponents([.year, .month], from: self)
return calendar.date(from: components)!
}
/**
Returns the start of the year for the given date.
*/
public var startOfYear: Date {
let calendar = Calendar.current
let components = calendar.dateComponents([.year], from: self)
return calendar.date(from: components)!
}
/**
Creates a new date from year/month/day values using the current `Calendar`.
*/
public init(year: Int, month: Int, day: Int) {
let components = DateComponents(year: year, month: month, day: day)
self = Calendar.current.date(from: components)!
}
}

View File

@ -0,0 +1,37 @@
import Foundation
/**
Full version and build number of the main bundle in the format;
`v[MAJOR].[MINOR].[PATCH]-[BUILD]`
Example:
```
v1.2.3-456
```
*/
public let mainBundleVersion: String = {
// Version number
let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "?"
// Build number
let appBuild = Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "?"
return "v\(appVersion)-\(appBuild)"
}()
/**
Returns true if the main bundle was installed from the app store (not in a sandbox environment), false otherwise.
NOTE: An app is considered installed from the App Store if-and-only-if:
- the install was not in the sandbox environment
- the app has mobile provisioning
*/
public let isInstalledFromAppstore: Bool = {
let isSandbox =
Bundle.main.appStoreReceiptURL?.lastPathComponent == "sandboxReceipt"
let hasMobileProvisioning =
Bundle.main.path(forResource: "embedded", ofType: "mobileprovision") != nil
return !isSandbox && hasMobileProvisioning
}()

View File

@ -0,0 +1,49 @@
import Foundation
/**
Extend Error to be able to send a user readable message to `OSLog` (via `Logger`).
*/
extension Error {
/**
Returns the description of this error.
Useful for printing error names via `OSLog` (via `Logger`).
*/
public var description: String {
String(describing: self)
}
/**
Wraps an `Error` to a `ErrorWithTrace`, capturing the call site's file/line.
`withTrace` should only be called at the call site where the error is thrown (or sent through the continuation) like this:
```
throw MyErrorEnum.MyErrorValue(someArg).withTrace()
```
*/
public func withTrace(filePath: String = #file, line: Int = #line) -> ErrorWithTrace {
ErrorWithTrace(
file: (filePath as NSString).lastPathComponent,
line: line,
error: self
)
}
/**
Provides the ability to convert an `ErrorWithTrace` to it's underlying error or cast a general `Error` to a specific type.
Attempts to cast the current error as the requested target, returns the value if the cast is successful.
Then checks if the error is a `ErrorWithTrace`, attempting to cast its underlying `error` as the target, returning the value.
If either conversion fails, nil is returned.
*/
public func castError<Target : Error>(to type: Target.Type) -> Target? {
if let directCast = self as? Target {
return directCast
}
if let indirectCast = (self as? ErrorWithTrace)?.error as? Target {
return indirectCast
}
return nil
}
}

View File

@ -0,0 +1,37 @@
import Foundation
/**
A Specialized error that captures the file/line number of a thrown error to aid in debugging.
NOTE: This struct is not initialized directly but rather the `Error.withTrace()` extension should be used instead.
*/
public struct ErrorWithTrace : Error, CustomStringConvertible {
/**
The name of the file where `.withTrace()` conversion was called.
*/
let file: String
/**
The line within the file where `.withTrace()` conversion was called.
*/
let line: Int
/**
The underlying error.
*/
let error: Error
/**
Returns the decorated description of this error.
*/
public var description: String {
"⚠️ \(error.description) @ \(file):\(line)"
}
/**
Returns the decorated description, same as `description`.
*/
public var localizedDescription: String {
description
}
}

View File

@ -0,0 +1,37 @@
import Foundation
import os
extension Logger {
// Retrieve the bundle id so that it doesn't have be retrieved every time we build a Logger.
private static var subsystem = Bundle.main.bundleIdentifier!
/**
Returns a Logger instance for the given category and the current application bundle id.
Usage:
```
private struct FooView : View {
private static let logger: Logger = .loggerFor("FooView")
...
}
```
*/
public static func loggerFor(_ category: String) -> Logger {
Logger(subsystem: subsystem, category: category)
}
/**
Returns a Logger instance for the given type and the current application bundle id.
Usage:
```
private struct FooView : View {
private static let logger: Logger = .loggerFor(FooView.self)
...
}
```
*/
public static func loggerFor<Type>(_ type: Type.Type) -> Logger {
loggerFor(String(reflecting: type))
}
}

View File

@ -0,0 +1,19 @@
import Foundation
extension Comparable {
/**
Returns the value clamped to the provided closed range.
This is equivalent to: `min(max([VALUE], [LOWER_BOUND]), [UPPER_BOUND])`.
*/
public func clamped(to limits: ClosedRange<Self>) -> Self {
min(max(self, limits.lowerBound), limits.upperBound)
}
/**
Returns the value clamped to the provided half-open range.
This is equivalent to: `min(max([VALUE], [LOWER_BOUND]), [UPPER_BOUND] - 1)`.
*/
public func clamped(to limits: Range<Self>) -> Self where Self : Strideable {
min(max(self, limits.lowerBound), limits.upperBound.advanced(by: -1))
}
}

View File

@ -0,0 +1,54 @@
import Foundation
extension Double {
/**
The level of tolerance for Double "closeness" comparisons = 4 decimal places.
*/
public static let CLOSENESS_TOLERANCE = 0.0001
/**
Returns true if the current value and the provided one are closer than the "closeness" tolerance level, false otherwise.
NOTE: This is needed due to the way Double/Float instances are stored in memory on devices.
The number 0.5 may be stored as 0.49999999997 and if you attempt to compare these it will result in false.
*/
public func isCloseTo(_ other: Double) -> Bool {
abs(self - other) < Self.CLOSENESS_TOLERANCE
}
/**
Returns true if the current value is greater than the provided one, or if they are closer than the "closeness" tolerance level,
false otherwise.
NOTE: This is needed due to the way Double/Float instances are stored in memory on devices.
The number 0.5 may be stored as 0.49999999997 and if you attempt to compare these it will result in false.
*/
public func isCloseToOrGreaterThan(_ other: Double) -> Bool {
(self - other) > -Self.CLOSENESS_TOLERANCE
}
/**
Returns true if the current value is less than the provided one, or if they are closer than the "closeness" tolerance level, false otherwise.
NOTE: This is needed due to the way Double/Float instances are stored in memory on devices.
The number 0.5 may be stored as 0.49999999997 and if you attempt to compare these it will result in false.
*/
public func isCloseToOrLessThan(_ other: Double) -> Bool {
(other - self) > -Self.CLOSENESS_TOLERANCE
}
/**
Returns the value rounded upwards and converted to an `Int`.
*/
public var roundedUp: Int {
Int(rounded(.up))
}
/**
Returns the value rounded downwards and converted to an `Int`.
*/
public var roundedDown: Int {
Int(rounded(.down))
}
}