Skip to content

Commit 25cf83d

Browse files
committed
Add partitioned(_:)
`partitioned(_:)` works like `filter(_:)`, but also returns the excluded elements by returning a tuple of two `Array`s
1 parent 50be2c8 commit 25cf83d

File tree

4 files changed

+250
-2
lines changed

4 files changed

+250
-2
lines changed

Guides/Partition.md

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,32 @@ let p = numbers.partitioningIndex(where: { $0.isMultiple(of: 20) })
4242
// numbers[p...] = [20, 40, 60]
4343
```
4444

45+
The standard library’s existing `filter(_:)` method provides functionality to
46+
get the elements that do match a given predicate. `partitioned(_:)` returns
47+
both the elements that match the preciate as well as those that don’t, as a
48+
tuple.
49+
50+
```swift
51+
let cast = ["Vivien", "Marlon", "Kim", "Karl"]
52+
let (longNames , shortNames) = cast.bifurcate({ $0.count < 5 })
53+
print(longNames)
54+
// Prints "["Vivien", "Marlon"]"
55+
print(shortNames)
56+
// Prints "["Kim", "Karl"]"
57+
```
58+
59+
There’s also a function to bifurcate a collection into a prefix and a suffix, up
60+
to but not including a given index:
61+
62+
```swift
63+
let cast = ["Vivien", "Marlon", "Kim", "Karl"]
64+
let (callbacks, alternates) = cast.bifurcate(upTo: 2)
65+
print(callbacks)
66+
// Prints "["Vivien", "Marlon"]"
67+
print(alternates)
68+
// Prints "["Kim", "Karl"]"
69+
```
70+
4571
## Detailed Design
4672

4773
All mutating methods are declared as extensions to `MutableCollection`.
@@ -69,11 +95,17 @@ extension Collection {
6995
where belongsInSecondPartition: (Element) throws -> Bool
7096
) rethrows -> Index
7197
}
98+
99+
extension Sequence {
100+
public func bifurcate(
101+
_ belongsInFirstCollection: (Element) throws -> Bool
102+
) rethrows -> ([Element], [Element])
103+
}
72104
```
73105

74106
### Complexity
75107

76-
The existing partition is an O(_n_) operations, where _n_ is the length of the
108+
The existing partition is an O(_n_) operation, where _n_ is the length of the
77109
range to be partitioned, while the stable partition is O(_n_ log _n_). Both
78110
partitions have algorithms with improved performance for bidirectional
79111
collections, so it would be ideal for those to be customization points were they
@@ -82,6 +114,9 @@ to eventually land in the standard library.
82114
`partitioningIndex(where:)` is a slight generalization of a binary search, and
83115
is an O(log _n_) operation for random-access collections; O(_n_) otherwise.
84116

117+
`partitioned(_:)` is an O(_n_) operation, where _n_ is the number of elements in
118+
the original sequence.
119+
85120
### Comparison with other languages
86121

