Skip to content

Commit 799bc7c

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

File tree

2 files changed

+351
-0
lines changed

2 files changed

+351
-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,180 @@
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+
// Inserting: non-empty
98+
do {
99+
let base = 0..<10
100+
let i = base.index(base.startIndex, offsetBy: 5)
101+
let result = base.lazy.replacingSubrange(i..<i, with: 20..<25)
102+
XCTAssertEqualCollections(result, [0, 1, 2, 3, 4, 20, 21, 22, 23, 24, 5, 6, 7, 8, 9])
103+
IndexValidator().validate(result, expectedCount: 15)
104+
}
105+
106+
// Inserting: empty
107+
do {
108+
let base = 0..<10
109+
let i = base.index(base.startIndex, offsetBy: 5)
110+
let result = base.lazy.replacingSubrange(i..<i, with: EmptyCollection())
111+
XCTAssertEqualCollections(result, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
112+
IndexValidator().validate(result, expectedCount: 10)
113+
}
114+
}
115+
116+
func testReplace() {
117+
118+
// Location: start
119+
// Replacement: non-empty
120+
do {
121+
let base = "hello, world!"
122+
let i = base.index(base.startIndex, offsetBy: 3)
123+
let result = base.lazy.replacingSubrange(base.startIndex..<i, with: "goodbye".reversed())
124+
XCTAssertEqualCollections(result, "eybdooglo, world!")
125+
IndexValidator().validate(result, expectedCount: 17)
126+
}
127+
128+
// Location: start
129+
// Replacement: empty
130+
do {
131+
let base = "hello, world!"
132+
let i = base.index(base.startIndex, offsetBy: 3)
133+
let result = base.lazy.replacingSubrange(base.startIndex..<i, with: EmptyCollection())
134+
XCTAssertEqualCollections(result, "lo, world!")
135+
IndexValidator().validate(result, expectedCount: 10)
136+
}
137+
138+
// Location: middle
139+
// Replacement: non-empty
140+
do {
141+
let base = "hello, world!"
142+
let start = base.index(base.startIndex, offsetBy: 3)
143+
let end = base.index(start, offsetBy: 4)
144+
let result = base.lazy.replacingSubrange(start..<end, with: "goodbye".reversed())
145+
XCTAssertEqualCollections(result, "heleybdoogworld!")
146+
IndexValidator().validate(result, expectedCount: 16)
147+
}
148+
149+
// Location: middle
150+
// Replacement: empty
151+
do {
152+
let base = "hello, world!"
153+
let start = base.index(base.startIndex, offsetBy: 3)
154+
let end = base.index(start, offsetBy: 4)
155+
let result = base.lazy.replacingSubrange(start..<end, with: EmptyCollection())
156+
XCTAssertEqualCollections(result, "helworld!")
157+
IndexValidator().validate(result, expectedCount: 9)
158+
}
159+
160+
// Location: end
161+
// Replacement: non-empty
162+
do {
163+
let base = "hello, world!"
164+
let start = base.index(base.endIndex, offsetBy: -4)
165+
let result = base.lazy.replacingSubrange(start..<base.endIndex, with: "goodbye".reversed())
166+
XCTAssertEqualCollections(result, "hello, woeybdoog")
167+
IndexValidator().validate(result, expectedCount: 16)
168+
}
169+
170+
// Location: end
171+
// Replacement: empty
172+
do {
173+
let base = "hello, world!"
174+
let start = base.index(base.endIndex, offsetBy: -4)
175+
let result = base.lazy.replacingSubrange(start..<base.endIndex, with: EmptyCollection())
176+
XCTAssertEqualCollections(result, "hello, wo")
177+
IndexValidator().validate(result, expectedCount: 9)
178+
}
179+
}
180+
}

0 commit comments

Comments
 (0)