Skip to content

Commit b5d20c8

Browse files
authored
Merge pull request #18 from RougeWare/feature/Mutation
Introduced mutation! 🎉
2 parents 244ed5b + 5c92e47 commit b5d20c8

File tree

10 files changed

+1099
-101
lines changed

10 files changed

+1099
-101
lines changed

.github/workflows/swift.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: Swift
1+
name: Tests
22

33
on: [push]
44

.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Package.resolved

Lines changed: 16 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Package.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// swift-tools-version:5.1
1+
// swift-tools-version:5.2
22
// The swift-tools-version declares the minimum version of Swift required to build this package.
33

44
import PackageDescription
@@ -13,14 +13,14 @@ let package = Package(
1313
],
1414
dependencies: [
1515
// Dependencies declare other packages that this package depends on.
16-
// .package(url: /* package url */, from: "1.0.0"),
16+
.package(name: "RangeTools", url: "https://github.com/RougeWare/Swift-Range-Tools.git", from: "1.2.1"),
1717
],
1818
targets: [
1919
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
2020
// Targets can depend on other targets in this package, and on products in packages which this package depends on.
2121
.target(
2222
name: "SafeCollectionAccess",
23-
dependencies: []),
23+
dependencies: ["RangeTools"]),
2424
.testTarget(
2525
name: "SafeCollectionAccessTests",
2626
dependencies: ["SafeCollectionAccess"]),

README.md

