diff --git a/Sources/OTCore/Extensions/Swift/Collections.swift b/Sources/OTCore/Extensions/Swift/Collections.swift index cd73992..c1b07e8 100644 --- a/Sources/OTCore/Extensions/Swift/Collections.swift +++ b/Sources/OTCore/Extensions/Swift/Collections.swift @@ -18,7 +18,7 @@ extension Collection where Self: RangeReplaceableCollection, } -// MARK: - Safe subscripts +// MARK: - [safe:] // MutableCollection: // (inherits from Sequence, Collection) @@ -116,6 +116,152 @@ extension MutableCollection where Element : OptionalType { } +// Collection: +// (inherits from Sequence) +// (conformance on Array, ArraySlice, ContiguousArray, Dictionary, Set, Range, ClosedRange, KeyValuePairs, CollectionOfOne, EmptyCollection, etc.) +// (does not conform on NSArray, NSDictionary, etc.) +extension Collection { + + /// **OTCore:** + /// Access collection indexes safely. + /// + /// Get: if index does not exist (out-of-bounds), `nil` is returned. + /// + /// Set: if index does not exist, set fails silently and the value is not stored. + /// + /// Example: + /// + /// let arr = [1, 2, 3] + /// arr[safe: 0] // Optional(1) + /// arr[safe: 3] // nil + /// + /// // for slice, index numbers are preserved like native subscript + /// let arrSlice = [1, 2, 3].suffix(2) + /// arrSlice[safe: 0] // nil + /// arrSlice[safe: 1] // Optional(2) + /// arrSlice[safe: 2] // Optional(3) + /// arrSlice[safe: 3] // nil + /// + @_disfavoredOverload + @inlinable public subscript(safe index: Index) -> Element? { + + indices.contains(index) ? self[index] : nil + + } + + /// **OTCore:** + /// Access collection indexes safely. + /// If index does not exist (out-of-bounds), `defaultValue` is returned. + @_disfavoredOverload + @inlinable public subscript( + safe index: Index, + default defaultValue: @autoclosure () -> Element + ) -> Element { + + indices.contains(index) ? self[index] : defaultValue() + + } + +} + +extension Collection where Index == Int { + + /// **OTCore:** + /// Access collection indexes safely. + /// + /// Get: if index does not exist (out-of-bounds), `nil` is returned. + /// + /// Set: if index does not exist, set fails silently and the value is not stored. + /// + /// Example: + /// + /// let arr = [1, 2, 3] + /// arr[safe: 0] // Optional(1) + /// arr[safe: 3] // nil + /// + /// // for slice, index numbers are preserved like native subscript + /// let arrSlice = [1, 2, 3].suffix(2) + /// arrSlice[safe: 0] // nil + /// arrSlice[safe: 1] // Optional(2) + /// arrSlice[safe: 2] // Optional(3) + /// arrSlice[safe: 3] // nil + /// + @inlinable public subscript(safe index: Int) -> Element? { + + indices.contains(index) ? self[index] : nil + + } + +} + +// MARK: - [safe: Range] + +extension Collection { + + /// **OTCore:** + /// Access collection indexes safely. + /// If index range is not fully contained within the collection's indices, `nil` is returned. + @inlinable public subscript(safe range: ClosedRange) -> SubSequence? { + + guard range.lowerBound >= startIndex, + range.upperBound < endIndex else { return nil } + + return self[range.lowerBound...range.upperBound] + + } + + /// **OTCore:** + /// Access collection indexes safely. + /// If index range is not fully contained within the collection's indices, `nil` is returned. + @inlinable public subscript(safe range: Range) -> SubSequence? { + + guard range.lowerBound >= startIndex, + range.upperBound <= endIndex else { return nil } + + return self[range.lowerBound..) -> SubSequence? { + + guard range.lowerBound >= startIndex, + range.lowerBound <= endIndex else { return nil } + + return self[range.lowerBound...] + + } + + /// **OTCore:** + /// Access collection indexes safely. + /// If index range is not fully contained within the collection's indices, `nil` is returned. + @inlinable public subscript(safe range: PartialRangeThrough) -> SubSequence? { + + guard range.upperBound >= startIndex, + range.upperBound < endIndex else { return nil } + + return self[...range.upperBound] + + } + + /// **OTCore:** + /// Access collection indexes safely. + /// If index range is not fully contained within the collection's indices, `nil` is returned. + @inlinable public subscript(safe range: PartialRangeUpTo) -> SubSequence? { + + guard range.upperBound >= startIndex, + range.upperBound <= endIndex else { return nil } + + return self[.. Element? { - - indices.contains(index) ? self[index] : nil - - } - - /// **OTCore:** - /// Access collection indexes safely. - /// If index does not exist (out-of-bounds), `defaultValue` is returned. - @_disfavoredOverload - @inlinable public subscript( - safe index: Index, - default defaultValue: @autoclosure () -> Element - ) -> Element { - - indices.contains(index) ? self[index] : defaultValue() - - } - -} - extension Collection where Index == Int { - /// **OTCore:** - /// Access collection indexes safely. - /// - /// Get: if index does not exist (out-of-bounds), `nil` is returned. - /// - /// Set: if index does not exist, set fails silently and the value is not stored. - /// - /// Example: - /// - /// let arr = [1, 2, 3] - /// arr[safe: 0] // Optional(1) - /// arr[safe: 3] // nil - /// - /// // for slice, index numbers are preserved like native subscript - /// let arrSlice = [1, 2, 3].suffix(2) - /// arrSlice[safe: 0] // nil - /// arrSlice[safe: 1] // Optional(2) - /// arrSlice[safe: 2] // Optional(3) - /// arrSlice[safe: 3] // nil - /// - @inlinable public subscript(safe index: Int) -> Element? { - - indices.contains(index) ? self[index] : nil - - } - /// **OTCore:** /// Access collection indexes safely, referenced by position offset `0..) -> SubSequence? { - - guard range.lowerBound >= startIndex, - range.upperBound < endIndex else { return nil } - - return self[range.lowerBound...range.upperBound] - - } - - /// **OTCore:** - /// Access collection indexes safely. - /// If index range is not fully contained within the collection's indices, `nil` is returned. - @inlinable public subscript(safe range: Range) -> SubSequence? { - - guard range.lowerBound >= startIndex, - range.upperBound <= endIndex else { return nil } - - return self[range.lowerBound..) -> SubSequence? { + + let fromIndex = index(startIndex, offsetBy: range.lowerBound) + + guard fromIndex >= startIndex, + fromIndex <= endIndex else { return nil } + + return self[fromIndex...] + + } + + /// **OTCore:** + /// Access collection indexes safely, referenced by position offset `0..) -> SubSequence? { + + let toIndex = index(startIndex, offsetBy: range.upperBound) + + guard toIndex >= startIndex, + toIndex < endIndex else { return nil } + + return self[...toIndex] + + } + + /// **OTCore:** + /// Access collection indexes safely, referenced by position offset `0..) -> SubSequence? { + + let toIndex = index(startIndex, offsetBy: range.upperBound) + + guard toIndex >= startIndex, + toIndex <= endIndex else { return nil } + + return self[.. + let slice = arr.suffix(2) + + XCTAssertEqual(slice[safe: -1, default: 99], 99) + XCTAssertEqual(slice[safe: 0, default: 99], 99) + XCTAssertEqual(slice[safe: 1, default: 99], 2) + XCTAssertEqual(slice[safe: 2, default: 99], 3) + XCTAssertEqual(slice[safe: 3, default: 99], 99) + + } + + func testSubscript_Safe_Get_DefaultValue_EdgeCases() { + + // empty array + XCTAssertEqual([Int]()[safe: -1, default: 99], 99) + XCTAssertEqual([Int]()[safe: 0, default: 99], 99) + XCTAssertEqual([Int]()[safe: 1, default: 99], 99) + + // single element array + XCTAssertEqual([1][safe: -1, default: 99], 99) + XCTAssertEqual([1][safe: 0, default: 99], 1) + XCTAssertEqual([1][safe: 1, default: 99], 99) + + } + + // MARK: - [safe: i...i] + + func testSubscript_Safe_ClosedRange_Index() { + + // [Int] + let arr = [1, 2, 3, 4, 5, 6] + + do { + let fromIndex = arr.index(arr.startIndex, offsetBy: 0) + let toIndex = arr.index(arr.startIndex, offsetBy: 3) + let slice = arr[safe: fromIndex...toIndex] + XCTAssertEqual(slice, [1, 2, 3, 4]) + } + + do { + let fromIndex = arr.index(arr.startIndex, offsetBy: 1) + let toIndex = arr.index(arr.startIndex, offsetBy: 5) + let slice = arr[safe: fromIndex...toIndex] + XCTAssertEqual(slice, [2, 3, 4, 5, 6]) + } + + do { + let fromIndex = arr.index(arr.startIndex, offsetBy: 1) + let toIndex = arr.index(arr.startIndex, offsetBy: 6) + let slice = arr[safe: fromIndex...toIndex] + XCTAssertEqual(slice, nil) + } + + } + + func testSubscript_Safe_ClosedRange_Index_EdgeCases() { + + // empty array + do { + let arr: [Int] = [] + + let fromIndex = arr.index(arr.startIndex, offsetBy: 1) + let toIndex = arr.index(arr.startIndex, offsetBy: 3) + let slice = arr[safe: fromIndex...toIndex] + XCTAssertEqual(slice, nil) + } + + // single element array + do { + let arr: [Int] = [1] + + let fromIndex = arr.index(arr.startIndex, offsetBy: 1) + let toIndex = arr.index(arr.startIndex, offsetBy: 3) + let slice = arr[safe: fromIndex...toIndex] + XCTAssertEqual(slice, nil) + } + + do { + let arr: [Int] = [1] + + let fromIndex = arr.index(arr.startIndex, offsetBy: 0) + let toIndex = arr.index(arr.startIndex, offsetBy: 0) + let slice = arr[safe: fromIndex...toIndex] + XCTAssertEqual(slice, [1]) + } + + do { + let arr: [Int] = [1] + + let fromIndex = arr.index(arr.startIndex, offsetBy: -1) + let toIndex = arr.index(arr.startIndex, offsetBy: 0) + let slice = arr[safe: fromIndex...toIndex] + XCTAssertEqual(slice, nil) + } + + } + + func testSubscript_Safe_ClosedRange_IndexInt() { + + // [Int] + let arr = [1, 2, 3, 4, 5, 6] + + do { + let slice = arr[safe: 0...3] + XCTAssertEqual(slice, [1, 2, 3, 4]) + } + + do { + let slice = arr[safe: 1...5] + XCTAssertEqual(slice, [2, 3, 4, 5, 6]) + } + + do { + let slice = arr[safe: 1...6] + XCTAssertEqual(slice, nil) + } + + do { + let slice = arr[safe: -1...3] + XCTAssertEqual(slice, nil) + } + + } + + // MARK: - [safe: i.. - let slice = arr.suffix(2) - - XCTAssertEqual(slice[safe: -1, default: 99], 99) - XCTAssertEqual(slice[safe: 0, default: 99], 99) - XCTAssertEqual(slice[safe: 1, default: 99], 2) - XCTAssertEqual(slice[safe: 2, default: 99], 3) - XCTAssertEqual(slice[safe: 3, default: 99], 99) - + slice[safePosition: -1] = 0 // silently fails + slice[safePosition: 4] = 7 // silently fails + + XCTAssertEqual(slice, [1, 4, 5, 6]) + } - func testSubscript_Safe_Get_DefaultValue_EdgeCases() { + func testSubscript_SafePosition_Set_EdgeCases() throws { - // empty array - XCTAssertEqual([Int]()[safe: -1, default: 99], 99) - XCTAssertEqual([Int]()[safe: 0, default: 99], 99) - XCTAssertEqual([Int]()[safe: 1, default: 99], 99) + // setting an existing element to nil currently + // throws a preconditionFailure that we can't catch + // in unit tests without it halting all tests execution, + // so that can't explicitly be tested here - // single element array - XCTAssertEqual([1][safe: -1, default: 99], 99) - XCTAssertEqual([1][safe: 0, default: 99], 1) - XCTAssertEqual([1][safe: 1, default: 99], 99) + // [Int] + var arr = [1, 2, 3, 4, 5, 6] + + arr[safePosition: -1] = nil // silently fails, out of bounds + //arr[safePosition: 0] = nil // throws precondition failure + arr[safePosition: 6] = nil // silently fails, out of bounds + + XCTAssertEqual(arr, [1, 2, 3, 4, 5, 6]) + + // [Int?] + var arr2: [Int?] = [1, 2, 3, 4, 5, 6] + + arr2[safePosition: -1] = nil // silently fails + arr2[safePosition: 0] = nil // succeeds + arr2[safePosition: 6] = nil // silently fails + + XCTAssertEqual(arr2, [nil, 2, 3, 4, 5, 6]) } + // MARK: - [safePosition: Index, defaultValue:] + func testSubscript_SafePosition_Get_DefaultValue() { // [Int] @@ -315,92 +773,7 @@ class Extensions_Swift_Collections_Tests: XCTestCase { } - func testSubscript_Safe_ClosedRange_Index() { - - // [Int] - let arr = [1, 2, 3, 4, 5, 6] - - do { - let fromIndex = arr.index(arr.startIndex, offsetBy: 0) - let toIndex = arr.index(arr.startIndex, offsetBy: 3) - let slice = arr[safe: fromIndex...toIndex] - XCTAssertEqual(slice, [1, 2, 3, 4]) - } - - do { - let fromIndex = arr.index(arr.startIndex, offsetBy: 1) - let toIndex = arr.index(arr.startIndex, offsetBy: 5) - let slice = arr[safe: fromIndex...toIndex] - XCTAssertEqual(slice, [2, 3, 4, 5, 6]) - } - - do { - let fromIndex = arr.index(arr.startIndex, offsetBy: 1) - let toIndex = arr.index(arr.startIndex, offsetBy: 6) - let slice = arr[safe: fromIndex...toIndex] - XCTAssertEqual(slice, nil) - } - - } - - func testSubscript_Safe_ClosedRange_Index_EdgeCases() { - - // empty array - do { - let arr: [Int] = [] - - let fromIndex = arr.index(arr.startIndex, offsetBy: 1) - let toIndex = arr.index(arr.startIndex, offsetBy: 3) - let slice = arr[safe: fromIndex...toIndex] - XCTAssertEqual(slice, nil) - } - - // single element array - do { - let arr: [Int] = [1] - - let fromIndex = arr.index(arr.startIndex, offsetBy: 1) - let toIndex = arr.index(arr.startIndex, offsetBy: 3) - let slice = arr[safe: fromIndex...toIndex] - XCTAssertEqual(slice, nil) - } - - do { - let arr: [Int] = [1] - - let fromIndex = arr.index(arr.startIndex, offsetBy: 0) - let toIndex = arr.index(arr.startIndex, offsetBy: 0) - let slice = arr[safe: fromIndex...toIndex] - XCTAssertEqual(slice, [1]) - } - } - - func testSubscript_Safe_ClosedRange_Int() { - - // [Int] - let arr = [1, 2, 3, 4, 5, 6] - - do { - let slice = arr[safe: 0...3] - XCTAssertEqual(slice, [1, 2, 3, 4]) - } - - do { - let slice = arr[safe: 1...5] - XCTAssertEqual(slice, [2, 3, 4, 5, 6]) - } - - do { - let slice = arr[safe: 1...6] - XCTAssertEqual(slice, nil) - } - - do { - let slice = arr[safe: -1...3] - XCTAssertEqual(slice, nil) - } - - } + // MARK: - [safePosition: i...i] func testSubscript_SafePosition_ClosedRange_Int() { @@ -432,102 +805,7 @@ class Extensions_Swift_Collections_Tests: XCTestCase { } - func testSubscript_Safe_Range_Index() { - - // [Int] - let arr = [1, 2, 3, 4, 5, 6] - - do { - let fromIndex = arr.index(arr.startIndex, offsetBy: 0) - let toIndex = arr.index(arr.startIndex, offsetBy: 5) - let slice = arr[safe: fromIndex.. + let slice = [1, 2, 3, 4, 5, 6][2...] // [3, 4, 5, 6] + + XCTAssertEqual(slice[safePosition: (-1)...], nil) + XCTAssertEqual(slice[safePosition: 0...], [3, 4, 5, 6]) + XCTAssertEqual(slice[safePosition: 1...], [4, 5, 6]) + XCTAssertEqual(slice[safePosition: 3...], [6]) + XCTAssertEqual(slice[safePosition: 4...], []) + XCTAssertEqual(slice[safePosition: 5...], nil) + + } + // MARK: - [safePosition: ...i] + func testSubscript_SafePosition_PartialRangeThrough_Int() { + + // [Int].SubSequence a.k.a. ArraySlice + let slice = [1, 2, 3, 4, 5, 6][2...] // [3, 4, 5, 6] + + XCTAssertEqual(slice[safePosition: ...(-1)], nil) + XCTAssertEqual(slice[safePosition: ...0], [3]) + XCTAssertEqual(slice[safePosition: ...1], [3, 4]) + XCTAssertEqual(slice[safePosition: ...3], [3, 4, 5, 6]) + XCTAssertEqual(slice[safePosition: ...4], nil) + + } + // MARK: - [safePosition: .. + let slice = [1, 2, 3, 4, 5, 6][2...] // [3, 4, 5, 6] + + XCTAssertEqual(slice[safePosition: ..<(-1)], nil) + XCTAssertEqual(slice[safePosition: ..<0], []) + XCTAssertEqual(slice[safePosition: ..<1], [3]) + XCTAssertEqual(slice[safePosition: ..<2], [3, 4]) + XCTAssertEqual(slice[safePosition: ..<4], [3, 4, 5, 6]) + XCTAssertEqual(slice[safePosition: ..<5], nil) + + } - - - - - - - - - - + // MARK: - .remove(safeAt:) func testArrayRemoveSafeAt() { @@ -627,6 +937,8 @@ class Extensions_Swift_Collections_Tests: XCTestCase { } + // MARK: - Indexes + func testStartIndexOffsetBy() { // .startIndex(offsetBy:) @@ -655,6 +967,8 @@ class Extensions_Swift_Collections_Tests: XCTestCase { } + // MARK: - [position: Int] + func testSubscriptPosition_OffsetIndex() { let array = ["a", "b", "c", "1", "2", "3"] @@ -668,6 +982,8 @@ class Extensions_Swift_Collections_Tests: XCTestCase { } + // MARK: - [position: i...i] + func testSubscriptPosition_ClosedRange() { let array = ["a", "b", "c", "1", "2", "3"] @@ -681,6 +997,8 @@ class Extensions_Swift_Collections_Tests: XCTestCase { } + // MARK: - [position: i..