Skip to content

Commit f22e597

Browse files
committed
Add a non-mutating lazy replaceSubrange
1 parent fc8fdfd commit f22e597

File tree

2 files changed

+339
-0
lines changed

2 files changed

+339
-0
lines changed
+171
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift Algorithms open source project
4+
//
5+
// Copyright (c) 2020 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+
extension LazyCollection {
13+
14+
@inlinable
15+
public func replacingSubrange<Replacements>(
16+
_ subrange: Range<Index>, with newElements: Replacements
17+
) -> ReplacingSubrangeCollection<Base, Replacements> {
18+
ReplacingSubrangeCollection(base: elements, replacements: newElements, replacedRange: subrange)
19+
}
20+
}
21+
22+
public struct ReplacingSubrangeCollection<Base, Replacements>
23+
where Base: Collection, Replacements: Collection, Base.Element == Replacements.Element {
24+
25+
@usableFromInline
26+
internal var base: Base
27+
28+
@usableFromInline
29+
internal var replacements: Replacements
30+
31+
@usableFromInline
32+
internal var replacedRange: Range<Base.Index>
33+
34+
@inlinable
35+
internal init(base: Base, replacements: Replacements, replacedRange: Range<Base.Index>) {
36+
self.base = base
37+
self.replacements = replacements
38+
self.replacedRange = replacedRange
39+
}
40+
}
41+
42+
extension ReplacingSubrangeCollection: Collection {
43+
44+
public typealias Element = Base.Element
45+
46+
public struct Index: Comparable {
47+
48+
@usableFromInline
49+
internal enum Wrapped {
50+
case base(Base.Index)
51+
case replacement(Replacements.Index)
52+
}
53+
54+
/// The underlying base/replacements index.
55+
///
56+
@usableFromInline
57+
internal var wrapped: Wrapped
58+
59+
/// The base indices which have been replaced.
60+
///
61+
@usableFromInline
62+
internal var replacedRange: Range<Base.Index>
63+
64+
@inlinable
65+
internal init(wrapped: Wrapped, replacedRange: Range<Base.Index>) {
66+
self.wrapped = wrapped
67+
self.replacedRange = replacedRange
68+
}
69+
70+
@inlinable
71+
public static func < (lhs: Self, rhs: Self) -> Bool {
72+
switch (lhs.wrapped, rhs.wrapped) {
73+
case (.base(let unwrappedLeft), .base(let unwrappedRight)):
74+
return unwrappedLeft < unwrappedRight
75+
case (.replacement(let unwrappedLeft), .replacement(let unwrappedRight)):
76+
return unwrappedLeft < unwrappedRight
77+
case (.base(let unwrappedLeft), .replacement(_)):
78+
return unwrappedLeft < lhs.replacedRange.lowerBound
79+
case (.replacement(_), .base(let unwrappedRight)):
80+
return !(unwrappedRight < lhs.replacedRange.lowerBound)
81+
}
82+
}
83+
84+
@inlinable
85+
public static func == (lhs: Self, rhs: Self) -> Bool {
86+
// No need to check 'replacedRange', because it does not differ between indices from the same collection.
87+
switch (lhs.wrapped, rhs.wrapped) {
88+
case (.base(let unwrappedLeft), .base(let unwrappedRight)):
89+
return unwrappedLeft == unwrappedRight
90+
case (.replacement(let unwrappedLeft), .replacement(let unwrappedRight)):
91+
return unwrappedLeft == unwrappedRight
92+
default:
93+
return false
94+
}
95+
}
96+
}
97+
}
98+
99+
extension ReplacingSubrangeCollection {
100+
101+
@inlinable
102+
internal func makeIndex(_ position: Base.Index) -> Index {
103+
Index(wrapped: .base(position), replacedRange: replacedRange)
104+
}
105+
106+
@inlinable
107+
internal func makeIndex(_ position: Replacements.Index) -> Index {
108+
Index(wrapped: .replacement(position), replacedRange: replacedRange)
109+
}
110+
111+
@inlinable
112+
public var startIndex: Index {
113+
if replacedRange.lowerBound > base.startIndex {
114+
return makeIndex(base.startIndex)
115+
}
116+
if replacements.isEmpty {
117+
return makeIndex(replacedRange.upperBound)
118+
}
119+
return makeIndex(replacements.startIndex)
120+
}
121+
122+
@inlinable
123+
public var endIndex: Index {
124+
if replacedRange.lowerBound < base.endIndex || replacements.isEmpty {
125+
return makeIndex(base.endIndex)
126+
}
127+
return makeIndex(replacements.endIndex)
128+
}
129+
130+
@inlinable
131+
public var count: Int {
132+
base.distance(from: base.startIndex, to: replacedRange.lowerBound)
133+
+ replacements.count
134+
+ base.distance(from: replacedRange.upperBound, to: base.endIndex)
135+
}
136+
137+
@inlinable
138+
public func index(after i: Index) -> Index {
139+
switch i.wrapped {
140+
case .base(var baseIndex):
141+
base.formIndex(after: &baseIndex)
142+
if baseIndex == replacedRange.lowerBound {
143+
if !replacedRange.isEmpty, replacements.isEmpty {
144+
return makeIndex(replacedRange.upperBound)
145+
}
146+
if !replacements.isEmpty {
147+
return makeIndex(replacements.startIndex)
148+
}
149+
}
150+
return makeIndex(baseIndex)
151+
152+
case .replacement(var replacementIndex):
153+
replacements.formIndex(after: &replacementIndex)
154+
if replacedRange.lowerBound < base.endIndex, replacementIndex == replacements.endIndex {
155+
return makeIndex(replacedRange.upperBound)
156+
}
157+
return makeIndex(replacementIndex)
158+
}
159+
}
160+
161+
@inlinable
162+
public subscript(position: Index) -> Element {
163+
switch position.wrapped {
164+
case .base(let baseIndex):
165+
return base[baseIndex]
166+
case .replacement(let replacementIndex):
167+
return replacements[replacementIndex]
168+
}
169+
}
170+
}
171+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift Algorithms open source project
4+
//
5+
// Copyright (c) 2020 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+
@testable import Algorithms
14+
15+
final class ReplaceSubrangeTests: XCTestCase {
16+
17+
func testAppend() {
18+
19+
// Base: non-empty
20+
// Appending: non-empty
21+
do {
22+
let base = 0..<5
23+
let result = base.lazy.replacingSubrange(base.endIndex..<base.endIndex, with: [8, 9, 10])
24+
XCTAssertEqualCollections(result, [0, 1, 2, 3, 4, 8, 9, 10])
25+
IndexValidator().validate(result, expectedCount: 8)
26+
}
27+
28+
// Base: non-empty
29+
// Appending: empty
30+
do {
31+
let base = 0..<5
32+
let result = base.lazy.replacingSubrange(base.endIndex..<base.endIndex, with: EmptyCollection())
33+
XCTAssertEqualCollections(result, [0, 1, 2, 3, 4])
34+
IndexValidator().validate(result, expectedCount: 5)
35+
}
36+
37+
// Base: empty
38+
// Appending: non-empty
39+
do {
40+
let base = EmptyCollection<Int>()
41+
let result = base.lazy.replacingSubrange(base.endIndex..<base.endIndex, with: 5..<10)
42+
XCTAssertEqualCollections(result, [5, 6, 7, 8, 9])
43+
IndexValidator().validate(result, expectedCount: 5)
44+
}
45+
46+
// Base: empty
47+
// Appending: empty
48+
do {
49+
let base = EmptyCollection<Int>()
50+
let result = base.lazy.replacingSubrange(base.endIndex..<base.endIndex, with: EmptyCollection())
51+
XCTAssertEqualCollections(result, [])
52+
IndexValidator().validate(result, expectedCount: 0)
53+
}
54+
}
55+
56+
func testPrepend() {
57+
58+
// Base: non-empty
59+
// Prepending: non-empty
60+
do {
61+
let base = 0..<5
62+
let result = base.lazy.replacingSubrange(base.startIndex..<base.startIndex, with: [8, 9, 10])
63+
XCTAssertEqualCollections(result, [8, 9, 10, 0, 1, 2, 3, 4])
64+
IndexValidator().validate(result, expectedCount: 8)
65+
}
66+
67+
// Base: non-empty
68+
// Prepending: empty
69+
do {
70+
let base = 0..<5
71+
let result = base.lazy.replacingSubrange(base.startIndex..<base.startIndex, with: EmptyCollection())
72+
XCTAssertEqualCollections(result, [0, 1, 2, 3, 4])
73+
IndexValidator().validate(result, expectedCount: 5)
74+
}
75+
76+
// Base: empty
77+
// Prepending: non-empty
78+
do {
79+
let base = EmptyCollection<Int>()
80+
let result = base.lazy.replacingSubrange(base.startIndex..<base.startIndex, with: 5..<10)
81+
XCTAssertEqualCollections(result, [5, 6, 7, 8, 9])
82+
IndexValidator().validate(result, expectedCount: 5)
83+
}
84+
85+
// Base: empty
86+
// Prepending: empty
87+
do {
88+
let base = EmptyCollection<Int>()
89+
let result = base.lazy.replacingSubrange(base.startIndex..<base.startIndex, with: EmptyCollection())
90+
XCTAssertEqualCollections(result, [])
91+
IndexValidator().validate(result, expectedCount: 0)
92+
}
93+
}
94+
95+
func testInsert() {
96+
97+
let base = 0..<10
98+
let i = base.index(base.startIndex, offsetBy: 5)
99+
let result = base.lazy.replacingSubrange(i..<i, with: 20..<25)
100+
XCTAssertEqualCollections(result, [0, 1, 2, 3, 4, 20, 21, 22, 23, 24, 5, 6, 7, 8, 9])
101+
IndexValidator().validate(result, expectedCount: 15)
102+
}
103+
104+
func testReplace() {
105+
106+
// Location: start
107+
// Replacement: non-empty
108+
do {
109+
let base = "hello, world!"
110+
let i = base.index(base.startIndex, offsetBy: 3)
111+
let result = base.lazy.replacingSubrange(base.startIndex..<i, with: "goodbye".reversed())
112+
XCTAssertEqualCollections(result, "eybdooglo, world!")
113+
IndexValidator().validate(result, expectedCount: 17)
114+
}
115+
116+
// Location: start
117+
// Replacement: empty
118+
do {
119+
let base = "hello, world!"
120+
let i = base.index(base.startIndex, offsetBy: 3)
121+
let result = base.lazy.replacingSubrange(base.startIndex..<i, with: EmptyCollection())
122+
XCTAssertEqualCollections(result, "lo, world!")
123+
IndexValidator().validate(result, expectedCount: 10)
124+
}
125+
126+
// Location: middle
127+
// Replacement: non-empty
128+
do {
129+
let base = "hello, world!"
130+
let start = base.index(base.startIndex, offsetBy: 3)
131+
let end = base.index(start, offsetBy: 4)
132+
let result = base.lazy.replacingSubrange(start..<end, with: "goodbye".reversed())
133+
XCTAssertEqualCollections(result, "heleybdoogworld!")
134+
IndexValidator().validate(result, expectedCount: 16)
135+
}
136+
137+
// Location: middle
138+
// Replacement: empty
139+
do {
140+
let base = "hello, world!"
141+
let start = base.index(base.startIndex, offsetBy: 3)
142+
let end = base.index(start, offsetBy: 4)
143+
let result = base.lazy.replacingSubrange(start..<end, with: EmptyCollection())
144+
XCTAssertEqualCollections(result, "helworld!")
145+
IndexValidator().validate(result, expectedCount: 9)
146+
}
147+
148+
// Location: end
149+
// Replacement: non-empty
150+
do {
151+
let base = "hello, world!"
152+
let start = base.index(base.endIndex, offsetBy: -4)
153+
let result = base.lazy.replacingSubrange(start..<base.endIndex, with: "goodbye".reversed())
154+
XCTAssertEqualCollections(result, "hello, woeybdoog")
155+
IndexValidator().validate(result, expectedCount: 16)
156+
}
157+
158+
// Location: end
159+
// Replacement: empty
160+
do {
161+
let base = "hello, world!"
162+
let start = base.index(base.endIndex, offsetBy: -4)
163+
let result = base.lazy.replacingSubrange(start..<base.endIndex, with: EmptyCollection())
164+
XCTAssertEqualCollections(result, "hello, wo")
165+
IndexValidator().validate(result, expectedCount: 9)
166+
}
167+
}
168+
}

0 commit comments

Comments
 (0)