From 9cfd3382b4ccb14226fa12733e007165a53b6c17 Mon Sep 17 00:00:00 2001 From: Lukas Kollmer Date: Mon, 6 Jan 2025 14:08:36 +0100 Subject: [PATCH 01/11] tmp commit --- Package.swift | 14 +- Sources/SpeziFoundation/Misc/Calendar.swift | 342 ++++++++++++++++ .../Misc/ObjCExceptionHandling.swift | 55 +++ .../RangeReplaceableCollectionBuilder.swift | 72 ++++ .../Misc/SequenceExtensions.swift | 71 ++++ Sources/SpeziFoundation/Misc/SetBuilder.swift | 57 +++ .../SpeziFoundation/Misc/SortedArray.swift | 370 ++++++++++++++++++ Sources/SpeziFoundation/Misc/UUID.swift | 73 ++++ .../ObjCExceptionHandling.m | 17 + .../include/ObjCExceptionHandling.h | 20 + .../ExceptionHandlingTests.swift | 79 ++++ 11 files changed, 1167 insertions(+), 3 deletions(-) create mode 100644 Sources/SpeziFoundation/Misc/Calendar.swift create mode 100644 Sources/SpeziFoundation/Misc/ObjCExceptionHandling.swift create mode 100644 Sources/SpeziFoundation/Misc/RangeReplaceableCollectionBuilder.swift create mode 100644 Sources/SpeziFoundation/Misc/SequenceExtensions.swift create mode 100644 Sources/SpeziFoundation/Misc/SetBuilder.swift create mode 100644 Sources/SpeziFoundation/Misc/SortedArray.swift create mode 100644 Sources/SpeziFoundation/Misc/UUID.swift create mode 100644 Sources/SpeziFoundationObjC/ObjCExceptionHandling.m create mode 100644 Sources/SpeziFoundationObjC/include/ObjCExceptionHandling.h create mode 100644 Tests/SpeziFoundationTests/ExceptionHandlingTests.swift diff --git a/Package.swift b/Package.swift index b8aece2..20bff4e 100644 --- a/Package.swift +++ b/Package.swift @@ -23,22 +23,30 @@ let package = Package( .tvOS(.v17) ], products: [ - .library(name: "SpeziFoundation", targets: ["SpeziFoundation"]) + .library(name: "SpeziFoundation", targets: ["SpeziFoundation"]), + //.library(name: "SpeziFoundationObjC", targets: ["SpeziFoundationObjC"]) ], dependencies: [ - .package(url: "https://github.com/apple/swift-atomics.git", from: "1.2.0") + .package(url: "https://github.com/apple/swift-atomics.git", from: "1.2.0"), + .package(url: "https://github.com/apple/swift-algorithms", from: "1.2.0") ] + swiftLintPackage(), targets: [ .target( name: "SpeziFoundation", dependencies: [ - .product(name: "Atomics", package: "swift-atomics") + .product(name: "Atomics", package: "swift-atomics"), + .product(name: "Algorithms", package: "swift-algorithms"), + .target(name: "SpeziFoundationObjC") ], resources: [ .process("Resources") ], plugins: [] + swiftLintPlugin() ), + .target( + name: "SpeziFoundationObjC", + sources: nil + ), .testTarget( name: "SpeziFoundationTests", dependencies: [ diff --git a/Sources/SpeziFoundation/Misc/Calendar.swift b/Sources/SpeziFoundation/Misc/Calendar.swift new file mode 100644 index 0000000..ec1ea89 --- /dev/null +++ b/Sources/SpeziFoundation/Misc/Calendar.swift @@ -0,0 +1,342 @@ +// +// File.swift +// SpeziFoundation +// +// Created by Lukas Kollmer on 2024-12-17. +// + +import Foundation + + + + +// MARK: Date Ranges + + +extension Calendar { + /// Returns a `Date` which represents the start of the hour into which `date` falls. + public func startOfHour(for date: Date) -> Date { + var retval = date + for component in [Calendar.Component.minute, .second, .nanosecond] { + retval = self.date(bySettingComponentToZero: component, of: retval, adjustOtherComponents: false) + } + precondition([Component.year, .month, .day, .hour].allSatisfy { + self.component($0, from: retval) == self.component($0, from: date) + }) + return retval + } + + /// Returns a `Date` which represents the start of the next hour, relative to `date`. + public func startOfNextHour(for date: Date) -> Date { + let startOfHour = startOfHour(for: date) + return self.date(byAdding: .hour, value: 1, to: startOfHour)! + } + + /// Returns a `Range` representing the range of the hour into which `date` falls. + public func rangeOfHour(for date: Date) -> Range { + return startOfHour(for: date).. Date { + let startOfDay = self.startOfDay(for: date) + return self.date(byAdding: .day, value: 1, to: startOfDay)! + } + + /// Returns a `Date` which represents the start of the previous day, relative to `date`. + public func startOfPrevDay(for date: Date) -> Date { + let startOfDay = self.startOfDay(for: date) + return self.date(byAdding: .day, value: -1, to: startOfDay)! + } + + /// Returns a `Range` representing the range of the day into which `date` falls. + public func rangeOfDay(for date: Date) -> Range { + return startOfDay(for: date).. Date { + let date = self.startOfDay(for: date) + var weekday = self.component(.weekday, from: date) + // We have to adjust the weekday to avoid going into the next week instead. + // The issue here is that firstWeekday can be larger than the current weekday, + // in which case `weekdayDiff` is a) negative and b) incorrect, even if we look at the absolute value. + // Example: We're in the german locale (firstWeekday = Monday) and the day represents a Sunday. + // Since `Calendar`'s weekday numbers start at Sunday=1, we'd calculate diff = sunday - firstWeekday = sunday - monday = 1 - 2 = -1. + // But what we actually want is diff = sunday - monday = 6 + if weekday < self.firstWeekday { + weekday += self.weekdaySymbols.count + } + let weekdayDiff = weekday - self.firstWeekday + return self.date(byAdding: .weekday, value: -weekdayDiff, to: date)! + } + + /// Returns a `Date` which represents the start of the next week, relative to `date`. + public func startOfNextWeek(for date: Date) -> Date { + let start = startOfWeek(for: date) + return self.date(byAdding: .weekOfYear, value: 1, to: start)! + } + + /// Returns a `Range` representing the range of the week into which `date` falls. + public func rangeOfWeek(for date: Date) -> Range { + return startOfWeek(for: date).. Date { + var adjustedDate = self.startOfDay(for: date) + adjustedDate = self.date(bySetting: .day, value: 1, of: adjustedDate)! + if adjustedDate <= date { + precondition(self.component(.day, from: adjustedDate) == 1) + return adjustedDate + } else { + let startOfMonth = self.date(byAdding: .month, value: -1, to: adjustedDate)! + precondition(self.component(.day, from: startOfMonth) == 1) + return startOfMonth + } + } + + /// Returns a `Date` which represents the start of the next month, relative to `date`. + public func startOfNextMonth(for date: Date) -> Date { + let start = startOfMonth(for: date) + return self.date(byAdding: .month, value: 1, to: start)! + } + + + /// Returns the exclusive range from the beginning of the month into which `date` falls, to the beginning of the next + public func rangeOfMonth(for date: Date) -> Range { + let start = startOfMonth(for: date) + let end = startOfNextMonth(for: start) + precondition(startOfNextMonth(for: start) == startOfNextMonth(for: date)) + return start.. Date { + var adjustedDate = startOfMonth(for: date) + precondition(adjustedDate <= date) + adjustedDate = self.date(bySetting: .month, value: 1, of: adjustedDate)! + if adjustedDate > date { + // Setting the month to 1 made the date larger, i.e. moved it one year ahead :/ + adjustedDate = self.date(byAdding: .year, value: -1, to: adjustedDate)! + return adjustedDate + } + + precondition({ () -> Bool in + let components = self.dateComponents([.year, .month, .day, .hour, .minute, .second, .nanosecond], from: adjustedDate) + return components.year == self.component(.year, from: date) && components.month == 1 && components.day == 1 + && components.hour == 0 && components.minute == 0 && components.second == 0 && components.nanosecond == 0 + }()) + return adjustedDate + } + + + /// Returns a `Date` which represents the start of the previous year, relative to `date`. + public func startOfPrevYear(for date: Date) -> Date { + return self.date(byAdding: .year, value: -1, to: startOfYear(for: date))! + } + + /// Returns a `Date` which represents the start of the next year, relative to `date`. + public func startOfNextYear(for date: Date) -> Date { + return self.date(byAdding: .year, value: 1, to: startOfYear(for: date))! + } + + /// Returns a `Range` representing the range of the year into which `date` falls. + public func rangeOfYear(for date: Date) -> Range { + return startOfYear(for: date).. Date + ) -> Int { + guard startDate <= endDate else { + return _countDistinctNumberOfComponentUnits( + from: endDate, + to: startDate, + for: component, + startOfComponentFn: startOfComponentFn + ) + } + if startDate == endDate { + return 1 + } else if self.isDate(startDate, equalTo: endDate, toGranularity: component) { + return 1 + } else { + let diff = self.dateComponents( + [component], + from: startOfComponentFn(startDate), + to: startOfComponentFn(endDate) + ) + return diff[component]! + 1 + } + } + + + public func countDistinctYears(from startDate: Date, to endDate: Date) -> Int { + _countDistinctNumberOfComponentUnits( + from: startDate, + to: endDate, + for: .year, + startOfComponentFn: startOfYear + ) + } + + /// Returns the rounded up number of months between the two dates. + /// E.g., if the first date is 25.02 and the second is 12.04, this would return 3. + public func countDistinctMonths(from startDate: Date, to endDate: Date) -> Int { + _countDistinctNumberOfComponentUnits( + from: startDate, + to: endDate, + for: .month, + startOfComponentFn: startOfMonth + ) + } + + public func countDistinctWeeks(from startDate: Date, to endDate: Date) -> Int { + _countDistinctNumberOfComponentUnits( + from: startDate, + to: endDate, + for: .weekOfYear, + startOfComponentFn: startOfWeek + ) + } + + + public func countDistinctDays(from startDate: Date, to endDate: Date) -> Int { + _countDistinctNumberOfComponentUnits( + from: startDate, + to: endDate, + for: .day, + startOfComponentFn: startOfDay + ) + } + + + public func countDistinctHours(from startDate: Date, to endDate: Date) -> Int { + _countDistinctNumberOfComponentUnits( + from: startDate, + to: endDate, + for: .hour, + startOfComponentFn: startOfHour + ) + } + + + public func offsetInDays(from startDate: Date, to endDate: Date) -> Int { + guard !isDate(startDate, inSameDayAs: endDate) else { + return 0 + } + if startDate < endDate { + return countDistinctDays(from: startDate, to: endDate) - 1 + } else { + return -(countDistinctDays(from: endDate, to: startDate) - 1) + } + } + + + public func dateIsSunday(_ date: Date) -> Bool { + // NSCalendar starts weeks on sundays, ?regardless of locale? + return self.component(.weekday, from: date) == self.firstWeekday + } + + + public func numberOfDaysInMonth(for date: Date) -> Int { + return self.range(of: .day, in: .month, for: date)!.count + } +} + + + + +// MARK: Date Components + + +extension Calendar { + public func date(bySettingComponentToZero component: Component, of date: Date, adjustOtherComponents: Bool) -> Date { + if adjustOtherComponents { + return self.date(bySetting: component, value: 0, of: date)! + } else { + let compValue = self.component(component, from: date) + return self.date(byAdding: component, value: -compValue, to: date, wrappingComponents: true)! + } + } + + + + public func date(bySetting component: Component, of date: Date, to value: Int, adjustOtherComponents: Bool) -> Date { + if adjustOtherComponents { + return self.date(bySetting: component, value: value, of: date)! + } else { + let compValue = self.component(component, from: date) + let diff = value - compValue + return self.date(byAdding: component, value: diff, to: date, wrappingComponents: true)! + } + } +} + + + + +extension DateComponents { + public subscript(component: Calendar.Component) -> Int? { + switch component { + case .era: + return self.era + case .year: + return self.year + case .month: + return self.month + case .day: + return self.day + case .hour: + return self.hour + case .minute: + return self.minute + case .second: + return self.second + case .weekday: + return self.weekday + case .weekdayOrdinal: + return self.weekdayOrdinal + case .quarter: + return self.quarter + case .weekOfMonth: + return self.weekOfMonth + case .weekOfYear: + return self.weekOfYear + case .yearForWeekOfYear: + return self.yearForWeekOfYear + case .nanosecond: + return self.nanosecond + case .dayOfYear: + if #available(iOS 18, macOS 15, *) { + return self.dayOfYear + } else { + // The crash here is fine, since the enum case itself is also only available on iOS 18+ + fatalError() + } + case .calendar, .timeZone, .isLeapMonth: + fatalError("not supported") // different type (not an int) :/ + @unknown default: + return nil + } + } +} + + diff --git a/Sources/SpeziFoundation/Misc/ObjCExceptionHandling.swift b/Sources/SpeziFoundation/Misc/ObjCExceptionHandling.swift new file mode 100644 index 0000000..4f8fcb1 --- /dev/null +++ b/Sources/SpeziFoundation/Misc/ObjCExceptionHandling.swift @@ -0,0 +1,55 @@ +// +// File.swift +// SpeziFoundation +// +// Created by Lukas Kollmer on 2024-12-25. +// + +import Foundation +import SpeziFoundationObjC + + +/// A `Swift.Error` wrapping around an `NSException`. +public struct CaughtNSException: Error, LocalizedError, @unchecked Sendable { + public let exception: NSException + + public var errorDescription: String? { + return "\(Self.self): \(exception.description)" + } +} + + +/// Invokes the specified closure and handles any potentially raised Objective-C `NSException` objects, +/// rethrowing them wrapped as ``CaughtNSException`` errors. Swift errors thrown in the closure will also be caught and rethrown. +/// - parameter block: the closure that should be invoked +/// - returns: the return value of `block`, provided that no errors or exceptions were thrown +/// - throws: if the block throws an `NSException` or a Swift `Error`, it will be rethrown. +public func catchingNSException(_ block: () throws -> T) throws -> T { + var retval: T? + var caughtSwiftError: (any Swift.Error)? + let caughtNSException = InvokeBlockCatchingNSExceptionIfThrown { + do { + retval = try block() + } catch { + caughtSwiftError = error + } + } + + switch (retval, caughtNSException, caughtSwiftError) { + case (.some(let retval), .none, .none): + return retval + case (.none, .some(let exc), .none): + throw CaughtNSException(exception: exc) + case (.none, .none, .some(let error)): + throw error + default: + // unreachable + fatalError(""" + Invalid state. Exactly one of retval, caughtNSException, and caughtSwiftError should be non-nil. + retval: \(String(describing: retval)) + caughtNSException: \(String(describing: caughtNSException)) + caughtSwiftError: \(String(describing: caughtSwiftError)) + """ + ) + } +} diff --git a/Sources/SpeziFoundation/Misc/RangeReplaceableCollectionBuilder.swift b/Sources/SpeziFoundation/Misc/RangeReplaceableCollectionBuilder.swift new file mode 100644 index 0000000..229f8c1 --- /dev/null +++ b/Sources/SpeziFoundation/Misc/RangeReplaceableCollectionBuilder.swift @@ -0,0 +1,72 @@ +// +// File.swift +// SpeziFoundation +// +// Created by Lukas Kollmer on 2024-11-30. +// + + +public typealias ArrayBuilder = RangeReplaceableCollectionBuilder<[T]> + +public extension Array { + init(@ArrayBuilder build: () -> Self) { + self = build() + } +} + + +// MARK: RangeReplaceableCollectionBuilder + +@resultBuilder +public enum RangeReplaceableCollectionBuilder { + public typealias Element = C.Element + public static func make(@RangeReplaceableCollectionBuilder build: () -> C) -> C { + build() + } +} + +public extension RangeReplaceableCollectionBuilder { + static func buildExpression(_ expression: Element) -> C { + C(CollectionOfOne(expression)) + } + + static func buildExpression(_ expression: some Sequence) -> C { + C(expression) + } + + static func buildOptional(_ expression: C?) -> C { + expression ?? C() + } + + static func buildOptional(_ expression: (some Sequence)?) -> C { + expression.map(C.init) ?? C() + } + + static func buildEither(first expression: some Sequence) -> C { + C(expression) + } + + static func buildEither(second expression: some Sequence) -> C { + C(expression) + } + + static func buildPartialBlock(first: some Sequence) -> C { + C(first) + } + + static func buildPartialBlock(accumulated: some Sequence, next: some Sequence) -> C { + C(accumulated) + C(next) + } + + static func buildBlock() -> C { + C() + } + + static func buildBlock(_ components: Element...) -> C { + C(components) + } + + static func buildArray(_ components: [some Sequence]) -> C { + components.reduce(into: C()) { $0.append(contentsOf: $1) } + } +} diff --git a/Sources/SpeziFoundation/Misc/SequenceExtensions.swift b/Sources/SpeziFoundation/Misc/SequenceExtensions.swift new file mode 100644 index 0000000..6ec3b2d --- /dev/null +++ b/Sources/SpeziFoundation/Misc/SequenceExtensions.swift @@ -0,0 +1,71 @@ +// +// File.swift +// SpeziFoundation +// +// Created by Lukas Kollmer on 2024-12-27. +// + +import Foundation +import Algorithms + + +extension Sequence { + /// Maps a `Sequence` into a `Set`. + /// Compared to instead mapping the sequence into an Array (the default `map` function's return type) and then constructing a `Set` from that, + /// this implementation can offer improved performance, since the intermediate Array is skipped. + /// - Returns: a `Set` containing the results of applying `transform` to each element in the sequence. + /// - Throws: If `transform` throws. + public func mapIntoSet(_ transform: (Element) throws -> NewElement) rethrows -> Set { + var retval = Set() + for element in self { + retval.insert(try transform(element)) + } + return retval + } +} + + + +extension Sequence { + public func lk_isSorted(by areInIncreasingOrder: (Element, Element) -> Bool) -> Bool { + // ISSUE HERE: if we have a collection containing duplicate objects (eg: `[0, 0]`), and we want to check if it's sorted + // properly (passing `{ $0 < $1 }`), that would incorrectly return false, because not all elements are ordered strictly ascending. + // BUT: were we to sort the collection using the specified comparator, the sort result would be equivalent to the collection + // itself. Meaning that we should consider it properly sorted. + // We achieve this by reversing the comparator operands, and negating the result. + return self.adjacentPairs().allSatisfy { a, b in + !areInIncreasingOrder(b, a) + } + } + + + /// Returns whether the sequence is sorted ascending, w.r.t. the specified key path. + /// - Note: if two or more adjacent elements in the sequence are equal to each other, the sequence will still be considered sorted. + public func lk_isSorted(by keyPath: KeyPath) -> Bool { + return self.adjacentPairs().allSatisfy { + $0[keyPath: keyPath] <= $1[keyPath: keyPath] + } + } + + public func lk_isSortedStrictlyAscending(by keyPath: KeyPath) -> Bool { + return self.adjacentPairs().allSatisfy { + $0[keyPath: keyPath] < $1[keyPath: keyPath] + } + } +} + + + + +extension RangeReplaceableCollection { + public mutating func remove(at indices: some Sequence) { + for idx in indices.sorted().reversed() { + self.remove(at: idx) + } + } + + + public mutating func removeAll(where predicate: (Element) throws -> Bool) rethrows { + self = try self.filter { try !predicate($0) } + } +} diff --git a/Sources/SpeziFoundation/Misc/SetBuilder.swift b/Sources/SpeziFoundation/Misc/SetBuilder.swift new file mode 100644 index 0000000..12bca48 --- /dev/null +++ b/Sources/SpeziFoundation/Misc/SetBuilder.swift @@ -0,0 +1,57 @@ +// +// File.swift +// SpeziFoundation +// +// Created by Lukas Kollmer on 2024-12-20. +// + + +@resultBuilder +public enum SetBuilder { + public static func make(@SetBuilder build: () -> Set) -> Set { + build() + } +} + +public extension SetBuilder { + static func buildExpression(_ expression: Element) -> Set { + [expression] + } + + static func buildExpression(_ expression: some Sequence) -> Set { + Set(expression) + } + + static func buildOptional(_ expression: Set?) -> Set { + expression ?? Set() + } + + static func buildOptional(_ expression: (some Sequence)?) -> Set { + expression.map(Set.init) ?? Set() + } + + static func buildEither(first expression: some Sequence) -> Set { + Set(expression) + } + + static func buildEither(second expression: some Sequence) -> Set { + Set(expression) + } + + static func buildPartialBlock(first: some Sequence) -> Set { + Set(first) + } + + static func buildPartialBlock(accumulated: some Sequence, next: some Sequence) -> Set { + Set(accumulated).union(next) + } + + static func buildBlock(_ components: Element...) -> Set { + Set(components) + } + + static func buildArray(_ components: [some Sequence]) -> Set { + components.reduce(into: Set()) { $0.formUnion($1) } + } +} + diff --git a/Sources/SpeziFoundation/Misc/SortedArray.swift b/Sources/SpeziFoundation/Misc/SortedArray.swift new file mode 100644 index 0000000..123e88a --- /dev/null +++ b/Sources/SpeziFoundation/Misc/SortedArray.swift @@ -0,0 +1,370 @@ +// +// File.swift +// SpeziFoundation +// +// Created by Lukas Kollmer on 2024-12-27. +// + +import Foundation + + + +/// An array that keeps its elements sorted in a way that satisfies a user-provided total order. +public struct SortedArray { // TODO SortedArray? OrderedArray? + public typealias Comparator = (Element, Element) -> Bool + public typealias Storage = Array + + /// The total order used to compare elements. + /// When comparing two elements, it should return `true` if their ordering satisfies the relation, otherwise `false`. + public let areInIncreasingOrder: Comparator + + private var storage: Storage { + didSet { if shouldAssertInvariantAfterMutations { assertInvariant() } } + } + + /// Whether the array should assert its invariant after every mutation + var shouldAssertInvariantAfterMutations = true { + didSet { + if shouldAssertInvariantAfterMutations && !oldValue { + assertInvariant() + } + } + } + + + public init() where Element: Comparable { + self.storage = [] + self.areInIncreasingOrder = { $0 < $1 } + } + + public init(areInIncreasingOrder: @escaping Comparator) { + self.storage = [] + self.areInIncreasingOrder = areInIncreasingOrder + } + + public init(keyPath: KeyPath) { + self.storage = [] + self.areInIncreasingOrder = { $0[keyPath: keyPath] < $1[keyPath: keyPath] } + } + + public init(_ sequence: some Sequence, areInIncreasingOrder: @escaping Comparator) { + self.storage = sequence.sorted(by: areInIncreasingOrder) + self.areInIncreasingOrder = areInIncreasingOrder + } + + + /// Performs mutations on the array as a single transaction, with invariant checks disabled for the duration of the mutations + mutating func withInvariantCheckingTemporarilyDisabled(_ block: (inout SortedArray) -> Result) -> Result { + let prevValue = shouldAssertInvariantAfterMutations + shouldAssertInvariantAfterMutations = false + let retval = block(&self) + shouldAssertInvariantAfterMutations = prevValue + return retval + } + + +// func lk_intoArray() -> [Element] { +// return self.storage +// } +} + + +//extension Array { +// init(_ other: SortedArray) { +// self = other.lk_intoArray() +// } +//} + + +extension SortedArray: CustomStringConvertible, CustomDebugStringConvertible { + public var description: String { + storage.description + } + + public var debugDescription: String { + storage.debugDescription + } +} + + +// MARK: RandomAccessCollection + +extension SortedArray: RandomAccessCollection { + public typealias Index = Storage.Index + + public var startIndex: Index { + storage.startIndex + } + public var endIndex: Index { + storage.endIndex + } + + public func index(before idx: Index) -> Index { + storage.index(before: idx) + } + + public func index(after idx: Index) -> Index { + storage.index(after: idx) + } + + public subscript(position: Index) -> Element { + storage[position] + } + + public func _customIndexOfEquatableElement(_ element: Element) -> Index?? { + return Optional.some(firstIndex(of: element)) + } +} + + + +// MARK: Equatable, Hashable + +extension SortedArray: Equatable where Element: Equatable { + /// - Note: This ignores the comparator!!! + public static func == (lhs: SortedArray, rhs: SortedArray) -> Bool { + return lhs.storage == rhs.storage + } +} + + +extension SortedArray: Hashable where Element: Hashable { + public func hash(into hasher: inout Hasher) { + hasher.combine(storage) + } +} + + + + + + +// MARK: Invariant Checking + +extension SortedArray { + /// Assert that the array is sorted according to its invariant. + public func assertInvariant() { + precondition(self.lk_isSorted(by: areInIncreasingOrder)) + } +} + + + + + +// MARK: Insertion, Removal, Mutation, Other + +extension SortedArray { + @discardableResult + public mutating func insert(_ element: Element) -> Index { + let insertionIdx: Index + switch search(for: element) { + case .found(let idx): + insertionIdx = idx + case .notFound(let idx): + insertionIdx = idx + } + storage.insert(element, at: insertionIdx) + return insertionIdx + } + + + public mutating func unsafelyInsert(_ element: Element, at idx: Index) { + storage.insert(element, at: idx) + } + + + public mutating func insert(contentsOf sequence: some Sequence) { + withInvariantCheckingTemporarilyDisabled { `self` in + for element in sequence { + self.insert(element) + } + } + } + + + public mutating func insert2(contentsOf sequence: some Sequence) -> Set { // TODO better name! + return withInvariantCheckingTemporarilyDisabled { `self` in + var insertionIndices = Set() + for element in sequence { + let idx = self.insert(element) + // since the insertions won't necessarily happen in order, we need to adjust the indices as we insert. + insertionIndices = insertionIndices.mapIntoSet { + $0 < idx ? $0 : self.index(after: $0) + } + insertionIndices.insert(idx) + } + return insertionIndices + } + } + + + @discardableResult + public mutating func remove(at index: Index) -> Element { + return storage.remove(at: index) + } + + + /// Removes all occurrences of the objects in `elements` from the array. + /// - returns: the indices of the removed objects + @discardableResult + public mutating func remove(contentsOf elements: some Sequence) -> [Index] where Element: Equatable { + return withInvariantCheckingTemporarilyDisabled { `self` in + let indices = elements.compactMap { element in + // In the case of removals, we also perform a cringe O(n) search for the to-be-removed element, + // if the default binary search didn't yield a result. + // This is requied because we might be removing an element whose "correct" position in the + // array has changed, in which case we would not necessarily be able to find it via the normal search. + self.firstIndex(of: element) ?? self.indices.first { self[$0] == element } + } + self.storage.remove(at: indices) + return indices + } + } + + + /// Removes from the array all elements which match the predicate + @discardableResult + public mutating func removeAll(where predicate: (Element) -> Bool) -> [Index] { + return withInvariantCheckingTemporarilyDisabled { `self` in + let indices = self.enumerated().compactMap { predicate($0.element) ? $0.offset : nil } + self.storage.remove(at: indices) + return indices + } + } + + + public mutating func removeAll(keepingCapacity: Bool = false) { + storage.removeAll(keepingCapacity: keepingCapacity) + } + + + public mutating func unsafelyMutate(at index: Index, with transform: (inout Element) throws -> Void) rethrows { + try transform(&storage[index]) + } + + + + public func firstIndex(of element: Element) -> Index? { + switch search(for: element) { + case .found(let idx): + return idx + case .notFound: + return nil + } + } + + public mutating func removeFirstOccurrence(of element: Element) -> Index? { + if let idx = firstIndex(of: element) { + remove(at: idx) + return idx + } else { + return nil + } + } + + + public func contains(_ element: Element) -> Bool { + return firstIndex(of: element) != nil + } +} + + + +// MARK: Position Finding etc + +extension SortedArray { + fileprivate func compare(_ lhs: Element, _ rhs: Element) -> ComparisonResult { + if areInIncreasingOrder(lhs, rhs) { + return .orderedAscending + } else if areInIncreasingOrder(rhs, lhs) { + return .orderedDescending + } else { + return .orderedSame + } + } + + public typealias SearchResult = BinarySearchIndexResult + + + public func search(for element: Element) -> SearchResult { + return lk_binarySearchFirstIndex { + switch compare(element, $0) { + case .orderedSame: + return .match + case .orderedAscending: + return .continueLeft + case .orderedDescending: + return .continueRight + } + } + } +} + + + + +// MARK: Binary Search + + +public enum BinarySearchIndexResult { + case found(Index) + case notFound(Index) + + public var index: Index { + switch self { + case .found(let index), .notFound(let index): + return index + } + } +} + +extension BinarySearchIndexResult: Equatable where Index: Equatable {} +extension BinarySearchIndexResult: Hashable where Index: Hashable {} + + +public enum BinarySearchComparisonResult { + case match + case continueLeft + case continueRight +} + + +extension Collection { + /// - parameter compare: closure that gets called with an element of the collection to determine, whether the binary search algorithm should go left/right, or has already found its target. + /// The closure should return `.orderedSame` if the element passed to it matches the search destination, `.orderedAscending` if the search should continue to the left, + /// and `.orderedDescending` if it should continue to the right. + /// E.g., when looking for a position for an element `x`, the closure should perform a `compare(x, $0)`. + public func lk_binarySearchFirstIndex(using compare: (Element) -> BinarySearchComparisonResult) -> BinarySearchIndexResult { + return lk_binarySearchFirstIndex(in: startIndex.., using compare: (Element) -> BinarySearchComparisonResult) -> BinarySearchIndexResult { + guard let middle: Self.Index = lk_middleIndex(of: range) else { + return .notFound(range.upperBound) + } + switch compare(self[middle]) { + case .continueLeft: // lhs < rhs + return lk_binarySearchFirstIndex(in: range.lowerBound.. rhs + return lk_binarySearchFirstIndex(in: index(after: middle)..) -> Index? { + guard !range.isEmpty else { + return nil + } + let distance = self.distance(from: range.lowerBound, to: range.upperBound) + let resultIdx = self.index(range.lowerBound, offsetBy: distance / 2) + return resultIdx + } +} + diff --git a/Sources/SpeziFoundation/Misc/UUID.swift b/Sources/SpeziFoundation/Misc/UUID.swift new file mode 100644 index 0000000..f16b8db --- /dev/null +++ b/Sources/SpeziFoundation/Misc/UUID.swift @@ -0,0 +1,73 @@ +// +// File.swift +// SpeziFoundation +// +// Created by Lukas Kollmer on 2024-12-27. +// + +import Foundation + + + +//extension UUID: Comparable { +//// @ThreadSafe2 private(set) static var totalNumComparisons: UInt64 = 0 +// +// +// /// Stolen from https://github.com/apple/swift-foundation/blob/71dfec2bc4a9b48f3575af4ceca7b6af65198fa9/Sources/FoundationEssentials/UUID.swift#L128 +// /// Does not necessarily produce meaningful results, the main point here is having some kind of stable (ie, deterministic) sorting on UUIDs. +// public static func < (lhs: UUID, rhs: UUID) -> Bool { +//// Self.totalNumComparisons += 1 +//// let result2 = measure(addDurationTo: &mTotalTimeSpentComparingUUIDs_me) { cmp_lt_me(lhs: lhs, rhs: rhs) } +//// let result1 = measure(addDurationTo: &mTotalTimeSpentComparingUUIDs_apple) { cmp_lt_apple(lhs: lhs, rhs: rhs) } +//// precondition(result1 == result2) +//// return result1 +// // TODO it seems that both of these implementations (_apple and _me) are functionally equivalent? +// // (i had the check above on for like millions of comparisons and it never failed.) +// // but somehow the _me version is way(!) faster than the _apple version??? +// return cmp_lt_me(lhs: lhs, rhs: rhs) +// } +// +// +// @_transparent +// private static func cmp_lt_apple(lhs: UUID, rhs: UUID) -> Bool { +// var leftUUID = lhs.uuid +// var rightUUID = rhs.uuid +// var result: Int = 0 +// var diff: Int = 0 +// withUnsafeBytes(of: &leftUUID) { leftPtr in +// withUnsafeBytes(of: &rightUUID) { rightPtr in +// for offset in (0 ..< MemoryLayout.size).reversed() { +// diff = Int(leftPtr.load(fromByteOffset: offset, as: UInt8.self)) - +// Int(rightPtr.load(fromByteOffset: offset, as: UInt8.self)) +// // Constant time, no branching equivalent of +// // if (diff != 0) { +// // result = diff; +// // } +// result = (result & (((diff - 1) & ~diff) >> 8)) | diff +// } +// } +// } +// return result < 0 +// } +// +// +// @_transparent +// private static func cmp_lt_me(lhs: UUID, rhs: UUID) -> Bool { +// return withUnsafeBytes(of: lhs.uuid) { lhsUUID -> Bool in +// return withUnsafeBytes(of: rhs.uuid) { rhsUUID -> Bool in +// for idx in 0...size { +// let lhs = lhsUUID[idx] +// let rhs = rhsUUID[idx] +// if lhs < rhs { +// return true +// } else if lhs > rhs { +// return false +// } else { +// continue +// } +// } +// return false // all bytes are equal +// } +// } +// } +//} diff --git a/Sources/SpeziFoundationObjC/ObjCExceptionHandling.m b/Sources/SpeziFoundationObjC/ObjCExceptionHandling.m new file mode 100644 index 0000000..01a20ec --- /dev/null +++ b/Sources/SpeziFoundationObjC/ObjCExceptionHandling.m @@ -0,0 +1,17 @@ +// +// ObjCExceptionHandling.m +// SpeziFoundation +// +// Created by Lukas Kollmer on 2024-12-25. +// + +#import "ObjCExceptionHandling.h" + +NSException *_Nullable InvokeBlockCatchingNSExceptionIfThrown(NS_NOESCAPE void(^block)(void)) { + @try { + block(); + return nil; + } @catch (NSException *exception) { + return exception; + } +} diff --git a/Sources/SpeziFoundationObjC/include/ObjCExceptionHandling.h b/Sources/SpeziFoundationObjC/include/ObjCExceptionHandling.h new file mode 100644 index 0000000..39d8316 --- /dev/null +++ b/Sources/SpeziFoundationObjC/include/ObjCExceptionHandling.h @@ -0,0 +1,20 @@ +// +// ObjCExceptionHandling.h +// SpeziFoundation +// +// Created by Lukas Kollmer on 2024-12-25. +// + +#ifndef ObjCExceptionHandling_h +#define ObjCExceptionHandling_h + +#include +#include + +NS_ASSUME_NONNULL_BEGIN + +NSException *_Nullable InvokeBlockCatchingNSExceptionIfThrown(NS_NOESCAPE void(^block)(void)); + +NS_ASSUME_NONNULL_END + +#endif /* ObjCExceptionHandling_h */ diff --git a/Tests/SpeziFoundationTests/ExceptionHandlingTests.swift b/Tests/SpeziFoundationTests/ExceptionHandlingTests.swift new file mode 100644 index 0000000..b049f5c --- /dev/null +++ b/Tests/SpeziFoundationTests/ExceptionHandlingTests.swift @@ -0,0 +1,79 @@ +// +// This source file is part of the Stanford Spezi open-source project +// +// SPDX-FileCopyrightText: 2024 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + + +import SpeziFoundation +import XCTest + + +final class ExceptionHandlingTests: XCTestCase { + func testNothingThrown() { + do { + let value = try catchingNSException { + 5 + } + XCTAssertEqual(value, 5) + } catch { + XCTFail("Threw an error :/") + } + } + + func testNSExceptionThrown1() { + do { + let _: Void = try catchingNSException { + let string = "Hello there :)" as NSString + _ = string.substring(with: NSRange(location: 12, length: 7)) + } + XCTFail("Didn't throw an error :/") + } catch { + guard let error = error as? CaughtNSException else { + XCTFail("Not a \(CaughtNSException.self)") + return + } + XCTAssertEqual(error.exception.name, .invalidArgumentException) + XCTAssertEqual(error.exception.reason, "-[__NSCFString substringWithRange:]: Range {12, 7} out of bounds; string length 14") + } + } + + func testNSExceptionThrown2() { + let exceptionName = NSExceptionName("CustomExceptionName") + let exceptionReason = "There was a non-recoverable issue" + do { + let _: Void = try catchingNSException { + NSException(name: exceptionName, reason: exceptionReason).raise() + fatalError() // unreachable, but the compiler doesn't know about this, because `-[NSException raise]` isn't annotated as being oneway... + } + XCTFail("Didn't throw an error :/") + } catch { + guard let error = error as? CaughtNSException else { + XCTFail("Not a \(CaughtNSException.self)") + return + } + XCTAssertEqual(error.exception.name, exceptionName) + XCTAssertEqual(error.exception.reason, exceptionReason) + } + } + + func testSwiftErrorThrown() { + enum TestError: Error, Equatable { + case abc + } + do { + let _: Void = try catchingNSException { + throw TestError.abc + } + XCTFail("Didn't throw an error :/") + } catch { + guard let error = error as? TestError else { + XCTFail("Not a \(TestError.self)") + return + } + XCTAssertEqual(error, TestError.abc) + } + } +} From fb256e521ebc3bf71945fd2fd3cbe67789ebc6d9 Mon Sep 17 00:00:00 2001 From: Lukas Kollmer Date: Sat, 11 Jan 2025 21:03:05 +0100 Subject: [PATCH 02/11] finish implementation, write tests --- Package.swift | 3 +- .../Collection Builders/ArrayBuilder.swift | 18 + .../RangeReplaceableCollectionBuilder.swift | 68 ++++ .../Collection Builders/SetBuilder.swift | 73 ++++ .../SpeziFoundation/Misc/BinarySearch.swift | 81 ++++ Sources/SpeziFoundation/Misc/Calendar.swift | 256 +++++++----- .../Misc/ObjCExceptionHandling.swift | 14 +- .../SpeziFoundation/Misc/OrderedArray.swift | 243 ++++++++++++ .../RangeReplaceableCollectionBuilder.swift | 72 ---- .../Misc/SequenceExtensions.swift | 39 +- Sources/SpeziFoundation/Misc/SetBuilder.swift | 57 --- .../SpeziFoundation/Misc/SortedArray.swift | 370 ------------------ Sources/SpeziFoundation/Misc/UUID.swift | 73 ---- .../ObjCExceptionHandling.m | 7 +- .../include/ObjCExceptionHandling.h | 9 +- .../CalendarExtensionsTests.swift | 251 ++++++++++++ .../CollectionBuildersTests.swift | 138 +++++++ .../ExceptionHandlingTests.swift | 6 +- .../OrderedArrayTests.swift | 92 +++++ 19 files changed, 1150 insertions(+), 720 deletions(-) create mode 100644 Sources/SpeziFoundation/Collection Builders/ArrayBuilder.swift create mode 100644 Sources/SpeziFoundation/Collection Builders/RangeReplaceableCollectionBuilder.swift create mode 100644 Sources/SpeziFoundation/Collection Builders/SetBuilder.swift create mode 100644 Sources/SpeziFoundation/Misc/BinarySearch.swift create mode 100644 Sources/SpeziFoundation/Misc/OrderedArray.swift delete mode 100644 Sources/SpeziFoundation/Misc/RangeReplaceableCollectionBuilder.swift delete mode 100644 Sources/SpeziFoundation/Misc/SetBuilder.swift delete mode 100644 Sources/SpeziFoundation/Misc/SortedArray.swift delete mode 100644 Sources/SpeziFoundation/Misc/UUID.swift create mode 100644 Tests/SpeziFoundationTests/CalendarExtensionsTests.swift create mode 100644 Tests/SpeziFoundationTests/CollectionBuildersTests.swift create mode 100644 Tests/SpeziFoundationTests/OrderedArrayTests.swift diff --git a/Package.swift b/Package.swift index 20bff4e..71e0a83 100644 --- a/Package.swift +++ b/Package.swift @@ -23,8 +23,7 @@ let package = Package( .tvOS(.v17) ], products: [ - .library(name: "SpeziFoundation", targets: ["SpeziFoundation"]), - //.library(name: "SpeziFoundationObjC", targets: ["SpeziFoundationObjC"]) + .library(name: "SpeziFoundation", targets: ["SpeziFoundation"]) ], dependencies: [ .package(url: "https://github.com/apple/swift-atomics.git", from: "1.2.0"), diff --git a/Sources/SpeziFoundation/Collection Builders/ArrayBuilder.swift b/Sources/SpeziFoundation/Collection Builders/ArrayBuilder.swift new file mode 100644 index 0000000..b652cb1 --- /dev/null +++ b/Sources/SpeziFoundation/Collection Builders/ArrayBuilder.swift @@ -0,0 +1,18 @@ +// +// This source file is part of the Stanford Spezi open-source project +// +// SPDX-FileCopyrightText: 2024 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + + +/// An `ArrayBuilder` is a result builder that constructs an `Array`. +public typealias ArrayBuilder = RangeReplaceableCollectionBuilder<[T]> + +extension Array { + /// Constructs a new array, using a result builder. + public init(@ArrayBuilder build: () -> Self) { + self = build() + } +} diff --git a/Sources/SpeziFoundation/Collection Builders/RangeReplaceableCollectionBuilder.swift b/Sources/SpeziFoundation/Collection Builders/RangeReplaceableCollectionBuilder.swift new file mode 100644 index 0000000..57e2fed --- /dev/null +++ b/Sources/SpeziFoundation/Collection Builders/RangeReplaceableCollectionBuilder.swift @@ -0,0 +1,68 @@ +// +// This source file is part of the Stanford Spezi open-source project +// +// SPDX-FileCopyrightText: 2024 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + + +/// A result builder that constructs an instance of a `RangeReplaceableCollection`. +@resultBuilder +public enum RangeReplaceableCollectionBuilder { + /// The `Element` of the `RangeReplaceableCollection` that will be built up. + public typealias Element = C.Element +} + + +extension RangeReplaceableCollectionBuilder { + /// :nodoc: + public static func buildExpression(_ expression: Element) -> C { + C(CollectionOfOne(expression)) + } + + /// :nodoc: + public static func buildExpression(_ expression: some Sequence) -> C { + C(expression) + } + + /// :nodoc: + public static func buildOptional(_ expression: C?) -> C { + expression ?? C() + } + + /// :nodoc: + public static func buildEither(first expression: some Sequence) -> C { + C(expression) + } + + /// :nodoc: + public static func buildEither(second expression: some Sequence) -> C { + C(expression) + } + + /// :nodoc: + public static func buildPartialBlock(first: some Sequence) -> C { + C(first) + } + + /// :nodoc: + public static func buildPartialBlock(accumulated: some Sequence, next: some Sequence) -> C { + C(accumulated) + C(next) + } + + /// :nodoc: + public static func buildBlock() -> C { + C() + } + + /// :nodoc: + public static func buildArray(_ components: [some Sequence]) -> C { + components.reduce(into: C()) { $0.append(contentsOf: $1) } + } + + /// :nodoc: + public static func buildFinalResult(_ component: C) -> C { + component + } +} diff --git a/Sources/SpeziFoundation/Collection Builders/SetBuilder.swift b/Sources/SpeziFoundation/Collection Builders/SetBuilder.swift new file mode 100644 index 0000000..f656cba --- /dev/null +++ b/Sources/SpeziFoundation/Collection Builders/SetBuilder.swift @@ -0,0 +1,73 @@ +// +// This source file is part of the Stanford Spezi open-source project +// +// SPDX-FileCopyrightText: 2024 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + + +/// ``SetBuilder`` is a result builder for constructing `Set`s. +@resultBuilder +public enum SetBuilder {} + + +extension Set { + /// Constructs a new `Set` using a result builder. + public init(@SetBuilder build: () -> Set) { + self = build() + } +} + + +extension SetBuilder { + /// :nodoc: + public static func buildExpression(_ expression: Element) -> Set { + Set(CollectionOfOne(expression)) + } + + /// :nodoc: + public static func buildExpression(_ expression: some Sequence) -> Set { + Set(expression) + } + + /// :nodoc: + public static func buildOptional(_ expression: Set?) -> Set { // swiftlint:disable:this discouraged_optional_collection + expression ?? Set() + } + + /// :nodoc: + public static func buildEither(first expression: some Sequence) -> Set { + Set(expression) + } + + /// :nodoc: + public static func buildEither(second expression: some Sequence) -> Set { + Set(expression) + } + + /// :nodoc: + public static func buildPartialBlock(first: some Sequence) -> Set { + Set(first) + } + + /// :nodoc: + public static func buildPartialBlock(accumulated: some Sequence, next: some Sequence) -> Set { + Set(accumulated).union(next) + } + + /// :nodoc: + public static func buildBlock() -> Set { + Set() + } + + /// :nodoc: + public static func buildArray(_ components: [some Sequence]) -> Set { + components.reduce(into: Set()) { $0.formUnion($1) } + } + + /// :nodoc: + public static func buildFinalResult(_ component: Set) -> Set { + component + } +} diff --git a/Sources/SpeziFoundation/Misc/BinarySearch.swift b/Sources/SpeziFoundation/Misc/BinarySearch.swift new file mode 100644 index 0000000..04afa9e --- /dev/null +++ b/Sources/SpeziFoundation/Misc/BinarySearch.swift @@ -0,0 +1,81 @@ +// +// This source file is part of the Stanford Spezi open-source project +// +// SPDX-FileCopyrightText: 2025 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import enum Foundation.ComparisonResult + + +/// The result of a binary search looking for some element in a collection. +public enum BinarySearchIndexResult { + /// The searched-for element was found in the collection, at the specified index. + case found(Index) + /// The searched-for element was not found in the collection, but if it were a member of the collection, it would belong at the specified index. + case notFound(Index) + + /// The index at which the element is/belongs. + public var index: Index { + switch self { + case .found(let index), .notFound(let index): + index + } + } +} + + +extension BinarySearchIndexResult: Equatable where Index: Equatable {} +extension BinarySearchIndexResult: Hashable where Index: Hashable {} + + +extension Collection { + /// Performs a binary search over the collection, looking for + /// - parameter compare: closure that gets called with an element of the collection to determine, whether the binary search algorithm should go left/right, or has already found its target. + /// The closure should return `.orderedSame` if the element passed to it matches the search destination, `.orderedAscending` if the search should continue to the left, + /// and `.orderedDescending` if it should continue to the right. + /// E.g., when looking for a position for an element `x`, the closure should perform a `compare(x, $0)`. + public func binarySearchForIndex( + of element: Element, + using compare: (Element, Element) -> ComparisonResult + ) -> BinarySearchIndexResult { + binarySearchForIndex(of: element, in: startIndex.., + using compare: (Element, Element) -> ComparisonResult + ) -> BinarySearchIndexResult { + guard let middle: Self.Index = middleIndex(of: range) else { + return .notFound(range.upperBound) + } + switch compare(element, self[middle]) { + case .orderedAscending: // lhs < rhs + return binarySearchForIndex(of: element, in: range.lowerBound.. rhs + return binarySearchForIndex(of: element, in: index(after: middle)..) -> Index? { + guard !range.isEmpty else { + return nil + } + let distance = self.distance(from: range.lowerBound, to: range.upperBound) + let resultIdx = self.index(range.lowerBound, offsetBy: distance / 2) + return resultIdx + } +} diff --git a/Sources/SpeziFoundation/Misc/Calendar.swift b/Sources/SpeziFoundation/Misc/Calendar.swift index ec1ea89..48c6c51 100644 --- a/Sources/SpeziFoundation/Misc/Calendar.swift +++ b/Sources/SpeziFoundation/Misc/Calendar.swift @@ -1,16 +1,21 @@ // -// File.swift -// SpeziFoundation +// This source file is part of the Stanford Spezi open-source project // -// Created by Lukas Kollmer on 2024-12-17. +// SPDX-FileCopyrightText: 2024 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT // import Foundation - - -// MARK: Date Ranges +private func tryUnwrap(_ value: T?, _ message: String) -> T { + if let value { + return value + } else { + fatalError(message) + } +} extension Calendar { @@ -18,41 +23,47 @@ extension Calendar { public func startOfHour(for date: Date) -> Date { var retval = date for component in [Calendar.Component.minute, .second, .nanosecond] { - retval = self.date(bySettingComponentToZero: component, of: retval, adjustOtherComponents: false) + retval = tryUnwrap( + self.date(bySettingComponentToZero: component, of: retval, adjustOtherComponents: false), + "Unable to compute start of hour" + ) } - precondition([Component.year, .month, .day, .hour].allSatisfy { - self.component($0, from: retval) == self.component($0, from: date) - }) return retval } /// Returns a `Date` which represents the start of the next hour, relative to `date`. public func startOfNextHour(for date: Date) -> Date { - let startOfHour = startOfHour(for: date) - return self.date(byAdding: .hour, value: 1, to: startOfHour)! + tryUnwrap( + self.date(byAdding: .hour, value: 1, to: startOfHour(for: date)), + "Unable to compute start of next hour" + ) } /// Returns a `Range` representing the range of the hour into which `date` falls. public func rangeOfHour(for date: Date) -> Range { - return startOfHour(for: date).. Date { - let startOfDay = self.startOfDay(for: date) - return self.date(byAdding: .day, value: 1, to: startOfDay)! + tryUnwrap( + self.date(byAdding: .day, value: 1, to: startOfDay(for: date)), + "Unable to compute start of next day" + ) } /// Returns a `Date` which represents the start of the previous day, relative to `date`. public func startOfPrevDay(for date: Date) -> Date { - let startOfDay = self.startOfDay(for: date) - return self.date(byAdding: .day, value: -1, to: startOfDay)! + tryUnwrap( + self.date(byAdding: .day, value: -1, to: startOfDay(for: date)), + "Unable to compute start of previous day" + ) } /// Returns a `Range` representing the range of the day into which `date` falls. public func rangeOfDay(for date: Date) -> Range { - return startOfDay(for: date).. Date { - let start = startOfWeek(for: date) - return self.date(byAdding: .weekOfYear, value: 1, to: start)! + tryUnwrap( + self.date(byAdding: .weekOfYear, value: 1, to: startOfWeek(for: date)), + "Unable to compute start of next week" + ) } /// Returns a `Range` representing the range of the week into which `date` falls. public func rangeOfWeek(for date: Date) -> Range { - return startOfWeek(for: date).. Date { var adjustedDate = self.startOfDay(for: date) - adjustedDate = self.date(bySetting: .day, value: 1, of: adjustedDate)! - if adjustedDate <= date { - precondition(self.component(.day, from: adjustedDate) == 1) - return adjustedDate + adjustedDate = tryUnwrap( + self.date(bySetting: .day, value: 1, of: adjustedDate), + "Unable to compute start of month" + ) + if adjustedDate > date { + // Setting the day to 1 made the date larger, i.e. moved it one month ahead :/ + return tryUnwrap( + self.date(byAdding: .month, value: -1, to: adjustedDate), + "Unable to compute start of month" + ) } else { - let startOfMonth = self.date(byAdding: .month, value: -1, to: adjustedDate)! - precondition(self.component(.day, from: startOfMonth) == 1) - return startOfMonth + // we were able to set the day to 1, and can simply return the date. + return adjustedDate } } /// Returns a `Date` which represents the start of the next month, relative to `date`. public func startOfNextMonth(for date: Date) -> Date { - let start = startOfMonth(for: date) - return self.date(byAdding: .month, value: 1, to: start)! + tryUnwrap( + self.date(byAdding: .month, value: 1, to: startOfMonth(for: date)), + "Unable to compute start of next month" + ) } /// Returns the exclusive range from the beginning of the month into which `date` falls, to the beginning of the next public func rangeOfMonth(for date: Date) -> Range { - let start = startOfMonth(for: date) - let end = startOfNextMonth(for: start) - precondition(startOfNextMonth(for: start) == startOfNextMonth(for: date)) - return start.. Date { var adjustedDate = startOfMonth(for: date) - precondition(adjustedDate <= date) - adjustedDate = self.date(bySetting: .month, value: 1, of: adjustedDate)! + adjustedDate = tryUnwrap( + self.date(bySetting: .month, value: 1, of: adjustedDate), + "Unable to compute start of year" + ) if adjustedDate > date { // Setting the month to 1 made the date larger, i.e. moved it one year ahead :/ - adjustedDate = self.date(byAdding: .year, value: -1, to: adjustedDate)! + return tryUnwrap( + self.date(byAdding: .year, value: -1, to: adjustedDate), + "Unable to compute start of year" + ) + } else { + // we were able to set the month to 1, and can simply return the date. return adjustedDate } - - precondition({ () -> Bool in - let components = self.dateComponents([.year, .month, .day, .hour, .minute, .second, .nanosecond], from: adjustedDate) - return components.year == self.component(.year, from: date) && components.month == 1 && components.day == 1 - && components.hour == 0 && components.minute == 0 && components.second == 0 && components.nanosecond == 0 - }()) - return adjustedDate } /// Returns a `Date` which represents the start of the previous year, relative to `date`. public func startOfPrevYear(for date: Date) -> Date { - return self.date(byAdding: .year, value: -1, to: startOfYear(for: date))! + tryUnwrap( + self.date(byAdding: .year, value: -1, to: startOfYear(for: date)), + "Unable to compute start of previous year" + ) } /// Returns a `Date` which represents the start of the next year, relative to `date`. public func startOfNextYear(for date: Date) -> Date { - return self.date(byAdding: .year, value: 1, to: startOfYear(for: date))! + tryUnwrap( + self.date(byAdding: .year, value: 1, to: startOfYear(for: date)), + "Unable to compute start of next year" + ) } /// Returns a `Range` representing the range of the year into which `date` falls. public func rangeOfYear(for date: Date) -> Range { - return startOfYear(for: date).. Int { + /// Returns the number of distinct weeks between the two dates. + /// E.g., if the first date is 2021-02-02 07:20 and the second is 2021-02-02 08:05, this would return 2. + public func countDistinctHours(from startDate: Date, to endDate: Date) -> Int { _countDistinctNumberOfComponentUnits( from: startDate, to: endDate, - for: .year, - startOfComponentFn: startOfYear + for: .hour, + startOfComponentFn: startOfHour ) } - /// Returns the rounded up number of months between the two dates. - /// E.g., if the first date is 25.02 and the second is 12.04, this would return 3. - public func countDistinctMonths(from startDate: Date, to endDate: Date) -> Int { + /// Returns the number of distinct weeks between the two dates. + /// E.g., if the first date is 2021-02-02 09:00 and the second is 2021-02-03 07:00, this would return 2. + public func countDistinctDays(from startDate: Date, to endDate: Date) -> Int { _countDistinctNumberOfComponentUnits( from: startDate, to: endDate, - for: .month, - startOfComponentFn: startOfMonth + for: .day, + startOfComponentFn: startOfDay ) } + /// Returns the number of distinct weeks between the two dates. + /// E.g., if the first date is 2021-02-07 and the second is 2021-02-09, this would return 2. public func countDistinctWeeks(from startDate: Date, to endDate: Date) -> Int { _countDistinctNumberOfComponentUnits( from: startDate, @@ -217,27 +243,29 @@ extension Calendar { ) } - - public func countDistinctDays(from startDate: Date, to endDate: Date) -> Int { + /// Returns the number of distinct months between the two dates. + /// E.g., if the first date is 2021-02-25 and the second is 2021-04-12, this would return 3. + public func countDistinctMonths(from startDate: Date, to endDate: Date) -> Int { _countDistinctNumberOfComponentUnits( from: startDate, to: endDate, - for: .day, - startOfComponentFn: startOfDay + for: .month, + startOfComponentFn: startOfMonth ) } - - public func countDistinctHours(from startDate: Date, to endDate: Date) -> Int { + /// Returns the number of distinct years between the two dates. + /// E.g., if the first date is 2021-02-25 and the second is 2022-02-25, this would return 2. + public func countDistinctYears(from startDate: Date, to endDate: Date) -> Int { _countDistinctNumberOfComponentUnits( from: startDate, to: endDate, - for: .hour, - startOfComponentFn: startOfHour + for: .year, + startOfComponentFn: startOfYear ) } - + /// Returns the number of days `endDate` is offset from `startDate`. public func offsetInDays(from startDate: Date, to endDate: Date) -> Int { guard !isDate(startDate, inSameDayAs: endDate) else { return 0 @@ -249,51 +277,36 @@ extension Calendar { } } - - public func dateIsSunday(_ date: Date) -> Bool { - // NSCalendar starts weeks on sundays, ?regardless of locale? - return self.component(.weekday, from: date) == self.firstWeekday - } - - + /// Returns the number of days in the month into which `date` falls. public func numberOfDaysInMonth(for date: Date) -> Int { - return self.range(of: .day, in: .month, for: date)!.count + tryUnwrap( + self.range(of: .day, in: .month, for: date), + "Unable to get range of month" + ).count } } - - -// MARK: Date Components - - extension Calendar { - public func date(bySettingComponentToZero component: Component, of date: Date, adjustOtherComponents: Bool) -> Date { + /// Computes a new `Date` from `date`, obtained by setting `component` to zero. + /// - parameter adjustOtherComponents: Determines whether the other components in the date should be adjusted when changing the component. + public func date(bySettingComponentToZero component: Component, of date: Date, adjustOtherComponents: Bool) -> Date? { if adjustOtherComponents { - return self.date(bySetting: component, value: 0, of: date)! + // If we're asked to adjust the other components, we can use Calendar's -date(bySetting...) function. + return self.date(bySetting: component, value: 0, of: date) } else { - let compValue = self.component(component, from: date) - return self.date(byAdding: component, value: -compValue, to: date, wrappingComponents: true)! - } - } - - - - public func date(bySetting component: Component, of date: Date, to value: Int, adjustOtherComponents: Bool) -> Date { - if adjustOtherComponents { - return self.date(bySetting: component, value: value, of: date)! - } else { - let compValue = self.component(component, from: date) - let diff = value - compValue - return self.date(byAdding: component, value: diff, to: date, wrappingComponents: true)! + // Otherwise, we perform the adjustment manually, by subtracting the component's value from the date. + let componentValue = self.component(component, from: date) + return self.date(byAdding: component, value: -componentValue, to: date, wrappingComponents: true) } } } - - extension DateComponents { + /// Returns the integer value of the specified component. + /// - Note: This is only valid for components which have an `Int` value (e.g., `day`, `month`, `hour`, `minute`, etc.) + /// Passing a non-`Int` component (e.g., `calendar` or `timeZone`) will result in the program being terminated. public subscript(component: Calendar.Component) -> Int? { switch component { case .era: @@ -328,8 +341,11 @@ extension DateComponents { if #available(iOS 18, macOS 15, *) { return self.dayOfYear } else { - // The crash here is fine, since the enum case itself is also only available on iOS 18+ - fatalError() + // This branch is practically unreachable, since the availability of the `Calendar.Component.dayOfYear` + // case is the same as of the `DateComponents.dayOfYear` property. (Both are iOS 18+.) + // Meaning that in all situations where a caller is able to pass us this case, we'll also be able + // to access the property. + return nil } case .calendar, .timeZone, .isLeapMonth: fatalError("not supported") // different type (not an int) :/ @@ -340,3 +356,37 @@ extension DateComponents { } +// MARK: DST + +extension TimeZone { + /// Information about a daylight saving time transition. + public struct DSTTransition { + /// The instant when the transition happens + public let date: Date + /// The amount of seconds by which this transition changes clocks + public let change: TimeInterval + } + + /// The time zone's next DST transition. + public func nextDSTTransition(after referenceDate: Date = .now) -> DSTTransition? { + guard let nextDST = nextDaylightSavingTimeTransition(after: referenceDate) else { + return nil + } + let before = nextDST.addingTimeInterval(-1) + let after = nextDST.addingTimeInterval(1) + return DSTTransition( + date: nextDST, + change: daylightSavingTimeOffset(for: after) - daylightSavingTimeOffset(for: before) + ) + } + + + /// Returns the next `maxCount` daylight saving time transitions. + public func nextDSTTransitions(maxCount: Int) -> [DSTTransition] { + var transitions: [DSTTransition] = [] + while transitions.count < maxCount, let next = self.nextDSTTransition(after: transitions.last?.date ?? Date()) { + transitions.append(next) + } + return transitions + } +} diff --git a/Sources/SpeziFoundation/Misc/ObjCExceptionHandling.swift b/Sources/SpeziFoundation/Misc/ObjCExceptionHandling.swift index 4f8fcb1..8b5fb2e 100644 --- a/Sources/SpeziFoundation/Misc/ObjCExceptionHandling.swift +++ b/Sources/SpeziFoundation/Misc/ObjCExceptionHandling.swift @@ -1,8 +1,9 @@ // -// File.swift -// SpeziFoundation +// This source file is part of the Stanford Spezi open-source project // -// Created by Lukas Kollmer on 2024-12-25. +// SPDX-FileCopyrightText: 2024 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT // import Foundation @@ -13,8 +14,8 @@ import SpeziFoundationObjC public struct CaughtNSException: Error, LocalizedError, @unchecked Sendable { public let exception: NSException - public var errorDescription: String? { - return "\(Self.self): \(exception.description)" + public var errorDescription: String { + "\(Self.self): \(exception.description)" } } @@ -44,7 +45,8 @@ public func catchingNSException(_ block: () throws -> T) throws -> T { throw error default: // unreachable - fatalError(""" + fatalError( + """ Invalid state. Exactly one of retval, caughtNSException, and caughtSwiftError should be non-nil. retval: \(String(describing: retval)) caughtNSException: \(String(describing: caughtNSException)) diff --git a/Sources/SpeziFoundation/Misc/OrderedArray.swift b/Sources/SpeziFoundation/Misc/OrderedArray.swift new file mode 100644 index 0000000..a5ca310 --- /dev/null +++ b/Sources/SpeziFoundation/Misc/OrderedArray.swift @@ -0,0 +1,243 @@ +// +// This source file is part of the Stanford Spezi open-source project +// +// SPDX-FileCopyrightText: 2024 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation + + +/// An `Array`-like data structure that uses a user-defined total order to arrange its elements. +/// - Note: An `OrderedArray`'s `Element` should be a type which is either fully immutable, or at least immutable w.r.t. the array's comparator. +/// The array does not observe changes within individual elements, and does not automatically re-arrange its elements. +/// - Note: The `OrderedArray` type intentionally does not conform to `Equatable` or `Hashable`. +/// The reason for this is that, while we can compare or hash the elements in the array, we cannot do the same with the array's +/// comparator (which is a function). If you want to compare the elements of two `OrderedArray`s, use `Sequence`'s `elementsEqual(_:)` function. +public struct OrderedArray { + /// The comparator used to determine the ordering between two `Element`s. + /// - returns: `true` iff the first element comares less to the second one, `false` otherwise. + public typealias Comparator = (Element, Element) -> Bool + /// The ``OrderedArray``'s underlying storage type. + public typealias Storage = [Element] + + /// The total order used to compare elements. + /// When comparing two elements, it should return `true` if their ordering satisfies the relation, otherwise `false`. + public let areInIncreasingOrder: Comparator + + private var storage: Storage { + didSet { + if shouldAssertInvariantAfterMutations { + checkInvariant() + } + } + } + + /// Whether the array should assert its invariant after every mutation + var shouldAssertInvariantAfterMutations = true { + didSet { + if shouldAssertInvariantAfterMutations && !oldValue { + checkInvariant() + } + } + } + + + /// The total number of elements that the array can contain without allocating new storage. + public var capacity: Int { + storage.capacity + } + + + /// Creates a new ``OrderedArray`` using the specified comparator. + public init(areInIncreasingOrder: @escaping Comparator) { + self.storage = [] + self.areInIncreasingOrder = areInIncreasingOrder + } + + + /// Allows multiple operations on the array, which might temporarily break the invariant, to run as a single transaction, during which invariant checking is temporarily disabled. + /// - Note: The invariant must still be satisfied at the end of the mutations. + public mutating func withInvariantCheckingTemporarilyDisabled(_ block: (inout Self) -> Result) -> Result { + let prevValue = shouldAssertInvariantAfterMutations + shouldAssertInvariantAfterMutations = false + let retval = block(&self) + shouldAssertInvariantAfterMutations = prevValue + return retval + } + + + /// Checks that the array is ordered according to its invariant (i.e., the comparator). + /// If the invariant is satisfied, this function will terminate execution. + public func checkInvariant() { + precondition(self.isSorted(by: areInIncreasingOrder)) + } +} + + +extension OrderedArray: CustomStringConvertible, CustomDebugStringConvertible { + public var description: String { + "OrderedArray(\(storage.description))" + } + + public var debugDescription: String { + "OrderedArray(\(storage.debugDescription))" + } +} + + +// MARK: RandomAccessCollection + +extension OrderedArray: RandomAccessCollection { + public typealias Index = Storage.Index + + public var startIndex: Index { + storage.startIndex + } + public var endIndex: Index { + storage.endIndex + } + +// public func index(before idx: Index) -> Index { +// storage.index(before: idx) +// } + + public func index(after idx: Index) -> Index { + storage.index(after: idx) + } + + /// We implement this function in order to have the default `Collection.firstIndex(of:)` operation + /// take advantage of our invariant and binary-search based element lookup. + public func _customIndexOfEquatableElement(_ element: Element) -> Index?? { // swiftlint:disable:this identifier_name + Index??.some(firstIndex(of: element)) + } + + /// We implement this function in order to have the default `Collection.contains(_:)` operation + /// take advantage of our invariant and binary-search based element lookup. + public func _customContainsEquatableElement(_ element: Element) -> Bool? { // swiftlint:disable:this identifier_name discouraged_optional_boolean + Bool?.some(firstIndex(of: element) != nil) // swiftlint:disable:this discouraged_optional_boolean + } + + public subscript(position: Index) -> Element { + storage[position] + } +} + + +// MARK: Insertion, Removal, Mutation, Other + +extension OrderedArray { + /// Inserts a new element into the ``OrderedArray``, at the correct position based on the array's comparator. + /// - returns: The index at which the element was inserted. + @discardableResult + public mutating func insert(_ element: Element) -> Index { + let insertionIdx: Index + switch search(for: element) { + case .found(let idx): + insertionIdx = idx + case .notFound(let idx): + insertionIdx = idx + } + storage.insert(element, at: insertionIdx) + return insertionIdx + } + + + /// Inserts the elements of some other `Sequence` into the ``OrderedArray``, at their respective correct positions, based on the array's comparator. + public mutating func insert(contentsOf sequence: some Sequence) { + withInvariantCheckingTemporarilyDisabled { `self` in + for element in sequence { + self.insert(element) + } + } + } + + /// Removes the element at the specified index from the array. + /// - returns: The removed element. + @discardableResult + public mutating func remove(at index: Index) -> Element { + storage.remove(at: index) + } + + + /// Removes all occurrences of the objects in `elements` from the array. + /// - Note: if the array contains multiple occurrences of an element, all of them will be removed. + public mutating func remove(contentsOf elements: some Sequence) where Element: Equatable { + withInvariantCheckingTemporarilyDisabled { `self` in + for element in elements { + // In the case of removals, we also perform an O(n) search for the to-be-removed element, + // as a fallback if the default binary search didn't yield a result. + // This is requied because we might be removing an element whose "correct" position in the + // array has changed, in which case we would not necessarily be able to find it via the normal search. + // (E.g.: you have an array of objects ordered based on some property, and this property has changed for + // some of the elements, and you now want to remove them from their current positions in response.) + while let index = self.firstIndex(of: element) ?? self.indices.first(where: { self[$0] == element }) { + self.storage.remove(at: index) + } + } + } + } + + + /// Removes from the array all elements which match the predicate + /// - returns: the indices of the removed elements. + public mutating func removeAll(where predicate: (Element) -> Bool) { + withInvariantCheckingTemporarilyDisabled { `self` in + self.storage.removeAll(where: predicate) + } + } + + + /// Removes all elements from the array. + public mutating func removeAll(keepingCapacity: Bool = false) { + storage.removeAll(keepingCapacity: keepingCapacity) + } + + + /// Returns the first index of the specified element. + public func firstIndex(of element: Element) -> Index? { + switch search(for: element) { + case .found(let idx): + idx + case .notFound: + nil + } + } + + /// Removes the first occurrence of the specified element, if applicable. + /// - returns: the index from which the element was removed. `nil` if the element wasn't a member of the array. + @discardableResult + public mutating func removeFirstOccurrence(of element: Element) -> Index? { + if let idx = firstIndex(of: element) { + remove(at: idx) + return idx + } else { + return nil + } + } +} + + +// MARK: Position Finding etc + +extension OrderedArray { + /// The result of searching the array for an element. + public typealias SearchResult = BinarySearchIndexResult + + /// Searches the array for an element. + public func search(for element: Element) -> SearchResult { + binarySearchForIndex(of: element, using: compare) + } + + /// Compares two elements based on the comparator, and returns a matching `ComparisonResult`. + fileprivate func compare(_ lhs: Element, _ rhs: Element) -> ComparisonResult { + if areInIncreasingOrder(lhs, rhs) { + .orderedAscending + } else if areInIncreasingOrder(rhs, lhs) { + .orderedDescending + } else { + .orderedSame + } + } +} diff --git a/Sources/SpeziFoundation/Misc/RangeReplaceableCollectionBuilder.swift b/Sources/SpeziFoundation/Misc/RangeReplaceableCollectionBuilder.swift deleted file mode 100644 index 229f8c1..0000000 --- a/Sources/SpeziFoundation/Misc/RangeReplaceableCollectionBuilder.swift +++ /dev/null @@ -1,72 +0,0 @@ -// -// File.swift -// SpeziFoundation -// -// Created by Lukas Kollmer on 2024-11-30. -// - - -public typealias ArrayBuilder = RangeReplaceableCollectionBuilder<[T]> - -public extension Array { - init(@ArrayBuilder build: () -> Self) { - self = build() - } -} - - -// MARK: RangeReplaceableCollectionBuilder - -@resultBuilder -public enum RangeReplaceableCollectionBuilder { - public typealias Element = C.Element - public static func make(@RangeReplaceableCollectionBuilder build: () -> C) -> C { - build() - } -} - -public extension RangeReplaceableCollectionBuilder { - static func buildExpression(_ expression: Element) -> C { - C(CollectionOfOne(expression)) - } - - static func buildExpression(_ expression: some Sequence) -> C { - C(expression) - } - - static func buildOptional(_ expression: C?) -> C { - expression ?? C() - } - - static func buildOptional(_ expression: (some Sequence)?) -> C { - expression.map(C.init) ?? C() - } - - static func buildEither(first expression: some Sequence) -> C { - C(expression) - } - - static func buildEither(second expression: some Sequence) -> C { - C(expression) - } - - static func buildPartialBlock(first: some Sequence) -> C { - C(first) - } - - static func buildPartialBlock(accumulated: some Sequence, next: some Sequence) -> C { - C(accumulated) + C(next) - } - - static func buildBlock() -> C { - C() - } - - static func buildBlock(_ components: Element...) -> C { - C(components) - } - - static func buildArray(_ components: [some Sequence]) -> C { - components.reduce(into: C()) { $0.append(contentsOf: $1) } - } -} diff --git a/Sources/SpeziFoundation/Misc/SequenceExtensions.swift b/Sources/SpeziFoundation/Misc/SequenceExtensions.swift index 6ec3b2d..c7fc831 100644 --- a/Sources/SpeziFoundation/Misc/SequenceExtensions.swift +++ b/Sources/SpeziFoundation/Misc/SequenceExtensions.swift @@ -1,11 +1,11 @@ // -// File.swift -// SpeziFoundation +// This source file is part of the Stanford Spezi open-source project // -// Created by Lukas Kollmer on 2024-12-27. +// SPDX-FileCopyrightText: 2024 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT // -import Foundation import Algorithms @@ -25,47 +25,26 @@ extension Sequence { } - extension Sequence { - public func lk_isSorted(by areInIncreasingOrder: (Element, Element) -> Bool) -> Bool { + /// Determines whether the sequence is sorted w.r.t. the specified comparator. + public func isSorted(by areInIncreasingOrder: (Element, Element) -> Bool) -> Bool { // ISSUE HERE: if we have a collection containing duplicate objects (eg: `[0, 0]`), and we want to check if it's sorted // properly (passing `{ $0 < $1 }`), that would incorrectly return false, because not all elements are ordered strictly ascending. // BUT: were we to sort the collection using the specified comparator, the sort result would be equivalent to the collection // itself. Meaning that we should consider it properly sorted. // We achieve this by reversing the comparator operands, and negating the result. - return self.adjacentPairs().allSatisfy { a, b in - !areInIncreasingOrder(b, a) - } - } - - - /// Returns whether the sequence is sorted ascending, w.r.t. the specified key path. - /// - Note: if two or more adjacent elements in the sequence are equal to each other, the sequence will still be considered sorted. - public func lk_isSorted(by keyPath: KeyPath) -> Bool { - return self.adjacentPairs().allSatisfy { - $0[keyPath: keyPath] <= $1[keyPath: keyPath] - } - } - - public func lk_isSortedStrictlyAscending(by keyPath: KeyPath) -> Bool { - return self.adjacentPairs().allSatisfy { - $0[keyPath: keyPath] < $1[keyPath: keyPath] + self.adjacentPairs().allSatisfy { lhs, rhs in + !areInIncreasingOrder(rhs, lhs) } } } - - extension RangeReplaceableCollection { + /// Removes the elements at the specified indices from the collection. public mutating func remove(at indices: some Sequence) { for idx in indices.sorted().reversed() { self.remove(at: idx) } } - - - public mutating func removeAll(where predicate: (Element) throws -> Bool) rethrows { - self = try self.filter { try !predicate($0) } - } } diff --git a/Sources/SpeziFoundation/Misc/SetBuilder.swift b/Sources/SpeziFoundation/Misc/SetBuilder.swift deleted file mode 100644 index 12bca48..0000000 --- a/Sources/SpeziFoundation/Misc/SetBuilder.swift +++ /dev/null @@ -1,57 +0,0 @@ -// -// File.swift -// SpeziFoundation -// -// Created by Lukas Kollmer on 2024-12-20. -// - - -@resultBuilder -public enum SetBuilder { - public static func make(@SetBuilder build: () -> Set) -> Set { - build() - } -} - -public extension SetBuilder { - static func buildExpression(_ expression: Element) -> Set { - [expression] - } - - static func buildExpression(_ expression: some Sequence) -> Set { - Set(expression) - } - - static func buildOptional(_ expression: Set?) -> Set { - expression ?? Set() - } - - static func buildOptional(_ expression: (some Sequence)?) -> Set { - expression.map(Set.init) ?? Set() - } - - static func buildEither(first expression: some Sequence) -> Set { - Set(expression) - } - - static func buildEither(second expression: some Sequence) -> Set { - Set(expression) - } - - static func buildPartialBlock(first: some Sequence) -> Set { - Set(first) - } - - static func buildPartialBlock(accumulated: some Sequence, next: some Sequence) -> Set { - Set(accumulated).union(next) - } - - static func buildBlock(_ components: Element...) -> Set { - Set(components) - } - - static func buildArray(_ components: [some Sequence]) -> Set { - components.reduce(into: Set()) { $0.formUnion($1) } - } -} - diff --git a/Sources/SpeziFoundation/Misc/SortedArray.swift b/Sources/SpeziFoundation/Misc/SortedArray.swift deleted file mode 100644 index 123e88a..0000000 --- a/Sources/SpeziFoundation/Misc/SortedArray.swift +++ /dev/null @@ -1,370 +0,0 @@ -// -// File.swift -// SpeziFoundation -// -// Created by Lukas Kollmer on 2024-12-27. -// - -import Foundation - - - -/// An array that keeps its elements sorted in a way that satisfies a user-provided total order. -public struct SortedArray { // TODO SortedArray? OrderedArray? - public typealias Comparator = (Element, Element) -> Bool - public typealias Storage = Array - - /// The total order used to compare elements. - /// When comparing two elements, it should return `true` if their ordering satisfies the relation, otherwise `false`. - public let areInIncreasingOrder: Comparator - - private var storage: Storage { - didSet { if shouldAssertInvariantAfterMutations { assertInvariant() } } - } - - /// Whether the array should assert its invariant after every mutation - var shouldAssertInvariantAfterMutations = true { - didSet { - if shouldAssertInvariantAfterMutations && !oldValue { - assertInvariant() - } - } - } - - - public init() where Element: Comparable { - self.storage = [] - self.areInIncreasingOrder = { $0 < $1 } - } - - public init(areInIncreasingOrder: @escaping Comparator) { - self.storage = [] - self.areInIncreasingOrder = areInIncreasingOrder - } - - public init(keyPath: KeyPath) { - self.storage = [] - self.areInIncreasingOrder = { $0[keyPath: keyPath] < $1[keyPath: keyPath] } - } - - public init(_ sequence: some Sequence, areInIncreasingOrder: @escaping Comparator) { - self.storage = sequence.sorted(by: areInIncreasingOrder) - self.areInIncreasingOrder = areInIncreasingOrder - } - - - /// Performs mutations on the array as a single transaction, with invariant checks disabled for the duration of the mutations - mutating func withInvariantCheckingTemporarilyDisabled(_ block: (inout SortedArray) -> Result) -> Result { - let prevValue = shouldAssertInvariantAfterMutations - shouldAssertInvariantAfterMutations = false - let retval = block(&self) - shouldAssertInvariantAfterMutations = prevValue - return retval - } - - -// func lk_intoArray() -> [Element] { -// return self.storage -// } -} - - -//extension Array { -// init(_ other: SortedArray) { -// self = other.lk_intoArray() -// } -//} - - -extension SortedArray: CustomStringConvertible, CustomDebugStringConvertible { - public var description: String { - storage.description - } - - public var debugDescription: String { - storage.debugDescription - } -} - - -// MARK: RandomAccessCollection - -extension SortedArray: RandomAccessCollection { - public typealias Index = Storage.Index - - public var startIndex: Index { - storage.startIndex - } - public var endIndex: Index { - storage.endIndex - } - - public func index(before idx: Index) -> Index { - storage.index(before: idx) - } - - public func index(after idx: Index) -> Index { - storage.index(after: idx) - } - - public subscript(position: Index) -> Element { - storage[position] - } - - public func _customIndexOfEquatableElement(_ element: Element) -> Index?? { - return Optional.some(firstIndex(of: element)) - } -} - - - -// MARK: Equatable, Hashable - -extension SortedArray: Equatable where Element: Equatable { - /// - Note: This ignores the comparator!!! - public static func == (lhs: SortedArray, rhs: SortedArray) -> Bool { - return lhs.storage == rhs.storage - } -} - - -extension SortedArray: Hashable where Element: Hashable { - public func hash(into hasher: inout Hasher) { - hasher.combine(storage) - } -} - - - - - - -// MARK: Invariant Checking - -extension SortedArray { - /// Assert that the array is sorted according to its invariant. - public func assertInvariant() { - precondition(self.lk_isSorted(by: areInIncreasingOrder)) - } -} - - - - - -// MARK: Insertion, Removal, Mutation, Other - -extension SortedArray { - @discardableResult - public mutating func insert(_ element: Element) -> Index { - let insertionIdx: Index - switch search(for: element) { - case .found(let idx): - insertionIdx = idx - case .notFound(let idx): - insertionIdx = idx - } - storage.insert(element, at: insertionIdx) - return insertionIdx - } - - - public mutating func unsafelyInsert(_ element: Element, at idx: Index) { - storage.insert(element, at: idx) - } - - - public mutating func insert(contentsOf sequence: some Sequence) { - withInvariantCheckingTemporarilyDisabled { `self` in - for element in sequence { - self.insert(element) - } - } - } - - - public mutating func insert2(contentsOf sequence: some Sequence) -> Set { // TODO better name! - return withInvariantCheckingTemporarilyDisabled { `self` in - var insertionIndices = Set() - for element in sequence { - let idx = self.insert(element) - // since the insertions won't necessarily happen in order, we need to adjust the indices as we insert. - insertionIndices = insertionIndices.mapIntoSet { - $0 < idx ? $0 : self.index(after: $0) - } - insertionIndices.insert(idx) - } - return insertionIndices - } - } - - - @discardableResult - public mutating func remove(at index: Index) -> Element { - return storage.remove(at: index) - } - - - /// Removes all occurrences of the objects in `elements` from the array. - /// - returns: the indices of the removed objects - @discardableResult - public mutating func remove(contentsOf elements: some Sequence) -> [Index] where Element: Equatable { - return withInvariantCheckingTemporarilyDisabled { `self` in - let indices = elements.compactMap { element in - // In the case of removals, we also perform a cringe O(n) search for the to-be-removed element, - // if the default binary search didn't yield a result. - // This is requied because we might be removing an element whose "correct" position in the - // array has changed, in which case we would not necessarily be able to find it via the normal search. - self.firstIndex(of: element) ?? self.indices.first { self[$0] == element } - } - self.storage.remove(at: indices) - return indices - } - } - - - /// Removes from the array all elements which match the predicate - @discardableResult - public mutating func removeAll(where predicate: (Element) -> Bool) -> [Index] { - return withInvariantCheckingTemporarilyDisabled { `self` in - let indices = self.enumerated().compactMap { predicate($0.element) ? $0.offset : nil } - self.storage.remove(at: indices) - return indices - } - } - - - public mutating func removeAll(keepingCapacity: Bool = false) { - storage.removeAll(keepingCapacity: keepingCapacity) - } - - - public mutating func unsafelyMutate(at index: Index, with transform: (inout Element) throws -> Void) rethrows { - try transform(&storage[index]) - } - - - - public func firstIndex(of element: Element) -> Index? { - switch search(for: element) { - case .found(let idx): - return idx - case .notFound: - return nil - } - } - - public mutating func removeFirstOccurrence(of element: Element) -> Index? { - if let idx = firstIndex(of: element) { - remove(at: idx) - return idx - } else { - return nil - } - } - - - public func contains(_ element: Element) -> Bool { - return firstIndex(of: element) != nil - } -} - - - -// MARK: Position Finding etc - -extension SortedArray { - fileprivate func compare(_ lhs: Element, _ rhs: Element) -> ComparisonResult { - if areInIncreasingOrder(lhs, rhs) { - return .orderedAscending - } else if areInIncreasingOrder(rhs, lhs) { - return .orderedDescending - } else { - return .orderedSame - } - } - - public typealias SearchResult = BinarySearchIndexResult - - - public func search(for element: Element) -> SearchResult { - return lk_binarySearchFirstIndex { - switch compare(element, $0) { - case .orderedSame: - return .match - case .orderedAscending: - return .continueLeft - case .orderedDescending: - return .continueRight - } - } - } -} - - - - -// MARK: Binary Search - - -public enum BinarySearchIndexResult { - case found(Index) - case notFound(Index) - - public var index: Index { - switch self { - case .found(let index), .notFound(let index): - return index - } - } -} - -extension BinarySearchIndexResult: Equatable where Index: Equatable {} -extension BinarySearchIndexResult: Hashable where Index: Hashable {} - - -public enum BinarySearchComparisonResult { - case match - case continueLeft - case continueRight -} - - -extension Collection { - /// - parameter compare: closure that gets called with an element of the collection to determine, whether the binary search algorithm should go left/right, or has already found its target. - /// The closure should return `.orderedSame` if the element passed to it matches the search destination, `.orderedAscending` if the search should continue to the left, - /// and `.orderedDescending` if it should continue to the right. - /// E.g., when looking for a position for an element `x`, the closure should perform a `compare(x, $0)`. - public func lk_binarySearchFirstIndex(using compare: (Element) -> BinarySearchComparisonResult) -> BinarySearchIndexResult { - return lk_binarySearchFirstIndex(in: startIndex.., using compare: (Element) -> BinarySearchComparisonResult) -> BinarySearchIndexResult { - guard let middle: Self.Index = lk_middleIndex(of: range) else { - return .notFound(range.upperBound) - } - switch compare(self[middle]) { - case .continueLeft: // lhs < rhs - return lk_binarySearchFirstIndex(in: range.lowerBound.. rhs - return lk_binarySearchFirstIndex(in: index(after: middle)..) -> Index? { - guard !range.isEmpty else { - return nil - } - let distance = self.distance(from: range.lowerBound, to: range.upperBound) - let resultIdx = self.index(range.lowerBound, offsetBy: distance / 2) - return resultIdx - } -} - diff --git a/Sources/SpeziFoundation/Misc/UUID.swift b/Sources/SpeziFoundation/Misc/UUID.swift deleted file mode 100644 index f16b8db..0000000 --- a/Sources/SpeziFoundation/Misc/UUID.swift +++ /dev/null @@ -1,73 +0,0 @@ -// -// File.swift -// SpeziFoundation -// -// Created by Lukas Kollmer on 2024-12-27. -// - -import Foundation - - - -//extension UUID: Comparable { -//// @ThreadSafe2 private(set) static var totalNumComparisons: UInt64 = 0 -// -// -// /// Stolen from https://github.com/apple/swift-foundation/blob/71dfec2bc4a9b48f3575af4ceca7b6af65198fa9/Sources/FoundationEssentials/UUID.swift#L128 -// /// Does not necessarily produce meaningful results, the main point here is having some kind of stable (ie, deterministic) sorting on UUIDs. -// public static func < (lhs: UUID, rhs: UUID) -> Bool { -//// Self.totalNumComparisons += 1 -//// let result2 = measure(addDurationTo: &mTotalTimeSpentComparingUUIDs_me) { cmp_lt_me(lhs: lhs, rhs: rhs) } -//// let result1 = measure(addDurationTo: &mTotalTimeSpentComparingUUIDs_apple) { cmp_lt_apple(lhs: lhs, rhs: rhs) } -//// precondition(result1 == result2) -//// return result1 -// // TODO it seems that both of these implementations (_apple and _me) are functionally equivalent? -// // (i had the check above on for like millions of comparisons and it never failed.) -// // but somehow the _me version is way(!) faster than the _apple version??? -// return cmp_lt_me(lhs: lhs, rhs: rhs) -// } -// -// -// @_transparent -// private static func cmp_lt_apple(lhs: UUID, rhs: UUID) -> Bool { -// var leftUUID = lhs.uuid -// var rightUUID = rhs.uuid -// var result: Int = 0 -// var diff: Int = 0 -// withUnsafeBytes(of: &leftUUID) { leftPtr in -// withUnsafeBytes(of: &rightUUID) { rightPtr in -// for offset in (0 ..< MemoryLayout.size).reversed() { -// diff = Int(leftPtr.load(fromByteOffset: offset, as: UInt8.self)) - -// Int(rightPtr.load(fromByteOffset: offset, as: UInt8.self)) -// // Constant time, no branching equivalent of -// // if (diff != 0) { -// // result = diff; -// // } -// result = (result & (((diff - 1) & ~diff) >> 8)) | diff -// } -// } -// } -// return result < 0 -// } -// -// -// @_transparent -// private static func cmp_lt_me(lhs: UUID, rhs: UUID) -> Bool { -// return withUnsafeBytes(of: lhs.uuid) { lhsUUID -> Bool in -// return withUnsafeBytes(of: rhs.uuid) { rhsUUID -> Bool in -// for idx in 0...size { -// let lhs = lhsUUID[idx] -// let rhs = rhsUUID[idx] -// if lhs < rhs { -// return true -// } else if lhs > rhs { -// return false -// } else { -// continue -// } -// } -// return false // all bytes are equal -// } -// } -// } -//} diff --git a/Sources/SpeziFoundationObjC/ObjCExceptionHandling.m b/Sources/SpeziFoundationObjC/ObjCExceptionHandling.m index 01a20ec..bd2418e 100644 --- a/Sources/SpeziFoundationObjC/ObjCExceptionHandling.m +++ b/Sources/SpeziFoundationObjC/ObjCExceptionHandling.m @@ -1,8 +1,9 @@ // -// ObjCExceptionHandling.m -// SpeziFoundation +// This source file is part of the Stanford Spezi open-source project // -// Created by Lukas Kollmer on 2024-12-25. +// SPDX-FileCopyrightText: 2024 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT // #import "ObjCExceptionHandling.h" diff --git a/Sources/SpeziFoundationObjC/include/ObjCExceptionHandling.h b/Sources/SpeziFoundationObjC/include/ObjCExceptionHandling.h index 39d8316..2cec996 100644 --- a/Sources/SpeziFoundationObjC/include/ObjCExceptionHandling.h +++ b/Sources/SpeziFoundationObjC/include/ObjCExceptionHandling.h @@ -1,8 +1,9 @@ // -// ObjCExceptionHandling.h -// SpeziFoundation +// This source file is part of the Stanford Spezi open-source project // -// Created by Lukas Kollmer on 2024-12-25. +// SPDX-FileCopyrightText: 2024 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT // #ifndef ObjCExceptionHandling_h @@ -13,6 +14,8 @@ NS_ASSUME_NONNULL_BEGIN +/// Invokes the specified block, catching any `NSException`s that are thrown in the block. +/// - returns: `Nil` if the block didn't throw any exceptions, otherwise the caught exception. NSException *_Nullable InvokeBlockCatchingNSExceptionIfThrown(NS_NOESCAPE void(^block)(void)); NS_ASSUME_NONNULL_END diff --git a/Tests/SpeziFoundationTests/CalendarExtensionsTests.swift b/Tests/SpeziFoundationTests/CalendarExtensionsTests.swift new file mode 100644 index 0000000..85ca275 --- /dev/null +++ b/Tests/SpeziFoundationTests/CalendarExtensionsTests.swift @@ -0,0 +1,251 @@ +// This source file is part of the Stanford Spezi open-source project +// +// SPDX-FileCopyrightText: 2024 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import SpeziFoundation +import XCTest + + +final class CalendarExtensionsTests: XCTestCase { + private let cal = Calendar.current + + private func makeDate(year: Int, month: Int, day: Int, hour: Int, minute: Int = 0, second: Int = 0) throws -> Date { + let components = DateComponents(year: year, month: month, day: day, hour: hour, minute: minute, second: second) + return try XCTUnwrap(cal.date(from: components)) + } + + + func testRangeComputations() throws { + XCTAssertEqual( + cal.rangeOfHour(for: try makeDate(year: 2024, month: 12, day: 27, hour: 14, minute: 12)), + (try makeDate(year: 2024, month: 12, day: 27, hour: 14))..<(try makeDate(year: 2024, month: 12, day: 27, hour: 15)) + ) + + XCTAssertEqual( + cal.rangeOfDay(for: try makeDate(year: 2024, month: 12, day: 27, hour: 14, minute: 12)), + (try makeDate(year: 2024, month: 12, day: 27, hour: 00))..<(try makeDate(year: 2024, month: 12, day: 28, hour: 0)) + ) + + XCTAssertEqual( + cal.rangeOfWeek(for: try makeDate(year: 2024, month: 12, day: 27, hour: 14, minute: 12)), + (try makeDate(year: 2024, month: 12, day: 23, hour: 00))..<(try makeDate(year: 2024, month: 12, day: 30, hour: 0)) + ) + XCTAssertEqual( + cal.rangeOfWeek(for: try makeDate(year: 2024, month: 12, day: 31, hour: 14, minute: 12)), + (try makeDate(year: 2024, month: 12, day: 30, hour: 00))..<(try makeDate(year: 2025, month: 01, day: 06, hour: 0)) + ) + + XCTAssertEqual( + cal.rangeOfMonth(for: try makeDate(year: 2024, month: 12, day: 31, hour: 14, minute: 12)), + (try makeDate(year: 2024, month: 12, day: 01, hour: 00))..<(try makeDate(year: 2025, month: 01, day: 01, hour: 0)) + ) + + XCTAssertEqual( + cal.rangeOfYear(for: try makeDate(year: 2024, month: 12, day: 31, hour: 14, minute: 12)), + (try makeDate(year: 2024, month: 01, day: 01, hour: 00))..<(try makeDate(year: 2025, month: 01, day: 01, hour: 0)) + ) + + XCTAssertEqual( + cal.rangeOfMonth(for: try makeDate(year: 2024, month: 02, day: 29, hour: 14, minute: 12)), + (try makeDate(year: 2024, month: 02, day: 01, hour: 0))..<(try makeDate(year: 2024, month: 03, day: 01, hour: 0)) + ) + } + + + func testDistinctDistances() throws { // swiftlint:disable:this function_body_length + func imp( + start: Date, + end: Date, + fn: (Date, Date) -> Int, // swiftlint:disable:this identifier_name + expected: Int, + file: StaticString = #filePath, + line: UInt = #line + ) { + let distance = fn(start, end) + XCTAssertEqual(distance, expected, file: file, line: line) + XCTAssertEqual(fn(start, end), fn(end, start), file: file, line: line) + } + + func imp( + start: (year: Int, month: Int, day: Int, hour: Int, minute: Int), // swiftlint:disable:this large_tuple + end: (year: Int, month: Int, day: Int, hour: Int, minute: Int), // swiftlint:disable:this large_tuple + fn: (Date, Date) -> Int, // swiftlint:disable:this identifier_name + expected: Int, + file: StaticString = #filePath, + line: UInt = #line + ) throws { + imp( + start: try makeDate(year: start.year, month: start.month, day: start.day, hour: start.hour, minute: start.minute), + end: try makeDate(year: end.year, month: end.month, day: end.day, hour: end.hour, minute: end.minute), + fn: fn, + expected: expected, + file: file, + line: line + ) + } + + try imp( + start: (year: 2021, month: 02, day: 02, hour: 07, minute: 20), + end: (year: 2021, month: 02, day: 02, hour: 08, minute: 05), + fn: cal.countDistinctHours(from:to:), + expected: 2 + ) + try imp( + start: (year: 2021, month: 02, day: 02, hour: 07, minute: 20), + end: (year: 2021, month: 02, day: 02, hour: 07, minute: 21), + fn: cal.countDistinctHours(from:to:), + expected: 1 + ) + try imp( + start: (year: 2021, month: 02, day: 02, hour: 07, minute: 20), + end: (year: 2021, month: 02, day: 02, hour: 07, minute: 21), + fn: cal.countDistinctHours(from:to:), + expected: 1 + ) + + for transition in cal.timeZone.nextDSTTransitions(maxCount: 10) { + let range = cal.rangeOfDay(for: transition.date) + print(transition) + XCTAssertEqual( + cal.countDistinctHours(from: range.lowerBound, to: range.upperBound.addingTimeInterval(-1)), + 24 - Int(transition.change / 3600) + ) + } + + + try imp( + start: (year: 2021, month: 02, day: 02, hour: 09, minute: 00), + end: (year: 2021, month: 02, day: 03, hour: 07, minute: 00), + fn: cal.countDistinctDays(from:to:), + expected: 2 + ) + try imp( + start: (year: 2021, month: 02, day: 28, hour: 09, minute: 00), + end: (year: 2021, month: 03, day: 01, hour: 07, minute: 00), + fn: cal.countDistinctDays(from:to:), + expected: 2 + ) + try imp( + start: (year: 2024, month: 02, day: 28, hour: 09, minute: 00), + end: (year: 2024, month: 03, day: 01, hour: 07, minute: 00), + fn: cal.countDistinctDays(from:to:), + expected: 3 // leap year + ) + let now = Date() + imp( + start: now, + end: now, + fn: cal.countDistinctDays(from:to:), + expected: 1 + ) + imp( + start: cal.startOfDay(for: now), + end: cal.startOfNextDay(for: now), + fn: cal.countDistinctDays(from:to:), + expected: 2 + ) + + try imp( + start: (year: 2021, month: 02, day: 07, hour: 07, minute: 00), + end: (year: 2021, month: 02, day: 07, hour: 07, minute: 15), + fn: cal.countDistinctWeeks(from:to:), + expected: 1 + ) + try imp( + start: (year: 2021, month: 02, day: 07, hour: 07, minute: 00), + end: (year: 2021, month: 02, day: 08, hour: 07, minute: 15), + fn: cal.countDistinctWeeks(from:to:), + expected: 2 + ) + try imp( + start: (year: 2024, month: 12, day: 29, hour: 07, minute: 00), + end: (year: 2025, month: 01, day: 02, hour: 07, minute: 15), + fn: cal.countDistinctWeeks(from:to:), + expected: 2 + ) + try imp( + start: (year: 2024, month: 12, day: 30, hour: 07, minute: 00), + end: (year: 2025, month: 01, day: 02, hour: 07, minute: 15), + fn: cal.countDistinctWeeks(from:to:), + expected: 1 + ) + + try imp( + start: (year: 2022, month: 01, day: 12, hour: 05, minute: 00), + end: (year: 2022, month: 08, day: 11, hour: 17, minute: 00), + fn: cal.countDistinctMonths(from:to:), + expected: 8 + ) + + try imp( + start: (year: 2022, month: 01, day: 12, hour: 05, minute: 00), + end: (year: 2022, month: 08, day: 11, hour: 17, minute: 00), + fn: cal.countDistinctYears(from:to:), + expected: 1 + ) + try imp( + start: (year: 2022, month: 01, day: 12, hour: 05, minute: 00), + end: (year: 2023, month: 08, day: 11, hour: 17, minute: 00), + fn: cal.countDistinctYears(from:to:), + expected: 2 + ) + } + + + func testOffsets() throws { + let now = Date() + XCTAssertEqual(cal.offsetInDays( + from: now, + to: now + ), 0) + + XCTAssertEqual(cal.offsetInDays( + from: try makeDate(year: 2024, month: 01, day: 01, hour: 00), + to: try makeDate(year: 2024, month: 01, day: 08, hour: 00) + ), 7) + + XCTAssertEqual(cal.offsetInDays( + from: try makeDate(year: 2024, month: 01, day: 08, hour: 00), + to: try makeDate(year: 2024, month: 01, day: 01, hour: 00) + ), -7) + + XCTAssertEqual(cal.offsetInDays( + from: try makeDate(year: 2025, month: 02, day: 27, hour: 00), + to: try makeDate(year: 2025, month: 03, day: 02, hour: 00) + ), 3) + XCTAssertEqual(cal.offsetInDays( + from: try makeDate(year: 2024, month: 02, day: 27, hour: 00), + to: try makeDate(year: 2024, month: 03, day: 02, hour: 00) + ), 4) + } + + + func testRelativeOperations() throws { + XCTAssertEqual( + cal.startOfPrevDay(for: try makeDate(year: 2025, month: 01, day: 11, hour: 19, minute: 07)), + try makeDate(year: 2025, month: 01, day: 10, hour: 00, minute: 00) + ) + + XCTAssertEqual( + cal.startOfNextMonth(for: try makeDate(year: 2025, month: 01, day: 11, hour: 19, minute: 07)), + try makeDate(year: 2025, month: 02, day: 01, hour: 00, minute: 00) + ) + + XCTAssertEqual( + cal.startOfPrevYear(for: try makeDate(year: 2025, month: 01, day: 11, hour: 19, minute: 07)), + try makeDate(year: 2024, month: 01, day: 01, hour: 00, minute: 00) + ) + } + + + func testNumberOfDaysInMonth() throws { + XCTAssertEqual(cal.numberOfDaysInMonth(for: try makeDate(year: 2025, month: 01, day: 01, hour: 00)), 31) + XCTAssertEqual(cal.numberOfDaysInMonth(for: try makeDate(year: 2024, month: 12, day: 01, hour: 00)), 31) + XCTAssertEqual(cal.numberOfDaysInMonth(for: try makeDate(year: 2024, month: 11, day: 01, hour: 00)), 30) + XCTAssertEqual(cal.numberOfDaysInMonth(for: try makeDate(year: 2025, month: 02, day: 01, hour: 00)), 28) + XCTAssertEqual(cal.numberOfDaysInMonth(for: try makeDate(year: 2024, month: 02, day: 01, hour: 00)), 29) + } +} diff --git a/Tests/SpeziFoundationTests/CollectionBuildersTests.swift b/Tests/SpeziFoundationTests/CollectionBuildersTests.swift new file mode 100644 index 0000000..51f20ef --- /dev/null +++ b/Tests/SpeziFoundationTests/CollectionBuildersTests.swift @@ -0,0 +1,138 @@ +// +// This source file is part of the Stanford Spezi open-source project +// +// SPDX-FileCopyrightText: 2024 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import SpeziFoundation +import XCTest + + +final class CollectionBuildersTests: XCTestCase { + private func _imp( + _: C.Type, + expected: C, + @RangeReplaceableCollectionBuilder _ make: () -> C, + file: StaticString = #filePath, + line: UInt = #line + ) where C: Equatable { + let collection = make() + XCTAssertEqual(collection, expected, file: file, line: line) + } + + + func testArrayBuilder() { + _imp([Int].self, expected: [1, 2, 3, 4, 5, 7, 8, 9, 52]) { + 1 + 2 + [3, 4, 5] + if Date() < Date.distantPast { + 6 + } else { + [7, 8] + 9 + } + if let value = Int?.some(52) { + value + } + } + + XCTAssertEqual(Array {}, []) // swiftlint:disable:this syntactic_sugar + } + + func testStringBuilder() { + let greet = { + "Hello, \($0 as String) 🚀\n" + } + + _imp( + String.self, + expected: """ + Hello, Lukas 🚀 + Hello, Lukas 🚀 + Hello, Paul 🚀 + Hello, Paul 🚀 + Hello, World 🚀 + abc + we're in the present day + it's fine + def + """ + ) { + for name in ["Lukas", "Paul"] { + greet(name) + greet(name) + } + greet("World") + "abc\n" + if Date() < .distantFuture { + "we're in the present day\n" + } else { + "we're in the future, babyyy\n" + } + if let name = [String]().randomElement() { + name + } + if Date() > .distantFuture { + "concerning\n" + } else { + "it's fine\n" + } + ("def"[...] as Substring) + } + } + + + func testSetBuilder() { + XCTAssertEqual(Set {}, Set()) + + let greet = { + "Hello, \($0 as String) 🚀" + } + let set = Set { + for name in ["Lukas", "Paul"] { + greet(name) + greet(name) + } + greet("World") + "abc" + if Date() < .distantFuture { + "we're in the present day" + } else { + "we're in the future, babyyy" + } + if Date() > .distantFuture { + "concerning" + } else { + "it's fine" + } + if let name = ["Jakob"].randomElement() { + name + } + if let name = [String]().randomElement() { + name + } + if let abc = [String]?.some(["a", "b", "c"]) { + abc + } + if true { + ["a", "b", "c"] + } + } + let expected: Set = [ + "Hello, Lukas 🚀", + "Hello, Paul 🚀", + "Hello, World 🚀", + "abc", + "we're in the present day", + "Jakob", + "it's fine", + "a", + "b", + "c" + ] + XCTAssertEqual(set, expected) + } +} diff --git a/Tests/SpeziFoundationTests/ExceptionHandlingTests.swift b/Tests/SpeziFoundationTests/ExceptionHandlingTests.swift index b049f5c..6c6d4ec 100644 --- a/Tests/SpeziFoundationTests/ExceptionHandlingTests.swift +++ b/Tests/SpeziFoundationTests/ExceptionHandlingTests.swift @@ -24,6 +24,7 @@ final class ExceptionHandlingTests: XCTestCase { } func testNSExceptionThrown1() { + // test that we can catch NSExceptions raised by Objective-C code. do { let _: Void = try catchingNSException { let string = "Hello there :)" as NSString @@ -41,12 +42,14 @@ final class ExceptionHandlingTests: XCTestCase { } func testNSExceptionThrown2() { + // test that we can catch (custom) NSExceptions raised by Swift code. let exceptionName = NSExceptionName("CustomExceptionName") let exceptionReason = "There was a non-recoverable issue" do { let _: Void = try catchingNSException { NSException(name: exceptionName, reason: exceptionReason).raise() - fatalError() // unreachable, but the compiler doesn't know about this, because `-[NSException raise]` isn't annotated as being oneway... + // unreachable, but the compiler doesn't know about this, because `-[NSException raise]` isn't annotated as being oneway... + fatalError("unreachable") } XCTFail("Didn't throw an error :/") } catch { @@ -60,6 +63,7 @@ final class ExceptionHandlingTests: XCTestCase { } func testSwiftErrorThrown() { + // test that we can catch normal Swift errors. enum TestError: Error, Equatable { case abc } diff --git a/Tests/SpeziFoundationTests/OrderedArrayTests.swift b/Tests/SpeziFoundationTests/OrderedArrayTests.swift new file mode 100644 index 0000000..784ea88 --- /dev/null +++ b/Tests/SpeziFoundationTests/OrderedArrayTests.swift @@ -0,0 +1,92 @@ +// +// This source file is part of the Stanford Spezi open-source project +// +// SPDX-FileCopyrightText: 2024 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import SpeziFoundation +import XCTest + + +final class OrderedArrayTests: XCTestCase { + func testOrderedArray() { + var array = OrderedArray { $0 < $1 } + array.insert(12) + XCTAssertTrue(array.elementsEqual([12])) + array.insert(14) + XCTAssertTrue(array.elementsEqual([12, 14])) + array.insert(7) + XCTAssertTrue(array.elementsEqual([7, 12, 14])) + array.insert(contentsOf: [0, 1, 8, 13, 19]) + XCTAssertTrue(array.elementsEqual([0, 1, 7, 8, 12, 13, 14, 19])) + array.insert(7) + XCTAssertTrue(array.elementsEqual([0, 1, 7, 7, 8, 12, 13, 14, 19])) + array.removeFirstOccurrence(of: 8) + XCTAssertTrue(array.elementsEqual([0, 1, 7, 7, 12, 13, 14, 19])) + XCTAssertTrue(array.contains(12)) + XCTAssertEqual(array.search(for: 7), .found(2)) + XCTAssertEqual(array.search(for: 8), .notFound(4)) + } + + + func testOrderedArray2() { + var array = OrderedArray { $0 > $1 } + array.insert(contentsOf: (0..<10_000).map { _ in Int.random(in: Int.min...Int.max) }) + XCTAssertTrue(array.isSorted(by: { $0 > $1 })) + } + + + func testFindElement() { + var array = OrderedArray { $0 < $1 } + array.insert(contentsOf: [0, 5, 2, 9, 5, 2, 7, 6, 5, 3, 2, 1]) + XCTAssertTrue(array.elementsEqual([0, 1, 2, 2, 2, 3, 5, 5, 5, 6, 7, 9])) + // we type-erase the concrete type, in order to test the _customIndexOfEquatableElement and _customContainsEquatableElement implementations + func imp(_ col: some Collection) { + XCTAssertTrue(col.contains(0)) + XCTAssertTrue(col.contains(1)) + XCTAssertTrue(col.contains(3)) + XCTAssertFalse(col.contains(4)) + XCTAssertTrue(col.contains(5)) + XCTAssertTrue(col.contains(6)) + XCTAssertTrue(col.contains(7)) + XCTAssertFalse(col.contains(8)) + XCTAssertTrue(col.contains(9)) + XCTAssertFalse(col.contains(10)) + + XCTAssertNotNil(col.firstIndex(of: 0)) + XCTAssertNotNil(col.firstIndex(of: 1)) + XCTAssertNotNil(col.firstIndex(of: 3)) + XCTAssertNil(col.firstIndex(of: 4)) + XCTAssertNotNil(col.firstIndex(of: 5)) + XCTAssertNotNil(col.firstIndex(of: 6)) + XCTAssertNotNil(col.firstIndex(of: 7)) + XCTAssertNil(col.firstIndex(of: 8)) + XCTAssertNotNil(col.firstIndex(of: 9)) + XCTAssertNil(col.firstIndex(of: 10)) + } + imp(array) + } + + + func testElementRemoval() throws { + var array = OrderedArray { $0 < $1 } + array.insert(contentsOf: [0, 5, 2, 9, 5, 2, 7, 6, 5, 3, 2, 1]) + XCTAssertTrue(array.elementsEqual([0, 1, 2, 2, 2, 3, 5, 5, 5, 6, 7, 9])) + array.removeFirstOccurrence(of: 2) + XCTAssertTrue(array.elementsEqual([0, 1, 2, 2, 3, 5, 5, 5, 6, 7, 9])) + array.remove(at: try XCTUnwrap(array.firstIndex(of: 5))) + XCTAssertTrue(array.elementsEqual([0, 1, 2, 2, 3, 5, 5, 6, 7, 9])) + array.removeAll(where: { $0 < 4 }) + XCTAssertTrue(array.elementsEqual([5, 5, 6, 7, 9])) + array.removeFirstOccurrence(of: 2) + XCTAssertTrue(array.elementsEqual([5, 5, 6, 7, 9])) + array.remove(contentsOf: [5, 6]) + XCTAssertTrue(array.elementsEqual([7, 9])) + let capacity = array.capacity + array.removeAll(keepingCapacity: true) + XCTAssertTrue(array.isEmpty) + XCTAssertEqual(array.capacity, array.capacity) + } +} From 2917ced9201fa759879e4b16821d16ad430b6581 Mon Sep 17 00:00:00 2001 From: Lukas Kollmer Date: Sat, 11 Jan 2025 21:12:03 +0100 Subject: [PATCH 03/11] vanity commit --- CONTRIBUTORS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 7c5f783..25751eb 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -16,3 +16,4 @@ SpeziFoundation contributors * [Paul Schmiedmayer](https://github.com/PSchmiedmayer) * [Andreas Bauer](https://github.com/Supereg) * [Philipp Zagar](https://github.com/philippzagar) +* [Lukas Kollmer](https://github.com/lukaskollmer) From dade7c549178b47fce4314f74e3ce768bffae5a6 Mon Sep 17 00:00:00 2001 From: Lukas Kollmer Date: Sat, 11 Jan 2025 21:17:28 +0100 Subject: [PATCH 04/11] remove commented-out function --- Sources/SpeziFoundation/Misc/OrderedArray.swift | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Sources/SpeziFoundation/Misc/OrderedArray.swift b/Sources/SpeziFoundation/Misc/OrderedArray.swift index a5ca310..543f236 100644 --- a/Sources/SpeziFoundation/Misc/OrderedArray.swift +++ b/Sources/SpeziFoundation/Misc/OrderedArray.swift @@ -99,10 +99,6 @@ extension OrderedArray: RandomAccessCollection { storage.endIndex } -// public func index(before idx: Index) -> Index { -// storage.index(before: idx) -// } - public func index(after idx: Index) -> Index { storage.index(after: idx) } From f384810f007edea7e596c112f719415f4a498e28 Mon Sep 17 00:00:00 2001 From: Lukas Kollmer Date: Sun, 12 Jan 2025 12:12:24 +0100 Subject: [PATCH 05/11] fix a test, try to figure out which locale the GH runners use --- Package.swift | 4 ++-- .../CalendarExtensionsTests.swift | 11 +++++++++++ Tests/SpeziFoundationTests/OrderedArrayTests.swift | 2 +- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/Package.swift b/Package.swift index 71e0a83..55371a4 100644 --- a/Package.swift +++ b/Package.swift @@ -33,9 +33,9 @@ let package = Package( .target( name: "SpeziFoundation", dependencies: [ + .target(name: "SpeziFoundationObjC"), .product(name: "Atomics", package: "swift-atomics"), - .product(name: "Algorithms", package: "swift-algorithms"), - .target(name: "SpeziFoundationObjC") + .product(name: "Algorithms", package: "swift-algorithms") ], resources: [ .process("Resources") diff --git a/Tests/SpeziFoundationTests/CalendarExtensionsTests.swift b/Tests/SpeziFoundationTests/CalendarExtensionsTests.swift index 85ca275..e546af6 100644 --- a/Tests/SpeziFoundationTests/CalendarExtensionsTests.swift +++ b/Tests/SpeziFoundationTests/CalendarExtensionsTests.swift @@ -12,6 +12,17 @@ import XCTest final class CalendarExtensionsTests: XCTestCase { private let cal = Calendar.current + override class func setUp() { + super.setUp() + XCTFail( + """ + XCTest environment info: + - calendar: \(Calendar.current) + - locale: \(Locale.current) + """ + ) + } + private func makeDate(year: Int, month: Int, day: Int, hour: Int, minute: Int = 0, second: Int = 0) throws -> Date { let components = DateComponents(year: year, month: month, day: day, hour: hour, minute: minute, second: second) return try XCTUnwrap(cal.date(from: components)) diff --git a/Tests/SpeziFoundationTests/OrderedArrayTests.swift b/Tests/SpeziFoundationTests/OrderedArrayTests.swift index 784ea88..3f2cc4c 100644 --- a/Tests/SpeziFoundationTests/OrderedArrayTests.swift +++ b/Tests/SpeziFoundationTests/OrderedArrayTests.swift @@ -87,6 +87,6 @@ final class OrderedArrayTests: XCTestCase { let capacity = array.capacity array.removeAll(keepingCapacity: true) XCTAssertTrue(array.isEmpty) - XCTAssertEqual(array.capacity, array.capacity) + XCTAssertEqual(array.capacity, capacity) } } From 298832b9ca3414c9f1ec4bb9af22391b50baa9d2 Mon Sep 17 00:00:00 2001 From: Lukas Kollmer Date: Sun, 12 Jan 2025 13:18:25 +0100 Subject: [PATCH 06/11] try if this works --- .../CalendarExtensionsTests.swift | 461 +++++++++++------- 1 file changed, 275 insertions(+), 186 deletions(-) diff --git a/Tests/SpeziFoundationTests/CalendarExtensionsTests.swift b/Tests/SpeziFoundationTests/CalendarExtensionsTests.swift index e546af6..8b9257c 100644 --- a/Tests/SpeziFoundationTests/CalendarExtensionsTests.swift +++ b/Tests/SpeziFoundationTests/CalendarExtensionsTests.swift @@ -8,20 +8,38 @@ import SpeziFoundation import XCTest - -final class CalendarExtensionsTests: XCTestCase { - private let cal = Calendar.current +private struct RegionConfiguration: Hashable { + let locale: Locale // swiftlint:disable:this type_contents_order + let timeZone: TimeZone // swiftlint:disable:this type_contents_order + + static let losAngeles = Self( + locale: .init(identifier: "en_US"), + timeZone: .init(identifier: "America/Los_Angeles")! // swiftlint:disable:this force_unwrapping + ) + + static let berlin = Self( + locale: .init(identifier: "en_DE"), + timeZone: .init(identifier: "Europe/Berlin")! // swiftlint:disable:this force_unwrapping + ) - override class func setUp() { - super.setUp() - XCTFail( - """ - XCTest environment info: - - calendar: \(Calendar.current) - - locale: \(Locale.current) - """ - ) + static let current = Self(locale: .current, timeZone: .current) + + /// Returns a copy of the calendar, with the locale and time zone set based on the region. + func applying(to calendar: Calendar) -> Calendar { + var cal = calendar + cal.locale = locale + cal.timeZone = timeZone + return cal } +} + + +/// Tests for the `Calendar` extensions. +/// - Note: most tests, by default simply run in the context of the current system locale and time zone. +/// For some tests, however, this is manually overwritten (mainly to deal with locale-dependent differences +/// such as DST, first weekday, etc). +final class CalendarExtensionsTests: XCTestCase { // swiftlint:disable:this type_body_length + private var cal = Calendar.current private func makeDate(year: Int, month: Int, day: Int, hour: Int, minute: Int = 0, second: Int = 0) throws -> Date { let components = DateComponents(year: year, month: month, day: day, hour: hour, minute: minute, second: second) @@ -29,40 +47,72 @@ final class CalendarExtensionsTests: XCTestCase { } + /// Runs a block in the calendar-context of a region + private func withRegion(_ region: RegionConfiguration, _ block: () throws -> Void) rethrows { + let prevCal = cal + cal = region.applying(to: cal) + defer { + cal = prevCal + } + try block() + } + + /// Runs a block multiple times, in different calendar-contexts, once for each specified region. + private func withRegions(_ regions: RegionConfiguration..., block: () throws -> Void) rethrows { + for region in Set(regions) { + try withRegion(region, block) + } + } + + func testRangeComputations() throws { - XCTAssertEqual( - cal.rangeOfHour(for: try makeDate(year: 2024, month: 12, day: 27, hour: 14, minute: 12)), - (try makeDate(year: 2024, month: 12, day: 27, hour: 14))..<(try makeDate(year: 2024, month: 12, day: 27, hour: 15)) - ) - - XCTAssertEqual( - cal.rangeOfDay(for: try makeDate(year: 2024, month: 12, day: 27, hour: 14, minute: 12)), - (try makeDate(year: 2024, month: 12, day: 27, hour: 00))..<(try makeDate(year: 2024, month: 12, day: 28, hour: 0)) - ) - - XCTAssertEqual( - cal.rangeOfWeek(for: try makeDate(year: 2024, month: 12, day: 27, hour: 14, minute: 12)), - (try makeDate(year: 2024, month: 12, day: 23, hour: 00))..<(try makeDate(year: 2024, month: 12, day: 30, hour: 0)) - ) - XCTAssertEqual( - cal.rangeOfWeek(for: try makeDate(year: 2024, month: 12, day: 31, hour: 14, minute: 12)), - (try makeDate(year: 2024, month: 12, day: 30, hour: 00))..<(try makeDate(year: 2025, month: 01, day: 06, hour: 0)) - ) + try withRegions(.current, .losAngeles, .berlin) { + XCTAssertEqual( + cal.rangeOfHour(for: try makeDate(year: 2024, month: 12, day: 27, hour: 14, minute: 12)), + (try makeDate(year: 2024, month: 12, day: 27, hour: 14))..<(try makeDate(year: 2024, month: 12, day: 27, hour: 15)) + ) + XCTAssertEqual( + cal.rangeOfDay(for: try makeDate(year: 2024, month: 12, day: 27, hour: 14, minute: 12)), + (try makeDate(year: 2024, month: 12, day: 27, hour: 00))..<(try makeDate(year: 2024, month: 12, day: 28, hour: 0)) + ) + } - XCTAssertEqual( - cal.rangeOfMonth(for: try makeDate(year: 2024, month: 12, day: 31, hour: 14, minute: 12)), - (try makeDate(year: 2024, month: 12, day: 01, hour: 00))..<(try makeDate(year: 2025, month: 01, day: 01, hour: 0)) - ) + try withRegion(.losAngeles) { + XCTAssertEqual( + cal.rangeOfWeek(for: try makeDate(year: 2024, month: 12, day: 27, hour: 14, minute: 12)), + (try makeDate(year: 2024, month: 12, day: 22, hour: 00))..<(try makeDate(year: 2024, month: 12, day: 29, hour: 0)) + ) + XCTAssertEqual( + cal.rangeOfWeek(for: try makeDate(year: 2024, month: 12, day: 31, hour: 14, minute: 12)), + (try makeDate(year: 2024, month: 12, day: 29, hour: 00))..<(try makeDate(year: 2025, month: 01, day: 05, hour: 0)) + ) + } - XCTAssertEqual( - cal.rangeOfYear(for: try makeDate(year: 2024, month: 12, day: 31, hour: 14, minute: 12)), - (try makeDate(year: 2024, month: 01, day: 01, hour: 00))..<(try makeDate(year: 2025, month: 01, day: 01, hour: 0)) - ) + try withRegion(.berlin) { + XCTAssertEqual( + cal.rangeOfWeek(for: try makeDate(year: 2024, month: 12, day: 27, hour: 14, minute: 12)), + (try makeDate(year: 2024, month: 12, day: 23, hour: 00))..<(try makeDate(year: 2024, month: 12, day: 30, hour: 0)) + ) + XCTAssertEqual( + cal.rangeOfWeek(for: try makeDate(year: 2024, month: 12, day: 31, hour: 14, minute: 12)), + (try makeDate(year: 2024, month: 12, day: 30, hour: 00))..<(try makeDate(year: 2025, month: 01, day: 06, hour: 0)) + ) + } - XCTAssertEqual( - cal.rangeOfMonth(for: try makeDate(year: 2024, month: 02, day: 29, hour: 14, minute: 12)), - (try makeDate(year: 2024, month: 02, day: 01, hour: 0))..<(try makeDate(year: 2024, month: 03, day: 01, hour: 0)) - ) + try withRegions(.current, .losAngeles, .berlin) { + XCTAssertEqual( + cal.rangeOfMonth(for: try makeDate(year: 2024, month: 12, day: 31, hour: 14, minute: 12)), + (try makeDate(year: 2024, month: 12, day: 01, hour: 00))..<(try makeDate(year: 2025, month: 01, day: 01, hour: 0)) + ) + XCTAssertEqual( + cal.rangeOfYear(for: try makeDate(year: 2024, month: 12, day: 31, hour: 14, minute: 12)), + (try makeDate(year: 2024, month: 01, day: 01, hour: 00))..<(try makeDate(year: 2025, month: 01, day: 01, hour: 0)) + ) + XCTAssertEqual( + cal.rangeOfMonth(for: try makeDate(year: 2024, month: 02, day: 29, hour: 14, minute: 12)), + (try makeDate(year: 2024, month: 02, day: 01, hour: 0))..<(try makeDate(year: 2024, month: 03, day: 01, hour: 0)) + ) + } } @@ -98,165 +148,204 @@ final class CalendarExtensionsTests: XCTestCase { ) } - try imp( - start: (year: 2021, month: 02, day: 02, hour: 07, minute: 20), - end: (year: 2021, month: 02, day: 02, hour: 08, minute: 05), - fn: cal.countDistinctHours(from:to:), - expected: 2 - ) - try imp( - start: (year: 2021, month: 02, day: 02, hour: 07, minute: 20), - end: (year: 2021, month: 02, day: 02, hour: 07, minute: 21), - fn: cal.countDistinctHours(from:to:), - expected: 1 - ) - try imp( - start: (year: 2021, month: 02, day: 02, hour: 07, minute: 20), - end: (year: 2021, month: 02, day: 02, hour: 07, minute: 21), - fn: cal.countDistinctHours(from:to:), - expected: 1 - ) - - for transition in cal.timeZone.nextDSTTransitions(maxCount: 10) { - let range = cal.rangeOfDay(for: transition.date) - print(transition) - XCTAssertEqual( - cal.countDistinctHours(from: range.lowerBound, to: range.upperBound.addingTimeInterval(-1)), - 24 - Int(transition.change / 3600) + try withRegions(.current, .losAngeles, .berlin) { // swiftlint:disable:this closure_body_length + try imp( + start: (year: 2021, month: 02, day: 02, hour: 07, minute: 20), + end: (year: 2021, month: 02, day: 02, hour: 08, minute: 05), + fn: cal.countDistinctHours(from:to:), + expected: 2 + ) + try imp( + start: (year: 2021, month: 02, day: 02, hour: 07, minute: 20), + end: (year: 2021, month: 02, day: 02, hour: 07, minute: 21), + fn: cal.countDistinctHours(from:to:), + expected: 1 + ) + try imp( + start: (year: 2021, month: 02, day: 02, hour: 07, minute: 20), + end: (year: 2021, month: 02, day: 02, hour: 07, minute: 21), + fn: cal.countDistinctHours(from:to:), + expected: 1 + ) + + for transition in cal.timeZone.nextDSTTransitions(maxCount: 10) { + let range = cal.rangeOfDay(for: transition.date) + XCTAssertEqual( + cal.countDistinctHours(from: range.lowerBound, to: range.upperBound.addingTimeInterval(-1)), + 24 - Int(transition.change / 3600) + ) + } + + try imp( + start: (year: 2021, month: 02, day: 02, hour: 09, minute: 00), + end: (year: 2021, month: 02, day: 03, hour: 07, minute: 00), + fn: cal.countDistinctDays(from:to:), + expected: 2 + ) + try imp( + start: (year: 2021, month: 02, day: 28, hour: 09, minute: 00), + end: (year: 2021, month: 03, day: 01, hour: 07, minute: 00), + fn: cal.countDistinctDays(from:to:), + expected: 2 + ) + try imp( + start: (year: 2024, month: 02, day: 28, hour: 09, minute: 00), + end: (year: 2024, month: 03, day: 01, hour: 07, minute: 00), + fn: cal.countDistinctDays(from:to:), + expected: 3 // leap year + ) + + let now = Date() + imp( + start: now, + end: now, + fn: cal.countDistinctDays(from:to:), + expected: 1 + ) + + imp( + start: cal.startOfDay(for: now), + end: cal.startOfNextDay(for: now), + fn: cal.countDistinctDays(from:to:), + expected: 2 + ) + try imp( + start: (year: 2021, month: 02, day: 07, hour: 07, minute: 00), + end: (year: 2021, month: 02, day: 07, hour: 07, minute: 15), + fn: cal.countDistinctWeeks(from:to:), + expected: 1 ) } + try withRegion(.losAngeles) { + try imp( + start: (year: 2021, month: 02, day: 07, hour: 07, minute: 00), + end: (year: 2021, month: 02, day: 08, hour: 07, minute: 15), + fn: cal.countDistinctWeeks(from:to:), + expected: 1 + ) + try imp( + start: (year: 2024, month: 12, day: 29, hour: 07, minute: 00), + end: (year: 2025, month: 01, day: 02, hour: 07, minute: 15), + fn: cal.countDistinctWeeks(from:to:), + expected: 1 + ) + try imp( + start: (year: 2021, month: 02, day: 06, hour: 07, minute: 00), + end: (year: 2021, month: 02, day: 07, hour: 07, minute: 15), + fn: cal.countDistinctWeeks(from:to:), + expected: 2 + ) + try imp( + start: (year: 2024, month: 02, day: 29, hour: 07, minute: 00), + end: (year: 2024, month: 03, day: 03, hour: 07, minute: 15), + fn: cal.countDistinctWeeks(from:to:), + expected: 2 + ) + } + try withRegion(.berlin) { + try imp( + start: (year: 2021, month: 02, day: 07, hour: 07, minute: 00), + end: (year: 2021, month: 02, day: 08, hour: 07, minute: 15), + fn: cal.countDistinctWeeks(from:to:), + expected: 2 + ) + try imp( + start: (year: 2024, month: 12, day: 29, hour: 07, minute: 00), + end: (year: 2025, month: 01, day: 02, hour: 07, minute: 15), + fn: cal.countDistinctWeeks(from:to:), + expected: 2 + ) + } - try imp( - start: (year: 2021, month: 02, day: 02, hour: 09, minute: 00), - end: (year: 2021, month: 02, day: 03, hour: 07, minute: 00), - fn: cal.countDistinctDays(from:to:), - expected: 2 - ) - try imp( - start: (year: 2021, month: 02, day: 28, hour: 09, minute: 00), - end: (year: 2021, month: 03, day: 01, hour: 07, minute: 00), - fn: cal.countDistinctDays(from:to:), - expected: 2 - ) - try imp( - start: (year: 2024, month: 02, day: 28, hour: 09, minute: 00), - end: (year: 2024, month: 03, day: 01, hour: 07, minute: 00), - fn: cal.countDistinctDays(from:to:), - expected: 3 // leap year - ) - let now = Date() - imp( - start: now, - end: now, - fn: cal.countDistinctDays(from:to:), - expected: 1 - ) - imp( - start: cal.startOfDay(for: now), - end: cal.startOfNextDay(for: now), - fn: cal.countDistinctDays(from:to:), - expected: 2 - ) - - try imp( - start: (year: 2021, month: 02, day: 07, hour: 07, minute: 00), - end: (year: 2021, month: 02, day: 07, hour: 07, minute: 15), - fn: cal.countDistinctWeeks(from:to:), - expected: 1 - ) - try imp( - start: (year: 2021, month: 02, day: 07, hour: 07, minute: 00), - end: (year: 2021, month: 02, day: 08, hour: 07, minute: 15), - fn: cal.countDistinctWeeks(from:to:), - expected: 2 - ) - try imp( - start: (year: 2024, month: 12, day: 29, hour: 07, minute: 00), - end: (year: 2025, month: 01, day: 02, hour: 07, minute: 15), - fn: cal.countDistinctWeeks(from:to:), - expected: 2 - ) - try imp( - start: (year: 2024, month: 12, day: 30, hour: 07, minute: 00), - end: (year: 2025, month: 01, day: 02, hour: 07, minute: 15), - fn: cal.countDistinctWeeks(from:to:), - expected: 1 - ) - - try imp( - start: (year: 2022, month: 01, day: 12, hour: 05, minute: 00), - end: (year: 2022, month: 08, day: 11, hour: 17, minute: 00), - fn: cal.countDistinctMonths(from:to:), - expected: 8 - ) - - try imp( - start: (year: 2022, month: 01, day: 12, hour: 05, minute: 00), - end: (year: 2022, month: 08, day: 11, hour: 17, minute: 00), - fn: cal.countDistinctYears(from:to:), - expected: 1 - ) - try imp( - start: (year: 2022, month: 01, day: 12, hour: 05, minute: 00), - end: (year: 2023, month: 08, day: 11, hour: 17, minute: 00), - fn: cal.countDistinctYears(from:to:), - expected: 2 - ) + try withRegions(.current, .losAngeles, .berlin) { + try imp( + start: (year: 2024, month: 12, day: 30, hour: 07, minute: 00), + end: (year: 2025, month: 01, day: 02, hour: 07, minute: 15), + fn: cal.countDistinctWeeks(from:to:), + expected: 1 + ) + + try imp( + start: (year: 2022, month: 01, day: 12, hour: 05, minute: 00), + end: (year: 2022, month: 08, day: 11, hour: 17, minute: 00), + fn: cal.countDistinctMonths(from:to:), + expected: 8 + ) + + try imp( + start: (year: 2022, month: 01, day: 12, hour: 05, minute: 00), + end: (year: 2022, month: 08, day: 11, hour: 17, minute: 00), + fn: cal.countDistinctYears(from:to:), + expected: 1 + ) + try imp( + start: (year: 2022, month: 01, day: 12, hour: 05, minute: 00), + end: (year: 2023, month: 08, day: 11, hour: 17, minute: 00), + fn: cal.countDistinctYears(from:to:), + expected: 2 + ) + } } func testOffsets() throws { - let now = Date() - XCTAssertEqual(cal.offsetInDays( - from: now, - to: now - ), 0) - - XCTAssertEqual(cal.offsetInDays( - from: try makeDate(year: 2024, month: 01, day: 01, hour: 00), - to: try makeDate(year: 2024, month: 01, day: 08, hour: 00) - ), 7) - - XCTAssertEqual(cal.offsetInDays( - from: try makeDate(year: 2024, month: 01, day: 08, hour: 00), - to: try makeDate(year: 2024, month: 01, day: 01, hour: 00) - ), -7) - - XCTAssertEqual(cal.offsetInDays( - from: try makeDate(year: 2025, month: 02, day: 27, hour: 00), - to: try makeDate(year: 2025, month: 03, day: 02, hour: 00) - ), 3) - XCTAssertEqual(cal.offsetInDays( - from: try makeDate(year: 2024, month: 02, day: 27, hour: 00), - to: try makeDate(year: 2024, month: 03, day: 02, hour: 00) - ), 4) + try withRegions(.current, .losAngeles, .berlin) { + let now = Date() + XCTAssertEqual(cal.offsetInDays( + from: now, + to: now + ), 0) + + XCTAssertEqual(cal.offsetInDays( + from: try makeDate(year: 2024, month: 01, day: 01, hour: 00), + to: try makeDate(year: 2024, month: 01, day: 08, hour: 00) + ), 7) + + XCTAssertEqual(cal.offsetInDays( + from: try makeDate(year: 2024, month: 01, day: 08, hour: 00), + to: try makeDate(year: 2024, month: 01, day: 01, hour: 00) + ), -7) + + XCTAssertEqual(cal.offsetInDays( + from: try makeDate(year: 2025, month: 02, day: 27, hour: 00), + to: try makeDate(year: 2025, month: 03, day: 02, hour: 00) + ), 3) + XCTAssertEqual(cal.offsetInDays( + from: try makeDate(year: 2024, month: 02, day: 27, hour: 00), + to: try makeDate(year: 2024, month: 03, day: 02, hour: 00) + ), 4) + } } func testRelativeOperations() throws { - XCTAssertEqual( - cal.startOfPrevDay(for: try makeDate(year: 2025, month: 01, day: 11, hour: 19, minute: 07)), - try makeDate(year: 2025, month: 01, day: 10, hour: 00, minute: 00) - ) - - XCTAssertEqual( - cal.startOfNextMonth(for: try makeDate(year: 2025, month: 01, day: 11, hour: 19, minute: 07)), - try makeDate(year: 2025, month: 02, day: 01, hour: 00, minute: 00) - ) - - XCTAssertEqual( - cal.startOfPrevYear(for: try makeDate(year: 2025, month: 01, day: 11, hour: 19, minute: 07)), - try makeDate(year: 2024, month: 01, day: 01, hour: 00, minute: 00) - ) + try withRegions(.current, .losAngeles, .berlin) { + XCTAssertEqual( + cal.startOfPrevDay(for: try makeDate(year: 2025, month: 01, day: 11, hour: 19, minute: 07)), + try makeDate(year: 2025, month: 01, day: 10, hour: 00, minute: 00) + ) + + XCTAssertEqual( + cal.startOfNextMonth(for: try makeDate(year: 2025, month: 01, day: 11, hour: 19, minute: 07)), + try makeDate(year: 2025, month: 02, day: 01, hour: 00, minute: 00) + ) + + XCTAssertEqual( + cal.startOfPrevYear(for: try makeDate(year: 2025, month: 01, day: 11, hour: 19, minute: 07)), + try makeDate(year: 2024, month: 01, day: 01, hour: 00, minute: 00) + ) + } } func testNumberOfDaysInMonth() throws { - XCTAssertEqual(cal.numberOfDaysInMonth(for: try makeDate(year: 2025, month: 01, day: 01, hour: 00)), 31) - XCTAssertEqual(cal.numberOfDaysInMonth(for: try makeDate(year: 2024, month: 12, day: 01, hour: 00)), 31) - XCTAssertEqual(cal.numberOfDaysInMonth(for: try makeDate(year: 2024, month: 11, day: 01, hour: 00)), 30) - XCTAssertEqual(cal.numberOfDaysInMonth(for: try makeDate(year: 2025, month: 02, day: 01, hour: 00)), 28) - XCTAssertEqual(cal.numberOfDaysInMonth(for: try makeDate(year: 2024, month: 02, day: 01, hour: 00)), 29) + try withRegions(.current, .losAngeles, .berlin) { + XCTAssertEqual(cal.numberOfDaysInMonth(for: try makeDate(year: 2025, month: 01, day: 01, hour: 00)), 31) + XCTAssertEqual(cal.numberOfDaysInMonth(for: try makeDate(year: 2024, month: 12, day: 01, hour: 00)), 31) + XCTAssertEqual(cal.numberOfDaysInMonth(for: try makeDate(year: 2024, month: 11, day: 01, hour: 00)), 30) + XCTAssertEqual(cal.numberOfDaysInMonth(for: try makeDate(year: 2025, month: 02, day: 01, hour: 00)), 28) + XCTAssertEqual(cal.numberOfDaysInMonth(for: try makeDate(year: 2024, month: 02, day: 01, hour: 00)), 29) + } } } From 6220a5edd6e7260d5cfb3c5f5ed0d190c5188614 Mon Sep 17 00:00:00 2001 From: Lukas Kollmer Date: Sun, 12 Jan 2025 13:21:32 +0100 Subject: [PATCH 07/11] fix availability check --- Sources/SpeziFoundation/Misc/Calendar.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/SpeziFoundation/Misc/Calendar.swift b/Sources/SpeziFoundation/Misc/Calendar.swift index 48c6c51..dec5e1f 100644 --- a/Sources/SpeziFoundation/Misc/Calendar.swift +++ b/Sources/SpeziFoundation/Misc/Calendar.swift @@ -338,7 +338,7 @@ extension DateComponents { case .nanosecond: return self.nanosecond case .dayOfYear: - if #available(iOS 18, macOS 15, *) { + if #available(iOS 18, macOS 15, watchOS 11, tvOS 18, visionOS 2, *) { return self.dayOfYear } else { // This branch is practically unreachable, since the availability of the `Calendar.Component.dayOfYear` From d9065fe2d5ae6a1e253bb3b50b75dd0d735cf393 Mon Sep 17 00:00:00 2001 From: Lukas Kollmer Date: Sun, 12 Jan 2025 14:22:54 +0100 Subject: [PATCH 08/11] some more tests --- Package.swift | 9 ++-- .../SpeziFoundation/Misc/BinarySearch.swift | 8 ---- Sources/SpeziFoundation/Misc/Calendar.swift | 5 +- .../Misc/ObjCExceptionHandling.swift | 6 +-- .../CalendarExtensionsTests.swift | 48 ++++++++++++++++++- .../SequenceExtensionTests.swift | 29 +++++++++++ 6 files changed, 86 insertions(+), 19 deletions(-) create mode 100644 Tests/SpeziFoundationTests/SequenceExtensionTests.swift diff --git a/Package.swift b/Package.swift index 55371a4..e1f90cb 100644 --- a/Package.swift +++ b/Package.swift @@ -27,7 +27,8 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/apple/swift-atomics.git", from: "1.2.0"), - .package(url: "https://github.com/apple/swift-algorithms", from: "1.2.0") + .package(url: "https://github.com/apple/swift-algorithms", from: "1.2.0"), + .package(url: "https://github.com/StanfordBDHG/XCTRuntimeAssertions", from: "1.1.3") ] + swiftLintPackage(), targets: [ .target( @@ -35,7 +36,8 @@ let package = Package( dependencies: [ .target(name: "SpeziFoundationObjC"), .product(name: "Atomics", package: "swift-atomics"), - .product(name: "Algorithms", package: "swift-algorithms") + .product(name: "Algorithms", package: "swift-algorithms"), + .product(name: "XCTRuntimeAssertions", package: "XCTRuntimeAssertions") ], resources: [ .process("Resources") @@ -49,7 +51,8 @@ let package = Package( .testTarget( name: "SpeziFoundationTests", dependencies: [ - .target(name: "SpeziFoundation") + .target(name: "SpeziFoundation"), + .product(name: "XCTRuntimeAssertions", package: "XCTRuntimeAssertions") ], plugins: [] + swiftLintPlugin() ) diff --git a/Sources/SpeziFoundation/Misc/BinarySearch.swift b/Sources/SpeziFoundation/Misc/BinarySearch.swift index 04afa9e..b268a48 100644 --- a/Sources/SpeziFoundation/Misc/BinarySearch.swift +++ b/Sources/SpeziFoundation/Misc/BinarySearch.swift @@ -15,14 +15,6 @@ public enum BinarySearchIndexResult { case found(Index) /// The searched-for element was not found in the collection, but if it were a member of the collection, it would belong at the specified index. case notFound(Index) - - /// The index at which the element is/belongs. - public var index: Index { - switch self { - case .found(let index), .notFound(let index): - index - } - } } diff --git a/Sources/SpeziFoundation/Misc/Calendar.swift b/Sources/SpeziFoundation/Misc/Calendar.swift index dec5e1f..83b933f 100644 --- a/Sources/SpeziFoundation/Misc/Calendar.swift +++ b/Sources/SpeziFoundation/Misc/Calendar.swift @@ -7,13 +7,14 @@ // import Foundation +import XCTRuntimeAssertions private func tryUnwrap(_ value: T?, _ message: String) -> T { if let value { return value } else { - fatalError(message) + preconditionFailure(message) } } @@ -348,7 +349,7 @@ extension DateComponents { return nil } case .calendar, .timeZone, .isLeapMonth: - fatalError("not supported") // different type (not an int) :/ + preconditionFailure("not supported") // different type (not an int) :/ @unknown default: return nil } diff --git a/Sources/SpeziFoundation/Misc/ObjCExceptionHandling.swift b/Sources/SpeziFoundation/Misc/ObjCExceptionHandling.swift index 8b5fb2e..013670e 100644 --- a/Sources/SpeziFoundation/Misc/ObjCExceptionHandling.swift +++ b/Sources/SpeziFoundation/Misc/ObjCExceptionHandling.swift @@ -11,12 +11,8 @@ import SpeziFoundationObjC /// A `Swift.Error` wrapping around an `NSException`. -public struct CaughtNSException: Error, LocalizedError, @unchecked Sendable { +public struct CaughtNSException: Error, @unchecked Sendable { public let exception: NSException - - public var errorDescription: String { - "\(Self.self): \(exception.description)" - } } diff --git a/Tests/SpeziFoundationTests/CalendarExtensionsTests.swift b/Tests/SpeziFoundationTests/CalendarExtensionsTests.swift index 8b9257c..18308db 100644 --- a/Tests/SpeziFoundationTests/CalendarExtensionsTests.swift +++ b/Tests/SpeziFoundationTests/CalendarExtensionsTests.swift @@ -7,6 +7,7 @@ import SpeziFoundation import XCTest +import XCTRuntimeAssertions private struct RegionConfiguration: Hashable { let locale: Locale // swiftlint:disable:this type_contents_order @@ -330,7 +331,6 @@ final class CalendarExtensionsTests: XCTestCase { // swiftlint:disable:this type cal.startOfNextMonth(for: try makeDate(year: 2025, month: 01, day: 11, hour: 19, minute: 07)), try makeDate(year: 2025, month: 02, day: 01, hour: 00, minute: 00) ) - XCTAssertEqual( cal.startOfPrevYear(for: try makeDate(year: 2025, month: 01, day: 11, hour: 19, minute: 07)), try makeDate(year: 2024, month: 01, day: 01, hour: 00, minute: 00) @@ -348,4 +348,50 @@ final class CalendarExtensionsTests: XCTestCase { // swiftlint:disable:this type XCTAssertEqual(cal.numberOfDaysInMonth(for: try makeDate(year: 2024, month: 02, day: 01, hour: 00)), 29) } } + + + func testDateComponentsSubscript() throws { + let components = DateComponents( + calendar: .current, + timeZone: .current, + era: .random(in: 0...1000), + year: .random(in: 0...4000), + month: .random(in: 1...12), + day: .random(in: 1...31), + hour: .random(in: 0...23), + minute: .random(in: 0...59), + second: .random(in: 0...59), + nanosecond: .random(in: 0..<1_000_000_000), + weekday: .random(in: 1...7), + weekdayOrdinal: .random(in: 1...4), + quarter: .random(in: 1...4), + weekOfMonth: .random(in: 1...4), + weekOfYear: .random(in: 1...52), + yearForWeekOfYear: .random(in: 0...4000) + ) + + try XCTRuntimePrecondition { + _ = components[.calendar] + _ = components[.timeZone] + _ = components[.isLeapMonth] + } + + XCTAssertEqual(components[.era], components.era) + XCTAssertEqual(components[.year], components.year) + XCTAssertEqual(components[.quarter], components.quarter) + XCTAssertEqual(components[.month], components.month) + XCTAssertEqual(components[.day], components.day) + XCTAssertEqual(components[.hour], components.hour) + XCTAssertEqual(components[.minute], components.minute) + XCTAssertEqual(components[.second], components.second) + XCTAssertEqual(components[.nanosecond], components.nanosecond) + XCTAssertEqual(components[.weekday], components.weekday) + XCTAssertEqual(components[.weekdayOrdinal], components.weekdayOrdinal) + XCTAssertEqual(components[.weekOfMonth], components.weekOfMonth) + XCTAssertEqual(components[.weekOfYear], components.weekOfYear) + XCTAssertEqual(components[.yearForWeekOfYear], components.yearForWeekOfYear) + if #available(macOS 15, iOS 18, watchOS 11, tvOS 18, visionOS 2, *) { + XCTAssertEqual(components[.dayOfYear], components.dayOfYear) + } + } } diff --git a/Tests/SpeziFoundationTests/SequenceExtensionTests.swift b/Tests/SpeziFoundationTests/SequenceExtensionTests.swift new file mode 100644 index 0000000..9f0f306 --- /dev/null +++ b/Tests/SpeziFoundationTests/SequenceExtensionTests.swift @@ -0,0 +1,29 @@ +// +// This source file is part of the Stanford Spezi open-source project +// +// SPDX-FileCopyrightText: 2025 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import SpeziFoundation +import XCTest + + +final class SequenceExtensionTests: XCTestCase { + func testMapIntoSet() { + XCTAssertEqual([0, 1, 2, 3, 4].mapIntoSet { $0 * 2 }, [0, 2, 4, 6, 8]) + XCTAssertEqual([0, 1, 2, 3, 4].mapIntoSet { $0 / 2 }, [0, 1, 2]) + } + + + func testRemoveAtIndices() { + var array = Array(0...9) + array.remove(at: [0, 7, 5, 2]) + XCTAssertEqual(array, [1, 3, 4, 6, 8, 9]) + + array = Array(0...9) + array.remove(at: [0, 7, 5, 2] as IndexSet) + XCTAssertEqual(array, [1, 3, 4, 6, 8, 9]) + } +} From fdc21ce73baeef8a9c796b05db1d758e132772e5 Mon Sep 17 00:00:00 2001 From: Lukas Kollmer Date: Wed, 15 Jan 2025 02:49:07 +0100 Subject: [PATCH 09/11] turn's out `DateComponents.value(for:)` this is already a thing --- Sources/SpeziFoundation/Misc/Calendar.swift | 55 +------------------ .../CalendarExtensionsTests.swift | 46 ---------------- 2 files changed, 1 insertion(+), 100 deletions(-) diff --git a/Sources/SpeziFoundation/Misc/Calendar.swift b/Sources/SpeziFoundation/Misc/Calendar.swift index 83b933f..871141e 100644 --- a/Sources/SpeziFoundation/Misc/Calendar.swift +++ b/Sources/SpeziFoundation/Misc/Calendar.swift @@ -206,7 +206,7 @@ extension Calendar { from: startOfComponentFn(startDate), to: startOfComponentFn(endDate) ) - return tryUnwrap(diff[component], "Unable to get component '\(component)'") + 1 + return tryUnwrap(diff.value(for: component), "Unable to get component '\(component)'") + 1 } } @@ -304,59 +304,6 @@ extension Calendar { } -extension DateComponents { - /// Returns the integer value of the specified component. - /// - Note: This is only valid for components which have an `Int` value (e.g., `day`, `month`, `hour`, `minute`, etc.) - /// Passing a non-`Int` component (e.g., `calendar` or `timeZone`) will result in the program being terminated. - public subscript(component: Calendar.Component) -> Int? { - switch component { - case .era: - return self.era - case .year: - return self.year - case .month: - return self.month - case .day: - return self.day - case .hour: - return self.hour - case .minute: - return self.minute - case .second: - return self.second - case .weekday: - return self.weekday - case .weekdayOrdinal: - return self.weekdayOrdinal - case .quarter: - return self.quarter - case .weekOfMonth: - return self.weekOfMonth - case .weekOfYear: - return self.weekOfYear - case .yearForWeekOfYear: - return self.yearForWeekOfYear - case .nanosecond: - return self.nanosecond - case .dayOfYear: - if #available(iOS 18, macOS 15, watchOS 11, tvOS 18, visionOS 2, *) { - return self.dayOfYear - } else { - // This branch is practically unreachable, since the availability of the `Calendar.Component.dayOfYear` - // case is the same as of the `DateComponents.dayOfYear` property. (Both are iOS 18+.) - // Meaning that in all situations where a caller is able to pass us this case, we'll also be able - // to access the property. - return nil - } - case .calendar, .timeZone, .isLeapMonth: - preconditionFailure("not supported") // different type (not an int) :/ - @unknown default: - return nil - } - } -} - - // MARK: DST extension TimeZone { diff --git a/Tests/SpeziFoundationTests/CalendarExtensionsTests.swift b/Tests/SpeziFoundationTests/CalendarExtensionsTests.swift index 18308db..842e603 100644 --- a/Tests/SpeziFoundationTests/CalendarExtensionsTests.swift +++ b/Tests/SpeziFoundationTests/CalendarExtensionsTests.swift @@ -348,50 +348,4 @@ final class CalendarExtensionsTests: XCTestCase { // swiftlint:disable:this type XCTAssertEqual(cal.numberOfDaysInMonth(for: try makeDate(year: 2024, month: 02, day: 01, hour: 00)), 29) } } - - - func testDateComponentsSubscript() throws { - let components = DateComponents( - calendar: .current, - timeZone: .current, - era: .random(in: 0...1000), - year: .random(in: 0...4000), - month: .random(in: 1...12), - day: .random(in: 1...31), - hour: .random(in: 0...23), - minute: .random(in: 0...59), - second: .random(in: 0...59), - nanosecond: .random(in: 0..<1_000_000_000), - weekday: .random(in: 1...7), - weekdayOrdinal: .random(in: 1...4), - quarter: .random(in: 1...4), - weekOfMonth: .random(in: 1...4), - weekOfYear: .random(in: 1...52), - yearForWeekOfYear: .random(in: 0...4000) - ) - - try XCTRuntimePrecondition { - _ = components[.calendar] - _ = components[.timeZone] - _ = components[.isLeapMonth] - } - - XCTAssertEqual(components[.era], components.era) - XCTAssertEqual(components[.year], components.year) - XCTAssertEqual(components[.quarter], components.quarter) - XCTAssertEqual(components[.month], components.month) - XCTAssertEqual(components[.day], components.day) - XCTAssertEqual(components[.hour], components.hour) - XCTAssertEqual(components[.minute], components.minute) - XCTAssertEqual(components[.second], components.second) - XCTAssertEqual(components[.nanosecond], components.nanosecond) - XCTAssertEqual(components[.weekday], components.weekday) - XCTAssertEqual(components[.weekdayOrdinal], components.weekdayOrdinal) - XCTAssertEqual(components[.weekOfMonth], components.weekOfMonth) - XCTAssertEqual(components[.weekOfYear], components.weekOfYear) - XCTAssertEqual(components[.yearForWeekOfYear], components.yearForWeekOfYear) - if #available(macOS 15, iOS 18, watchOS 11, tvOS 18, visionOS 2, *) { - XCTAssertEqual(components[.dayOfYear], components.dayOfYear) - } - } } From 0991350a35f451e69f2c3326050f8ee988d7f3e6 Mon Sep 17 00:00:00 2001 From: Lukas Kollmer Date: Sun, 19 Jan 2025 12:40:49 +0100 Subject: [PATCH 10/11] incorporate feedback --- Package.swift | 7 +-- .../Collection Builders/SetBuilder.swift | 2 +- Sources/SpeziFoundation/Misc/AnyArray.swift | 3 +- .../SpeziFoundation/Misc/BinarySearch.swift | 30 +++++----- Sources/SpeziFoundation/Misc/Calendar.swift | 27 ++++++--- .../Misc/ObjCExceptionHandling.swift | 2 +- .../SpeziFoundation/Misc/OrderedArray.swift | 1 - .../Misc/SequenceExtensions.swift | 29 +++++++++- .../SpeziFoundation/Misc/TimeoutError.swift | 1 - .../SpeziFoundation.docc/Calendar.md | 55 +++++++++++++++++++ .../CollectionAlgorithms.md | 26 +++++++++ .../SpeziFoundation.docc/SpeziFoundation.md | 28 +++++++--- .../SequenceExtensionTests.swift | 19 +++++++ 13 files changed, 189 insertions(+), 41 deletions(-) create mode 100644 Sources/SpeziFoundation/SpeziFoundation.docc/Calendar.md create mode 100644 Sources/SpeziFoundation/SpeziFoundation.docc/CollectionAlgorithms.md diff --git a/Package.swift b/Package.swift index e1f90cb..15375a7 100644 --- a/Package.swift +++ b/Package.swift @@ -27,8 +27,8 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/apple/swift-atomics.git", from: "1.2.0"), - .package(url: "https://github.com/apple/swift-algorithms", from: "1.2.0"), - .package(url: "https://github.com/StanfordBDHG/XCTRuntimeAssertions", from: "1.1.3") + .package(url: "https://github.com/apple/swift-algorithms.git", from: "1.2.0"), + .package(url: "https://github.com/StanfordBDHG/XCTRuntimeAssertions.git", from: "1.1.3") ] + swiftLintPackage(), targets: [ .target( @@ -45,8 +45,7 @@ let package = Package( plugins: [] + swiftLintPlugin() ), .target( - name: "SpeziFoundationObjC", - sources: nil + name: "SpeziFoundationObjC" ), .testTarget( name: "SpeziFoundationTests", diff --git a/Sources/SpeziFoundation/Collection Builders/SetBuilder.swift b/Sources/SpeziFoundation/Collection Builders/SetBuilder.swift index f656cba..de4c910 100644 --- a/Sources/SpeziFoundation/Collection Builders/SetBuilder.swift +++ b/Sources/SpeziFoundation/Collection Builders/SetBuilder.swift @@ -7,7 +7,7 @@ // -/// ``SetBuilder`` is a result builder for constructing `Set`s. +/// Result builder for constructing `Set`s. @resultBuilder public enum SetBuilder {} diff --git a/Sources/SpeziFoundation/Misc/AnyArray.swift b/Sources/SpeziFoundation/Misc/AnyArray.swift index 2fff9bf..6620d76 100644 --- a/Sources/SpeziFoundation/Misc/AnyArray.swift +++ b/Sources/SpeziFoundation/Misc/AnyArray.swift @@ -7,12 +7,11 @@ // -/// A type erase `Array`. +/// A type erased `Array`. public protocol AnyArray { /// The `Element` type of the Array. associatedtype Element - /// Provides access to the unwrapped array type. var unwrappedArray: [Element] { get } } diff --git a/Sources/SpeziFoundation/Misc/BinarySearch.swift b/Sources/SpeziFoundation/Misc/BinarySearch.swift index b268a48..3d51fd6 100644 --- a/Sources/SpeziFoundation/Misc/BinarySearch.swift +++ b/Sources/SpeziFoundation/Misc/BinarySearch.swift @@ -17,17 +17,18 @@ public enum BinarySearchIndexResult { case notFound(Index) } - extension BinarySearchIndexResult: Equatable where Index: Equatable {} extension BinarySearchIndexResult: Hashable where Index: Hashable {} extension Collection { - /// Performs a binary search over the collection, looking for - /// - parameter compare: closure that gets called with an element of the collection to determine, whether the binary search algorithm should go left/right, or has already found its target. - /// The closure should return `.orderedSame` if the element passed to it matches the search destination, `.orderedAscending` if the search should continue to the left, - /// and `.orderedDescending` if it should continue to the right. - /// E.g., when looking for a position for an element `x`, the closure should perform a `compare(x, $0)`. + /// Performs a binary search over the collection, determining the index of an element. + /// - parameter element: The element to locate + /// - parameter compare: Closure that gets called to determine how two `Element`s compare to each other. + /// The return value determines how the algorithm proceeds: if the closure returns `.orderedAscending` the search will continue to the left; + /// for `.orderedDescending` it will continue to the right. + /// - Note: If the element is not in the collection (i.e., the `compare` closure never returns `.orderedSame`), + /// the algorithm will compute and return the index where the element should be, if it were to become a member of the collection. public func binarySearchForIndex( of element: Element, using compare: (Element, Element) -> ComparisonResult @@ -35,14 +36,15 @@ extension Collection { binarySearchForIndex(of: element, in: startIndex.., using compare: (Element, Element) -> ComparisonResult @@ -62,7 +64,7 @@ extension Collection { /// Computes, for a non-empty range over the collection, the middle of the range. /// If the range is empty, this function will return `nil`. - public func middleIndex(of range: Range) -> Index? { + private func middleIndex(of range: Range) -> Index? { guard !range.isEmpty else { return nil } diff --git a/Sources/SpeziFoundation/Misc/Calendar.swift b/Sources/SpeziFoundation/Misc/Calendar.swift index 871141e..a6b52d3 100644 --- a/Sources/SpeziFoundation/Misc/Calendar.swift +++ b/Sources/SpeziFoundation/Misc/Calendar.swift @@ -212,7 +212,8 @@ extension Calendar { /// Returns the number of distinct weeks between the two dates. - /// E.g., if the first date is 2021-02-02 07:20 and the second is 2021-02-02 08:05, this would return 2. + /// + /// E.g., if the first date is `2021-02-02 07:20` and the second is `2021-02-02 08:05`, this would return 2. public func countDistinctHours(from startDate: Date, to endDate: Date) -> Int { _countDistinctNumberOfComponentUnits( from: startDate, @@ -223,7 +224,8 @@ extension Calendar { } /// Returns the number of distinct weeks between the two dates. - /// E.g., if the first date is 2021-02-02 09:00 and the second is 2021-02-03 07:00, this would return 2. + /// + /// E.g., if the first date is `2021-02-02 09:00` and the second is `2021-02-03 07:00`, this would return 2. public func countDistinctDays(from startDate: Date, to endDate: Date) -> Int { _countDistinctNumberOfComponentUnits( from: startDate, @@ -234,7 +236,8 @@ extension Calendar { } /// Returns the number of distinct weeks between the two dates. - /// E.g., if the first date is 2021-02-07 and the second is 2021-02-09, this would return 2. + /// + /// E.g., if the first date is `2021-02-07` and the second is `2021-02-09`, this would return 2. public func countDistinctWeeks(from startDate: Date, to endDate: Date) -> Int { _countDistinctNumberOfComponentUnits( from: startDate, @@ -245,7 +248,8 @@ extension Calendar { } /// Returns the number of distinct months between the two dates. - /// E.g., if the first date is 2021-02-25 and the second is 2021-04-12, this would return 3. + /// + /// E.g., if the first date is `2021-02-25` and the second is `2021-04-12`, this would return 3. public func countDistinctMonths(from startDate: Date, to endDate: Date) -> Int { _countDistinctNumberOfComponentUnits( from: startDate, @@ -256,7 +260,8 @@ extension Calendar { } /// Returns the number of distinct years between the two dates. - /// E.g., if the first date is 2021-02-25 and the second is 2022-02-25, this would return 2. + /// + /// E.g., if the first date is `2021-02-25` and the second is `2022-02-25`, this would return 2. public func countDistinctYears(from startDate: Date, to endDate: Date) -> Int { _countDistinctNumberOfComponentUnits( from: startDate, @@ -289,9 +294,10 @@ extension Calendar { extension Calendar { - /// Computes a new `Date` from `date`, obtained by setting `component` to zero. + /// Computes a new `Date` by setting a component to zero. + /// - parameter component: The component to set to zero. /// - parameter adjustOtherComponents: Determines whether the other components in the date should be adjusted when changing the component. - public func date(bySettingComponentToZero component: Component, of date: Date, adjustOtherComponents: Bool) -> Date? { + private func date(bySettingComponentToZero component: Component, of date: Date, adjustOtherComponents: Bool) -> Date? { if adjustOtherComponents { // If we're asked to adjust the other components, we can use Calendar's -date(bySetting...) function. return self.date(bySetting: component, value: 0, of: date) @@ -316,8 +322,11 @@ extension TimeZone { } /// The time zone's next DST transition. - public func nextDSTTransition(after referenceDate: Date = .now) -> DSTTransition? { - guard let nextDST = nextDaylightSavingTimeTransition(after: referenceDate) else { + /// - parameter date: The reference date for the DST transition check. + /// - returns: A ``DSTTransition`` object with information about the first DST transition that will occur after `date`, in the current time zone. + /// `nil` if the time zone was unable to determine the next transition. + public func nextDSTTransition(after date: Date = .now) -> DSTTransition? { + guard let nextDST = nextDaylightSavingTimeTransition(after: date) else { return nil } let before = nextDST.addingTimeInterval(-1) diff --git a/Sources/SpeziFoundation/Misc/ObjCExceptionHandling.swift b/Sources/SpeziFoundation/Misc/ObjCExceptionHandling.swift index 013670e..b49b78a 100644 --- a/Sources/SpeziFoundation/Misc/ObjCExceptionHandling.swift +++ b/Sources/SpeziFoundation/Misc/ObjCExceptionHandling.swift @@ -10,7 +10,7 @@ import Foundation import SpeziFoundationObjC -/// A `Swift.Error` wrapping around an `NSException`. +/// A Swift `Error` wrapping around a caught `NSException`. public struct CaughtNSException: Error, @unchecked Sendable { public let exception: NSException } diff --git a/Sources/SpeziFoundation/Misc/OrderedArray.swift b/Sources/SpeziFoundation/Misc/OrderedArray.swift index 543f236..e6b1f3d 100644 --- a/Sources/SpeziFoundation/Misc/OrderedArray.swift +++ b/Sources/SpeziFoundation/Misc/OrderedArray.swift @@ -177,7 +177,6 @@ extension OrderedArray { /// Removes from the array all elements which match the predicate - /// - returns: the indices of the removed elements. public mutating func removeAll(where predicate: (Element) -> Bool) { withInvariantCheckingTemporarilyDisabled { `self` in self.storage.removeAll(where: predicate) diff --git a/Sources/SpeziFoundation/Misc/SequenceExtensions.swift b/Sources/SpeziFoundation/Misc/SequenceExtensions.swift index c7fc831..869d4dd 100644 --- a/Sources/SpeziFoundation/Misc/SequenceExtensions.swift +++ b/Sources/SpeziFoundation/Misc/SequenceExtensions.swift @@ -11,10 +11,10 @@ import Algorithms extension Sequence { /// Maps a `Sequence` into a `Set`. + /// /// Compared to instead mapping the sequence into an Array (the default `map` function's return type) and then constructing a `Set` from that, /// this implementation can offer improved performance, since the intermediate Array is skipped. /// - Returns: a `Set` containing the results of applying `transform` to each element in the sequence. - /// - Throws: If `transform` throws. public func mapIntoSet(_ transform: (Element) throws -> NewElement) rethrows -> Set { var retval = Set() for element in self { @@ -22,6 +22,30 @@ extension Sequence { } return retval } + + /// An asynchronous version of Swift's `Sequence.reduce(_:_:)` function. + public func reduce( + _ initialResult: Result, + _ nextPartialResult: (Result, Element) async throws -> Result + ) async rethrows -> Result { + var result = initialResult + for element in self { + result = try await nextPartialResult(result, element) + } + return result + } + + /// An asynchronous version of Swift's `Sequence.reduce(into:_:)` function. + public func reduce( + into initial: Result, + _ nextPartialResult: (inout Result, Element) async throws -> Void + ) async rethrows -> Result { + var result = initial + for element in self { + try await nextPartialResult(&result, element) + } + return result + } } @@ -42,6 +66,9 @@ extension Sequence { extension RangeReplaceableCollection { /// Removes the elements at the specified indices from the collection. + /// - parameter indices: The indices at which elements should be removed. + /// + /// Useful e.g. when working with SwiftUI's `onDelete(perform:)` modifier. public mutating func remove(at indices: some Sequence) { for idx in indices.sorted().reversed() { self.remove(at: idx) diff --git a/Sources/SpeziFoundation/Misc/TimeoutError.swift b/Sources/SpeziFoundation/Misc/TimeoutError.swift index e82c036..4e906de 100644 --- a/Sources/SpeziFoundation/Misc/TimeoutError.swift +++ b/Sources/SpeziFoundation/Misc/TimeoutError.swift @@ -103,6 +103,5 @@ public func withTimeout(of timeout: Duration, perform action: sending () async - guard !Task.isCancelled else { return } - await action() } diff --git a/Sources/SpeziFoundation/SpeziFoundation.docc/Calendar.md b/Sources/SpeziFoundation/SpeziFoundation.docc/Calendar.md new file mode 100644 index 0000000..686ff68 --- /dev/null +++ b/Sources/SpeziFoundation/SpeziFoundation.docc/Calendar.md @@ -0,0 +1,55 @@ +# Calendar + + + +Extensions on the `Calendar` and `TimeZone` types, implementing operations + +## Topics + +### Relative date calculations +- ``Foundation/Calendar/startOfHour(for:)`` +- ``Foundation/Calendar/startOfNextHour(for:)`` +- ``Foundation/Calendar/startOfNextDay(for:)`` +- ``Foundation/Calendar/startOfPrevDay(for:)`` +- ``Foundation/Calendar/startOfWeek(for:)`` +- ``Foundation/Calendar/startOfNextWeek(for:)`` +- ``Foundation/Calendar/startOfMonth(for:)`` +- ``Foundation/Calendar/startOfNextMonth(for:)`` +- ``Foundation/Calendar/startOfYear(for:)`` +- ``Foundation/Calendar/startOfPrevYear(for:)`` +- ``Foundation/Calendar/startOfNextYear(for:)`` + +### Determining component-based Date ranges +- ``Foundation/Calendar/rangeOfHour(for:)`` +- ``Foundation/Calendar/rangeOfDay(for:)`` +- ``Foundation/Calendar/rangeOfWeek(for:)`` +- ``Foundation/Calendar/rangeOfMonth(for:)`` +- ``Foundation/Calendar/rangeOfYear(for:)`` + +### Date distances +- ``Foundation/Calendar/countDistinctHours(from:to:)`` +- ``Foundation/Calendar/countDistinctDays(from:to:)`` +- ``Foundation/Calendar/countDistinctWeeks(from:to:)`` +- ``Foundation/Calendar/countDistinctMonths(from:to:)`` +- ``Foundation/Calendar/countDistinctYears(from:to:)`` + +### Other +- ``Foundation/Calendar/numberOfDaysInMonth(for:)`` +- ``Foundation/Calendar/offsetInDays(from:to:)`` + + +### Time Zone + +Improved DST handling for Foundation's TimeZone type. + +- ``Foundation/TimeZone/DSTTransition`` +- ``Foundation/TimeZone/nextDSTTransition(after:)`` +- ``Foundation/TimeZone/nextDSTTransitions(maxCount:)`` diff --git a/Sources/SpeziFoundation/SpeziFoundation.docc/CollectionAlgorithms.md b/Sources/SpeziFoundation/SpeziFoundation.docc/CollectionAlgorithms.md new file mode 100644 index 0000000..173073f --- /dev/null +++ b/Sources/SpeziFoundation/SpeziFoundation.docc/CollectionAlgorithms.md @@ -0,0 +1,26 @@ +# CollectionAlgorithms + + + +Binary search over Collections, and other Sequence extensions + +## Topics + +### Binary Search +- ``Swift/Collection/binarySearchForIndex(of:using:)`` +- ``BinarySearchIndexResult`` + +### Sequence and Collection operations +- ``Swift/Sequence/isSorted(by:)`` +- ``Swift/Sequence/mapIntoSet(_:)`` +- ``Swift/Sequence/reduce(_:_:)`` +- ``Swift/Sequence/reduce(into:_:)`` +- ``Swift/RangeReplaceableCollection/remove(at:)`` diff --git a/Sources/SpeziFoundation/SpeziFoundation.docc/SpeziFoundation.md b/Sources/SpeziFoundation/SpeziFoundation.docc/SpeziFoundation.md index 430fb60..50a67ba 100644 --- a/Sources/SpeziFoundation/SpeziFoundation.docc/SpeziFoundation.md +++ b/Sources/SpeziFoundation/SpeziFoundation.docc/SpeziFoundation.md @@ -15,33 +15,47 @@ Spezi Foundation provides a base layer of functionality useful in many applicati ## Topics ### Data Structures - - +- ``OrderedArray`` -### Introspection +### Calendar and Time Zone handling +- -- ``AnyArray`` -- ``AnyOptional`` +### Sequence and Collection utilities +- ### Concurrency - - ``RWLock`` - ``RecursiveRWLock`` - ``AsyncSemaphore`` - ``ManagedAsynchronousAccess`` +- ``runOrScheduleOnMainActor(_:)`` ### Encoders and Decoders - ``TopLevelEncoder`` - ``TopLevelDecoder`` +### Generic Result Builders +- ``RangeReplaceableCollectionBuilder`` +- ``ArrayBuilder`` +- ``SetBuilder`` +- ``Swift/Array/init(build:)`` +- ``Swift/Set/init(build:)`` + +### Introspection +- ``AnyArray`` +- ``AnyOptional`` + ### Data - ``DataDescriptor`` ### Timeout - - ``TimeoutError`` - ``withTimeout(of:perform:)`` -### System Programming Interfaces +### Objective-C Exception Handling +- ``catchingNSException(_:)`` +- ``CaughtNSException`` +### System Programming Interfaces - diff --git a/Tests/SpeziFoundationTests/SequenceExtensionTests.swift b/Tests/SpeziFoundationTests/SequenceExtensionTests.swift index 9f0f306..5b0b3cf 100644 --- a/Tests/SpeziFoundationTests/SequenceExtensionTests.swift +++ b/Tests/SpeziFoundationTests/SequenceExtensionTests.swift @@ -26,4 +26,23 @@ final class SequenceExtensionTests: XCTestCase { array.remove(at: [0, 7, 5, 2] as IndexSet) XCTAssertEqual(array, [1, 3, 4, 6, 8, 9]) } + + + func testAsyncReduce() async throws { + let names = ["Paul", "Lukas"] + let reduced = try await names.reduce(0) { acc, name in + try await Task.sleep(for: .seconds(0.2)) // best i could think of to get some trivial async-ness in here... + return acc + name.count + } + XCTAssertEqual(reduced, 9) + } + + func testAsyncReduceInto() async throws { + let names = ["Paul", "Lukas"] + let reduced = try await names.reduce(into: 0) { acc, name in + try await Task.sleep(for: .seconds(0.2)) // best i could think of to get some trivial async-ness in here... + acc += name.count + } + XCTAssertEqual(reduced, 9) + } } From 630478e532328751a5944d01fa3613db37ccf8fa Mon Sep 17 00:00:00 2001 From: Lukas Kollmer Date: Sun, 19 Jan 2025 16:37:04 +0100 Subject: [PATCH 11/11] mark a couple things as being @inlinable --- .../Collection Builders/ArrayBuilder.swift | 1 + .../RangeReplaceableCollectionBuilder.swift | 10 ++++++++++ .../Collection Builders/SetBuilder.swift | 11 +++++++++++ Sources/SpeziFoundation/Misc/BinarySearch.swift | 4 +++- Sources/SpeziFoundation/Misc/Calendar.swift | 5 +++++ Sources/SpeziFoundation/Misc/SequenceExtensions.swift | 4 ++++ 6 files changed, 34 insertions(+), 1 deletion(-) diff --git a/Sources/SpeziFoundation/Collection Builders/ArrayBuilder.swift b/Sources/SpeziFoundation/Collection Builders/ArrayBuilder.swift index b652cb1..b766644 100644 --- a/Sources/SpeziFoundation/Collection Builders/ArrayBuilder.swift +++ b/Sources/SpeziFoundation/Collection Builders/ArrayBuilder.swift @@ -12,6 +12,7 @@ public typealias ArrayBuilder = RangeReplaceableCollectionBuilder<[T]> extension Array { /// Constructs a new array, using a result builder. + @inlinable public init(@ArrayBuilder build: () -> Self) { self = build() } diff --git a/Sources/SpeziFoundation/Collection Builders/RangeReplaceableCollectionBuilder.swift b/Sources/SpeziFoundation/Collection Builders/RangeReplaceableCollectionBuilder.swift index 57e2fed..573518e 100644 --- a/Sources/SpeziFoundation/Collection Builders/RangeReplaceableCollectionBuilder.swift +++ b/Sources/SpeziFoundation/Collection Builders/RangeReplaceableCollectionBuilder.swift @@ -17,51 +17,61 @@ public enum RangeReplaceableCollectionBuilder { extension RangeReplaceableCollectionBuilder { /// :nodoc: + @inlinable public static func buildExpression(_ expression: Element) -> C { C(CollectionOfOne(expression)) } /// :nodoc: + @inlinable public static func buildExpression(_ expression: some Sequence) -> C { C(expression) } /// :nodoc: + @inlinable public static func buildOptional(_ expression: C?) -> C { expression ?? C() } /// :nodoc: + @inlinable public static func buildEither(first expression: some Sequence) -> C { C(expression) } /// :nodoc: + @inlinable public static func buildEither(second expression: some Sequence) -> C { C(expression) } /// :nodoc: + @inlinable public static func buildPartialBlock(first: some Sequence) -> C { C(first) } /// :nodoc: + @inlinable public static func buildPartialBlock(accumulated: some Sequence, next: some Sequence) -> C { C(accumulated) + C(next) } /// :nodoc: + @inlinable public static func buildBlock() -> C { C() } /// :nodoc: + @inlinable public static func buildArray(_ components: [some Sequence]) -> C { components.reduce(into: C()) { $0.append(contentsOf: $1) } } /// :nodoc: + @inlinable public static func buildFinalResult(_ component: C) -> C { component } diff --git a/Sources/SpeziFoundation/Collection Builders/SetBuilder.swift b/Sources/SpeziFoundation/Collection Builders/SetBuilder.swift index de4c910..fcbdf4d 100644 --- a/Sources/SpeziFoundation/Collection Builders/SetBuilder.swift +++ b/Sources/SpeziFoundation/Collection Builders/SetBuilder.swift @@ -14,6 +14,7 @@ public enum SetBuilder {} extension Set { /// Constructs a new `Set` using a result builder. + @inlinable public init(@SetBuilder build: () -> Set) { self = build() } @@ -22,51 +23,61 @@ extension Set { extension SetBuilder { /// :nodoc: + @inlinable public static func buildExpression(_ expression: Element) -> Set { Set(CollectionOfOne(expression)) } /// :nodoc: + @inlinable public static func buildExpression(_ expression: some Sequence) -> Set { Set(expression) } /// :nodoc: + @inlinable public static func buildOptional(_ expression: Set?) -> Set { // swiftlint:disable:this discouraged_optional_collection expression ?? Set() } /// :nodoc: + @inlinable public static func buildEither(first expression: some Sequence) -> Set { Set(expression) } /// :nodoc: + @inlinable public static func buildEither(second expression: some Sequence) -> Set { Set(expression) } /// :nodoc: + @inlinable public static func buildPartialBlock(first: some Sequence) -> Set { Set(first) } /// :nodoc: + @inlinable public static func buildPartialBlock(accumulated: some Sequence, next: some Sequence) -> Set { Set(accumulated).union(next) } /// :nodoc: + @inlinable public static func buildBlock() -> Set { Set() } /// :nodoc: + @inlinable public static func buildArray(_ components: [some Sequence]) -> Set { components.reduce(into: Set()) { $0.formUnion($1) } } /// :nodoc: + @inlinable public static func buildFinalResult(_ component: Set) -> Set { component } diff --git a/Sources/SpeziFoundation/Misc/BinarySearch.swift b/Sources/SpeziFoundation/Misc/BinarySearch.swift index 3d51fd6..1532d81 100644 --- a/Sources/SpeziFoundation/Misc/BinarySearch.swift +++ b/Sources/SpeziFoundation/Misc/BinarySearch.swift @@ -29,6 +29,7 @@ extension Collection { /// for `.orderedDescending` it will continue to the right. /// - Note: If the element is not in the collection (i.e., the `compare` closure never returns `.orderedSame`), /// the algorithm will compute and return the index where the element should be, if it were to become a member of the collection. + @inlinable public func binarySearchForIndex( of element: Element, using compare: (Element, Element) -> ComparisonResult @@ -44,7 +45,8 @@ extension Collection { /// for `.orderedDescending` it will continue to the right. /// - Note: If the element is not in the collection (i.e., the `compare` closure never returns `.orderedSame`), /// the algorithm will compute and return the index where the element should be, if it were to become a member of the collection. - private func binarySearchForIndex( + @usableFromInline + internal func binarySearchForIndex( of element: Element, in range: Range, using compare: (Element, Element) -> ComparisonResult diff --git a/Sources/SpeziFoundation/Misc/Calendar.swift b/Sources/SpeziFoundation/Misc/Calendar.swift index a6b52d3..86dd1b7 100644 --- a/Sources/SpeziFoundation/Misc/Calendar.swift +++ b/Sources/SpeziFoundation/Misc/Calendar.swift @@ -41,6 +41,7 @@ extension Calendar { } /// Returns a `Range` representing the range of the hour into which `date` falls. + @inlinable public func rangeOfHour(for date: Date) -> Range { startOfHour(for: date)..` representing the range of the day into which `date` falls. + @inlinable public func rangeOfDay(for date: Date) -> Range { startOfDay(for: date)..` representing the range of the week into which `date` falls. + @inlinable public func rangeOfWeek(for date: Date) -> Range { startOfWeek(for: date).. Range { startOfMonth(for: date)..` representing the range of the year into which `date` falls. + @inlinable public func rangeOfYear(for date: Date) -> Range { startOfYear(for: date)..(_ transform: (Element) throws -> NewElement) rethrows -> Set { var retval = Set() for element in self { @@ -24,6 +25,7 @@ extension Sequence { } /// An asynchronous version of Swift's `Sequence.reduce(_:_:)` function. + @inlinable public func reduce( _ initialResult: Result, _ nextPartialResult: (Result, Element) async throws -> Result @@ -36,6 +38,7 @@ extension Sequence { } /// An asynchronous version of Swift's `Sequence.reduce(into:_:)` function. + @inlinable public func reduce( into initial: Result, _ nextPartialResult: (inout Result, Element) async throws -> Void @@ -69,6 +72,7 @@ extension RangeReplaceableCollection { /// - parameter indices: The indices at which elements should be removed. /// /// Useful e.g. when working with SwiftUI's `onDelete(perform:)` modifier. + @inlinable public mutating func remove(at indices: some Sequence) { for idx in indices.sorted().reversed() { self.remove(at: idx)