Skip to content

Commit 93fea86

Browse files
author
Tim Vermeulen
committed
Add Collection.evenChunks(count:)
1 parent dbc48be commit 93fea86

File tree

2 files changed

+262
-0
lines changed

2 files changed

+262
-0
lines changed

Sources/Algorithms/Chunked.swift

Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,9 +131,242 @@ extension Chunked: BidirectionalCollection
131131
}
132132
}
133133

134+
134135
@available(*, deprecated, renamed: "Chunked")
135136
public typealias LazyChunked<Base: Collection, Subject> = Chunked<Base, Subject>
136137

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

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).evenlyChunked(into: 1),
152+
(0..<10).evenlyChunked(into: 2),
153+
(0..<10).evenlyChunked(into: 3),
154+
(0..<10).evenlyChunked(into: 10),
155+
(0..<10).evenlyChunked(into: 20),
156+
(0..<0).evenlyChunked(into: 0),
157+
(0..<0).evenlyChunked(into: 1),
158+
(0..<0).evenlyChunked(into: 10))
159+
}
148160
}

0 commit comments

Comments
 (0)