Skip to content

Commit

Permalink
Add new FiniteCycle type as the return type of `Collection.cycled(t…
Browse files Browse the repository at this point in the history
…imes:)` (#106)
  • Loading branch information
LemonSpike authored Apr 7, 2021
1 parent 718220d commit bcdbac8
Show file tree
Hide file tree
Showing 4 changed files with 170 additions and 17 deletions.
12 changes: 6 additions & 6 deletions Guides/Cycle.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -28,17 +27,18 @@ Two new methods are added to collections:
extension Collection {
func cycled() -> Cycle<Self>

func cycled(times: Int) -> FlattenSequence<Repeated<Self>>
func cycled(times: Int) -> FiniteCycle<Self>
}
```

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

Expand Down
113 changes: 111 additions & 2 deletions Sources/Algorithms/Cycle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<Base: Collection> {
/// A Product2 instance for iterating the Base collection.
@usableFromInline
internal let product: Product2<Range<Int>, Base>

@inlinable
internal init(base: Base, times: Int) {
self.product = Product2(0..<times, base)
}
}

extension FiniteCycle: LazySequenceProtocol, LazyCollectionProtocol
where Base: LazyCollectionProtocol { }

extension FiniteCycle: Collection {

public typealias Element = Base.Element

public struct Index: Comparable {
/// The index corresponding to the Product2 index at this position.
@usableFromInline
internal let productIndex: Product2<Range<Int>, Base>.Index

@inlinable
internal init(_ productIndex: Product2<Range<Int>, 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()
//===----------------------------------------------------------------------===//
Expand Down Expand Up @@ -110,7 +219,7 @@ extension Collection {
///
/// - Complexity: O(1)
@inlinable
public func cycled(times: Int) -> FlattenSequence<Repeated<Self>> {
repeatElement(self, count: times).joined()
public func cycled(times: Int) -> FiniteCycle<Self> {
FiniteCycle(base: self, times: times)
}
}
6 changes: 4 additions & 2 deletions Sources/Algorithms/Product.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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])
}

Expand Down
56 changes: 49 additions & 7 deletions Tests/SwiftAlgorithmsTests/CycleTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}

0 comments on commit bcdbac8

Please sign in to comment.