From 3484ac31f6623d3758e03894f3f62aa902f2e7a1 Mon Sep 17 00:00:00 2001 From: Nate Cook Date: Sun, 16 May 2021 15:02:03 -0500 Subject: [PATCH] Make chunked(on:) include subject value in Element type (#142) --- Guides/Chunked.md | 32 +++-- Sources/Algorithms/Chunked.swift | 130 +++++++++++++----- Tests/SwiftAlgorithmsTests/ChunkedTests.swift | 16 ++- 3 files changed, 123 insertions(+), 55 deletions(-) diff --git a/Guides/Chunked.md b/Guides/Chunked.md index 2b5444c1..b8d7fca5 100644 --- a/Guides/Chunked.md +++ b/Guides/Chunked.md @@ -22,18 +22,20 @@ let chunks = numbers.chunked(by: { $0 <= $1 }) The `chunk(on:)` method, by contrast, takes a projection of each element and separates chunks where the projection of two consecutive elements is not equal. +The result includes both the projected value and the subsequence +that groups elements with that projected value: ```swift let names = ["David", "Kyle", "Karoy", "Nate"] let chunks = names.chunked(on: \.first!) -// [["David"], ["Kyle", "Karoy"], ["Nate"]] +// [("D", ["David"]), ("K", ["Kyle", "Karoy"]), ("N", ["Nate"])] ``` -The `chunks(ofCount:)` takes a `count` parameter (required to be > 0) and separates -the collection into `n` chunks of this given count. If the `count` parameter is -evenly divided by the count of the base `Collection` all the chunks will have -the count equals to the parameter. Otherwise, the last chunk will contain the -remaining elements. +The `chunks(ofCount:)` method takes a `count` parameter (greater than zero) +and separates the collection into chunks of this given count. +If the `count` parameter is evenly divided by the count of the base `Collection`, +all the chunks will have a count equal to the parameter. +Otherwise, the last chunk will contain the remaining elements. ```swift let names = ["David", "Kyle", "Karoy", "Nate"] @@ -44,17 +46,17 @@ let remaining = names.chunks(ofCount: 3) // equivalent to [["David", "Kyle", "Karoy"], ["Nate"]] ``` -The `chunks(ofCount:)` is the method of the [existing SE proposal][proposal]. -Unlike the `split` family of methods, the entire collection is included in the -chunked result — joining the resulting chunks recreates the original collection. +The `chunks(ofCount:)` is the subject of an [existing SE proposal][proposal]. + +When "chunking" a collection, the entire collection is included in the result, +unlike the `split` family of methods, where separators are dropped. +Joining the result of a chunking method call recreates the original collection. ```swift c.elementsEqual(c.chunked(...).joined()) // true ``` -Check the [proposal][proposal] detailed design section for more info. - [proposal]: https://github.com/apple/swift-evolution/pull/935 ## Detailed Design @@ -70,21 +72,21 @@ extension Collection { public func chunked( on projection: (Element) -> Subject - ) -> [SubSequence] + ) -> [(Subject, SubSequence)] } extension LazyCollectionProtocol { public func chunked( by belongInSameGroup: @escaping (Element, Element) -> Bool - ) -> Chunked + ) -> ChunkedBy public func chunked( on projection: @escaping (Element) -> Subject - ) -> Chunked + ) -> ChunkedOn } ``` -The `Chunked` type is bidirectional when the wrapped collection is +The `ChunkedBy` and `ChunkedOn` types are bidirectional when the wrapped collection is bidirectional. ### Complexity diff --git a/Sources/Algorithms/Chunked.swift b/Sources/Algorithms/Chunked.swift index 66f6bf57..30db1607 100644 --- a/Sources/Algorithms/Chunked.swift +++ b/Sources/Algorithms/Chunked.swift @@ -10,8 +10,10 @@ //===----------------------------------------------------------------------===// /// A collection wrapper that breaks a collection into chunks based on a -/// predicate or projection. -public struct Chunked { +/// predicate. +/// +/// Call `lazy.chunked(by:)` on a collection to create an instance of this type. +public struct ChunkedBy { /// The collection that this instance provides a view onto. @usableFromInline internal let base: Base @@ -45,7 +47,7 @@ public struct Chunked { } } -extension Chunked: LazyCollectionProtocol { +extension ChunkedBy: LazyCollectionProtocol { /// A position in a chunked collection. public struct Index: Comparable { /// The range corresponding to the chunk at this position. @@ -106,9 +108,9 @@ extension Chunked: LazyCollectionProtocol { } } -extension Chunked.Index: Hashable where Base.Index: Hashable {} +extension ChunkedBy.Index: Hashable where Base.Index: Hashable {} -extension Chunked: BidirectionalCollection +extension ChunkedBy: BidirectionalCollection where Base: BidirectionalCollection { /// Returns the index in the base collection of the start of the chunk ending @@ -131,11 +133,64 @@ extension Chunked: BidirectionalCollection } } -@available(*, deprecated, renamed: "Chunked") -public typealias LazyChunked = Chunked +@available(*, deprecated, renamed: "ChunkedBy") +public typealias LazyChunked = ChunkedBy + +@available(*, deprecated, renamed: "ChunkedBy") +public typealias Chunked = ChunkedBy + +/// A collection wrapper that breaks a collection into chunks based on a +/// predicate. +/// +/// Call `lazy.chunked(on:)` on a collection to create an instance of this type. +public struct ChunkedOn { + @usableFromInline + internal var chunked: ChunkedBy + + @inlinable + internal init( + base: Base, + projection: @escaping (Base.Element) -> Subject, + belongInSameGroup: @escaping (Subject, Subject) -> Bool + ) { + self.chunked = ChunkedBy(base: base, projection: projection, belongInSameGroup: belongInSameGroup) + } +} + +extension ChunkedOn: LazyCollectionProtocol { + public typealias Index = ChunkedBy.Index + + @inlinable + public var startIndex: Index { + chunked.startIndex + } + + @inlinable + public var endIndex: Index { + chunked.endIndex + } + + @inlinable + public subscript(position: Index) -> (Subject, Base.SubSequence) { + let subsequence = chunked[position] + let subject = chunked.projection(subsequence.first!) + return (subject, subsequence) + } + + @inlinable + public func index(after i: Index) -> Index { + chunked.index(after: i) + } +} + +extension ChunkedOn: BidirectionalCollection where Base: BidirectionalCollection { + public func index(before i: Index) -> Index { + chunked.index(before: i) + } +} //===----------------------------------------------------------------------===// -// lazy.chunked(by:) +// lazy.chunked(by:) / lazy.chunked(on:) //===----------------------------------------------------------------------===// extension LazyCollectionProtocol { @@ -146,8 +201,8 @@ extension LazyCollectionProtocol { @inlinable public func chunked( by belongInSameGroup: @escaping (Element, Element) -> Bool - ) -> Chunked { - Chunked( + ) -> ChunkedBy { + ChunkedBy( base: elements, projection: { $0 }, belongInSameGroup: belongInSameGroup) @@ -160,8 +215,8 @@ extension LazyCollectionProtocol { @inlinable public func chunked( on projection: @escaping (Element) -> Subject - ) -> Chunked { - Chunked( + ) -> ChunkedOn { + ChunkedOn( base: elements, projection: projection, belongInSameGroup: ==) @@ -169,32 +224,29 @@ extension LazyCollectionProtocol { } //===----------------------------------------------------------------------===// -// chunked(by:) +// chunked(by:) / chunked(on:) //===----------------------------------------------------------------------===// extension Collection { /// Returns a collection of subsequences of this collection, chunked by - /// grouping elements that project to the same value according to the given - /// predicate. + /// the given predicate. /// /// - Complexity: O(*n*), where *n* is the length of this collection. @inlinable - internal func chunked( - on projection: (Element) throws -> Subject, - by belongInSameGroup: (Subject, Subject) throws -> Bool + public func chunked( + by belongInSameGroup: (Element, Element) throws -> Bool ) rethrows -> [SubSequence] { guard !isEmpty else { return [] } var result: [SubSequence] = [] var start = startIndex - var subject = try projection(self[start]) + var current = self[start] for (index, element) in indexed().dropFirst() { - let nextSubject = try projection(element) - if try !belongInSameGroup(subject, nextSubject) { + if try !belongInSameGroup(current, element) { result.append(self[start.. Bool - ) rethrows -> [SubSequence] { - try chunked(on: { $0 }, by: belongInSameGroup) - } /// Returns a collection of subsequences of this collection, chunked by /// grouping elements that project to the same value. @@ -223,8 +264,27 @@ extension Collection { @inlinable public func chunked( on projection: (Element) throws -> Subject - ) rethrows -> [SubSequence] { - try chunked(on: projection, by: ==) + ) rethrows -> [(Subject, SubSequence)] { + guard !isEmpty else { return [] } + var result: [(Subject, SubSequence)] = [] + + var start = startIndex + var subject = try projection(self[start]) + + for (index, element) in indexed().dropFirst() { + let nextSubject = try projection(element) + if subject != nextSubject { + result.append((subject, self[start..)] = [ + ("D", ["David"]), + ("K", ["Kyle", "Karoy"]), + ("N", ["Nate"])] + XCTAssertEqualSequences(expected, chunks, by: ==) // Empty sequence XCTAssertEqual(0, names.prefix(0).chunked(on: { $0.first }).count) @@ -59,10 +63,11 @@ final class ChunkedTests: XCTestCase { } func testChunkedOn() { - validateFruitChunks(fruits.chunked(on: { $0.first })) + validateFruitChunks(fruits.chunked(on: { $0.first }).map { $1 }) let lazyChunks = fruits.lazy.chunked(on: { $0.first }) - validateFruitChunks(lazyChunks) + validateFruitChunks(lazyChunks.map { $1 }) + validateIndexTraversals(lazyChunks) } func testChunkedBy() { @@ -70,6 +75,7 @@ final class ChunkedTests: XCTestCase { let lazyChunks = fruits.lazy.chunked(by: { $0.first == $1.first }) validateFruitChunks(lazyChunks) + validateIndexTraversals(lazyChunks) } func testChunkedLazy() { @@ -77,10 +83,10 @@ final class ChunkedTests: XCTestCase { XCTAssertLazySequence(fruits.lazy.chunked(on: { $0.first })) } - //===----------------------------------------------------------------------===// // Tests for `chunks(ofCount:)` //===----------------------------------------------------------------------===// + func testChunksOfCount() { XCTAssertEqualSequences([Int]().chunks(ofCount: 1), []) XCTAssertEqualSequences([Int]().chunks(ofCount: 5), [])