@@ -246,3 +246,307 @@ extension Collection {
246
246
try chunked ( on: projection, by: == )
247
247
}
248
248
}
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 startUpperBound : Base . 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
+ self . startUpperBound = _base. index (
288
+ _base. startIndex, offsetBy: _chunkCount,
289
+ limitedBy: _base. endIndex
290
+ ) ?? _base. endIndex
291
+ }
292
+ }
293
+
294
+ extension ChunkedByCount : Collection {
295
+ public struct Index {
296
+ @usableFromInline
297
+ internal let baseRange : Range < Base . Index >
298
+
299
+ @usableFromInline
300
+ internal init ( _baseRange: Range < Base . Index > ) {
301
+ self . baseRange = _baseRange
302
+ }
303
+ }
304
+
305
+ /// - Complexity: O(1)
306
+ @inlinable
307
+ public var startIndex : Index {
308
+ Index ( _baseRange: base. startIndex..< startUpperBound)
309
+ }
310
+ @inlinable
311
+ public var endIndex : Index {
312
+ Index ( _baseRange: base. endIndex..< base. endIndex)
313
+ }
314
+
315
+ /// - Complexity: O(1)
316
+ public subscript( i: Index ) -> Element {
317
+ precondition ( i < endIndex, " Index out of range " )
318
+ return base [ i. baseRange]
319
+ }
320
+
321
+ @inlinable
322
+ public func index( after i: Index ) -> Index {
323
+ precondition ( i < endIndex, " Advancing past end index " )
324
+ let baseIdx = base. index (
325
+ i. baseRange. upperBound, offsetBy: chunkCount,
326
+ limitedBy: base. endIndex
327
+ ) ?? base. endIndex
328
+ return Index ( _baseRange: i. baseRange. upperBound..< baseIdx)
329
+ }
330
+ }
331
+
332
+ extension ChunkedByCount . Index : Comparable {
333
+ @inlinable
334
+ public static func == ( lhs: ChunkedByCount . Index ,
335
+ rhs: ChunkedByCount . Index ) -> Bool {
336
+ lhs. baseRange. lowerBound == rhs. baseRange. lowerBound
337
+ }
338
+
339
+ @inlinable
340
+ public static func < ( lhs: ChunkedByCount . Index ,
341
+ rhs: ChunkedByCount . Index ) -> Bool {
342
+ lhs. baseRange. lowerBound < rhs. baseRange. lowerBound
343
+ }
344
+ }
345
+
346
+ extension ChunkedByCount :
347
+ BidirectionalCollection , RandomAccessCollection
348
+ where Base: RandomAccessCollection {
349
+ @inlinable
350
+ public func index( before i: Index ) -> Index {
351
+ precondition ( i > startIndex, " Advancing past start index " )
352
+
353
+ var offset = chunkCount
354
+ if i. baseRange. lowerBound == base. endIndex {
355
+ let remainder = base. count% chunkCount
356
+ if remainder != 0 {
357
+ offset = remainder
358
+ }
359
+ }
360
+
361
+ let baseIdx = base. index (
362
+ i. baseRange. lowerBound, offsetBy: - offset,
363
+ limitedBy: base. startIndex
364
+ ) ?? base. startIndex
365
+ return Index ( _baseRange: baseIdx..< i. baseRange. lowerBound)
366
+ }
367
+ }
368
+
369
+ extension ChunkedByCount {
370
+ @inlinable
371
+ public func distance( from start: Index , to end: Index ) -> Int {
372
+ let distance =
373
+ base. distance ( from: start. baseRange. lowerBound,
374
+ to: end. baseRange. lowerBound)
375
+ let ( quotient, remainder) =
376
+ distance. quotientAndRemainder ( dividingBy: chunkCount)
377
+ return quotient + remainder. signum ( )
378
+ }
379
+
380
+ @inlinable
381
+ public var count : Int {
382
+ let ( quotient, remainder) =
383
+ base. count. quotientAndRemainder ( dividingBy: chunkCount)
384
+ return quotient + remainder. signum ( )
385
+ }
386
+
387
+ @inlinable
388
+ public func index(
389
+ _ i: Index , offsetBy offset: Int , limitedBy limit: Index
390
+ ) -> Index ? {
391
+ guard offset != 0 else { return i }
392
+ guard limit != i else { return nil }
393
+
394
+ if offset > 0 {
395
+ return limit > i
396
+ ? offsetForward ( i, offsetBy: offset, limit: limit)
397
+ : offsetForward ( i, offsetBy: offset)
398
+ } else {
399
+ return limit < i
400
+ ? offsetBackward ( i, offsetBy: offset, limit: limit)
401
+ : offsetBackward ( i, offsetBy: offset)
402
+ }
403
+ }
404
+
405
+ @inlinable
406
+ public func index( _ i: Index , offsetBy distance: Int ) -> Index {
407
+ guard distance != 0 else { return i }
408
+
409
+ let idx = distance > 0
410
+ ? offsetForward ( i, offsetBy: distance)
411
+ : offsetBackward ( i, offsetBy: distance)
412
+ guard let index = idx else {
413
+ fatalError ( " Out of bounds " )
414
+ }
415
+ return index
416
+ }
417
+
418
+ @usableFromInline
419
+ internal func offsetForward(
420
+ _ i: Index , offsetBy distance: Int , limit: Index ? = nil
421
+ ) -> Index ? {
422
+ assert ( distance > 0 )
423
+
424
+ return makeOffsetIndex (
425
+ from: i, baseBound: base. endIndex,
426
+ distance: distance, baseDistance: distance * chunkCount,
427
+ limit: limit, by: >
428
+ )
429
+ }
430
+
431
+ // Convenience to compute offset backward base distance.
432
+ @inline ( __always)
433
+ private func computeOffsetBackwardBaseDistance(
434
+ _ i: Index , _ distance: Int
435
+ ) -> Int {
436
+ if i == endIndex {
437
+ let remainder = base. count% chunkCount
438
+ // We have to take it into account when calculating offsets.
439
+ if remainder != 0 {
440
+ // Distance "minus" one(at this point distance is negative)
441
+ // because we need to adjust for the last position that have
442
+ // a variadic(remainder) number of elements.
443
+ return ( ( distance + 1 ) * chunkCount) - remainder
444
+ }
445
+ }
446
+ return distance * chunkCount
447
+ }
448
+
449
+ @usableFromInline
450
+ internal func offsetBackward(
451
+ _ i: Index , offsetBy distance: Int , limit: Index ? = nil
452
+ ) -> Index ? {
453
+ assert ( distance < 0 )
454
+ let baseDistance =
455
+ computeOffsetBackwardBaseDistance ( i, distance)
456
+ return makeOffsetIndex (
457
+ from: i, baseBound: base. startIndex,
458
+ distance: distance, baseDistance: baseDistance,
459
+ limit: limit, by: <
460
+ )
461
+ }
462
+
463
+ // Helper to compute index(offsetBy:) index.
464
+ @inline ( __always)
465
+ private func makeOffsetIndex(
466
+ from i: Index , baseBound: Base . Index , distance: Int , baseDistance: Int ,
467
+ limit: Index ? , by limitFn: ( Base . Index , Base . Index ) -> Bool
468
+ ) -> Index ? {
469
+ let baseIdx = base. index (
470
+ i. baseRange. lowerBound, offsetBy: baseDistance,
471
+ limitedBy: baseBound
472
+ )
473
+
474
+ if let limit = limit {
475
+ if baseIdx == nil {
476
+ // If we past the bounds while advancing forward and the
477
+ // limit is the `endIndex`, since the computation on base
478
+ // don't take into account the remainder, we have to make
479
+ // sure that passing the bound was because of the distance
480
+ // not just because of a remainder. Special casing is less
481
+ // expensive than always use count(which could be O(n) for
482
+ // non-random access collection base) to compute the base
483
+ // distance taking remainder into account.
484
+ if baseDistance > 0 && limit == endIndex {
485
+ if self . distance ( from: i, to: limit) < distance {
486
+ return nil
487
+ }
488
+ } else {
489
+ return nil
490
+ }
491
+ }
492
+
493
+ // Checks for the limit.
494
+ let baseStartIdx = baseIdx ?? baseBound
495
+ if limitFn ( baseStartIdx, limit. baseRange. lowerBound) {
496
+ return nil
497
+ }
498
+ }
499
+
500
+ let baseStartIdx = baseIdx ?? baseBound
501
+ let baseEndIdx = base. index (
502
+ baseStartIdx, offsetBy: chunkCount, limitedBy: base. endIndex
503
+ ) ?? base. endIndex
504
+
505
+ return Index ( _baseRange: baseStartIdx..< baseEndIdx)
506
+ }
507
+ }
508
+
509
+ extension Collection {
510
+ /// Returns a `ChunkedCollection<Self>` view presenting the elements
511
+ /// in chunks with count of the given count parameter.
512
+ ///
513
+ /// - Parameter size: The size of the chunks. If the count parameter
514
+ /// is evenly divided by the count of the base `Collection` all the
515
+ /// chunks will have the count equals to size.
516
+ /// Otherwise, the last chunk will contain the remaining elements.
517
+ ///
518
+ /// let c = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
519
+ /// print(c.chunks(ofCount: 5).map(Array.init))
520
+ /// // [[1, 2, 3, 4, 5], [6, 7, 8, 9, 10]]
521
+ ///
522
+ /// print(c.chunks(ofCount: 3).map(Array.init))
523
+ /// // [[1, 2, 3], [4, 5, 6], [7, 8, 9], [10]]
524
+ ///
525
+ /// - Complexity: O(1)
526
+ @inlinable
527
+ public func chunks( ofCount count: Int ) -> ChunkedByCount < Self > {
528
+ precondition ( count > 0 , " Cannot chunk with count <= 0! " )
529
+ return ChunkedByCount ( _base: self , _chunkCount: count)
530
+ }
531
+ }
532
+
533
+ // Conditional conformances.
534
+ extension ChunkedByCount : Equatable where Base: Equatable { }
535
+
536
+ // Since we have another stored property of type `Index` on the
537
+ // collection, synthesis of `Hashble` conformace would require
538
+ // a `Base.Index: Hashable` constraint, so we implement the hasher
539
+ // only in terms of `base`. Since the computed index is based on it,
540
+ // it should not make a difference here.
541
+ extension ChunkedByCount : Hashable where Base: Hashable {
542
+ public func hash( into hasher: inout Hasher ) {
543
+ hasher. combine ( base)
544
+ }
545
+ }
546
+ extension ChunkedByCount . Index : Hashable where Base. Index: Hashable { }
547
+
548
+ // Lazy conditional conformance.
549
+ extension ChunkedByCount : LazySequenceProtocol
550
+ where Base: LazySequenceProtocol { }
551
+ extension ChunkedByCount : LazyCollectionProtocol
552
+ where Base: LazyCollectionProtocol { }
0 commit comments