87122
**C++:** The `<algorithm>` library defines `partition`, `stable_partition`, and

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ Read more about the package, and the intent behind it, in the [announcement on s
2727
#### Subsetting operations
2828

2929
- [`compacted()`](https://github.com/apple/swift-algorithms/blob/main/Guides/Compacted.md): Drops the `nil`s from a sequence or collection, unwrapping the remaining elements.
30+
- [`partitioned(_:)`](https://github.com/apple/swift-algorithms/blob/main/Guides/Partition.md): Returns the elements in a sequence or collection that do and not match a given predciate.
3031
- [`randomSample(count:)`, `randomSample(count:using:)`](https://github.com/apple/swift-algorithms/blob/main/Guides/RandomSampling.md): Randomly selects a specific number of elements from a collection.
3132
- [`randomStableSample(count:)`, `randomStableSample(count:using:)`](https://github.com/apple/swift-algorithms/blob/main/Guides/RandomSampling.md): Randomly selects a specific number of elements from a collection, preserving their original relative order.
3233
- [`striding(by:)`](https://github.com/apple/swift-algorithms/blob/main/Guides/Stride.md): Returns every nth element of a collection.

Sources/Algorithms/Partition.swift

Lines changed: 154 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
//
33
// This source file is part of the Swift Algorithms open source project
44
//
5-
// Copyright (c) 2020 Apple Inc. and the Swift project authors
5+
// Copyright (c) 2021 Apple Inc. and the Swift project authors
66
// Licensed under Apache License v2.0 with Runtime Library Exception
77
//
88
// See https://swift.org/LICENSE.txt for license information
@@ -204,3 +204,156 @@ extension Collection {
204204
}
205205
}
206206

207+
//===----------------------------------------------------------------------===//
208+
// partitioned(_:)
209+
//===----------------------------------------------------------------------===//
210+
211+
extension Sequence {
212+
/// Returns two arrays containing, in order, the elements of the sequence that
213+
/// do and don’t satisfy the given predicate, respectively.
214+
///
215+
/// In this example, `partitioned(_:)` is used to separate the input based on
216+
/// names that aren’t and are shorter than five characters, respectively:
217+
///
218+
/// let cast = ["Vivien", "Marlon", "Kim", "Karl"]
219+
/// let (longNames, shortNames) = cast.partitioned({ $0.count < 5 })
220+
/// print(longNames)
221+
/// // Prints "["Vivien", "Marlon"]"
222+
/// print(shortNames)
223+
/// // Prints "["Kim", "Karl"]"
224+
///
225+
/// - Parameter belongsInSecondCollection: A closure that takes an element of
226+
/// the sequence as its argument and returns a Boolean value indicating
227+
/// whether the element should be included in the second returned array.
228+
/// Otherwise, the element will appear in the first returned array.
229+
///
230+
/// - Returns: Two arrays with with all of the elements of the receiver. The
231+
/// first array contains all the elements that `belongsInSecondCollection`
232+
/// didn’t allow, and the second array contains all the elements that
233+
/// `belongsInSecondCollection` allowed.
234+
///
235+
/// - Complexity: O(*n*), where *n* is the length of the sequence.
236+
///
237+
/// - Note: This algorithm performs a bit slower than the same algorithm on
238+
/// `RandomAccessCollection` since the size of the sequence is unknown, unlike
239+
/// `RandomAccessCollection`.
240+
@inlinable
241+
public func partitioned(
242+
_ belongsInSecondCollection: (Element) throws -> Bool
243+
) rethrows -> ([Element], [Element]) {
244+
var lhs = ContiguousArray<Element>()
245+
var rhs = ContiguousArray<Element>()
246+
247+
for element in self {
248+
if try belongsInSecondCollection(element) {
249+
rhs.append(element)
250+
} else {
251+
lhs.append(element)
252+
}
253+
}
254+
255+
return _tupleMap((lhs, rhs), { Array($0) })
256+
}
257+
}
258+
259+
extension Collection {
260+
// This is a specialized version of the same algorithm on `Sequence` that
261+
// avoids reallocation of arrays since `count` is known ahead of time.
262+
@inlinable
263+
public func partitioned(
264+
_ belongsInSecondCollection: (Element) throws -> Bool
265+
) rethrows -> ([Element], [Element]) {
266+
guard !self.isEmpty else {
267+
return ([], [])
268+
}
269+
270+
// Since `RandomAccessCollection`s have known sizes (access to `count` is
271+
// constant time, O(1)), we can allocate one array of size `self.count`,
272+
// then insert items at the beginning or end of that contiguous block. This
273+
// way, we don’t have to do any dynamic array resizing. Since we insert the
274+
// right elements on the right side in reverse order, we need to reverse
275+
// them back to the original order at the end.
276+
277+
let count = self.count
278+
279+
// Inside of the `initializer` closure, we set what the actual mid-point is.
280+
// We will use this to partitioned the single array into two in constant time.
281+
var midPoint: Int = 0
282+
283+
let elements = try [Element](
284+
unsafeUninitializedCapacity: count,
285+
initializingWith: { buffer, initializedCount in
286+
var lhs = buffer.baseAddress!
287+
var rhs = lhs + buffer.count
288+
do {
289+
for element in self {
290+
if try belongsInSecondCollection(element) {
291+
rhs -= 1
292+
rhs.initialize(to: element)
293+
} else {
294+
lhs.initialize(to: element)
295+
lhs += 1
296+
}
297+
}
298+
299+
let rhsIndex = rhs - buffer.baseAddress!
300+
buffer[rhsIndex...].reverse()
301+
initializedCount = buffer.count
302+
303+
midPoint = rhsIndex
304+
} catch {
305+
let lhsCount = lhs - buffer.baseAddress!
306+
let rhsCount = (buffer.baseAddress! + buffer.count) - rhs
307+
buffer.baseAddress!.deinitialize(count: lhsCount)
308+
rhs.deinitialize(count: rhsCount)
309+
throw error
310+
}
311+
})
312+
313+
let collections = elements.partitioned(upTo: midPoint)
314+
return _tupleMap(collections, { Array($0) })
315+
}
316+
}
317+
318+
//===----------------------------------------------------------------------===//
319+
// partitioned(upTo:)
320+
//===----------------------------------------------------------------------===//
321+
322+
extension Collection {
323+
/// Splits the receiving collection into two at the specified index
324+
/// - Parameter index: The index within the receiver to split the collection
325+
/// - Returns: A tuple with the first and second parts of the receiving
326+
/// collection after splitting it
327+
/// - Note: The first subsequence in the returned tuple does *not* include
328+
/// the element at `index`. That element is in the second subsequence.
329+
/// - Complexity: O(*1*)
330+
@inlinable
331+
public func partitioned(upTo index: Index) -> (SubSequence, SubSequence) {
332+
return (
333+
self[self.startIndex..<index],
334+
self[index..<self.endIndex]
335+
)
336+
}
337+
}
338+
339+
//===----------------------------------------------------------------------===//
340+
// _tupleMap(_:_:)
341+
//===----------------------------------------------------------------------===//
342+
343+
/// Returns a tuple containing the results of mapping the given closure over
344+
/// each of the tuple’s elements.
345+
/// - Parameters:
346+
/// - x: The tuple to transform
347+
/// - transform: A mapping closure. `transform` accepts an element of this
348+
/// sequence as its parameter and returns a transformed
349+
/// - Returns: A tuple containing the transformed elements of this tuple.
350+
@usableFromInline
351+
internal func _tupleMap<T, U>(
352+
_ x: (T, T),
353+
_ transform: (T) throws -> U
354+
) rethrows -> (U, U) {
355+
return (
356+
try transform(x.0),
357+
try transform(x.1)
358+
)
359+
}

Tests/SwiftAlgorithmsTests/PartitionTests.swift

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,4 +133,63 @@ final class PartitionTests: XCTestCase {
133133
}
134134
}
135135
}
136+
137+
func testPartitionedWithEmptyInput() {
138+
let input: [Int] = []
139+
140+
let s0 = input.partitioned({ _ in return true })
141+
142+
XCTAssertTrue(s0.0.isEmpty)
143+
XCTAssertTrue(s0.1.isEmpty)
144+
}
145+
146+
/// Test the example given in the `partitioned(_:)` documentation
147+
func testPartitionedExample() throws {
148+
let cast = ["Vivien", "Marlon", "Kim", "Karl"]
149+
let (longNames, shortNames) = cast.partitioned({ $0.count < 5 })
150+
XCTAssertEqual(longNames, ["Vivien", "Marlon"])
151+
XCTAssertEqual(shortNames, ["Kim", "Karl"])
152+
}
153+
154+
func testPartitionedWithPredicate() throws {
155+
let s0 = ["A", "B", "C", "D"].partitioned({ $0 == $0.lowercased() })
156+
let s1 = ["a", "B", "C", "D"].partitioned({ $0 == $0.lowercased() })
157+
let s2 = ["a", "B", "c", "D"].partitioned({ $0 == $0.lowercased() })
158+
let s3 = ["a", "B", "c", "d"].partitioned({ $0 == $0.lowercased() })
159+
160+
XCTAssertEqual(s0.0, ["A", "B", "C", "D"])
161+
XCTAssertEqual(s0.1, [])
162+
163+
XCTAssertEqual(s1.0, ["B", "C", "D"])
164+
XCTAssertEqual(s1.1, ["a"])
165+
166+
XCTAssertEqual(s2.0, ["B", "D"])
167+
XCTAssertEqual(s2.1, ["a", "c"])
168+
169+
XCTAssertEqual(s3.0, ["B"])
170+
XCTAssertEqual(s3.1, ["a", "c", "d"])
171+
}
172+
173+
func testPartitionedUpToIndex() throws {
174+
let s0 = ["A", "B", "C", "D"].partitioned(upTo: 0)
175+
let s1 = ["A", "B", "C", "D"].partitioned(upTo: 1)
176+
let s2 = ["A", "B", "C", "D"].partitioned(upTo: 2)
177+
let s3 = ["A", "B", "C", "D"].partitioned(upTo: 3)
178+
let s4 = ["A", "B", "C", "D"].partitioned(upTo: 4)
179+
180+
XCTAssertEqual(s0.0, [])
181+
XCTAssertEqual(s0.1, ["A", "B", "C", "D"])
182+
183+
XCTAssertEqual(s1.0, ["A"])
184+
XCTAssertEqual(s1.1, ["B", "C", "D"])
185+
186+
XCTAssertEqual(s2.0, ["A", "B"])
187+
XCTAssertEqual(s2.1, ["C", "D"])
188+
189+
XCTAssertEqual(s3.0, ["A", "B", "C"])
190+
XCTAssertEqual(s3.1, ["D"])
191+
192+
XCTAssertEqual(s4.0, ["A", "B", "C", "D"])
193+
XCTAssertEqual(s4.1, [])
194+
}
136195
}

0 commit comments

Comments
 (0)