Some stride-related discussions took place on the forums today. I was inspired to play with strides as I took a break between my kids’ appointments. If any of this resonates with you as a potential user for it, let me know.
Index Strides
The following extension allows you to stride over a collection’s indices.
extension BidirectionalCollection where Index: Strideable { /// Returns the sequence of collection elements (`self[start]`, /// `self[start + stride]`, `self[start + 2 * stride]` ... *last* /// where *last* is the last element in the collection whose index /// is less than or equal to `end`. /// /// - Note: There is no guarantee that the element at index `end` /// is part of the sequence /// /// - Parameter start: The starting index /// - Parameter end: The upper index bound /// - Parameter distance: The stride for each successive index /// - Returns: A lazy sequence of collection members public func stride(from start: Index, through end: Index, by distance: Index.Stride) -> LazyMapSequence<StrideThrough<Index>, Element> { return Swift.stride(from: start, through: end, by: distance) .lazy.map({ self[$0] }) } /// Returns the sequence of collection elements (`self[start]`, /// `self[start + stride]`, `self[start + 2 * stride]` ... *last* /// where *last* is the last element in the collection whose index /// is strictly less than `end`. /// /// - Parameter start: The starting index /// - Parameter end: The upper index bound /// - Parameter distance: The stride for each successive index /// - Returns: A lazy sequence of collection members public func stride(from start: Index, to end: Index, by distance: Index.Stride) -> LazyMapSequence<StrideTo<Index>, Element> { return Swift.stride(from: start, to: end, by: distance) .lazy.map({ self[$0] }) } }
Here’s an example that uses this approach to form a sequence that strides through the collection:
let myArray = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"] for value in myArray.stride(from: 0, through: myArray.count, by: 3) { print(value) // a, d, g, j }
Range Operators
The next snippet uses the division operator to create its stride. This was pitched by “Tony Y” in a forum thread.
extension CountableClosedRange { static func / (range: CountableClosedRange, divisor: Bound.Stride) -> StrideThrough<Bound> { let distance = range.distance(from: range.startIndex, to: range.endIndex) / divisor // deprecated return Swift.stride(from: range.lowerBound, through: range.upperBound, by: distance) } }
In this example, you just “divide” the range:
for idx in (0 ... 100) / 5 { print(idx) // 0, 20, 40, 60, 80, 100 // yes that's 6 not 5, but isn't that what // you want? 5 subranges between each index? }
Because I wrote this fairly quickly, I used the deprecated range.distance
function as straight index arithmetic seemed to error:
error: binary operator '-' cannot be applied to two 'ClosedRangeIndex' operands let distance = (range.endIndex - range.startIndex) / divisor
As usual, I probably messed up somewhere (or several somewheres) or you might see ways I could improve the code. Let me know!
Update: After some thought, I decided I preferred a sequence of subranges (which should probably be a type and not like I do it here):
extension CountableClosedRange { static func / (range: CountableClosedRange, divisor: Bound.Stride) -> UnfoldSequence<CountableClosedRange<Bound>, (CountableClosedRange<Bound>?, Bool)> { let distance = range.distance(from: range.startIndex, to: range.endIndex) / divisor let rangeStride = Swift.stride(from: range.lowerBound, through: range.upperBound, by: distance) var indices = Array(rangeStride) indices.append(range.upperBound) indices.reverse() guard var current = indices.popLast(), var next = indices.popLast() else { fatalError("Guaranteed count failed in index population") } var done = false return sequence(first: current ... next.advanced(by: -1), next: { _ in guard !done else { return nil } (current, next) = (next, indices.popLast()!) if current == next { done = true return next ... next } else if next == range.upperBound && indices.isEmpty { done = true return current ... next } else { return current ... next.advanced(by: -1) } }) } } for idx in (0 ... 99) / 5 { print(idx) // 0...19, 20...39, 40...59, 60...79, 80...99 } for idx in (0 ... 100) / 5 { print(idx) // 0...19, 20...39, 40...59, 60...79, 80...99, 100...100 }
This isn’t a debugged solution. For example, trying to divide 1 … 1 by 1 or 1 … 2 by 1 will both fail.
3 Comments
Lovely, as usual. Thank you, Erica!
Where you are striding through a collection (as in the first example), it is important to use the `to` method since the `through` will crash where the count of elements is a multiple of the `stride`:
“`swift
let myArray = [1, 2, 3]
for _ in myArray.stride(from: 0, through: 3, by: 3) {} // Fatal error: Index out of range
“`
When I implemented `stride` method on `RandomAccessCollection` for our team, I took this into account and arrived at this api:
“`swift
let ints = 1…100
ints.prefix(9).stride(by: 3) // → [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
ints.prefix(9).stride(by: 3, spanning: 2) // → [[1, 2], [4, 5], [7, 8]]
“`
In my case, this method returns a lazy `StridingSubsequence<Slice<CountableClosedRange>>`.
Forgot to mention, I also have:
ints.prefix(4).strideAround(by: 1, spanning: 3) // [[1, 2, 3], [2, 3, 4], [3, 4, 1], [4, 1, 2]]
... although this would be more useful as an infinite sequence...
Soroush and I worked on this for a bit and here’s what we came up with: Adding Strideable Sequences