diff --git a/Sources/OpenSwiftUICore/Util/StandardLibraryAdditions.swift b/Sources/OpenSwiftUICore/Util/StandardLibraryAdditions.swift index b5a63bb48..7413f86c6 100644 --- a/Sources/OpenSwiftUICore/Util/StandardLibraryAdditions.swift +++ b/Sources/OpenSwiftUICore/Util/StandardLibraryAdditions.swift @@ -841,3 +841,78 @@ package struct EquatableOptionalObject: Equatable where T: AnyObject { return lhs.wrappedValue === rhs.wrappedValue } } + +// MARK: - BidirectionalCollection + insertionSort [6.5.4] + +extension BidirectionalCollection where Self: MutableCollection { + /// Sorts the collection in place using the insertion sort algorithm with a custom comparison. + /// + /// Insertion sort is a simple sorting algorithm that builds the sorted collection one element + /// at a time by repeatedly taking elements from the unsorted portion and inserting them + /// into their correct position in the sorted portion. + /// + /// This implementation is stable, meaning that elements that compare equal retain their + /// relative order from the original collection. + /// + /// - Parameter areInIncreasingOrder: A predicate that returns `true` if its first + /// argument should be ordered before its second argument; otherwise, `false`. + /// If `areInIncreasingOrder` throws an error during the sort, the elements may be + /// in an invalid order, but the `mutating` guarantee is still upheld. + /// - Complexity: O(*n*²) in the worst case, where *n* is the length of the collection. + /// Best case is O(*n*) when the collection is already sorted. + /// + /// Example usage: + /// ```swift + /// var numbers = [3, 1, 4, 1, 5, 9] + /// numbers.insertionSort(by: <) + /// // numbers is now [1, 1, 3, 4, 5, 9] + /// ``` + package mutating func insertionSort(by areInIncreasingOrder: (Element, Element) throws -> Bool) rethrows { + guard !isEmpty else { return } + var currentIndex = index(after: startIndex) + + while currentIndex != endIndex { + let currentElement = self[currentIndex] + var insertionIndex = currentIndex + repeat { + let previousIndex = index(before: insertionIndex) + let previousElement = self[previousIndex] + do { + guard try areInIncreasingOrder(currentElement, previousElement) else { + break + } + self[insertionIndex] = previousElement + } catch { + self[insertionIndex] = currentElement + throw error + } + formIndex(before: &insertionIndex) + } while insertionIndex != startIndex + + if insertionIndex != currentIndex { + self[insertionIndex] = currentElement + } + formIndex(after: ¤tIndex) + } + } +} + +extension BidirectionalCollection where Self: MutableCollection, Element: Comparable { + /// Sorts the collection in place using the insertion sort algorithm. + /// + /// This method sorts the collection using the less-than operator (`<`) for comparison. + /// Elements are arranged in ascending order. + /// + /// - Complexity: O(*n*²) in the worst case, where *n* is the length of the collection. + /// Best case is O(*n*) when the collection is already sorted. + /// + /// Example usage: + /// ```swift + /// var numbers = [3, 1, 4, 1, 5, 9] + /// numbers.insertionSort() + /// // numbers is now [1, 1, 3, 4, 5, 9] + /// ``` + package mutating func insertionSort() { + insertionSort(by: <) + } +} diff --git a/Sources/OpenSwiftUISymbolDualTestsSupport/Util/BidirectionalCollectionInsertionSortTestsStub.c b/Sources/OpenSwiftUISymbolDualTestsSupport/Util/BidirectionalCollectionInsertionSortTestsStub.c new file mode 100644 index 000000000..68f0bfad6 --- /dev/null +++ b/Sources/OpenSwiftUISymbolDualTestsSupport/Util/BidirectionalCollectionInsertionSortTestsStub.c @@ -0,0 +1,13 @@ +// +// BidirectionalCollectionInsertionSortTestsStub.c +// OpenSwiftUISymbolDualTestsSupport + +#include "OpenSwiftUIBase.h" + +#if OPENSWIFTUI_TARGET_OS_DARWIN + +#import + +DEFINE_SL_STUB_SLF(OpenSwiftUITestStub_BidirectionalCollectionInsertionSortBy, SwiftUI, $sSK7SwiftUISMRzrlE13insertionSort2byySb7ElementSTQz_AEtKXE_tKF); + +#endif diff --git a/Sources/OpenSwiftUISymbolDualTestsSupport/Extension/CGSize+ExtensionTestsStub.c b/Sources/OpenSwiftUISymbolDualTestsSupport/Util/Extension/CGSize+ExtensionTestsStub.c similarity index 100% rename from Sources/OpenSwiftUISymbolDualTestsSupport/Extension/CGSize+ExtensionTestsStub.c rename to Sources/OpenSwiftUISymbolDualTestsSupport/Util/Extension/CGSize+ExtensionTestsStub.c diff --git a/Tests/OpenSwiftUICoreTests/Util/StandardLibraryAdditionsTests.swift b/Tests/OpenSwiftUICoreTests/Util/StandardLibraryAdditionsTests.swift index fadebc76b..f272289c7 100644 --- a/Tests/OpenSwiftUICoreTests/Util/StandardLibraryAdditionsTests.swift +++ b/Tests/OpenSwiftUICoreTests/Util/StandardLibraryAdditionsTests.swift @@ -457,3 +457,146 @@ struct CollectionOfTwoTests { #expect(!collection.contains("orange")) } } + +// MARK: - BidirectionalCollectionInsertionSortTests + +struct BidirectionalCollectionInsertionSortTests { + @Test + func alreadySorted() { + var array = [1, 2, 3, 4, 5] + array.insertionSort() + #expect(array == [1, 2, 3, 4, 5]) + } + + @Test + func reverseSorted() { + var array = [5, 4, 3, 2, 1] + array.insertionSort() + #expect(array == [1, 2, 3, 4, 5]) + } + + @Test + func randomOrder() { + var array = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3] + array.insertionSort() + #expect(array == [1, 1, 2, 3, 3, 4, 5, 5, 6, 9]) + } + + @Test + func customComparison() { + var array = [1, 2, 3, 4, 5] + array.insertionSort(by: >) + #expect(array == [5, 4, 3, 2, 1]) + } + + @Test + func stringArray() { + var array = ["zebra", "apple", "banana", "cherry"] + array.insertionSort() + #expect(array == ["apple", "banana", "cherry", "zebra"]) + } + + @Test + func structComparison() { + struct Person: Comparable { + let name: String + let age: Int + + static func < (lhs: Person, rhs: Person) -> Bool { + lhs.age < rhs.age + } + + static func == (lhs: Person, rhs: Person) -> Bool { + lhs.name == rhs.name && lhs.age == rhs.age + } + } + + var people = [ + Person(name: "Alice", age: 30), + Person(name: "Bob", age: 25), + Person(name: "Charlie", age: 35), + ] + + people.insertionSort() + + #expect(people[0].name == "Bob") + #expect(people[1].name == "Alice") + #expect(people[2].name == "Charlie") + } + + @Test + func arraySliceSort() { + var array = [5, 1, 4, 2, 3] + var slice = array[1 ... 3] + slice.insertionSort() + + array[1 ... 3] = slice + #expect(array == [5, 1, 2, 4, 3]) + } + + @Test + func throwingComparison() { + enum V: Equatable, CustomStringConvertible { + case value(Int) + case invalid + + var description: String { + guard case let .value(value) = self else { return "invalid" } + return value.description + } + } + + enum ComparisonError: Error { + case invalid + } + var array: [V] = [.value(5), .value(4), .invalid, .value(2), .value(1)] + do { + try array.insertionSort { first, second in + guard case let .value(firstValue) = first, + case let .value(secondValue) = second + else { + throw ComparisonError.invalid + } + return firstValue < secondValue + } + } catch { + #expect(error is ComparisonError) + } + #expect(array == [.value(4), .value(5), .invalid, .value(2), .value(1)]) + } + + @Test("Verify self[insertionIndex] = currentElement") + func throwingComparison2() { + enum V: Equatable, CustomStringConvertible { + case value(Int) + case invalid + + var description: String { + guard case let .value(value) = self else { return "invalid" } + return value.description + } + } + + enum ComparisonError: Error { + case invalid + } + var array: [V] = [.value(5), .value(4), .invalid, .value(2), .value(1)] + do { + try array.insertionSort { first, second in + guard case let .value(firstValue) = first, + case let .value(secondValue) = second + else { + if first == .invalid, second == .value(4) { + throw ComparisonError.invalid + } else { + return true + } + } + return firstValue < secondValue + } + } catch { + #expect(error is ComparisonError) + } + #expect(array == [.value(4), .invalid, .value(5), .value(2), .value(1)]) + } +} diff --git a/Tests/OpenSwiftUISymbolDualTests/Extension/CGSize+ExtensionTests.swift b/Tests/OpenSwiftUISymbolDualTests/Util/Extension/CGSize+ExtensionTests.swift similarity index 100% rename from Tests/OpenSwiftUISymbolDualTests/Extension/CGSize+ExtensionTests.swift rename to Tests/OpenSwiftUISymbolDualTests/Util/Extension/CGSize+ExtensionTests.swift diff --git a/Tests/OpenSwiftUISymbolDualTests/Util/StandardLibraryAdditionsTests.swift b/Tests/OpenSwiftUISymbolDualTests/Util/StandardLibraryAdditionsTests.swift new file mode 100644 index 000000000..fcd78d380 --- /dev/null +++ b/Tests/OpenSwiftUISymbolDualTests/Util/StandardLibraryAdditionsTests.swift @@ -0,0 +1,156 @@ +// +// StandardLibraryAdditionsTests.swift +// OpenSwiftUISymbolDualTests + +#if canImport(SwiftUI, _underlyingVersion: 6.5.4) +import Testing + +// MARK: - BidirectionalCollectionInsertionSortTests + +extension BidirectionalCollection where Self: MutableCollection { + @_silgen_name("OpenSwiftUITestStub_BidirectionalCollectionInsertionSortBy") + mutating func insertionSort(by areInIncreasingOrder: (Element, Element) throws -> Bool) rethrows +} + +struct BidirectionalCollectionInsertionSortTests { + @Test + func alreadySorted() { + var array = [1, 2, 3, 4, 5] + array.insertionSort() + #expect(array == [1, 2, 3, 4, 5]) + } + + @Test + func reverseSorted() { + var array = [5, 4, 3, 2, 1] + array.insertionSort() + #expect(array == [1, 2, 3, 4, 5]) + } + + @Test + func randomOrder() { + var array = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3] + array.insertionSort() + #expect(array == [1, 1, 2, 3, 3, 4, 5, 5, 6, 9]) + } + + @Test + func customComparison() { + var array = [1, 2, 3, 4, 5] + array.insertionSort(by: >) + #expect(array == [5, 4, 3, 2, 1]) + } + + @Test + func stringArray() { + var array = ["zebra", "apple", "banana", "cherry"] + array.insertionSort() + #expect(array == ["apple", "banana", "cherry", "zebra"]) + } + + @Test + func structComparison() { + struct Person: Comparable { + let name: String + let age: Int + + static func < (lhs: Person, rhs: Person) -> Bool { + lhs.age < rhs.age + } + + static func == (lhs: Person, rhs: Person) -> Bool { + lhs.name == rhs.name && lhs.age == rhs.age + } + } + + var people = [ + Person(name: "Alice", age: 30), + Person(name: "Bob", age: 25), + Person(name: "Charlie", age: 35), + ] + + people.insertionSort() + + #expect(people[0].name == "Bob") + #expect(people[1].name == "Alice") + #expect(people[2].name == "Charlie") + } + + @Test + func arraySliceSort() { + var array = [5, 1, 4, 2, 3] + var slice = array[1 ... 3] + slice.insertionSort() + + array[1 ... 3] = slice + #expect(array == [5, 1, 2, 4, 3]) + } + + @Test + func throwingComparison() { + enum V: Equatable, CustomStringConvertible { + case value(Int) + case invalid + + var description: String { + guard case let .value(value) = self else { return "invalid" } + return value.description + } + } + + enum ComparisonError: Error { + case invalid + } + var array: [V] = [.value(5), .value(4), .invalid, .value(2), .value(1)] + do { + try array.insertionSort { first, second in + guard case let .value(firstValue) = first, + case let .value(secondValue) = second + else { + throw ComparisonError.invalid + } + return firstValue < secondValue + } + } catch { + #expect(error is ComparisonError) + } + #expect(array == [.value(4), .value(5), .invalid, .value(2), .value(1)]) + } + + @Test("Verify self[insertionIndex] = currentElement") + func throwingComparison2() { + enum V: Equatable, CustomStringConvertible { + case value(Int) + case invalid + + var description: String { + guard case let .value(value) = self else { return "invalid" } + return value.description + } + } + + enum ComparisonError: Error { + case invalid + } + var array: [V] = [.value(5), .value(4), .invalid, .value(2), .value(1)] + do { + try array.insertionSort { first, second in + guard case let .value(firstValue) = first, + case let .value(secondValue) = second + else { + if first == .invalid, second == .value(4) { + throw ComparisonError.invalid + } else { + return true + } + } + return firstValue < secondValue + } + } catch { + #expect(error is ComparisonError) + } + #expect(array == [.value(4), .invalid, .value(5), .value(2), .value(1)]) + } +} + +#endif