From bcdbac800a26acb017de528df9e52615f403a40c Mon Sep 17 00:00:00 2001 From: Pranav Kasetti Date: Wed, 7 Apr 2021 20:57:26 +0100 Subject: [PATCH] Add new `FiniteCycle` type as the return type of `Collection.cycled(times:)` (#106) --- Guides/Cycle.md | 12 +-- Sources/Algorithms/Cycle.swift | 113 +++++++++++++++++++- Sources/Algorithms/Product.swift | 6 +- Tests/SwiftAlgorithmsTests/CycleTests.swift | 56 ++++++++-- 4 files changed, 170 insertions(+), 17 deletions(-) diff --git a/Guides/Cycle.md b/Guides/Cycle.md index 727c6034..b137b25d 100644 --- a/Guides/Cycle.md +++ b/Guides/Cycle.md @@ -16,8 +16,7 @@ for x in (1...3).cycled(times: 3) { // Prints 1 through 3 three times ``` -`cycled(times:)` combines two other existing standard library functions -(`repeatElement` and `joined`) to provide a more expressive way of repeating a +`cycled(times:)` provides a more expressive way of repeating a collection's elements a limited number of times. ## Detailed Design @@ -28,7 +27,7 @@ Two new methods are added to collections: extension Collection { func cycled() -> Cycle - func cycled(times: Int) -> FlattenSequence> + func cycled(times: Int) -> FiniteCycle } ``` @@ -36,9 +35,10 @@ The new `Cycle` type is a sequence only, given that the `Collection` protocol design makes infinitely large types impossible/impractical. `Cycle` also conforms to `LazySequenceProtocol` when the base type conforms. -Note that despite its name, the returned `FlattenSequence` will always have -`Collection` conformance, and will have `BidirectionalCollection` conformance -when called on a bidirectional collection. +Note that the returned `FiniteCycle` will always have `Collection` +conformance, and will have `BidirectionalCollection` conformance +when called on a bidirectional collection. `FiniteCycle` also +conforms to `LazyCollectionProtocol` when the base type conforms. ### Complexity diff --git a/Sources/Algorithms/Cycle.swift b/Sources/Algorithms/Cycle.swift index d62ee49c..975211fb 100644 --- a/Sources/Algorithms/Cycle.swift +++ b/Sources/Algorithms/Cycle.swift @@ -57,6 +57,115 @@ extension Cycle: Sequence { extension Cycle: LazySequenceProtocol where Base: LazySequenceProtocol {} + +/// A collection wrapper that repeats the elements of a base collection for a +/// finite number of times. +public struct FiniteCycle { + /// A Product2 instance for iterating the Base collection. + @usableFromInline + internal let product: Product2, Base> + + @inlinable + internal init(base: Base, times: Int) { + self.product = Product2(0.., Base>.Index + + @inlinable + internal init(_ productIndex: Product2, Base>.Index) { + self.productIndex = productIndex + } + + @inlinable + public static func == (lhs: Index, rhs: Index) -> Bool { + lhs.productIndex == rhs.productIndex + } + + @inlinable + public static func < (lhs: Index, rhs: Index) -> Bool { + lhs.productIndex < rhs.productIndex + } + } + + @inlinable + public var startIndex: Index { + Index(product.startIndex) + } + + @inlinable + public var endIndex: Index { + Index(product.endIndex) + } + + @inlinable + public subscript(_ index: Index) -> Element { + product[index.productIndex].1 + } + + @inlinable + public func index(after i: Index) -> Index { + let productIndex = product.index(after: i.productIndex) + return Index(productIndex) + } + + @inlinable + public func distance(from start: Index, to end: Index) -> Int { + product.distance(from: start.productIndex, to: end.productIndex) + } + + @inlinable + public func index(_ i: Index, offsetBy distance: Int) -> Index { + let productIndex = product.index(i.productIndex, offsetBy: distance) + return Index(productIndex) + } + + @inlinable + public func index( + _ i: Index, + offsetBy distance: Int, + limitedBy limit: Index + ) -> Index? { + guard let productIndex = product.index(i.productIndex, + offsetBy: distance, + limitedBy: limit.productIndex) else { + return nil + } + return Index(productIndex) + } + + @inlinable + public var count: Int { + product.count + } +} + +extension FiniteCycle: BidirectionalCollection + where Base: BidirectionalCollection { + @inlinable + public func index(before i: Index) -> Index { + let productIndex = product.index(before: i.productIndex) + return Index(productIndex) + } +} + +extension FiniteCycle: RandomAccessCollection + where Base: RandomAccessCollection {} + +extension FiniteCycle: Equatable where Base: Equatable {} +extension FiniteCycle: Hashable where Base: Hashable {} + //===----------------------------------------------------------------------===// // cycled() //===----------------------------------------------------------------------===// @@ -110,7 +219,7 @@ extension Collection { /// /// - Complexity: O(1) @inlinable - public func cycled(times: Int) -> FlattenSequence> { - repeatElement(self, count: times).joined() + public func cycled(times: Int) -> FiniteCycle { + FiniteCycle(base: self, times: times) } } diff --git a/Sources/Algorithms/Product.swift b/Sources/Algorithms/Product.swift index b1cd8951..73017901 100644 --- a/Sources/Algorithms/Product.swift +++ b/Sources/Algorithms/Product.swift @@ -47,7 +47,8 @@ extension Product2: Sequence { } @inlinable - public mutating func next() -> (Base1.Element, Base2.Element)? { + public mutating func next() -> (Base1.Element, + Base2.Element)? { // This is the initial state, where i1.next() has never // been called, or the final state, where i1.next() has // already returned nil. @@ -124,7 +125,8 @@ extension Product2: Collection where Base1: Collection { } @inlinable - public subscript(position: Index) -> (Base1.Element, Base2.Element) { + public subscript(position: Index) -> (Base1.Element, + Base2.Element) { (base1[position.i1], base2[position.i2]) } diff --git a/Tests/SwiftAlgorithmsTests/CycleTests.swift b/Tests/SwiftAlgorithmsTests/CycleTests.swift index 0d52788b..6badaff8 100644 --- a/Tests/SwiftAlgorithmsTests/CycleTests.swift +++ b/Tests/SwiftAlgorithmsTests/CycleTests.swift @@ -19,28 +19,70 @@ final class CycleTests: XCTestCase { [1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4], cycle.prefix(20) ) + } + func testCycleClosedRangePrefix() { let a = Array((0..<17).cycled().prefix(10_000)) XCTAssertEqual(10_000, a.count) - + } + + func testEmptyCycle() { let empty = Array("".cycled()) XCTAssert(empty.isEmpty) } - + + func testCycleLazy() { + XCTAssertLazySequence((1...4).lazy.cycled()) + } + func testRepeated() { let repeats = (1...4).cycled(times: 3) XCTAssertEqualSequences( [1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4], repeats) - + } + + func testRepeatedClosedRange() { + let repeats = Array((1..<5).cycled(times: 2500)) + XCTAssertEqual(10_000, repeats.count) + } + + func testRepeatedEmptyCollection() { let empty1 = Array("".cycled(times: 100)) XCTAssert(empty1.isEmpty) - + } + + func testRepeatedZeroTimesCycle() { let empty2 = Array("Hello".cycled(times: 0)) XCTAssert(empty2.isEmpty) } - - func testCycleLazy() { - XCTAssertLazySequence((1...4).lazy.cycled()) + + func testRepeatedLazy() { + XCTAssertLazySequence((1...4).lazy.cycled(times: 3)) + } + + func testRepeatedIndexMethods() { + let cycle = (1..<5).cycled(times: 2) + let startIndex = cycle.startIndex + var nextIndex = cycle.index(after: startIndex) + XCTAssertEqual(cycle.distance(from: startIndex, to: nextIndex), 1) + + nextIndex = cycle.index(nextIndex, offsetBy: 5) + XCTAssertEqual(cycle.distance(from: startIndex, to: nextIndex), 6) + + nextIndex = cycle.index(nextIndex, offsetBy: 2, limitedBy: cycle.endIndex)! + XCTAssertEqual(cycle.distance(from: startIndex, to: nextIndex), 8) + + let outOfBounds = cycle.index(nextIndex, offsetBy: 1, + limitedBy: cycle.endIndex) + XCTAssertNil(outOfBounds) + + let previousIndex = cycle.index(before: nextIndex) + XCTAssertEqual(cycle.distance(from: startIndex, to: previousIndex), 7) + } + + func testRepeatedCount() { + let cycle = (1..<5).cycled(times: 2) + XCTAssertEqual(cycle.count, 8) } }