Skip to content

Commit 198eee0

Browse files
[Compacted] Adding compacted() method to remove all nils in a sequence or collection (#112)
1 parent 4cca489 commit 198eee0

File tree

5 files changed

+387
-0
lines changed

5 files changed

+387
-0
lines changed

Guides/Compacted.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Compacted
2+
3+
[[Source](https://github.com/apple/swift-algorithms/blob/main/Sources/Algorithms/Compacted.swift) |
4+
[Tests](https://github.com/apple/swift-algorithms/blob/main/Tests/SwiftAlgorithmsTests/CompactedTests.swift)]
5+
6+
Convenience method that flatten the `nil`s out of a sequence or collection.
7+
That behaves exactly one of the most common uses of `compactMap` which is `collection.lazy.compactMap { $0 }`
8+
which is only remove `nil`s without transforming the elements.
9+
10+
```swift
11+
let array: [Int?] = [10, nil, 30, nil, 2, 3, nil, 5]
12+
let withNoNils = array.compacted()
13+
// Array(withNoNils) == [10, 30, 2, 3, 5]
14+
15+
```
16+
17+
The most convenient part of `compacted()` is that we avoid the usage of a closure.
18+
19+
## Detailed Design
20+
21+
The `compacted()` methods has two overloads:
22+
23+
```swift
24+
extension Sequence {
25+
public func compacted<Unwrapped>() -> CompactedSequence<Self, Unwrapped> { ... }
26+
}
27+
28+
extension Collection {
29+
public func compacted<Unwrapped>() -> CompactedCollection<Self, Unwrapped> { ... }
30+
}
31+
```
32+
33+
One is a more general `CompactedSequence` for any `Sequence` base. And the other a more specialized `CompactedCollection`
34+
where base is a `Collection` and with conditional conformance to `BidirectionalCollection`, `RandomAccessCollection`,
35+
`LazyCollectionProtocol`, `Equatable` and `Hashable` when base collection conforms to them.
36+
37+
### Naming
38+
39+
The naming method name `compacted()` matches the current method `compactMap` that one of the most common usages `compactMap { $0 }` is abstracted by it.

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ Read more about the package, and the intent behind it, in the [announcement on s
4545
- [`reductions(_:)`, `reductions(_:_:)`](https://github.com/apple/swift-algorithms/blob/main/Guides/Reductions.md): Returns all the intermediate states of reducing the elements of a sequence or collection.
4646
- [`split(maxSplits:omittingEmptySubsequences:whereSeparator)`, `split(separator:maxSplits:omittingEmptySubsequences)`](https://github.com/apple/swift-algorithms/blob/main/Guides/LazySplit.md): Lazy versions of the Standard Library's eager operations that split sequences and collections into subsequences separated by the specified separator element.
4747
- [`windows(ofCount:)`](https://github.com/apple/swift-algorithms/blob/main/Guides/Windows.md): Breaks a collection into overlapping subsequences where elements are slices from the original collection.
48+
- [`compacted()`](https://github.com/apple/swift-algorithms/blob/main/Guides/Compacted.md): Flatten the `nil`s out of a sequence or collection.
4849

4950
## Adding Swift Algorithms as a Dependency
5051

Sources/Algorithms/Compacted.swift

Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift Algorithms open source project
4+
//
5+
// Copyright (c) 2021 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
//
10+
//===----------------------------------------------------------------------===//
11+
12+
/// A `Sequence` that iterates over every non-nil element from the original `Sequence`.
13+
public struct CompactedSequence<Base: Sequence, Element>: Sequence
14+
where Base.Element == Element? {
15+
16+
@usableFromInline
17+
let base: Base
18+
19+
@inlinable
20+
init(base: Base) {
21+
self.base = base
22+
}
23+
24+
public struct Iterator: IteratorProtocol {
25+
@usableFromInline
26+
var base: Base.Iterator
27+
28+
@inlinable
29+
init(base: Base.Iterator) {
30+
self.base = base
31+
}
32+
33+
@inlinable
34+
public mutating func next() -> Element? {
35+
while let wrapped = base.next() {
36+
guard let some = wrapped else { continue }
37+
return some
38+
}
39+
return nil
40+
}
41+
}
42+
43+
@inlinable
44+
public func makeIterator() -> Iterator {
45+
return Iterator(base: base.makeIterator())
46+
}
47+
}
48+
49+
extension Sequence {
50+
/// Returns a new `Sequence` that iterates over every non-nil element
51+
/// from the original `Sequence`.
52+
/// It produces the same result as `c.compactMap { $0 }`.
53+
///
54+
/// let c = [1, nil, 2, 3, nil]
55+
/// for num in c.compacted() {
56+
/// print(num)
57+
/// }
58+
/// // 1
59+
/// // 2
60+
/// // 3
61+
///
62+
/// - Returns: A `Sequence` where the element is the unwrapped original
63+
/// element and iterates over every non-nil element from the original
64+
/// `Sequence`.
65+
///
66+
/// Complexity: O(1)
67+
@inlinable
68+
public func compacted<Unwrapped>() -> CompactedSequence<Self, Unwrapped>
69+
where Element == Unwrapped? {
70+
CompactedSequence(base: self)
71+
}
72+
}
73+
74+
/// A `Collection` that iterates over every non-nil element from the original `Collection`.
75+
public struct CompactedCollection<Base: Collection, Element>: Collection
76+
where Base.Element == Element? {
77+
78+
@usableFromInline
79+
let base: Base
80+
81+
@inlinable
82+
init(base: Base) {
83+
self.base = base
84+
let idx = base.firstIndex(where: { $0 != nil }) ?? base.endIndex
85+
self.startIndex = Index(base: idx)
86+
}
87+
88+
public struct Index {
89+
@usableFromInline
90+
let base: Base.Index
91+
92+
@inlinable
93+
init(base: Base.Index) {
94+
self.base = base
95+
}
96+
}
97+
98+
public var startIndex: Index
99+
100+
@inlinable
101+
public var endIndex: Index { Index(base: base.endIndex) }
102+
103+
@inlinable
104+
public subscript(position: Index) -> Element {
105+
base[position.base]!
106+
}
107+
108+
@inlinable
109+
public func index(after i: Index) -> Index {
110+
precondition(i != endIndex, "Index out of bounds")
111+
112+
let baseIdx = base.index(after: i.base)
113+
guard let idx = base[baseIdx...].firstIndex(where: { $0 != nil })
114+
else { return endIndex }
115+
return Index(base: idx)
116+
}
117+
}
118+
119+
extension CompactedCollection: BidirectionalCollection
120+
where Base: BidirectionalCollection {
121+
122+
@inlinable
123+
public func index(before i: Index) -> Index {
124+
precondition(i != startIndex, "Index out of bounds")
125+
126+
guard let idx =
127+
base[startIndex.base..<i.base]
128+
.lastIndex(where: { $0 != nil })
129+
else { fatalError("Index out of bounds") }
130+
return Index(base: idx)
131+
}
132+
}
133+
134+
extension CompactedCollection.Index: Comparable {
135+
@inlinable
136+
public static func < (lhs: CompactedCollection.Index,
137+
rhs: CompactedCollection.Index) -> Bool {
138+
lhs.base < rhs.base
139+
}
140+
}
141+
142+
extension CompactedCollection.Index: Hashable
143+
where Base.Index: Hashable {}
144+
145+
extension Collection {
146+
/// Returns a new `Collection` that iterates over every non-nil element
147+
/// from the original `Collection`.
148+
/// It produces the same result as `c.compactMap { $0 }`.
149+
///
150+
/// let c = [1, nil, 2, 3, nil]
151+
/// for num in c.compacted() {
152+
/// print(num)
153+
/// }
154+
/// // 1
155+
/// // 2
156+
/// // 3
157+
///
158+
/// - Returns: A `Collection` where the element is the unwrapped original
159+
/// element and iterates over every non-nil element from the original
160+
/// `Collection`.
161+
///
162+
/// Complexity: O(*n*) where *n* is the number of elements in the
163+
/// original `Collection`.
164+
@inlinable
165+
public func compacted<Unwrapped>() -> CompactedCollection<Self, Unwrapped>
166+
where Element == Unwrapped? {
167+
CompactedCollection(base: self)
168+
}
169+
}
170+
171+
//===----------------------------------------------------------------------===//
172+
// Protocol Conformances
173+
//===----------------------------------------------------------------------===//
174+
175+
extension CompactedSequence: LazySequenceProtocol
176+
where Base: LazySequenceProtocol {}
177+
178+
extension CompactedCollection: LazySequenceProtocol
179+
where Base: LazySequenceProtocol {}
180+
extension CompactedCollection: LazyCollectionProtocol
181+
where Base: LazyCollectionProtocol {}
182+
183+
184+
// Hashable and Equatable conformance are based on each non-nil
185+
// element on base collection.
186+
extension CompactedSequence: Equatable
187+
where Base.Element: Equatable {
188+
189+
@inlinable
190+
public static func ==(lhs: CompactedSequence,
191+
rhs: CompactedSequence) -> Bool {
192+
lhs.elementsEqual(rhs)
193+
}
194+
}
195+
196+
extension CompactedSequence: Hashable
197+
where Element: Hashable {
198+
199+
@inlinable
200+
public func hash(into hasher: inout Hasher) {
201+
for element in self {
202+
hasher.combine(element)
203+
}
204+
}
205+
}
206+
207+
extension CompactedCollection: Equatable
208+
where Base.Element: Equatable {
209+
210+
@inlinable
211+
public static func ==(lhs: CompactedCollection,
212+
rhs: CompactedCollection) -> Bool {
213+
lhs.elementsEqual(rhs)
214+
}
215+
}
216+
217+
extension CompactedCollection: Hashable
218+
where Element: Hashable {
219+
220+
@inlinable
221+
public func hash(into hasher: inout Hasher) {
222+
for element in self {
223+
hasher.combine(element)
224+
}
225+
}
226+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift Algorithms open source project
4+
//
5+
// Copyright (c) 2021 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
//
10+
//===----------------------------------------------------------------------===//
11+
12+
import XCTest
13+
import Algorithms
14+
15+
final class CompactedTests: XCTestCase {
16+
17+
let tests: [[Int?]] =
18+
[nil, nil, nil, 0, 1, 2]
19+
.uniquePermutations(ofCount: 0...)
20+
.map(Array.init)
21+
22+
func testCompactedCompacted() {
23+
for collection in self.tests {
24+
let seq = AnySequence(collection)
25+
XCTAssertEqualSequences(
26+
seq.compactMap({ $0 }), seq.compacted())
27+
XCTAssertEqualSequences(
28+
collection.compactMap({ $0 }), collection.compacted())
29+
}
30+
}
31+
32+
func testCompactedBidirectionalCollection() {
33+
for array in self.tests {
34+
XCTAssertEqualSequences(array.compactMap({ $0 }).reversed(),
35+
array.compacted().reversed())
36+
}
37+
}
38+
39+
func testCollectionTraversals() {
40+
for array in self.tests {
41+
validateIndexTraversals(array.compacted())
42+
}
43+
}
44+
45+
func testCollectionEquatableConformances() {
46+
for array in self.tests {
47+
XCTAssertEqual(
48+
array.eraseToAnyHashableSequence().compacted(),
49+
array.compactMap({ $0 }).eraseToAnyHashableSequence().compacted()
50+
)
51+
XCTAssertEqual(
52+
array.compacted(), array.compactMap({ $0 }).compacted()
53+
)
54+
}
55+
}
56+
57+
func testCollectionHashableConformances() {
58+
for array1 in self.tests {
59+
for array2 in self.tests {
60+
// For non-equal Collections and Sequences that produce the same
61+
// compacted, the compacted wrapper should produce the same hash.
62+
// e.g. [1, 2, 3, nil, nil, 4].compacted() should produce the
63+
// same hash as [1, nil, 2, nil, 3, 4].compacted()
64+
guard !array1.elementsEqual(array2) &&
65+
array1.compacted() == array2.compacted() else {
66+
continue
67+
}
68+
69+
let seq = array1.eraseToAnyHashableSequence()
70+
let seq2 = array2.eraseToAnyHashableSequence()
71+
72+
XCTAssertEqualHashValue(
73+
seq.compacted(), seq2.compacted()
74+
)
75+
XCTAssertEqualHashValue(
76+
array1.compacted(), array2.compacted()
77+
)
78+
}
79+
}
80+
}
81+
}

0 commit comments

Comments
 (0)