Skip to content

Commit 99152ec

Browse files
authored
Unify chunked(by:) and chunked(on:) (#44)
* Unify the eager `chunked(on:)` and `chunked(by:)` * Unify the lazy `chunked(on:)` and `chunked(by:)` * Trap when going past the end * Add preconditions in `index(before:)`/`index(after:)`
1 parent 7af7ac2 commit 99152ec

File tree

1 file changed

+71
-41
lines changed

1 file changed

+71
-41
lines changed

Sources/Algorithms/Chunked.swift

+71-41
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,26 @@
99
//
1010
//===----------------------------------------------------------------------===//
1111

12-
public struct LazyChunked<Base: Collection> {
12+
public struct LazyChunked<Base: Collection, Subject> {
1313
/// The collection that this instance provides a view onto.
1414
public let base: Base
1515

1616
/// The projection function.
1717
@usableFromInline
18-
internal var belongInSameGroup: (Base.Element, Base.Element) -> Bool
18+
internal let projection: (Base.Element) -> Subject
1919

20+
/// The predicate.
2021
@usableFromInline
21-
internal init(base: Base, belongInSameGroup: @escaping (Base.Element, Base.Element) -> Bool) {
22+
internal let belongInSameGroup: (Subject, Subject) -> Bool
23+
24+
@usableFromInline
25+
internal init(
26+
base: Base,
27+
projection: @escaping (Base.Element) -> Subject,
28+
belongInSameGroup: @escaping (Subject, Subject) -> Bool
29+
) {
2230
self.base = base
31+
self.projection = projection
2332
self.belongInSameGroup = belongInSameGroup
2433
}
2534
}
@@ -64,13 +73,14 @@ extension LazyChunked: LazyCollectionProtocol {
6473
}
6574
}
6675

67-
/// Returns the index in the base collection for the first element that
68-
/// doesn't match the current chunk.
76+
/// Returns the index in the base collection of the end of the chunk starting
77+
/// at the given index.
6978
@usableFromInline
70-
internal func endOfChunk(from i: Base.Index) -> Base.Index {
71-
guard i != base.endIndex else { return base.endIndex }
72-
return base[base.index(after: i)...]
73-
.firstIndex(where: { !belongInSameGroup($0, base[i]) }) ?? base.endIndex
79+
internal func endOfChunk(startingAt start: Base.Index) -> Base.Index {
80+
let subject = projection(base[start])
81+
return base[base.index(after: start)...]
82+
.firstIndex(where: { !belongInSameGroup(subject, projection($0)) })
83+
?? base.endIndex
7484
}
7585

7686
@inlinable
@@ -80,20 +90,22 @@ extension LazyChunked: LazyCollectionProtocol {
8090

8191
@inlinable
8292
public var endIndex: Index {
83-
Index(lowerBound: base.endIndex, upperBound: base.endIndex)
93+
Index(lowerBound: base.endIndex)
8494
}
8595

8696
@inlinable
8797
public func index(after i: Index) -> Index {
88-
let upperBound = i.upperBound ?? endOfChunk(from: i.lowerBound)
89-
let end = endOfChunk(from: upperBound)
98+
precondition(i != endIndex, "Can't advance past endIndex")
99+
let upperBound = i.upperBound ?? endOfChunk(startingAt: i.lowerBound)
100+
guard upperBound != base.endIndex else { return endIndex }
101+
let end = endOfChunk(startingAt: upperBound)
90102
return Index(lowerBound: upperBound, upperBound: end)
91103
}
92104

93105
@inlinable
94106
public subscript(position: Index) -> Base.SubSequence {
95107
let upperBound = position.upperBound
96-
?? endOfChunk(from: position.lowerBound)
108+
?? endOfChunk(startingAt: position.lowerBound)
97109
return base[position.lowerBound..<upperBound]
98110
}
99111
}
@@ -103,17 +115,19 @@ extension LazyChunked.Index: Hashable where Base.Index: Hashable {}
103115
extension LazyChunked: BidirectionalCollection
104116
where Base: BidirectionalCollection
105117
{
106-
/// Returns the index in the base collection for the element that starts
107-
/// the chunk ending at the given index.
118+
/// Returns the index in the base collection of the start of the chunk ending
119+
/// at the given index.
108120
@usableFromInline
109121
internal func startOfChunk(endingAt end: Base.Index) -> Base.Index {
122+
let indexBeforeEnd = base.index(before: end)
123+
110124
// Get the projected value of the last element in the range ending at `end`.
111-
let lastOfPreviousChunk = base[base.index(before: end)]
125+
let subject = projection(base[indexBeforeEnd])
112126

113127
// Search backward from `end` for the first element whose projection isn't
114-
// equal to `lastOfPreviousChunk`.
115-
if let firstMismatch = base[..<end]
116-
.lastIndex(where: { !belongInSameGroup($0, lastOfPreviousChunk) })
128+
// equal to `subject`.
129+
if let firstMismatch = base[..<indexBeforeEnd]
130+
.lastIndex(where: { !belongInSameGroup(projection($0), subject) })
117131
{
118132
// If we found one, that's the last element of the _next_ previous chunk,
119133
// and therefore one position _before_ the start of this chunk.
@@ -127,6 +141,7 @@ extension LazyChunked: BidirectionalCollection
127141

128142
@inlinable
129143
public func index(before i: Index) -> Index {
144+
precondition(i != startIndex, "Can't advance before startIndex")
130145
let start = startOfChunk(endingAt: i.lowerBound)
131146
return Index(lowerBound: start, upperBound: i.lowerBound)
132147
}
@@ -146,8 +161,11 @@ extension LazyCollectionProtocol {
146161
@inlinable
147162
public func chunked(
148163
by belongInSameGroup: @escaping (Element, Element) -> Bool
149-
) -> LazyChunked<Elements> {
150-
LazyChunked(base: elements, belongInSameGroup: belongInSameGroup)
164+
) -> LazyChunked<Elements, Element> {
165+
LazyChunked(
166+
base: elements,
167+
projection: { $0 },
168+
belongInSameGroup: belongInSameGroup)
151169
}
152170

153171
/// Returns a lazy collection of subsequences of this collection, chunked by
@@ -159,10 +177,11 @@ extension LazyCollectionProtocol {
159177
@inlinable
160178
public func chunked<Subject: Equatable>(
161179
on projection: @escaping (Element) -> Subject
162-
) -> LazyChunked<Elements> {
180+
) -> LazyChunked<Elements, Subject> {
163181
LazyChunked(
164182
base: elements,
165-
belongInSameGroup: { projection($0) == projection($1) })
183+
projection: projection,
184+
belongInSameGroup: ==)
166185
}
167186
}
168187

@@ -172,27 +191,28 @@ extension LazyCollectionProtocol {
172191

173192
extension Collection {
174193
/// Returns a collection of subsequences of this collection, chunked by
175-
/// the given predicate.
194+
/// grouping elements that project to the same value according to the given
195+
/// predicate.
176196
///
177197
/// - Complexity: O(*n*), where *n* is the length of this collection.
178-
@inlinable
179-
public func chunked(
180-
by belongInSameGroup: (Element, Element) -> Bool
181-
) -> [SubSequence] {
198+
@usableFromInline
199+
internal func chunked<Subject>(
200+
on projection: (Element) throws -> Subject,
201+
by belongInSameGroup: (Subject, Subject) throws -> Bool
202+
) rethrows -> [SubSequence] {
182203
guard !isEmpty else { return [] }
183204
var result: [SubSequence] = []
184205

185206
var start = startIndex
186-
var current = startIndex
187-
while true {
188-
let next = index(after: current)
189-
if next == endIndex { break }
190-
191-
if !belongInSameGroup(self[current], self[next]) {
192-
result.append(self[start..<next])
193-
start = next
207+
var subject = try projection(self[start])
208+
209+
for (index, element) in indexed().dropFirst() {
210+
let nextSubject = try projection(element)
211+
if try !belongInSameGroup(subject, nextSubject) {
212+
result.append(self[start..<index])
213+
start = index
214+
subject = nextSubject
194215
}
195-
current = next
196216
}
197217

198218
if start != endIndex {
@@ -201,16 +221,26 @@ extension Collection {
201221

202222
return result
203223
}
224+
225+
/// Returns a collection of subsequences of this collection, chunked by
226+
/// the given predicate.
227+
///
228+
/// - Complexity: O(*n*), where *n* is the length of this collection.
229+
@inlinable
230+
public func chunked(
231+
by belongInSameGroup: (Element, Element) throws -> Bool
232+
) rethrows -> [SubSequence] {
233+
try chunked(on: { $0 }, by: belongInSameGroup)
234+
}
204235

205236
/// Returns a collection of subsequences of this collection, chunked by
206237
/// grouping elements that project to the same value.
207238
///
208239
/// - Complexity: O(*n*), where *n* is the length of this collection.
209240
@inlinable
210241
public func chunked<Subject: Equatable>(
211-
on projection: (Element) -> Subject
212-
) -> [SubSequence] {
213-
chunked(by: { projection($0) == projection($1) })
242+
on projection: (Element) throws -> Subject
243+
) rethrows -> [SubSequence] {
244+
try chunked(on: projection, by: ==)
214245
}
215246
}
216-

0 commit comments

Comments
 (0)