Skip to content

Commit 03d0f4d

Browse files
authored
Merge pull request #19 from RougeWare/feature/Mutation
Enhanced support for `String`s
2 parents b5d20c8 + 9b1d904 commit 03d0f4d

File tree

4 files changed

+139
-39
lines changed

4 files changed

+139
-39
lines changed

Sources/SafeCollectionAccess/SafeRangeExpression.swift

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import RangeTools
1010

1111

1212

13+
/// A `RangeExpression` which can safely represent a slice of a collection
1314
public protocol SafeRangeExpression: RangeExpression {
1415

1516
/// Returns the range of indices described by this range expression within the given collection, guaranteeing it does not overextend the collection's boundaries.
@@ -68,7 +69,7 @@ extension RangeWithLowerBound where Self: SafeRangeExpression {
6869
where RelativeCollection: Collection,
6970
Bound == RelativeCollection.Index
7071
{
71-
bound(lowerBound, isWithin: collection, lookBeyondStartIndex: false, lookBeyondEndIndex: true)
72+
bound(lowerBound, isWithin: collection, lenientStartIndex: false, lookBeyondEndIndex: true)
7273
}
7374

7475

@@ -111,7 +112,7 @@ extension RangeWithUpperBound where Self: SafeRangeExpression {
111112
where RelativeCollection: Collection,
112113
Bound == RelativeCollection.Index
113114
{
114-
bound(upperBound, isWithin: collection, lookBeyondStartIndex: Self.upperBoundIsInclusive, lookBeyondEndIndex: false) // Ranges with an upper but no lower bound can place their upper bound just before the start index for an empty slice
115+
bound(upperBound, isWithin: collection, lenientStartIndex: Self.upperBoundIsInclusive, lookBeyondEndIndex: false) // Ranges with an upper but no lower bound can place their upper bound just before the start index for an empty slice
115116
}
116117

117118

@@ -179,21 +180,42 @@ private extension SafeRangeExpression where Self: RangeProtocol {
179180
/// Determines whether the given bound is within the collection
180181
///
181182
/// - Parameters:
182-
/// - bound: The bound to check
183-
/// - collection: The collection to check against
184-
/// - lookBeyondStartIndex: If `true`, then `bound` can be up to `1` beyond the collection's start index
185-
/// - lookBeyondEndIndex: if `true`, then `bound` can be up to `1` beyond the collection's end index
183+
/// - bound: The bound to check
184+
/// - collection: The collection to check against
185+
/// - lenientStartIndex: If `true`, then `bound` can be up to `1` beyond the collection's start index
186+
/// - lookBeyondEndIndex: if `true`, then `bound` can be up to `1` beyond the collection's end index
186187
///
187188
/// - Returns: `true` iff the given bound is in the given collection
188189
@inline(__always)
189-
func bound<RelativeCollection>(_ bound: Bound, isWithin collection: RelativeCollection, lookBeyondStartIndex: Bool, lookBeyondEndIndex: Bool) -> Bool
190+
func bound<RelativeCollection>(
191+
_ bound: Bound,
192+
isWithin collection: RelativeCollection,
193+
lenientStartIndex: Bool,
194+
lookBeyondEndIndex: Bool)
195+
-> Bool
190196
where RelativeCollection: Collection,
191197
Bound == RelativeCollection.Index
192198
{
193-
let startIndex = lookBeyondStartIndex ? collection.index(collection.startIndex, offsetBy: -1) : collection.startIndex
194-
195-
guard bound >= startIndex else {
196-
return false
199+
if lenientStartIndex {
200+
let startBound: Bound
201+
202+
if bound < collection.endIndex,
203+
collection.index(after: bound) == collection.startIndex
204+
{
205+
startBound = collection.startIndex
206+
}
207+
else {
208+
startBound = bound
209+
}
210+
211+
guard startBound >= collection.startIndex else {
212+
return false
213+
}
214+
}
215+
else {
216+
guard bound >= collection.startIndex else {
217+
return false
218+
}
197219
}
198220

199221
if !Self.upperBoundIsInclusive

Tests/SafeCollectionAccessTests/RangesOrNilTests.swift

Lines changed: 31 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,6 @@ import XCTest
1111

1212

1313

14-
internal let first5Fibonacci = [1, 1, 2, 3, 5]
15-
16-
17-
1814
final class RangeOrNilTests: XCTestCase {
1915

2016

@@ -61,6 +57,14 @@ final class RangeOrNilTests: XCTestCase {
6157
XCTAssertNil(first5Fibonacci[orNil: 7 ..< 9])
6258

6359

60+
XCTAssertEqual(helloWorld[orNil: helloWorld.startIndex ..< helloWorld.endIndex ], "Hello, World!")
61+
XCTAssertEqual(helloWorld[orNil: helloWorld.startIndex ..< helloWorld.index(helloWorld.startIndex, offsetBy: 1) ], "H")
62+
XCTAssertEqual(helloWorld[orNil: helloWorld.index(helloWorld.startIndex, offsetBy: 1) ..< helloWorld.index(helloWorld.startIndex, offsetBy: 5) ], "ello")
63+
XCTAssertEqual(helloWorld[orNil: helloWorld.index(helloWorld.startIndex, offsetBy: 5) ..< helloWorld.index(helloWorld.startIndex, offsetBy: 5) ], "")
64+
XCTAssertEqual(helloWorld[orNil: helloWorld.index(helloWorld.startIndex, offsetBy: 12) ..< helloWorld.index(helloWorld.startIndex, offsetBy: 12)], "")
65+
XCTAssertEqual(helloWorld[orNil: helloWorld.index(helloWorld.startIndex, offsetBy: 11) ..< helloWorld.endIndex ], "d!")
66+
67+
6468
mutationTest { copy1, copy2 in
6569
copy2[ 1..<3] = [9]
6670
copy1[orNil: 1..<3] = [9]
@@ -142,6 +146,16 @@ final class RangeOrNilTests: XCTestCase {
142146
XCTAssertNil(first5Fibonacci[orNil: 7 ... 9])
143147

144148

149+
XCTAssertEqual(helloWorld[orNil: helloWorld.startIndex ... helloWorld.index(helloWorld.startIndex, offsetBy: 12)], "Hello, World!")
150+
XCTAssertEqual(helloWorld[orNil: helloWorld.startIndex ... helloWorld.index(helloWorld.startIndex, offsetBy: 1)], "He")
151+
XCTAssertEqual(helloWorld[orNil: helloWorld.index(helloWorld.startIndex, offsetBy: 1) ... helloWorld.index(helloWorld.startIndex, offsetBy: 5)], "ello,")
152+
XCTAssertEqual(helloWorld[orNil: helloWorld.index(helloWorld.startIndex, offsetBy: 12) ... helloWorld.index(helloWorld.startIndex, offsetBy: 12)], "!")
153+
XCTAssertEqual(helloWorld[orNil: helloWorld.index(helloWorld.startIndex, offsetBy: 11) ... helloWorld.index(helloWorld.startIndex, offsetBy: 12)], "d!")
154+
155+
156+
XCTAssertNil( helloWorld[orNil: helloWorld.startIndex ... helloWorld.index(helloWorld.startIndex, offsetBy: 13)])
157+
158+
145159
mutationTest { copy1, copy2 in
146160
copy2[ 1...3] = [9]
147161
copy1[orNil: 1...3] = [9]
@@ -293,6 +307,13 @@ final class RangeOrNilTests: XCTestCase {
293307
XCTAssertNil(first5Fibonacci[orNil: ..<(-10)])
294308
XCTAssertNil(first5Fibonacci[orNil: ..<( 9 )])
295309

310+
XCTAssertEqual(helloWorld[orNil: ..<helloWorld.startIndex ], "")
311+
XCTAssertEqual(helloWorld[orNil: ..<helloWorld.index(helloWorld.startIndex, offsetBy: 1) ], "H")
312+
XCTAssertEqual(helloWorld[orNil: ..<helloWorld.index(helloWorld.startIndex, offsetBy: 5) ], "Hello")
313+
XCTAssertEqual(helloWorld[orNil: ..<helloWorld.index(helloWorld.startIndex, offsetBy: 12)], "Hello, World")
314+
XCTAssertEqual(helloWorld[orNil: ..<helloWorld.index(helloWorld.startIndex, offsetBy: 12)], "Hello, World")
315+
XCTAssertEqual(helloWorld[orNil: ..<helloWorld.index(helloWorld.startIndex, offsetBy: 13)], "Hello, World!")
316+
296317

297318

298319
mutationTest { copy1, copy2 in
@@ -376,6 +397,12 @@ final class RangeOrNilTests: XCTestCase {
376397
XCTAssertNil(first5Fibonacci[orNil: ...( 6 )])
377398
XCTAssertNil(first5Fibonacci[orNil: ...(-10)])
378399
XCTAssertNil(first5Fibonacci[orNil: ...( 9 )])
400+
401+
XCTAssertEqual(helloWorld[orNil: ...helloWorld.startIndex ], "H")
402+
XCTAssertEqual(helloWorld[orNil: ...helloWorld.index(helloWorld.startIndex, offsetBy: 1) ], "He")
403+
XCTAssertEqual(helloWorld[orNil: ...helloWorld.index(helloWorld.startIndex, offsetBy: 5) ], "Hello,")
404+
XCTAssertEqual(helloWorld[orNil: ...helloWorld.index(helloWorld.startIndex, offsetBy: 11)], "Hello, World")
405+
XCTAssertEqual(helloWorld[orNil: ...helloWorld.index(helloWorld.startIndex, offsetBy: 12)], "Hello, World!")
379406

380407

381408

@@ -446,27 +473,3 @@ final class RangeOrNilTests: XCTestCase {
446473
("testSubscriptOrNil_PartialRangeUpThrough", testSubscriptOrNil_PartialRangeUpThrough),
447474
]
448475
}
449-
450-
451-
452-
internal func mutationTest<Value>(with value: Value, do closure: (_ copy: inout Value) -> Void) {
453-
var copy = value
454-
closure(&copy)
455-
}
456-
457-
458-
internal func mutationTest(do test: (_ copy: inout [Int]) -> Void) {
459-
mutationTest(with: first5Fibonacci, do: test)
460-
}
461-
462-
463-
internal func mutationTest<Value>(with value: Value, do closure: (_ copy1: inout Value, _ copy2: inout Value) -> Void) {
464-
var copy1 = value
465-
var copy2 = value
466-
closure(&copy1, &copy2)
467-
}
468-
469-
470-
internal func mutationTest(do test: (_ copy1: inout [Int], _ copy2: inout [Int]) -> Void) {
471-
mutationTest(with: first5Fibonacci, do: test)
472-
}

Tests/SafeCollectionAccessTests/SafeCollectionAccessTests.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,13 @@ final class SafeCollectionAccessTests: XCTestCase {
4242
XCTAssertNil(first5Fibonacci[orNil: 6])
4343

4444

45+
XCTAssertEqual(helloWorld[orNil: helloWorld.startIndex], "H")
46+
XCTAssertEqual(helloWorld[orNil: helloWorld.index(helloWorld.startIndex, offsetBy: 0)], "H")
47+
XCTAssertEqual(helloWorld[orNil: helloWorld.index(helloWorld.startIndex, offsetBy: 1)], "e")
48+
XCTAssertEqual(helloWorld[orNil: helloWorld.index(helloWorld.startIndex, offsetBy: 12)], "!")
49+
XCTAssertEqual(helloWorld[orNil: helloWorld.index(helloWorld.startIndex, offsetBy: 11)], "d")
50+
51+
4552
// MARK: Mutation
4653

4754
mutationTest { copy in
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
//
2+
// Test Tools.swift
3+
//
4+
//
5+
// Created by Ben Leggiero on 2021-06-10.
6+
//
7+
8+
import Foundation
9+
10+
11+
12+
// MARK: - Shared values
13+
14+
internal let first5Fibonacci = [1, 1, 2, 3, 5]
15+
internal let helloWorld = "Hello, World!"
16+
17+
18+
19+
// MARK: - Convenience functions
20+
21+
internal func mutationTest<Value>(with value: Value, do closure: (_ copy: inout Value) -> Void) {
22+
var copy = value
23+
closure(&copy)
24+
}
25+
26+
27+
internal func mutationTest(with value: String, do closure: (_ copy: inout String) -> Void) {
28+
var copy = value
29+
closure(&copy)
30+
}
31+
32+
33+
internal func mutationTest(do test: (_ copy: inout [Int]) -> Void) {
34+
mutationTest(with: first5Fibonacci, do: test)
35+
}
36+
37+
38+
internal func mutationTest<Value>(with value: Value, do closure: (_ copy1: inout Value, _ copy2: inout Value) -> Void) {
39+
var copy1 = value
40+
var copy2 = value
41+
closure(&copy1, &copy2)
42+
}
43+
44+
45+
internal func mutationTest(with value: String, do closure: (_ copy1: inout String, _ copy2: inout String) -> Void) {
46+
var copy1 = value
47+
var copy2 = value
48+
closure(&copy1, &copy2)
49+
}
50+
51+
52+
internal func mutationTest(do test: (_ copy1: inout [Int], _ copy2: inout [Int]) -> Void) {
53+
mutationTest(with: first5Fibonacci, do: test)
54+
}
55+
56+
57+
58+
// MARK: - Test conformance
59+
60+
extension String: RandomAccessCollection {
61+
public typealias SubSequence = Substring
62+
}
63+
64+
65+
66+
extension Substring: RandomAccessCollection {
67+
public typealias SubSequence = Substring
68+
}

0 commit comments

Comments
 (0)