Skip to content

Commit 3484ac3

Browse files
authored
Make chunked(on:) include subject value in Element type (#142)
1 parent f9f3a5c commit 3484ac3

File tree

3 files changed

+123
-55
lines changed

3 files changed

+123
-55
lines changed

Guides/Chunked.md

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -22,18 +22,20 @@ let chunks = numbers.chunked(by: { $0 <= $1 })
2222

2323
The `chunk(on:)` method, by contrast, takes a projection of each element and
2424
separates chunks where the projection of two consecutive elements is not equal.
25+
The result includes both the projected value and the subsequence
26+
that groups elements with that projected value:
2527

2628
```swift
2729
let names = ["David", "Kyle", "Karoy", "Nate"]
2830
let chunks = names.chunked(on: \.first!)
29-
// [["David"], ["Kyle", "Karoy"], ["Nate"]]
31+
// [("D", ["David"]), ("K", ["Kyle", "Karoy"]), ("N", ["Nate"])]
3032
```
3133

32-
The `chunks(ofCount:)` takes a `count` parameter (required to be > 0) and separates
33-
the collection into `n` chunks of this given count. If the `count` parameter is
34-
evenly divided by the count of the base `Collection` all the chunks will have
35-
the count equals to the parameter. Otherwise, the last chunk will contain the
36-
remaining elements.
34+
The `chunks(ofCount:)` method takes a `count` parameter (greater than zero)
35+
and separates the collection into chunks of this given count.
36+
If the `count` parameter is evenly divided by the count of the base `Collection`,
37+
all the chunks will have a count equal to the parameter.
38+
Otherwise, the last chunk will contain the remaining elements.
3739

3840
```swift
3941
let names = ["David", "Kyle", "Karoy", "Nate"]
@@ -44,17 +46,17 @@ let remaining = names.chunks(ofCount: 3)
4446
// equivalent to [["David", "Kyle", "Karoy"], ["Nate"]]
4547
```
4648

47-
The `chunks(ofCount:)` is the method of the [existing SE proposal][proposal].
48-
Unlike the `split` family of methods, the entire collection is included in the
49-
chunked result — joining the resulting chunks recreates the original collection.
49+
The `chunks(ofCount:)` is the subject of an [existing SE proposal][proposal].
50+
51+
When "chunking" a collection, the entire collection is included in the result,
52+
unlike the `split` family of methods, where separators are dropped.
53+
Joining the result of a chunking method call recreates the original collection.
5054

5155
```swift
5256
c.elementsEqual(c.chunked(...).joined())
5357
// true
5458
```
5559

56-
Check the [proposal][proposal] detailed design section for more info.
57-
5860
[proposal]: https://github.com/apple/swift-evolution/pull/935
5961

6062
## Detailed Design
@@ -70,21 +72,21 @@ extension Collection {
7072

7173
public func chunked<Subject: Equatable>(
7274
on projection: (Element) -> Subject
73-
) -> [SubSequence]
75+
) -> [(Subject, SubSequence)]
7476
}
7577

7678
extension LazyCollectionProtocol {
7779
public func chunked(
7880
by belongInSameGroup: @escaping (Element, Element) -> Bool
79-
) -> Chunked<Elements>
81+
) -> ChunkedBy<Elements, Element>
8082

