Skip to content

Commit ca42a0c

Browse files
committed
Add Stride for Collection
1 parent b28d248 commit ca42a0c

File tree

3 files changed

+376
-0
lines changed

3 files changed

+376
-0
lines changed

Guides/Stride.md

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# Stride
2+
3+
[[Source](https://github.com/apple/swift-algorithms/blob/main/Sources/Algorithms/Stride.swift) |
4+
[Tests](https://github.com/apple/swift-algorithms/blob/main/Tests/SwiftAlgorithmsTests/StrideTests.swift)]
5+
6+
A type that steps over a collection’s elements by the specified amount.
7+
8+
This is available through the `striding(by:)` method on any `Collection`.
9+
10+
```swift
11+
(0...10).striding(by: 2) // == [0, 2, 4, 6, 8, 10]
12+
```
13+
14+
If the stride is larger than the collection count, the resulting wrapper only contains the
15+
first element.
16+
17+
The stride amount must be a positive value.
18+
19+
## Detailed Design
20+
21+
The `striding(by:)` method is declared as a `Collection` extension, and returns a
22+
`Stride` type:
23+
24+
```swift
25+
extension Collection {
26+
public func striding(by step: Int) -> Stride<Self>
27+
}
28+
```
29+
30+
A custom `Index` type is defined so that it's not possible to get confused when trying
31+
to access an index of the stride collection.
32+
33+
```swift
34+
[0, 1, 2, 3, 4].striding(by: 2)[1] // == 1
35+
[0, 1, 2, 3, 4].striding(by: 2).map { $0 }[1] // == 2
36+
```
37+
38+
A careful thought was given to the composition of these strides by giving a custom
39+
implementation to `index(_:offsetBy:limitedBy)` which multiplies the offset by the
40+
stride amount.
41+
42+
```swift
43+
base.index(i.base, offsetBy: distance * stride, limitedBy: base.endIndex)
44+
```
45+
46+
The following two lines of code are equivalent, including performance:
47+
48+
```swift
49+
(0...10).striding(by: 6)
50+
(0...10).striding(by: 2).stride(by: 3)
51+
```
52+
53+
### Complexity
54+
55+
The call to `striding(by: k)` is always O(_1_) and access to the next value in the stride
56+
is O(_1_) if the collection conforms to `RandomAccessCollection`, otherwise O(_k_).
57+
58+
### Comparison with other languages
59+
60+
[rust has `Strided`](https://docs.rs/strided/0.2.9/strided/) available in a crate.
61+
[c++ has std::slice::stride](http://www.cplusplus.com/reference/valarray/slice/stride/)
62+
63+
The semantics of `striding` described in this documentation are equivalent.

Sources/Algorithms/Stride.swift

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift Algorithms open source project
4+
//
5+
// Copyright (c) 2020 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
//
10+
//===----------------------------------------------------------------------===//
11+
12+
//===----------------------------------------------------------------------===//
13+
// striding(by:)
14+
//===----------------------------------------------------------------------===//
15+
16+
extension Collection {
17+
/// Returns a collection stepping through the elements every `step` starting
18+
/// at the first value. Any remainders of the stride will be trimmed.
19+
///
20+
/// (0...10).striding(by: 2) // == [0, 2, 4, 6, 8, 10]
21+
/// (0...10).striding(by: 3) // == [0, 3, 6, 9]
22+
///
23+
/// - Complexity: O(1). Access to successive values is O(1) if the
24+
/// collection conforms to `RandomAccessCollection`; otherwise,
25+
/// O(_k_), where _k_ is the striding `step`.
26+
///
27+
/// - Parameter step: The amount to step with each iteration.
28+
/// - Returns: Returns a collection stepping through the elements by the
29+
/// specified amount.
30+
public func striding(by step: Int) -> Stride<Self> {
31+
Stride(base: self, stride: step)
32+
}
33+
}
34+
35+
public struct Stride<Base: Collection> {
36+
37+
public let base: Base
38+
public let stride: Int
39+
40+
init(base: Base, stride: Int) {
41+
precondition(stride > 0, "striding must be greater than zero")
42+
self.base = base
43+
self.stride = stride
44+
}
45+
}
46+
47+
extension Stride {
48+
public func striding(by step: Int) -> Self {
49+
Stride(base: base, stride: stride * step)
50+
}
51+
}
52+
53+
extension Stride: Collection {
54+
55+
public struct Index: Comparable {
56+
57+
let base: Base.Index
58+
59+
init(_ base: Base.Index) {
60+
self.base = base
61+
}
62+
63+
public static func < (lhs: Index, rhs: Index) -> Bool {
64+
lhs.base < rhs.base
65+
}
66+
}
67+
68+
public var startIndex: Index {
69+
Index(base.startIndex)
70+
}
71+
72+
public var endIndex: Index {
73+
Index(base.endIndex)
74+
}
75+
76+
public subscript(i: Index) -> Base.Element {
77+
base[i.base]
78+
}
79+
80+
public func index(after i: Index) -> Index {
81+
precondition(i.base < base.endIndex, "Advancing past end index")
82+
return base.index(i.base, offsetBy: stride, limitedBy: base.endIndex)
83+
.map(Index.init) ?? endIndex
84+
}
85+
86+
public func index(
87+
_ i: Index,
88+
offsetBy distance: Int,
89+
limitedBy limit: Index
90+
) -> Index? {
91+
base.index(i.base, offsetBy: distance * stride, limitedBy: limit.base)
92+
.map(Index.init)
93+
}
94+
95+
public var count: Int {
96+
let limit = base.count - 1
97+
return limit / stride + (limit < 0 ? 0 : 1)
98+
}
99+
100+
public func distance(from start: Index, to end: Index) -> Int {
101+
let distance = base.distance(from: start.base, to: end.base)
102+
return distance / stride + (distance % stride > 0 ? 1 : 0)
103+
}
104+
105+
public func index(_ i: Index, offsetBy distance: Int) -> Index {
106+
precondition(i.base < base.endIndex, "Advancing past end index")
107+
let limit = distance > 0 ? endIndex : startIndex
108+
return index(i, offsetBy: distance, limitedBy: limit) ?? limit
109+
}
110+
}
111+
112+
extension Stride: BidirectionalCollection
113+
where Base: RandomAccessCollection {
114+
115+
public func index(before i: Index) -> Index {
116+
precondition(i.base > base.startIndex, "Incrementing past start index")
117+
if i == endIndex {
118+
let count = base.count
119+
precondition(count > 0, "Can't move before the starting index")
120+
return Index(
121+
base.index(base.endIndex, offsetBy: -((count - 1) % stride + 1))
122+
)
123+
} else {
124+
guard let step = base.index(
125+
i.base,
126+
offsetBy: -stride,
127+
limitedBy: startIndex.base
128+
) else {
129+
fatalError("Incrementing past start index")
130+
}
131+
return Index(step)
132+
}
133+
}
134+
}
135+
136+
extension Stride: RandomAccessCollection
137+
where Base: RandomAccessCollection {}
138+
139+
extension Stride: Equatable
140+
where Base.Element: Equatable {
141+
142+
public static func == (lhs: Stride, rhs: Stride) -> Bool {
143+
lhs.elementsEqual(rhs, by: ==)
144+
}
145+
146+
}
147+
148+
extension Stride: Hashable
149+
where Base.Element: Hashable {
150+
151+
public func hash(into hasher: inout Hasher) {
152+
hasher.combine(stride)
153+
for element in self {
154+
hasher.combine(element)
155+
}
156+
}
157+
158+
}
159+
160+
extension Stride.Index: Hashable
161+
where Base.Index: Hashable {}
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift Algorithms open source project
4+
//
5+
// Copyright (c) 2020 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
//
10+
//===----------------------------------------------------------------------===//
11+
12+
import XCTest
13+
import Algorithms
14+
15+
final class StridingTests: XCTestCase {
16+
17+
func testStride() {
18+
let a = (0...10)
19+
XCTAssertEqualSequences(a.striding(by: 1), (0...10))
20+
XCTAssertEqualSequences(a.striding(by: 2), [0, 2, 4, 6, 8, 10])
21+
XCTAssertEqualSequences(a.striding(by: 3), [0, 3, 6, 9])
22+
XCTAssertEqualSequences(a.striding(by: 4), [0, 4, 8])
23+
XCTAssertEqualSequences(a.striding(by: 5), [0, 5, 10])
24+
XCTAssertEqualSequences(a.striding(by: 10), [0, 10])
25+
XCTAssertEqualSequences(a.striding(by: 11), [0])
26+
}
27+
28+
func testStrideString() {
29+
let s = "swift"
30+
XCTAssertEqualSequences(s.striding(by: 2), ["s", "i", "t"])
31+
}
32+
33+
func testStrideReversed() {
34+
let a = [0, 1, 2, 3, 4, 5]
35+
XCTAssertEqualSequences(a.striding(by: 3).reversed(), [3, 0])
36+
XCTAssertEqualSequences(a.reversed().striding(by: 2), [5, 3, 1])
37+
}
38+
39+
func testStrideIndexes() {
40+
let a = [0, 1, 2, 3, 4, 5].striding(by: 2)
41+
var i = a.startIndex
42+
XCTAssertEqual(a[i], 0)
43+
a.formIndex(after: &i)
44+
XCTAssertEqual(a[i], 2)
45+
a.formIndex(after: &i)
46+
XCTAssertEqual(a[i], 4)
47+
a.formIndex(before: &i)
48+
XCTAssertEqual(a[i], 2)
49+
a.formIndex(before: &i)
50+
XCTAssertEqual(a[i], 0)
51+
// a.formIndex(before: &i) // Precondition failed: Advancing past start index
52+
// a.index(after: a.endIndex) // Precondition failed: Advancing past end index
53+
}
54+
55+
func testStrideCompositionEquivalence() {
56+
let a = (0...10)
57+
XCTAssertEqualSequences(a.striding(by: 6), a.striding(by: 2).striding(by: 3))
58+
XCTAssertTrue(a.striding(by: 6) == a.striding(by: 2).striding(by: 3))
59+
XCTAssert(type(of: a.striding(by: 2).striding(by: 3)) == Stride<ClosedRange<Int>>.self)
60+
}
61+
62+
func testEquality() {
63+
let a = [1, 2, 3, 4, 5].striding(by: 2)
64+
let b = [1, 0, 3, 0, 5].striding(by: 2)
65+
XCTAssertEqual(a, b)
66+
}
67+
68+
func testStrideLast() {
69+
XCTAssertEqual((1...10).striding(by: 2).last, 9) // 1, 3, 5, 7, 9
70+
XCTAssertEqual((1...10).striding(by: 3).last, 10) // 1, 4, 7, 10
71+
XCTAssertEqual((1...10).striding(by: 4).last, 9) // 1, 5, 9
72+
XCTAssertEqual((1...10).striding(by: 5).last, 6) // 1, 6
73+
XCTAssertEqual((1...100).striding(by: 50).last, 51) // 1, 51
74+
XCTAssertEqual((1...5).striding(by: 2).last, 5) // 1, 3, 5
75+
XCTAssertEqual([Int]().striding(by: 2).last, nil) // empty
76+
}
77+
78+
func testCount() {
79+
let empty = [Int]().striding(by: 2)
80+
XCTAssertEqual(empty.count, 0)
81+
let a = (0...10)
82+
XCTAssertEqual(a.striding(by: 1).count, (0...10).count)
83+
XCTAssertEqual(a.striding(by: 2).count, [0, 2, 4, 6, 8, 10].count)
84+
XCTAssertEqual(a.striding(by: 3).count, [0, 3, 6, 9].count)
85+
XCTAssertEqual(a.striding(by: 4).count, [0, 4, 8].count)
86+
XCTAssertEqual(a.striding(by: 5).count, [0, 5, 10].count)
87+
XCTAssertEqual(a.striding(by: 10).count, [0, 10].count)
88+
XCTAssertEqual(a.striding(by: 11).count, [0].count)
89+
}
90+
91+
func testDistance() {
92+
93+
do {
94+
let a = (0...100).striding(by: 11)
95+
XCTAssertEqual(a.distance(from: a.startIndex, to: a.endIndex), a.count)
96+
for (i, index) in a.indices.enumerated() {
97+
XCTAssertEqual(a.distance(from: a.startIndex, to: index), i)
98+
}
99+
100+
var i = a.startIndex
101+
a.formIndex(&i, offsetBy: 3)
102+
XCTAssertEqual(a.distance(from: a.startIndex, to: i), 3)
103+
XCTAssertEqual(a[i], 33)
104+
}
105+
106+
do {
107+
108+
let a = (0...100).striding(by: 10)
109+
XCTAssertEqual(a.distance(from: a.startIndex, to: a.endIndex), a.count)
110+
111+
for (i, index) in a.indices.enumerated() {
112+
XCTAssertEqual(a.distance(from: a.startIndex, to: index), i)
113+
}
114+
115+
var i = a.startIndex
116+
a.formIndex(&i, offsetBy: 3)
117+
XCTAssertEqual(a.distance(from: a.startIndex, to: i), 3)
118+
XCTAssertEqual(a[i], 30)
119+
}
120+
121+
do {
122+
123+
let a = (0...100).striding(by: 101)
124+
XCTAssertEqual(a.distance(from: a.startIndex, to: a.endIndex), a.count)
125+
126+
for (i, index) in a.indices.enumerated() {
127+
XCTAssertEqual(a.distance(from: a.startIndex, to: index), i)
128+
}
129+
130+
var i = a.startIndex
131+
a.formIndex(&i, offsetBy: 3)
132+
XCTAssertEqual(a.distance(from: a.startIndex, to: i), a.count)
133+
XCTAssertEqual(i, a.endIndex)
134+
// a[i] // == Fatal error: Index out of range
135+
}
136+
}
137+
138+
func testOffsetBy() {
139+
let a = (0...100).striding(by: 22)
140+
let b = [0, 22, 44, 66, 88]
141+
for i in 0..<a.count {
142+
XCTAssertEqual(a[a.index(a.startIndex, offsetBy: i)], b[i])
143+
}
144+
}
145+
146+
func testOffsetByEndIndex() {
147+
let a = 1...5
148+
let b = a.striding(by: 3) // [1, 4]
149+
let i = b.index(b.startIndex, offsetBy: 2)
150+
XCTAssertEqual(i, b.endIndex)
151+
}
152+
}

0 commit comments

Comments
 (0)