Skip to content

Commit 29e66be

Browse files
author
Tim Vermeulen
committed
Add Collection.evenChunks(count:)
1 parent e7c6716 commit 29e66be

File tree

2 files changed

+261
-0
lines changed

2 files changed

+261
-0
lines changed

Sources/Algorithms/Chunked.swift

Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,238 @@ extension LazyChunked: BidirectionalCollection
143143
}
144144
}
145145

146+
/// A collection wrapper that evenly breaks a collection into a given number of
147+
/// chunks.
148+
public struct EvenChunks<Base: Collection> {
149+
/// The base collection.
150+
@usableFromInline
151+
internal let base: Base
152+
153+
/// The number of equal chunks the base collection is divided into.
154+
@usableFromInline
155+
internal let numberOfChunks: Int
156+
157+
/// The count of the base collection.
158+
@usableFromInline
159+
internal let baseCount: Int
160+
161+
/// The upper bound of the first chunk.
162+
@usableFromInline
163+
internal var firstUpperBound: Base.Index
164+
165+
@usableFromInline
166+
internal init(
167+
base: Base,
168+
numberOfChunks: Int,
169+
baseCount: Int,
170+
firstUpperBound: Base.Index
171+
) {
172+
self.base = base
173+
self.numberOfChunks = numberOfChunks
174+
self.baseCount = base.count
175+
self.firstUpperBound = firstUpperBound
176+
}
177+
178+
@usableFromInline
179+
internal init(base: Base, numberOfChunks: Int) {
180+
self.base = base
181+
self.numberOfChunks = numberOfChunks
182+
self.baseCount = base.count
183+
self.firstUpperBound = base.startIndex
184+
185+
if numberOfChunks > 0 {
186+
firstUpperBound = endOfChunk(startingAt: base.startIndex, offset: 0)
187+
}
188+
}
189+
}
190+
191+
extension EvenChunks {
192+
/// Returns the number of chunks with size `smallChunkSize + 1` at the start
193+
/// of this collection.
194+
@usableFromInline
195+
internal var numberOfLargeChunks: Int {
196+
baseCount % numberOfChunks
197+
}
198+
199+
/// Returns the size of the small chunks at the end of this collection.
200+
@usableFromInline
201+
internal var smallChunkSize: Int {
202+
baseCount / numberOfChunks
203+
}
204+
205+
/// Returns the size of a chunk at a given offset.
206+
@usableFromInline
207+
internal func sizeOfChunk(offset: Int) -> Int {
208+
let isLargeChunk = offset < numberOfLargeChunks
209+
return baseCount / numberOfChunks + (isLargeChunk ? 1 : 0)
210+
}
211+
212+
/// Returns the index in the base collection of the end of the chunk starting
213+
/// at the given index.
214+
@usableFromInline
215+
internal func endOfChunk(startingAt start: Base.Index, offset: Int) -> Base.Index {
216+
base.index(start, offsetBy: sizeOfChunk(offset: offset))
217+
}
218+
219+
/// Returns the index in the base collection of the start of the chunk ending
220+
/// at the given index.
221+
@usableFromInline
222+
internal func startOfChunk(endingAt end: Base.Index, offset: Int) -> Base.Index {
223+
base.index(end, offsetBy: -sizeOfChunk(offset: offset))
224+
}
225+
226+
/// Returns the index that corresponds to the chunk that starts at the given
227+
/// base index.
228+
@usableFromInline
229+
internal func indexOfChunk(startingAt start: Base.Index, offset: Int) -> Index {
230+
guard offset != numberOfChunks else { return endIndex }
231+
let end = endOfChunk(startingAt: start, offset: offset)
232+
return Index(start..<end, offset: offset)
233+
}
234+
235+
/// Returns the index that corresponds to the chunk that ends at the given
236+
/// base index.
237+
@usableFromInline
238+
internal func indexOfChunk(endingAt end: Base.Index, offset: Int) -> Index {
239+
let start = startOfChunk(endingAt: end, offset: offset)
240+
return Index(start..<end, offset: offset)
241+
}
242+
}
243+
244+
public struct EvenChunksIndex<Base: Comparable>: Comparable {
245+
/// The range corresponding to the chunk at this position.
246+
@usableFromInline
247+
internal var baseRange: Range<Base>
248+
249+
/// The offset corresponding to the chunk at this position. The first chunk
250+
/// has offset `0` and all other chunks have an offset `1` greater than the
251+
/// previous.
252+
@usableFromInline
253+
internal var offset: Int
254+
255+
@usableFromInline
256+
internal init(_ baseRange: Range<Base>, offset: Int) {
257+
self.baseRange = baseRange
258+
self.offset = offset
259+
}
260+
261+
@inlinable
262+
public static func == (lhs: Self, rhs: Self) -> Bool {
263+
lhs.offset == rhs.offset
264+
}
265+
266+
@inlinable
267+
public static func < (lhs: Self, rhs: Self) -> Bool {
268+
lhs.offset < rhs.offset
269+
}
270+
}
271+
272+
extension EvenChunks: Collection {
273+
public typealias Element = Base.SubSequence
274+
public typealias Index = EvenChunksIndex<Base.Index>
275+
public typealias SubSequence = EvenChunks<Base.SubSequence>
276+
277+
@inlinable
278+
public var startIndex: Index {
279+
Index(base.startIndex..<firstUpperBound, offset: 0)
280+
}
281+
282+
@inlinable
283+
public var endIndex: Index {
284+
Index(base.endIndex..<base.endIndex, offset: numberOfChunks)
285+
}
286+
287+
@inlinable
288+
public func index(after i: Index) -> Index {
289+
precondition(i != endIndex, "Can't advance past endIndex")
290+
let start = i.baseRange.upperBound
291+
return indexOfChunk(startingAt: start, offset: i.offset + 1)
292+
}
293+
294+
@inlinable
295+
public subscript(position: Index) -> Element {
296+
precondition(position != endIndex)
297+
return base[position.baseRange]
298+
}
299+
300+
@inlinable
301+
public subscript(bounds: Range<Index>) -> SubSequence {
302+
func baseCount(before index: Index) -> Int {
303+
let smallChunkSize = self.baseCount / numberOfChunks
304+
let numberOfLargeChunks = Swift.min(index.offset, self.numberOfLargeChunks)
305+
return index.offset * smallChunkSize + numberOfLargeChunks
306+
}
307+
308+
return .init(
309+
base: base[bounds.lowerBound.baseRange.lowerBound..<bounds.upperBound.baseRange.lowerBound],
310+
numberOfChunks: bounds.upperBound.offset - bounds.lowerBound.offset,
311+
baseCount: baseCount(before: bounds.upperBound) - baseCount(before: bounds.lowerBound),
312+
firstUpperBound: bounds.lowerBound.baseRange.upperBound
313+
)
314+
}
315+
316+
@inlinable
317+
public func index(_ i: Index, offsetBy distance: Int) -> Index {
318+
/// Returns the base distance between two `EvenChunks` indices from the end
319+
/// of one to the start of the other, when given their offsets.
320+
func baseDistance(from offsetA: Int, to offsetB: Int) -> Int {
321+
let smallChunkSize = baseCount / numberOfChunks
322+
let numberOfChunks = (offsetB - offsetA) - 1
323+
324+
let largeChunksEnd = Swift.min(self.numberOfLargeChunks, offsetB)
325+
let largeChunksStart = Swift.min(self.numberOfLargeChunks, offsetA + 1)
326+
let numberOfLargeChunks = largeChunksEnd - largeChunksStart
327+
328+
return smallChunkSize * numberOfChunks + numberOfLargeChunks
329+
}
330+
331+
if distance == 0 {
332+
return i
333+
} else if distance > 0 {
334+
let offset = i.offset + distance
335+
let baseOffset = baseDistance(from: i.offset, to: offset)
336+
let start = base.index(i.baseRange.upperBound, offsetBy: baseOffset)
337+
return indexOfChunk(startingAt: start, offset: offset)
338+
} else {
339+
let offset = i.offset + distance
340+
let baseOffset = baseDistance(from: offset, to: i.offset)
341+
let end = base.index(i.baseRange.lowerBound, offsetBy: -baseOffset)
342+
return indexOfChunk(endingAt: end, offset: offset)
343+
}
344+
}
345+
346+
@inlinable
347+
public func index(_ i: Index, offsetBy distance: Int, limitedBy limit: Index) -> Index? {
348+
if distance >= 0 {
349+
if (0..<distance).contains(self.distance(from: i, to: limit)) {
350+
return nil
351+
}
352+
} else {
353+
if (0..<(-distance)).contains(self.distance(from: limit, to: i)) {
354+
return nil
355+
}
356+
}
357+
return index(i, offsetBy: distance)
358+
}
359+
360+
@inlinable
361+
public func distance(from start: Index, to end: Index) -> Int {
362+
end.offset - start.offset
363+
}
364+
}
365+
366+
extension EvenChunksIndex: Hashable where Base: Hashable {}
367+
368+
extension EvenChunks: BidirectionalCollection
369+
where Base: BidirectionalCollection
370+
{
371+
@inlinable
372+
public func index(before i: Index) -> Index {
373+
precondition(i != startIndex, "Can't advance before startIndex")
374+
return indexOfChunk(endingAt: i.baseRange.lowerBound, offset: i.offset - 1)
375+
}
376+
}
377+
146378
//===----------------------------------------------------------------------===//
147379
// lazy.chunked(by:)
148380
//===----------------------------------------------------------------------===//
@@ -541,3 +773,20 @@ extension ChunkedByCount: LazySequenceProtocol
541773
where Base: LazySequenceProtocol {}
542774
extension ChunkedByCount: LazyCollectionProtocol
543775
where Base: LazyCollectionProtocol {}
776+
777+
//===----------------------------------------------------------------------===//
778+
// evenChunks(count:)
779+
//===----------------------------------------------------------------------===//
780+
781+
extension Collection {
782+
/// Returns a collection of `count` evenly divided subsequences of this
783+
/// collection.
784+
///
785+
/// - Complexity: TODO
786+
@inlinable
787+
public func evenChunks(count: Int) -> EvenChunks<Self> {
788+
precondition(count >= 0, "Can't divide into a negative number of chunks")
789+
precondition(count > 0 || isEmpty, "Can't divide a non-empty collection into 0 chunks")
790+
return EvenChunks(base: self, numberOfChunks: count)
791+
}
792+
}

Tests/SwiftAlgorithmsTests/ChunkedTests.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,4 +145,16 @@ final class ChunkedTests: XCTestCase {
145145
validateIndexTraversals(chunks)
146146
}
147147
}
148+
149+
func testEvenChunks() {
150+
validateIndexTraversals(
151+
(0..<10).evenChunks(count: 1),
152+
(0..<10).evenChunks(count: 2),
153+
(0..<10).evenChunks(count: 3),
154+
(0..<10).evenChunks(count: 10),
155+
(0..<10).evenChunks(count: 20),
156+
(0..<0).evenChunks(count: 0),
157+
(0..<0).evenChunks(count: 1),
158+
(0..<0).evenChunks(count: 10))
159+
}
148160
}

0 commit comments

Comments
 (0)