Skip to content

Commit 505b573

Browse files
[Chunked] Implementing chunked collection by a given count
1 parent 3864606 commit 505b573

File tree

1 file changed

+174
-0
lines changed

1 file changed

+174
-0
lines changed

Sources/Algorithms/Chunked.swift

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,3 +246,177 @@ extension Collection {
246246
try chunked(on: projection, by: ==)
247247
}
248248
}
249+
250+
//===----------------------------------------------------------------------===//
251+
// chunks(ofCount:)
252+
//===----------------------------------------------------------------------===//
253+
254+
/// A collection that presents the elements of its base collection
255+
/// in `SubSequence` chunks of any given count.
256+
///
257+
/// A `ChunkedByCount` is a lazy view on the base Collection, but it does not implicitly confer
258+
/// laziness on algorithms applied to its result. In other words, for ordinary collections `c`:
259+
///
260+
/// * `c.chunks(ofCount: 3)` does not create new storage
261+
/// * `c.chunks(ofCount: 3).map(f)` maps eagerly and returns a new array
262+
/// * `c.lazy.chunks(ofCount: 3).map(f)` maps lazily and returns a `LazyMapCollection`
263+
public struct ChunkedByCount<Base: Collection> {
264+
265+
public typealias Element = Base.SubSequence
266+
267+
@usableFromInline
268+
internal let base: Base
269+
270+
@usableFromInline
271+
internal let chunkCount: Int
272+
273+
@usableFromInline
274+
internal var computedStartIndex: Index
275+
276+
/// Creates a view instance that presents the elements of `base`
277+
/// in `SubSequence` chunks of the given count.
278+
///
279+
/// - Complexity: O(n)
280+
@inlinable
281+
internal init(_base: Base, _chunkCount: Int) {
282+
self.base = _base
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)
294+
}
295+
}
296+
297+
extension ChunkedByCount: Collection {
298+
public struct Index {
299+
@usableFromInline
300+
internal let baseRange: Range<Base.Index>
301+
302+
@usableFromInline
303+
internal init(_baseRange: Range<Base.Index>) {
304+
self.baseRange = _baseRange
305+
}
306+
}
307+
308+
/// - Complexity: O(n)
309+
public var startIndex: Index { computedStartIndex }
310+
public var endIndex: Index {
311+
Index(_baseRange: base.endIndex..<base.endIndex)
312+
}
313+
314+
/// - Complexity: O(n)
315+
public subscript(i: Index) -> Element {
316+
base[i.baseRange]
317+
}
318+
319+
@inlinable
320+
public func index(after i: Index) -> Index {
321+
let baseIdx = base.index(
322+
i.baseRange.upperBound, offsetBy: chunkCount,
323+
limitedBy: base.endIndex
324+
) ?? base.endIndex
325+
return Index(_baseRange: i.baseRange.upperBound..<baseIdx)
326+
}
327+
}
328+
329+
extension ChunkedByCount.Index: Comparable {
330+
@inlinable
331+
public static func < (lhs: ChunkedByCount.Index,
332+
rhs: ChunkedByCount.Index) -> Bool {
333+
lhs.baseRange.lowerBound < rhs.baseRange.lowerBound
334+
}
335+
}
336+
337+
extension ChunkedByCount:
338+
BidirectionalCollection, RandomAccessCollection
339+
where Base: RandomAccessCollection {
340+
@inlinable
341+
public func index(before i: Index) -> Index {
342+
var offset = chunkCount
343+
if i.baseRange.lowerBound == base.endIndex {
344+
let remainder = base.count%chunkCount
345+
if remainder != 0 {
346+
offset = remainder
347+
}
348+
}
349+
350+
let baseIdx = base.index(
351+
i.baseRange.lowerBound, offsetBy: -offset,
352+
limitedBy: base.startIndex
353+
) ?? base.startIndex
354+
return Index(_baseRange: baseIdx..<i.baseRange.lowerBound)
355+
}
356+
357+
@inlinable
358+
public func distance(from start: Index, to end: Index) -> Int {
359+
let distance =
360+
base.distance(from: start.baseRange.lowerBound,
361+
to: end.baseRange.lowerBound)
362+
let (quotient, remainder) =
363+
distance.quotientAndRemainder(dividingBy: chunkCount)
364+
// Increment should account for negative distances.
365+
if remainder < 0 {
366+
return quotient - 1
367+
}
368+
return quotient + (remainder == 0 ? 0 : 1)
369+
}
370+
371+
@inlinable
372+
public var count: Int {
373+
let (quotient, remainder) =
374+
base.count.quotientAndRemainder(dividingBy: chunkCount)
375+
return quotient + (remainder == 0 ? 0 : 1)
376+
}
377+
}
378+
379+
extension Collection {
380+
/// Returns a `ChunkedCollection<Self>` view presenting the elements
381+
/// in chunks with count of the given count parameter.
382+
///
383+
/// - Parameter size: The size of the chunks. If the count parameter
384+
/// is evenly divided by the count of the base `Collection` all the
385+
/// chunks will have the count equals to size.
386+
/// Otherwise, the last chunk will contain the remaining elements.
387+
///
388+
/// let c = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
389+
/// print(c.chunks(ofCount: 5).map(Array.init))
390+
/// // [[1, 2, 3, 4, 5], [6, 7, 8, 9, 10]]
391+
///
392+
/// print(c.chunks(ofCount: 3).map(Array.init))
393+
/// // [[1, 2, 3], [4, 5, 6], [7, 8, 9], [10]]
394+
///
395+
/// - Complexity: O(1)
396+
@inlinable
397+
public func chunks(ofCount count: Int) -> ChunkedByCount<Self> {
398+
precondition(count > 0, " Cannot chunk with count <= 0!")
399+
return ChunkedByCount(_base: self, _chunkCount: count)
400+
}
401+
}
402+
403+
// Conditional conformances.
404+
extension ChunkedByCount: Equatable where Base: Equatable {}
405+
406+
// Since we have another stored property of type `Index` on the
407+
// collection, synthetization of hashble conformace would require
408+
// a `Base.Index: Hashable` constraint, so we implement the hasher
409+
// only in terms of base. Since the computed index is based on it,
410+
// it should make a difference here.
411+
extension ChunkedByCount: Hashable where Base: Hashable {
412+
public func hash(into hasher: inout Hasher) {
413+
hasher.combine(base)
414+
}
415+
}
416+
extension ChunkedByCount.Index: Hashable where Base.Index: Hashable {}
417+
418+
// Lazy conditional conformance.
419+
extension ChunkedByCount: LazySequenceProtocol
420+
where Base: LazySequenceProtocol {}
421+
extension ChunkedByCount: LazyCollectionProtocol
422+
where Base: LazyCollectionProtocol {}

0 commit comments

Comments
 (0)