Skip to content

Commit 2f54c4f

Browse files
committed
[ReplaceSubrange] Conditional replacement
1 parent 4770b04 commit 2f54c4f

File tree

2 files changed

+87
-17
lines changed

2 files changed

+87
-17
lines changed

Sources/Algorithms/ReplaceSubrange.swift

+68-17
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@
99
//
1010
//===----------------------------------------------------------------------===//
1111

12-
/// A namespace for methods which overlay a collection of elements
13-
/// over a region of a base collection.
12+
/// A namespace for methods which return composed collections,
13+
/// formed by replacing a region of a base collection
14+
/// with another collection of elements.
1415
///
1516
/// Access the namespace via the `.overlay` member, available on all collections:
1617
///
@@ -24,8 +25,7 @@
2425
///
2526
public struct OverlayCollectionNamespace<Elements: Collection> {
2627

27-
@usableFromInline
28-
internal var elements: Elements
28+
public let elements: Elements
2929

3030
@inlinable
3131
internal init(elements: Elements) {
@@ -35,13 +35,44 @@ public struct OverlayCollectionNamespace<Elements: Collection> {
3535

3636
extension Collection {
3737

38-
/// A namespace for methods which overlay another collection of elements
39-
/// over a region of this collection.
38+
/// A namespace for methods which return composed collections,
39+
/// formed by replacing a region of this collection
40+
/// with another collection of elements.
4041
///
4142
@inlinable
4243
public var overlay: OverlayCollectionNamespace<Self> {
4344
OverlayCollectionNamespace(elements: self)
4445
}
46+
47+
/// If `condition` is true, returns an `OverlayCollection` by applying the given closure.
48+
/// Otherwise, returns an `OverlayCollection` containing the same elements as this collection.
49+
///
50+
/// The following example takes an array of products, lazily wraps them in a `ListItem` enum,
51+
/// and conditionally inserts a call-to-action element if `showCallToAction` is true.
52+
///
53+
/// ```swift
54+
/// var listItems: some Collection<ListItem> {
55+
/// let products: [Product] = ...
56+
/// return products
57+
/// .lazy.map {
58+
/// ListItem.product($0)
59+
/// }
60+
/// .overlay(if: showCallToAction) {
61+
/// $0.inserting(.callToAction, at: min(4, $0.elements.count))
62+
/// }
63+
/// }
64+
/// ```
65+
///
66+
@inlinable
67+
public func overlay<Overlay>(
68+
if condition: Bool, _ makeOverlay: (OverlayCollectionNamespace<Self>) -> OverlayCollection<Self, Overlay>
69+
) -> OverlayCollection<Self, Overlay> {
70+
if condition {
71+
return makeOverlay(overlay)
72+
} else {
73+
return OverlayCollection(base: self, overlay: nil, replacedRange: startIndex..<startIndex)
74+
}
75+
}
4576
}
4677

4778
extension OverlayCollectionNamespace {
@@ -96,20 +127,34 @@ extension OverlayCollectionNamespace {
96127
}
97128
}
98129

130+
/// A composed collections, formed by replacing a region of a base collection
131+
/// with another collection of elements.
132+
///
133+
/// To create an OverlayCollection, use the methods in the ``OverlayCollectionNamespace``
134+
/// namespace:
135+
///
136+
/// ```swift
137+
/// let base = 0..<5
138+
/// for n in base.overlay.inserting(42, at: 2) {
139+
/// print(n)
140+
/// }
141+
/// // Prints: 0, 1, 42, 2, 3, 4
142+
/// ```
143+
///
99144
public struct OverlayCollection<Base, Overlay>
100145
where Base: Collection, Overlay: Collection, Base.Element == Overlay.Element {
101146

102147
@usableFromInline
103148
internal var base: Base
104149

105150
@usableFromInline
106-
internal var overlay: Overlay
151+
internal var overlay: Optional<Overlay>
107152

108153
@usableFromInline
109154
internal var replacedRange: Range<Base.Index>
110155

111156
@inlinable
112-
internal init(base: Base, overlay: Overlay, replacedRange: Range<Base.Index>) {
157+
internal init(base: Base, overlay: Overlay?, replacedRange: Range<Base.Index>) {
113158
self.base = base
114159
self.overlay = overlay
115160
self.replacedRange = replacedRange
@@ -187,7 +232,7 @@ extension OverlayCollection {
187232

188233
@inlinable
189234
public var startIndex: Index {
190-
if base.startIndex == replacedRange.lowerBound {
235+
if let overlay = overlay, base.startIndex == replacedRange.lowerBound {
191236
if overlay.isEmpty {
192237
return makeIndex(replacedRange.upperBound)
193238
}
@@ -198,6 +243,9 @@ extension OverlayCollection {
198243

199244
@inlinable
200245
public var endIndex: Index {
246+
guard let overlay = overlay else {
247+
return makeIndex(base.endIndex)
248+
}
201249
if replacedRange.lowerBound != base.endIndex || overlay.isEmpty {
202250
return makeIndex(base.endIndex)
203251
}
@@ -206,7 +254,10 @@ extension OverlayCollection {
206254

207255
@inlinable
208256
public var count: Int {
209-
base.distance(from: base.startIndex, to: replacedRange.lowerBound)
257+
guard let overlay = overlay else {
258+
return base.count
259+
}
260+
return base.distance(from: base.startIndex, to: replacedRange.lowerBound)
210261
+ overlay.count
211262
+ base.distance(from: replacedRange.upperBound, to: base.endIndex)
212263
}
@@ -216,7 +267,7 @@ extension OverlayCollection {
216267
switch i.wrapped {
217268
case .base(var baseIndex):
218269
base.formIndex(after: &baseIndex)
219-
if baseIndex == replacedRange.lowerBound {
270+
if let overlay = overlay, baseIndex == replacedRange.lowerBound {
220271
if overlay.isEmpty {
221272
return makeIndex(replacedRange.upperBound)
222273
}
@@ -225,8 +276,8 @@ extension OverlayCollection {
225276
return makeIndex(baseIndex)
226277

227278
case .overlay(var overlayIndex):
228-
overlay.formIndex(after: &overlayIndex)
229-
if replacedRange.lowerBound != base.endIndex, overlayIndex == overlay.endIndex {
279+
overlay!.formIndex(after: &overlayIndex)
280+
if replacedRange.lowerBound != base.endIndex, overlayIndex == overlay!.endIndex {
230281
return makeIndex(replacedRange.upperBound)
231282
}
232283
return makeIndex(overlayIndex)
@@ -239,7 +290,7 @@ extension OverlayCollection {
239290
case .base(let baseIndex):
240291
return base[baseIndex]
241292
case .overlay(let overlayIndex):
242-
return overlay[overlayIndex]
293+
return overlay![overlayIndex]
243294
}
244295
}
245296
}
@@ -251,7 +302,7 @@ where Base: BidirectionalCollection, Overlay: BidirectionalCollection {
251302
public func index(before i: Index) -> Index {
252303
switch i.wrapped {
253304
case .base(var baseIndex):
254-
if baseIndex == replacedRange.upperBound {
305+
if let overlay = overlay, baseIndex == replacedRange.upperBound {
255306
if overlay.isEmpty {
256307
return makeIndex(base.index(before: replacedRange.lowerBound))
257308
}
@@ -261,10 +312,10 @@ where Base: BidirectionalCollection, Overlay: BidirectionalCollection {
261312
return makeIndex(baseIndex)
262313

263314
case .overlay(var overlayIndex):
264-
if overlayIndex == overlay.startIndex {
315+
if overlayIndex == overlay!.startIndex {
265316
return makeIndex(base.index(before: replacedRange.lowerBound))
266317
}
267-
overlay.formIndex(before: &overlayIndex)
318+
overlay!.formIndex(before: &overlayIndex)
268319
return makeIndex(overlayIndex)
269320
}
270321
}

Tests/SwiftAlgorithmsTests/ReplaceSubrangeTests.swift

+19
Original file line numberDiff line numberDiff line change
@@ -199,4 +199,23 @@ final class ReplaceSubrangeTests: XCTestCase {
199199
IndexValidator().validate(result, expectedCount: 0)
200200
}
201201
}
202+
203+
func testConditionalReplacement() {
204+
205+
func getNumbers(shouldInsert: Bool) -> OverlayCollection<Range<Int>, CollectionOfOne<Int>> {
206+
(0..<5).overlay(if: shouldInsert) { $0.inserting(42, at: 2) }
207+
}
208+
209+
do {
210+
let result = getNumbers(shouldInsert: true)
211+
XCTAssertEqualCollections(result, [0, 1, 42, 2, 3, 4])
212+
IndexValidator().validate(result, expectedCount: 6)
213+
}
214+
215+
do {
216+
let result = getNumbers(shouldInsert: false)
217+
XCTAssertEqualCollections(result, [0, 1, 2, 3, 4])
218+
IndexValidator().validate(result, expectedCount: 5)
219+
}
220+
}
202221
}

0 commit comments

Comments
 (0)