diff --git a/Guides/Stride.md b/Guides/Stride.md new file mode 100644 index 00000000..a309f250 --- /dev/null +++ b/Guides/Stride.md @@ -0,0 +1,63 @@ +# Stride + +[[Source](https://github.com/apple/swift-algorithms/blob/main/Sources/Algorithms/Stride.swift) | + [Tests](https://github.com/apple/swift-algorithms/blob/main/Tests/SwiftAlgorithmsTests/StrideTests.swift)] + +A type that steps over sequence elements by the specified amount. + +This is available through the `striding(by:)` method on any `Sequence`. + +```swift +(0...10).striding(by: 2) // == [0, 2, 4, 6, 8, 10] +``` + +If the stride is larger than the count, the resulting wrapper only contains the +first element. + +The stride amount must be a positive value. + +## Detailed Design + +The `striding(by:)` method is declared as a `Sequence` extension, and returns a +`Stride` type: + +```swift +extension Sequence { + public func striding(by step: Int) -> Stride +} +``` + +A custom `Index` type is defined so that it's not possible to get confused when trying +to access an index of the stride collection. + +```swift +[0, 1, 2, 3, 4].striding(by: 2)[1] // == 1 +[0, 1, 2, 3, 4].striding(by: 2).map { $0 }[1] // == 2 +``` + +A careful thought was given to the composition of these strides by giving a custom +implementation to `index(_:offsetBy:limitedBy)` which multiplies the offset by the +stride amount. + +```swift +base.index(i.base, offsetBy: distance * stride, limitedBy: base.endIndex) +``` + +The following two lines of code are equivalent, including performance: + +```swift +(0...10).striding(by: 6) +(0...10).striding(by: 2).stride(by: 3) +``` + +### Complexity + +The call to `striding(by: k)` is always O(_1_) and access to the next value in the stride +is O(_1_) if the collection conforms to `RandomAccessCollection`, otherwise O(_k_). + +### Comparison with other languages + +[rust has `Strided`](https://docs.rs/strided/0.2.9/strided/) available in a crate. +[c++ has std::slice::stride](http://www.cplusplus.com/reference/valarray/slice/stride/) + +The semantics of `striding` described in this documentation are equivalent. diff --git a/Sources/Algorithms/Stride.swift b/Sources/Algorithms/Stride.swift new file mode 100644 index 00000000..a9228956 --- /dev/null +++ b/Sources/Algorithms/Stride.swift @@ -0,0 +1,220 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Algorithms open source project +// +// Copyright (c) 2020 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +//===----------------------------------------------------------------------===// +// striding(by:) +//===----------------------------------------------------------------------===// + +extension Sequence { + /// Returns a sequence stepping through the elements every `step` starting + /// at the first value. Any remainders of the stride will be trimmed. + /// + /// (0...10).striding(by: 2) // == [0, 2, 4, 6, 8, 10] + /// (0...10).striding(by: 3) // == [0, 3, 6, 9] + /// + /// - Complexity: O(1). Access to successive values is O(1) if the + /// collection conforms to `RandomAccessCollection`; otherwise, + /// O(_k_), where _k_ is the striding `step`. + /// + /// - Parameter step: The amount to step with each iteration. + /// - Returns: Returns a sequence or collection for stepping through the + /// elements by the specified amount. + public func striding(by step: Int) -> Stride { + Stride(base: self, stride: step) + } +} + +public struct Stride { + + let base: Base + let stride: Int + + init(base: Base, stride: Int) { + precondition(stride > 0, "striding must be greater than zero") + self.base = base + self.stride = stride + } +} + +extension Stride { + public func striding(by step: Int) -> Self { + Stride(base: base, stride: stride * step) + } +} + +extension Stride: Sequence { + + public struct Iterator: IteratorProtocol { + + var iterator: Base.Iterator + let stride: Int + var striding: Bool = false + + public mutating func next() -> Base.Element? { + guard striding else { + striding = true + return iterator.next() + } + for _ in 0.. Stride.Iterator { + Iterator(iterator: base.makeIterator(), stride: stride) + } +} + +extension Stride: Collection where Base: Collection { + + public struct Index: Comparable { + + let base: Base.Index + + init(_ base: Base.Index) { + self.base = base + } + + public static func < (lhs: Index, rhs: Index) -> Bool { + lhs.base < rhs.base + } + } + + public var startIndex: Index { + Index(base.startIndex) + } + + public var endIndex: Index { + Index(base.endIndex) + } + + public subscript(i: Index) -> Base.Element { + base[i.base] + } + + public func index(after i: Index) -> Index { + precondition(i.base < base.endIndex, "Advancing past end index") + return index(i, offsetBy: 1) + } + + public func index( + _ i: Index, + offsetBy n: Int, + limitedBy limit: Index + ) -> Index? { + guard n != 0 else { return i } + guard limit != i else { return nil } + + return n > 0 + ? offsetForward(i, offsetBy: n, limitedBy: limit) + : offsetBackward(i, offsetBy: -n, limitedBy: limit) + } + + private func offsetForward( + _ i: Index, + offsetBy n: Int, + limitedBy limit: Index + ) -> Index? { + if limit < i { + if let idx = base.index( + i.base, + offsetBy: n * stride, + limitedBy: base.endIndex + ) { + return Index(idx) + } else { + assert(distance(from: i, to: endIndex) == n, "Advancing past end index") + return endIndex + } + } else if let idx = base.index( + i.base, + offsetBy: n * stride, + limitedBy: limit.base + ) { + return Index(idx) + } else { + return distance(from: i, to: limit) == n + ? endIndex + : nil + } + } + + private func offsetBackward( + _ i: Index, + offsetBy n: Int, + limitedBy limit: Index + ) -> Index? { + let distance = i == endIndex + ? -((base.count - 1) % stride + 1) + (n - 1) * -stride + : n * -stride + return base.index( + i.base, + offsetBy: distance, + limitedBy: limit.base + ).map(Index.init) + } + + public var count: Int { + base.isEmpty ? 0 : (base.count - 1) / stride + 1 + } + + public func distance(from start: Index, to end: Index) -> Int { + let distance = base.distance(from: start.base, to: end.base) + return distance / stride + (distance % stride).signum() + } + + public func index(_ i: Index, offsetBy distance: Int) -> Index { + precondition(distance <= 0 || i.base < base.endIndex, "Advancing past end index") + precondition(distance >= 0 || i.base > base.startIndex, "Incrementing past start index") + let limit = distance > 0 ? endIndex : startIndex + let idx = index(i, offsetBy: distance, limitedBy: limit) + precondition(idx != nil, "The distance \(distance) is not valid for this collection") + return idx! + } +} + +extension Stride: BidirectionalCollection + where Base: RandomAccessCollection { + + public func index(before i: Index) -> Index { + precondition(i.base > base.startIndex, "Incrementing past start index") + return index(i, offsetBy: -1) + } +} + +extension Stride: RandomAccessCollection + where Base: RandomAccessCollection {} + +extension Stride: Equatable + where Base.Element: Equatable { + + public static func == (lhs: Stride, rhs: Stride) -> Bool { + lhs.elementsEqual(rhs, by: ==) + } + +} + +extension Stride: Hashable + where Base.Element: Hashable { + + public func hash(into hasher: inout Hasher) { + hasher.combine(stride) + for element in self { + hasher.combine(element) + } + } + +} + +extension Stride.Index: Hashable + where Base.Index: Hashable {} diff --git a/Tests/SwiftAlgorithmsTests/StrideTests.swift b/Tests/SwiftAlgorithmsTests/StrideTests.swift new file mode 100644 index 00000000..536dbb1b --- /dev/null +++ b/Tests/SwiftAlgorithmsTests/StrideTests.swift @@ -0,0 +1,144 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Algorithms open source project +// +// Copyright (c) 2020 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +import XCTest +import Algorithms + +final class StridingTests: XCTestCase { + + func testStride() { + let a = 0...10 + XCTAssertEqualSequences(a.striding(by: 1), (0...10)) + XCTAssertEqualSequences(a.striding(by: 2), [0, 2, 4, 6, 8, 10]) + XCTAssertEqualSequences(a.striding(by: 3), [0, 3, 6, 9]) + XCTAssertEqualSequences(a.striding(by: 4), [0, 4, 8]) + XCTAssertEqualSequences(a.striding(by: 5), [0, 5, 10]) + XCTAssertEqualSequences(a.striding(by: 10), [0, 10]) + XCTAssertEqualSequences(a.striding(by: 11), [0]) + + let s = (0...).prefix(11) + XCTAssertEqualSequences(s.striding(by: 1), (0...10)) + XCTAssertEqualSequences(s.striding(by: 2), [0, 2, 4, 6, 8, 10]) + XCTAssertEqualSequences(s.striding(by: 3), [0, 3, 6, 9]) + XCTAssertEqualSequences(s.striding(by: 4), [0, 4, 8]) + XCTAssertEqualSequences(s.striding(by: 5), [0, 5, 10]) + XCTAssertEqualSequences(s.striding(by: 10), [0, 10]) + XCTAssertEqualSequences(s.striding(by: 11), [0]) + + let empty = (0...).prefix(0) + XCTAssertEqualSequences(empty.striding(by: 2), []) + } + + func testStrideString() { + let s = "swift" + XCTAssertEqualSequences(s.striding(by: 2), ["s", "i", "t"]) + } + + func testStrideReversed() { + let a = [0, 1, 2, 3, 4, 5] + XCTAssertEqualSequences(a.striding(by: 3).reversed(), [3, 0]) + XCTAssertEqualSequences(a.reversed().striding(by: 2), [5, 3, 1]) + } + + func testStrideIndexes() { + let a = [0, 1, 2, 3, 4, 5].striding(by: 2) + var i = a.startIndex + XCTAssertEqual(a[i], 0) + a.formIndex(after: &i) + XCTAssertEqual(a[i], 2) + a.formIndex(after: &i) + XCTAssertEqual(a[i], 4) + a.formIndex(before: &i) + XCTAssertEqual(a[i], 2) + a.formIndex(before: &i) + XCTAssertEqual(a[i], 0) +// a.formIndex(before: &i) // Precondition failed: Incrementing past start index +// a.index(after: a.endIndex) // Precondition failed: Advancing past end index + } + + func testStrideCompositionEquivalence() { + let a = (0...10) + XCTAssertEqualSequences(a.striding(by: 6), a.striding(by: 2).striding(by: 3)) + XCTAssertTrue(a.striding(by: 6) == a.striding(by: 2).striding(by: 3)) + XCTAssert(type(of: a.striding(by: 2).striding(by: 3)) == Stride>.self) + } + + func testEquality() { + let a = [1, 2, 3, 4, 5].striding(by: 2) + let b = [1, 0, 3, 0, 5].striding(by: 2) + XCTAssertEqual(a, b) + } + + func testStrideLast() { + XCTAssertEqual((1...10).striding(by: 2).last, 9) // 1, 3, 5, 7, 9 + XCTAssertEqual((1...10).striding(by: 3).last, 10) // 1, 4, 7, 10 + XCTAssertEqual((1...10).striding(by: 4).last, 9) // 1, 5, 9 + XCTAssertEqual((1...10).striding(by: 5).last, 6) // 1, 6 + XCTAssertEqual((1...100).striding(by: 50).last, 51) // 1, 51 + XCTAssertEqual((1...5).striding(by: 2).last, 5) // 1, 3, 5 + XCTAssertEqual([Int]().striding(by: 2).last, nil) // empty + } + + func testCount() { + let empty = [Int]().striding(by: 2) + XCTAssertEqual(empty.count, 0) + let a = (0...10) + XCTAssertEqual(a.striding(by: 1).count, (0...10).count) + XCTAssertEqual(a.striding(by: 2).count, [0, 2, 4, 6, 8, 10].count) + XCTAssertEqual(a.striding(by: 3).count, [0, 3, 6, 9].count) + XCTAssertEqual(a.striding(by: 4).count, [0, 4, 8].count) + XCTAssertEqual(a.striding(by: 5).count, [0, 5, 10].count) + XCTAssertEqual(a.striding(by: 10).count, [0, 10].count) + XCTAssertEqual(a.striding(by: 11).count, [0].count) + } + + func testIndexTraversals() { + let empty = [Int]() + validateIndexTraversals( + empty.striding(by: 1), + empty.striding(by: 2) + ) + let zero_to_one_hundered_range = 0...100 + validateIndexTraversals( + zero_to_one_hundered_range.striding(by: 10), + zero_to_one_hundered_range.striding(by: 11), + zero_to_one_hundered_range.striding(by: 101) + ) + let zero_to_one_hundered_array = Array(zero_to_one_hundered_range) + validateIndexTraversals( + zero_to_one_hundered_array.striding(by: 10), + zero_to_one_hundered_array.striding(by: 11), + zero_to_one_hundered_array.striding(by: 101) + ) + let string = "swift rocks".map(String.init) + validateIndexTraversals( + string.striding(by: 1), + string.striding(by: 2), + string.striding(by: 10) + ) + } + + + func testOffsetBy() { + let a = (0...100).striding(by: 22) + let b = [0, 22, 44, 66, 88] + for i in 0..