Skip to content

Commit dcade20

Browse files
Chaging implementation to store base slice range on the chunk index
1 parent 91f1e73 commit dcade20

File tree

3 files changed

+85
-93
lines changed

3 files changed

+85
-93
lines changed

Guides/Chunked.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,10 @@ remaining elements.
3838
```swift
3939
let names = ["David", "Kyle", "Karoy", "Nate"]
4040
let evenly = names.chunks(ofCount: 2)
41-
// [["David", "Kyle"], ["Karoy", "Nate"]]
41+
// equivalent to [["David", "Kyle"], ["Karoy", "Nate"]]
4242

4343
let remaining = names.chunks(ofCount: 3)
44-
// [["David", "Kyle", "Karoy"], "Nate"]]
44+
// equivalent to [["David", "Kyle", "Karoy"], ["Nate"]]
4545
```
4646

4747
The `chunks(ofCount:)` is the method of the [existing SE proposal][proposal].

Sources/Algorithms/Chunked.swift

Lines changed: 69 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -254,98 +254,123 @@ extension Collection {
254254
/// A collection that presents the elements of its base collection
255255
/// in `SubSequence` chunks of any given size.
256256
///
257-
/// A ChunkedCollection is a lazy view on the base Collection, but it does not implicitly confer
257+
/// A `ChunkedByCount` is a lazy view on the base Collection, but it does not implicitly confer
258258
/// laziness on algorithms applied to its result. In other words, for ordinary collections `c`:
259259
///
260260
/// * `c.chunks(of: 3)` does not create new storage
261261
/// * `c.chunks(of: 3).map(f)` maps eagerly and returns a new array
262262
/// * `c.lazy.chunks(of: 3).map(f)` maps lazily and returns a `LazyMapCollection`
263-
public struct ChunkedCollection<Base: Collection> {
263+
public struct ChunkedByCount<Base: Collection> {
264264

265265
public typealias Element = Base.SubSequence
266266

267-
public let base: Base
268267
@usableFromInline
269-
internal let _chunkCount: Int
268+
internal let base: Base
269+
270+
@usableFromInline
271+
internal let chunkCount: Int
270272

273+
@usableFromInline
274+
internal var computedStartIndex: Index
275+
271276
/// Creates a view instance that presents the elements of `base`
272277
/// in `SubSequence` chunks of the given size.
273278
///
274-
/// - Complexity: O(1)
279+
/// - Complexity: O(n)
275280
@inlinable
276281
internal init(_base: Base, _chunkCount: Int) {
277282
self.base = _base
278-
self._chunkCount = _chunkCount
283+
self.chunkCount = _chunkCount
284+
285+
// Compute the start index upfront in order to make
286+
// start index a O(1) lookup.
287+
let baseEnd = _base.index(
288+
_base.startIndex, offsetBy: _chunkCount,
289+
limitedBy: _base.endIndex
290+
) ?? _base.endIndex
291+
292+
self.computedStartIndex =
293+
Index(_baseRange: _base.startIndex..<baseEnd)
279294
}
280295
}
281296

282-
extension ChunkedCollection: Collection {
297+
extension ChunkedByCount: Collection {
283298
public struct Index {
284-
public let base: Base.Index
299+
@usableFromInline
300+
internal let baseRange: Range<Base.Index>
285301

286302
@usableFromInline
287-
init(_base: Base.Index) {
288-
self.base = _base
303+
internal init(_baseRange: Range<Base.Index>) {
304+
self.baseRange = _baseRange
289305
}
290306
}
291-
292-
public var startIndex: Index { Index(_base: base.startIndex) }
293-
public var endIndex: Index { Index(_base: base.endIndex) }
307+
308+
public var startIndex: Index { computedStartIndex }
309+
public var endIndex: Index {
310+
Index(_baseRange: base.endIndex..<base.endIndex)
311+
}
294312

295313
public subscript(i: Index) -> Element {
296-
base[i.base..<index(after: i).base]
314+
base[i.baseRange]
297315
}
298316

299317
@inlinable
300318
public func index(after i: Index) -> Index {
301319
let baseIdx = base.index(
302-
i.base, offsetBy: _chunkCount, limitedBy: base.endIndex)
303-
return Index(_base: baseIdx ?? base.endIndex)
320+
i.baseRange.upperBound, offsetBy: chunkCount,
321+
limitedBy: base.endIndex
322+
) ?? base.endIndex
323+
return Index(_baseRange: i.baseRange.upperBound..<baseIdx)
304324
}
305325
}
306326

307-
extension ChunkedCollection.Index: Comparable {
327+
extension ChunkedByCount.Index: Comparable {
308328
@inlinable
309-
public static func < (lhs: ChunkedCollection.Index,
310-
rhs: ChunkedCollection.Index) -> Bool {
311-
lhs.base < rhs.base
329+
public static func < (lhs: ChunkedByCount.Index,
330+
rhs: ChunkedByCount.Index) -> Bool {
331+
lhs.baseRange.lowerBound < rhs.baseRange.lowerBound
312332
}
313333
}
314334

315-
extension ChunkedCollection:
335+
extension ChunkedByCount:
316336
BidirectionalCollection, RandomAccessCollection
317337
where Base: RandomAccessCollection {
318338
@inlinable
319339
public func index(before i: Index) -> Index {
320-
if i.base == base.endIndex {
321-
let remainder = base.count%_chunkCount
340+
var offset = chunkCount
341+
if i.baseRange.lowerBound == base.endIndex {
342+
let remainder = base.count%chunkCount
322343
if remainder != 0 {
323-
return Index(_base: base.index(i.base, offsetBy: -remainder))
344+
offset = remainder
324345
}
325346
}
326-
return Index(_base: base.index(i.base, offsetBy: -_chunkCount))
347+
348+
let baseIdx = base.index(
349+
i.baseRange.lowerBound, offsetBy: -offset,
350+
limitedBy: base.startIndex
351+
) ?? base.startIndex
352+
return Index(_baseRange: baseIdx..<i.baseRange.lowerBound)
327353
}
328354

329355
@inlinable
330356
public func distance(from start: Index, to end: Index) -> Int {
331-
let distance = base.distance(from: start.base, to: end.base)
357+
let distance =
358+
base.distance(from: start.baseRange.lowerBound,
359+
to: end.baseRange.lowerBound)
332360
let (quotient, remainder) =
333-
distance.quotientAndRemainder(dividingBy: _chunkCount)
361+
distance.quotientAndRemainder(dividingBy: chunkCount)
334362
// Increment should account for negative distances.
335363
if remainder < 0 {
336364
return quotient - 1
337365
}
338366
return quotient + (remainder == 0 ? 0 : 1)
339367
}
340-
368+
341369
@inlinable
342-
public func index(_ i: Index, offsetBy n: Int) -> Index {
343-
guard n != 0 else { return i }
344-
345-
let bound = n > 0 ? base.endIndex : base.startIndex
346-
return Index(_base:
347-
base.index(i.base, offsetBy: n * _chunkCount, limitedBy: bound) ?? bound
348-
)
370+
public var count: Int {
371+
let (quotient, remainder) =
372+
base.count.quotientAndRemainder(dividingBy: chunkCount)
373+
return quotient + (remainder == 0 ? 0 : 1)
349374
}
350375
}
351376

@@ -367,12 +392,17 @@ extension Collection {
367392
///
368393
/// - Complexity: O(1)
369394
@inlinable
370-
public func chunks(ofCount count: Int) -> ChunkedCollection<Self> {
395+
public func chunks(ofCount count: Int) -> ChunkedByCount<Self> {
371396
precondition(count > 0, " Cannot chunk with count <= 0!")
372-
return ChunkedCollection(_base: self, _chunkCount: count)
397+
return ChunkedByCount(_base: self, _chunkCount: count)
373398
}
374399
}
375400

376401
// Conditional conformances.
377-
extension ChunkedCollection: Equatable where Base: Equatable {}
378-
extension ChunkedCollection: Hashable where Base: Hashable {}
402+
extension ChunkedByCount: Equatable where Base: Equatable {}
403+
extension ChunkedByCount: Hashable where Base: Hashable, Base.Index: Hashable {}
404+
extension ChunkedByCount.Index: Hashable where Base.Index: Hashable {}
405+
406+
// Lazy conditional conformance.
407+
extension ChunkedByCount: LazySequenceProtocol where Base: LazySequenceProtocol {}
408+
extension ChunkedByCount: LazyCollectionProtocol where Base: LazyCollectionProtocol {}

Tests/SwiftAlgorithmsTests/ChunkedTests.swift

Lines changed: 14 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ final class ChunkedTests: XCTestCase {
8181
//===----------------------------------------------------------------------===//
8282
// Tests for `chunks(ofCount:)`
8383
//===----------------------------------------------------------------------===//
84-
func testChunksOfSize() {
84+
func testChunksOfCount() {
8585
XCTAssertEqualSequences([Int]().chunks(ofCount: 1), [])
8686
XCTAssertEqualSequences([Int]().chunks(ofCount: 5), [])
8787

@@ -96,7 +96,7 @@ final class ChunkedTests: XCTestCase {
9696
[[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]])
9797
}
9898

99-
func testChunksOfSizeBidirectional() {
99+
func testChunksOfCountBidirectional() {
100100
let collection = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
101101

102102
XCTAssertEqualSequences(collection.chunks(ofCount: 1).reversed(),
@@ -109,7 +109,7 @@ final class ChunkedTests: XCTestCase {
109109
[[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]])
110110
}
111111

112-
func testChunksOfSizeCount() {
112+
func testChunksOfCountCount() {
113113
XCTAssertEqual([Int]().chunks(ofCount: 1).count, 0)
114114
XCTAssertEqual([Int]().chunks(ofCount: 5).count, 0)
115115

@@ -119,62 +119,24 @@ final class ChunkedTests: XCTestCase {
119119
XCTAssertEqual(collection.chunks(ofCount: 5).count, 2)
120120
XCTAssertEqual(collection.chunks(ofCount: 11).count, 1)
121121
}
122-
123-
func testChunksOfSizeDistance() {
122+
123+
func testEmptyChunksTraversal() {
124124
let emptyChunks = [Int]().chunks(ofCount: 1)
125125

126-
XCTAssertEqual(
127-
emptyChunks.distance(from: emptyChunks.startIndex,
128-
to: emptyChunks.endIndex), 0)
129-
126+
validateIndexTraversals(emptyChunks)
127+
}
128+
129+
func testChunksOfCountTraversal() {
130130
let collection = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
131-
let chunks = collection.chunks(ofCount: 3)
132-
133-
XCTAssertEqual(
134-
chunks.distance(from: chunks.startIndex, to: chunks.endIndex), 4)
135-
XCTAssertEqual(
136-
chunks.distance(from: chunks.endIndex, to: chunks.startIndex), -4)
137-
138-
var idx = chunks.startIndex
139-
chunks.formIndex(after: &idx)
140-
XCTAssertEqual(chunks.distance(from: chunks.startIndex, to: idx), 1)
141-
XCTAssertEqual(chunks.distance(from: idx, to: chunks.startIndex), -1)
142-
143-
chunks.formIndex(after: &idx)
144-
XCTAssertEqual(chunks.distance(from: chunks.startIndex, to: idx), 2)
145-
XCTAssertEqual(chunks.distance(from: idx, to: chunks.startIndex), -2)
131+
let chunks = collection.chunks(ofCount: 2)
132+
133+
validateIndexTraversals(chunks)
146134
}
147135

148-
func testChunksOfSizeOffset() {
149-
let emptyChunks = [Int]().chunks(ofCount: 1)
150-
151-
XCTAssertEqual(
152-
emptyChunks.index(emptyChunks.startIndex, offsetBy: 1),
153-
emptyChunks.endIndex)
154-
XCTAssertEqual(
155-
emptyChunks.index(emptyChunks.startIndex, offsetBy: -1),
156-
emptyChunks.endIndex)
157-
136+
func testChunksOfCountWithRemainderTraversal() {
158137
let collection = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
159138
let chunks = collection.chunks(ofCount: 3)
160139

161-
XCTAssertEqual(chunks.index(chunks.startIndex, offsetBy: 1),
162-
chunks.index(after: chunks.startIndex))
163-
164-
XCTAssertEqual(chunks.index(chunks.startIndex, offsetBy: 1),
165-
chunks.index(after: chunks.startIndex))
166-
167-
var expected = chunks.startIndex
168-
chunks.formIndex(after: &expected)
169-
chunks.formIndex(after: &expected)
170-
171-
XCTAssertEqual(chunks.index(chunks.startIndex, offsetBy: 2),
172-
expected)
173-
174-
// Offset is bounded
175-
XCTAssertEqual(chunks.index(chunks.startIndex, offsetBy: 5),
176-
chunks.endIndex)
177-
XCTAssertEqual(chunks.index(chunks.startIndex, offsetBy: -1),
178-
chunks.startIndex)
140+
validateIndexTraversals(chunks)
179141
}
180142
}

0 commit comments

Comments
 (0)