Lines changed: 53 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,67 @@
1+
[![Tested on GitHub Actions](https://github.com/RougeWare/Swift-Safe-Collection-Access/actions/workflows/swift.yml/badge.svg)](https://github.com/RougeWare/Swift-Safe-Collection-Access/actions/workflows/swift.yml) [![](https://www.codefactor.io/repository/github/rougeware/swift-safe-collection-access/badge)](https://www.codefactor.io/repository/github/rougeware/swift-safe-collection-access)
2+
3+
[![Swift 5](https://img.shields.io/badge/swift-5-brightgreen.svg?logo=swift&logoColor=white)](https://swift.org) [![swift package manager 5.2 is supported](https://img.shields.io/badge/swift%20package%20manager-5.2-brightgreen.svg)](https://swift.org/package-manager) [![Supports macOS, iOS, tvOS, watchOS, Linux, & Windows](https://img.shields.io/badge/macOS%20%7C%20iOS%20%7C%20tvOS%20%7C%20watchOS%20%7C%20Linux%20%7C%20Windows-grey.svg)](./Package.swift)
4+
[![](https://img.shields.io/github/release-date/rougeware/swift-safe-collection-access?label=latest%20release)](https://github.com/RougeWare/Swift-Safe-Collection-Access/releases/latest)
5+
6+
17
# Swift Safe Collection Access #
28

3-
Ever wonder why Swift crashes if you access a collection the wrong way? Especially an array? Me too here's some extensions.
9+
Ever wonder why Swift crashes if you access a collection the wrong way? Especially an array? Me too here's some extensions. 🎉
410

511

612

7-
## Example ##
13+
## `collection[orNil:]` ##
14+
15+
This subscript fails gracefully on invalid input by returning `nil` or refusing to mutate the collection. With valid input, it behaves precisely like Swift's builtin subscripts!
816

917
```swift
1018

1119
import SafeCollectionAccess
1220

1321

1422

15-
let first5Fibonacci = [1, 1, 2, 3, 5]
23+
var first5Fibonacci = [1, 1, 2, 3, 5]
24+
25+
first5Fibonacci[orNil: 0] // Optional(1)
26+
first5Fibonacci[orNil: 3] == first5Fibonacci[3] // true
27+
first5Fibonacci[orNil: 999] // `nil`
28+
first5Fibonacci[orNil: -1] // `nil`
29+
30+
first5Fibonacci[orNil: 2...4] // Optional([2, 4])
31+
first5Fibonacci[orNil: -2...2] // `nil`
32+
first5Fibonacci[orNil: ..<42] // `nil`
33+
34+
35+
first5Fibonacci[orNil: 1] = 42 // [1, 42, 2, 3, 5]
36+
first5Fibonacci[orNil: 999] = -1 // [1, 42, 2, 3, 5]
37+
first5Fibonacci[orNil: -1] = 777 // [1, 42, 2, 3, 5]
38+
39+
first5Fibonacci[orNil: 0...2] = [9, 5] // [9, 5, 3, 5]
40+
first5Fibonacci[orNil: -2...2] = [42] // [9, 5, 3, 5]
41+
first5Fibonacci[orNil: ..<42] = [7, 7, 7] // [9, 5, 3, 5]
42+
```
43+
44+
45+
46+
## `collection[clamping:]` ##
47+
48+
This subscript fails gracefully on invalid input by returning or mutating the closest valid element (or, with empty collections, returning `nil` or refusing to mutate). With valid input, it behaves precisely like Swift's builtin subscripts!
49+
50+
```swift
51+
52+
import SafeCollectionAccess
53+
54+
55+
56+
var first5Fibonacci = [1, 1, 2, 3, 5]
57+
58+
first5Fibonacci[clamping: 0] // 1
59+
first5Fibonacci[clamping: 3] == first5Fibonacci[3] // true
60+
first5Fibonacci[clamping: 999] // 5
61+
first5Fibonacci[clamping: -1] // 1
62+
1663

17-
print(first5Fibonacci[orNil: 0]) // Optional(1)
18-
print(first5Fibonacci[orNil: 0] == first5Fibonacci[safe: 1]) // true
19-
print(first5Fibonacci[safe: 5]) // nil
64+
first5Fibonacci[clamping: 1] = 42 // [1, 42, 2, 3, 5]
65+
first5Fibonacci[clamping: 999] = -1 // [1, 42, 2, 3, -1]
66+
first5Fibonacci[clamping: -1] = 777 // [777, 42, 2, 3, -1]
2067
```

Sources/SafeCollectionAccess/Ranges or nil.swift

Lines changed: 92 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -12,61 +12,81 @@ import Foundation
1212

1313
public extension RandomAccessCollection {
1414

15-
/// Safely access ranges in this collection.
16-
/// If the range you pass extends outside this collection, `nil` is returned.
17-
///
18-
/// - Complexity: O(1)
15+
/// Determines whether this collection contains elements throughout the given range
1916
///
20-
/// - Parameter range: A range of valid lower and upper indices of characters in the string.
21-
/// - Returns: The characters at the given index-range in this string, or `nil` if the given range is not contained
22-
/// within `0` (inclusive) and `count` (exclusive).
23-
@inlinable
24-
subscript(orNil range: ClosedRange<Index>) -> SubSequence? {
25-
return contains(index: range.lowerBound) && contains(index: range.upperBound)
26-
? self[range]
27-
: nil
17+
/// - Parameter range: The range of elements to test
18+
/// - Returns: `true` iff this collection contains elements at all the indices covered by the `range`
19+
func completelyContains<RangeType>(range: RangeType) -> Bool
20+
where RangeType: SafeRangeExpression,
21+
RangeType.Bound == Index
22+
{
23+
guard let range = range.relative(clampingIfContainedWithin: self) else {
24+
return false
25+
}
26+
27+
return completelyContains(range: range)
2828
}
2929

3030

31-
/// Safely access ranges in this collection.
32-
/// If the range you pass extends outside this collection, `nil` is returned.
31+
/// Determines whether this collection contains elements throughout the given range
3332
///
34-
/// - Complexity: O(1)
33+
/// - Parameter range: The range of elements to test
34+
/// - Returns: `true` iff this collection contains elements at all the indices covered by the `range`
35+
func completelyContains(range: Range<Index>) -> Bool {
36+
contains(index: range.lowerBound)
37+
&& contains(index: index(before: range.upperBound))
38+
}
39+
40+
41+
/// Determines whether this collection contains any elements at the given range, even if the range extends beyond this collection
3542
///
36-
/// - Parameter range: A range of valid lower and upper indices of characters in the string.
37-
/// - Returns: The characters at the given index-range in this string, or `nil` if the given range is not contained
38-
/// within `0` (inclusive) and `count` (inclusive).
39-
@inlinable
40-
subscript(orNil range: Range<Index>) -> SubSequence? {
41-
guard !range.isEmpty else {
42-
return self[range]
43+
/// - Parameter range: The range of elements to test
44+
/// - Returns: `true` iff this collection contains elements at any of the indices covered by the `range`
45+
func rangeOverlapsThisCollection<RangeType>(_ range: RangeType) -> Bool
46+
where RangeType: SafeRangeExpression,
47+
RangeType.Bound == Index
48+
{
49+
guard let range = range.relative(clampingIfContainedWithin: self) else {
50+
return false
4351
}
4452

45-
return contains(index: range.lowerBound) && contains(index: index(before: range.upperBound))
46-
? self[range]
47-
: nil
53+
return rangeOverlapsThisCollection(range)
4854
}
4955

5056

51-
/// Safely access ranges in this collection.
52-
/// If the range you pass extends outside this collection, `nil` is returned.
57+
/// Determines whether this collection contains any elements at the given range, even if the range extends beyond this collection
5358
///
54-
/// - Complexity: O(1)
59+
/// - Parameter range: The range of elements to test
60+
/// - Returns: `true` iff this collection contains elements at any of the indices covered by the `range`
61+
func rangeOverlapsThisCollection(_ range: Range<Index>) -> Bool {
62+
contains(index: range.lowerBound)
63+
|| contains(index: index(before: range.upperBound))
64+
}
65+
66+
67+
/// Some sugar for clamping the given range within this collection. If the given range doesn't overlap this collection at all, `nil` is returned.
5568
///
56-
/// - Parameter range: A range of a valid lower index of characters in the string.
57-
/// - Returns: The characters at the given index-range in this string, or `nil` if the given range is not contained
58-
/// within `0` (inclusive) and `count` (exclusive).
59-
@inlinable
60-
subscript(orNil range: PartialRangeFrom<Index>) -> SubSequence? {
61-
62-
guard range.lowerBound != endIndex else {
63-
// `self[self.endIndex...]` is always valid, resulting in an empty subsequence
64-
return self[range]
65-
}
66-
67-
return contains(index: range.lowerBound)
68-
? self[range.lowerBound...]
69-
: nil
69+
/// This sugar sweetens `SafeRangeExpression`'s `relative(clampingTo:)` method.
70+
///
71+
/// - Parameter range: The range to clamp to this collection
72+
/// - Returns: A range which is definitely within this collecion, or `nil` if it doesn't overlap this collection
73+
@inline(__always)
74+
func clamp<RangeType>(range: RangeType) -> Range<Index>?
75+
where RangeType: SafeRangeExpression,
76+
RangeType.Bound == Index
77+
{
78+
range.relative(clampingIfContainedWithin: self)
79+
}
80+
81+
82+
/// A generic implementation of the below `[orNil:]` subscripts
83+
@inline(__always)
84+
@usableFromInline
85+
internal subscript<RangeType>(_orNil_getOnly range: RangeType) -> SubSequence?
86+
where RangeType: SafeRangeExpression,
87+
RangeType.Bound == Index
88+
{
89+
clamp(range: range).map { self[$0] }
7090
}
7191

7292

@@ -75,35 +95,44 @@ public extension RandomAccessCollection {
7595
///
7696
/// - Complexity: O(1)
7797
///
78-
/// - Parameter range: A range of a valid upper index of characters in the string.
79-
/// - Returns: The characters at the given index-range in this string, or `nil` if the given range's bound is not
80-
/// within `0` (inclusive) and `count` (inclusive).
98+
/// - Parameter range: A range of valid indices of characters in the collection.
99+
/// - Returns: The characters at the given index-range in this string, or `nil` if the given range is not contained
100+
/// within `0` (inclusive) and `count` (exclusive).
81101
@inlinable
82-
subscript(orNil range: PartialRangeUpTo<Index>) -> SubSequence? {
83-
84-
guard range.upperBound != startIndex else {
85-
// `self[..<self.startIndex]` is always valid, resulting in an empty subsequence
86-
return self[range]
87-
}
88-
89-
return contains(index: index(before: range.upperBound))
90-
? self[orNil: startIndex ..< range.upperBound]
91-
: nil
102+
subscript<RangeType>(orNil range: RangeType) -> SubSequence?
103+
where RangeType: SafeRangeExpression,
104+
RangeType.Bound == Index {
105+
self[_orNil_getOnly: range]
92106
}
93107

94108

95-
/// Safely access ranges in this collection.
96-
/// If the range you pass extends outside this collection, `nil` is returned.
109+
/// Safely access or mutate ranges in this collection.
110+
/// If the range you pass extends outside this collection, `nil` is returned, or if you're trying to mutate this collection, nothing is done.
111+
/// Setting the value to `nil` is interpreted as removing the range from this collection
97112
///
98113
/// - Complexity: O(1)
99114
///
100-
/// - Parameter range: A range of a valid upper index of characters in the string.
101-
/// - Returns: The characters at the given index-range in this string, or `nil` if the given range's bound is not
115+
/// - Parameter range: A range of valid indices of characters in the collection.
116+
/// - Returns: The characters at the given index-range in this string, or `nil` if the given range is not contained
102117
/// within `0` (inclusive) and `count` (exclusive).
103118
@inlinable
104-
subscript(orNil range: PartialRangeThrough<Index>) -> SubSequence? {
105-
return contains(index: range.upperBound)
106-
? self[orNil: startIndex ... range.upperBound]
107-
: nil
119+
subscript<RangeType>(orNil range: RangeType) -> SubSequence?
120+
where RangeType: SafeRangeExpression,
121+
RangeType.Bound == Index,
122+
Self: MutableCollection,
123+
Self: RangeReplaceableCollection
124+
{
125+
get { self[_orNil_getOnly: range] }
126+
127+
set {
128+
guard rangeOverlapsThisCollection(range) else { return }
129+
130+
if let newValue = newValue {
131+
self[range] = newValue
132+
}
133+
else {
134+
self.removeSubrange(range)
135+
}
136+
}
108137
}
109138
}

0 commit comments

Comments
 (0)