8183
public func chunked<Subject: Equatable>(
8284
on projection: @escaping (Element) -> Subject
83-
) -> Chunked<Elements>
85+
) -> ChunkedOn<Elements, Subject>
8486
}
8587
```
8688

87-
The `Chunked` type is bidirectional when the wrapped collection is
89+
The `ChunkedBy` and `ChunkedOn` types are bidirectional when the wrapped collection is
8890
bidirectional.
8991

9092
### Complexity

Sources/Algorithms/Chunked.swift

Lines changed: 95 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@
1010
//===----------------------------------------------------------------------===//
1111

1212
/// A collection wrapper that breaks a collection into chunks based on a
13-
/// predicate or projection.
14-
public struct Chunked<Base: Collection, Subject> {
13+
/// predicate.
14+
///
15+
/// Call `lazy.chunked(by:)` on a collection to create an instance of this type.
16+
public struct ChunkedBy<Base: Collection, Subject> {
1517
/// The collection that this instance provides a view onto.
1618
@usableFromInline
1719
internal let base: Base
@@ -45,7 +47,7 @@ public struct Chunked<Base: Collection, Subject> {
4547
}
4648
}
4749

48-
extension Chunked: LazyCollectionProtocol {
50+
extension ChunkedBy: LazyCollectionProtocol {
4951
/// A position in a chunked collection.
5052
public struct Index: Comparable {
5153
/// The range corresponding to the chunk at this position.
@@ -106,9 +108,9 @@ extension Chunked: LazyCollectionProtocol {
106108
}
107109
}
108110

109-
extension Chunked.Index: Hashable where Base.Index: Hashable {}
111+
extension ChunkedBy.Index: Hashable where Base.Index: Hashable {}
110112

111-
extension Chunked: BidirectionalCollection
113+
extension ChunkedBy: BidirectionalCollection
112114
where Base: BidirectionalCollection
113115
{
114116
/// Returns the index in the base collection of the start of the chunk ending
@@ -131,11 +133,64 @@ extension Chunked: BidirectionalCollection
131133
}
132134
}
133135

134-
@available(*, deprecated, renamed: "Chunked")
135-
public typealias LazyChunked<Base: Collection, Subject> = Chunked<Base, Subject>
136+
@available(*, deprecated, renamed: "ChunkedBy")
137+
public typealias LazyChunked<Base: Collection, Subject> = ChunkedBy<Base, Subject>
138+
139+
@available(*, deprecated, renamed: "ChunkedBy")
140+
public typealias Chunked<Base: Collection, Subject> = ChunkedBy<Base, Subject>
141+
142+
/// A collection wrapper that breaks a collection into chunks based on a
143+
/// predicate.
144+
///
145+
/// Call `lazy.chunked(on:)` on a collection to create an instance of this type.
146+
public struct ChunkedOn<Base: Collection, Subject> {
147+
@usableFromInline
148+
internal var chunked: ChunkedBy<Base, Subject>
149+
150+
@inlinable
151+
internal init(
152+
base: Base,
153+
projection: @escaping (Base.Element) -> Subject,
154+
belongInSameGroup: @escaping (Subject, Subject) -> Bool
155+
) {
156+
self.chunked = ChunkedBy(base: base, projection: projection, belongInSameGroup: belongInSameGroup)
157+
}
158+
}
159+
160+
extension ChunkedOn: LazyCollectionProtocol {
161+
public typealias Index = ChunkedBy<Base, Subject>.Index
162+
163+
@inlinable
164+
public var startIndex: Index {
165+
chunked.startIndex
166+
}
167+
168+
@inlinable
169+
public var endIndex: Index {
170+
chunked.endIndex
171+
}
172+
173+
@inlinable
174+
public subscript(position: Index) -> (Subject, Base.SubSequence) {
175+
let subsequence = chunked[position]
176+
let subject = chunked.projection(subsequence.first!)
177+
return (subject, subsequence)
178+
}
179+
180+
@inlinable
181+
public func index(after i: Index) -> Index {
182+
chunked.index(after: i)
183+
}
184+
}
185+
186+
extension ChunkedOn: BidirectionalCollection where Base: BidirectionalCollection {
187+
public func index(before i: Index) -> Index {
188+
chunked.index(before: i)
189+
}
190+
}
136191

137192
//===----------------------------------------------------------------------===//
138-
// lazy.chunked(by:)
193+
// lazy.chunked(by:) / lazy.chunked(on:)
139194
//===----------------------------------------------------------------------===//
140195

141196
extension LazyCollectionProtocol {
@@ -146,8 +201,8 @@ extension LazyCollectionProtocol {
146201
@inlinable
147202
public func chunked(
148203
by belongInSameGroup: @escaping (Element, Element) -> Bool
149-
) -> Chunked<Elements, Element> {
150-
Chunked(
204+
) -> ChunkedBy<Elements, Element> {
205+
ChunkedBy(
151206
base: elements,
152207
projection: { $0 },
153208
belongInSameGroup: belongInSameGroup)
@@ -160,41 +215,38 @@ extension LazyCollectionProtocol {
160215
@inlinable
161216
public func chunked<Subject: Equatable>(
162217
on projection: @escaping (Element) -> Subject
163-
) -> Chunked<Elements, Subject> {
164-
Chunked(
218+
) -> ChunkedOn<Elements, Subject> {
219+
ChunkedOn(
165220
base: elements,
166221
projection: projection,
167222
belongInSameGroup: ==)
168223
}
169224
}
170225

171226
//===----------------------------------------------------------------------===//
172-
// chunked(by:)
227+
// chunked(by:) / chunked(on:)
173228
//===----------------------------------------------------------------------===//
174229

175230
extension Collection {
176231
/// Returns a collection of subsequences of this collection, chunked by
177-
/// grouping elements that project to the same value according to the given
178-
/// predicate.
232+
/// the given predicate.
179233
///
180234
/// - Complexity: O(*n*), where *n* is the length of this collection.
181235
@inlinable
182-
internal func chunked<Subject>(
183-
on projection: (Element) throws -> Subject,
184-
by belongInSameGroup: (Subject, Subject) throws -> Bool
236+
public func chunked(
237+
by belongInSameGroup: (Element, Element) throws -> Bool
185238
) rethrows -> [SubSequence] {
186239
guard !isEmpty else { return [] }
187240
var result: [SubSequence] = []
188241

189242
var start = startIndex
190-
var subject = try projection(self[start])
243+
var current = self[start]
191244

192245
for (index, element) in indexed().dropFirst() {
193-
let nextSubject = try projection(element)
194-
if try !belongInSameGroup(subject, nextSubject) {
246+
if try !belongInSameGroup(current, element) {
195247
result.append(self[start..<index])
196248
start = index
197-
subject = nextSubject
249+
current = element
198250
}
199251
}
200252

@@ -204,17 +256,6 @@ extension Collection {
204256

205257
return result
206258
}
207-
208-
/// Returns a collection of subsequences of this collection, chunked by
209-
/// the given predicate.
210-
///
211-
/// - Complexity: O(*n*), where *n* is the length of this collection.
212-
@inlinable
213-
public func chunked(
214-
by belongInSameGroup: (Element, Element) throws -> Bool
215-
) rethrows -> [SubSequence] {
216-
try chunked(on: { $0 }, by: belongInSameGroup)
217-
}
218259

219260
/// Returns a collection of subsequences of this collection, chunked by
220261
/// grouping elements that project to the same value.
@@ -223,8 +264,27 @@ extension Collection {
223264
@inlinable
224265
public func chunked<Subject: Equatable>(
225266
on projection: (Element) throws -> Subject
226-
) rethrows -> [SubSequence] {
227-
try chunked(on: projection, by: ==)
267+
) rethrows -> [(Subject, SubSequence)] {
268+
guard !isEmpty else { return [] }
269+
var result: [(Subject, SubSequence)] = []
270+
271+
var start = startIndex
272+
var subject = try projection(self[start])
273+
274+
for (index, element) in indexed().dropFirst() {
275+
let nextSubject = try projection(element)
276+
if subject != nextSubject {
277+
result.append((subject, self[start..<index]))
278+
start = index
279+
subject = nextSubject
280+
}
281+
}
282+
283+
if start != endIndex {
284+
result.append((subject, self[start..<endIndex]))
285+
}
286+
287+
return result
228288
}
229289
}
230290

Tests/SwiftAlgorithmsTests/ChunkedTests.swift

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,12 @@ final class ChunkedTests: XCTestCase {
4747
func testSimple() {
4848
// Example
4949
let names = ["David", "Kyle", "Karoy", "Nate"]
50-
let chunks = names.chunked(on: { $0.first })
51-
XCTAssertEqualSequences([["David"], ["Kyle", "Karoy"], ["Nate"]], chunks)
50+
let chunks = names.chunked(on: { $0.first! })
51+
let expected: [(Character, ArraySlice<String>)] = [
52+
("D", ["David"]),
53+
("K", ["Kyle", "Karoy"]),
54+
("N", ["Nate"])]
55+
XCTAssertEqualSequences(expected, chunks, by: ==)
5256

5357
// Empty sequence
5458
XCTAssertEqual(0, names.prefix(0).chunked(on: { $0.first }).count)
@@ -59,28 +63,30 @@ final class ChunkedTests: XCTestCase {
5963
}
6064

6165
func testChunkedOn() {
62-
validateFruitChunks(fruits.chunked(on: { $0.first }))
66+
validateFruitChunks(fruits.chunked(on: { $0.first }).map { $1 })
6367

6468
let lazyChunks = fruits.lazy.chunked(on: { $0.first })
65-
validateFruitChunks(lazyChunks)
69+
validateFruitChunks(lazyChunks.map { $1 })
70+
validateIndexTraversals(lazyChunks)
6671
}
6772

6873
func testChunkedBy() {
6974
validateFruitChunks(fruits.chunked(by: { $0.first == $1.first }))
7075

7176
let lazyChunks = fruits.lazy.chunked(by: { $0.first == $1.first })
7277
validateFruitChunks(lazyChunks)
78+
validateIndexTraversals(lazyChunks)
7379
}
7480

7581
func testChunkedLazy() {
7682
XCTAssertLazySequence(fruits.lazy.chunked(by: { $0.first == $1.first }))
7783
XCTAssertLazySequence(fruits.lazy.chunked(on: { $0.first }))
7884
}
7985

80-
8186
//===----------------------------------------------------------------------===//
8287
// Tests for `chunks(ofCount:)`
8388
//===----------------------------------------------------------------------===//
89+
8490
func testChunksOfCount() {
8591
XCTAssertEqualSequences([Int]().chunks(ofCount: 1), [])
8692
XCTAssertEqualSequences([Int]().chunks(ofCount: 5), [])

0 commit comments

Comments
 (0)