-
Notifications
You must be signed in to change notification settings - Fork 440
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add a non-mutating lazy replaceSubrange
- Loading branch information
Showing
2 changed files
with
367 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,169 @@ | ||
//===----------------------------------------------------------------------===// | ||
// | ||
// This source file is part of the Swift Algorithms open source project | ||
// | ||
// Copyright (c) 2020 Apple Inc. and the Swift project authors | ||
// Licensed under Apache License v2.0 with Runtime Library Exception | ||
// | ||
// See https://swift.org/LICENSE.txt for license information | ||
// | ||
//===----------------------------------------------------------------------===// | ||
|
||
extension LazyCollection { | ||
|
||
@inlinable | ||
public func replacingSubrange<Replacements>( | ||
_ subrange: Range<Index>, with newElements: Replacements | ||
) -> ReplacingSubrangeCollection<Base, Replacements> { | ||
ReplacingSubrangeCollection(base: elements, replacements: newElements, replacedRange: subrange) | ||
} | ||
} | ||
|
||
public struct ReplacingSubrangeCollection<Base, Replacements> | ||
where Base: Collection, Replacements: Collection, Base.Element == Replacements.Element { | ||
|
||
@usableFromInline | ||
internal var base: Base | ||
|
||
@usableFromInline | ||
internal var replacements: Replacements | ||
|
||
@usableFromInline | ||
internal var replacedRange: Range<Base.Index> | ||
|
||
@inlinable | ||
internal init(base: Base, replacements: Replacements, replacedRange: Range<Base.Index>) { | ||
self.base = base | ||
self.replacements = replacements | ||
self.replacedRange = replacedRange | ||
} | ||
} | ||
|
||
extension ReplacingSubrangeCollection: Collection { | ||
|
||
public typealias Element = Base.Element | ||
|
||
public struct Index: Comparable { | ||
|
||
@usableFromInline | ||
internal enum Wrapped { | ||
case base(Base.Index) | ||
case replacement(Replacements.Index) | ||
} | ||
|
||
/// The underlying base/replacements index. | ||
/// | ||
@usableFromInline | ||
internal var wrapped: Wrapped | ||
|
||
/// The base indices which have been replaced. | ||
/// | ||
@usableFromInline | ||
internal var replacedRange: Range<Base.Index> | ||
|
||
@inlinable | ||
internal init(wrapped: Wrapped, replacedRange: Range<Base.Index>) { | ||
self.wrapped = wrapped | ||
self.replacedRange = replacedRange | ||
} | ||
|
||
@inlinable | ||
public static func < (lhs: Self, rhs: Self) -> Bool { | ||
switch (lhs.wrapped, rhs.wrapped) { | ||
case (.base(let unwrappedLeft), .base(let unwrappedRight)): | ||
return unwrappedLeft < unwrappedRight | ||
case (.replacement(let unwrappedLeft), .replacement(let unwrappedRight)): | ||
return unwrappedLeft < unwrappedRight | ||
case (.base(let unwrappedLeft), .replacement(_)): | ||
return unwrappedLeft < lhs.replacedRange.lowerBound | ||
case (.replacement(_), .base(let unwrappedRight)): | ||
return !(unwrappedRight < lhs.replacedRange.lowerBound) | ||
} | ||
} | ||
|
||
@inlinable | ||
public static func == (lhs: Self, rhs: Self) -> Bool { | ||
// No need to check 'replacedRange', because it does not differ between indices from the same collection. | ||
switch (lhs.wrapped, rhs.wrapped) { | ||
case (.base(let unwrappedLeft), .base(let unwrappedRight)): | ||
return unwrappedLeft == unwrappedRight | ||
case (.replacement(let unwrappedLeft), .replacement(let unwrappedRight)): | ||
return unwrappedLeft == unwrappedRight | ||
default: | ||
return false | ||
} | ||
} | ||
} | ||
} | ||
|
||
extension ReplacingSubrangeCollection { | ||
|
||
@inlinable | ||
internal func makeIndex(_ position: Base.Index) -> Index { | ||
Index(wrapped: .base(position), replacedRange: replacedRange) | ||
} | ||
|
||
@inlinable | ||
internal func makeIndex(_ position: Replacements.Index) -> Index { | ||
Index(wrapped: .replacement(position), replacedRange: replacedRange) | ||
} | ||
|
||
@inlinable | ||
public var startIndex: Index { | ||
if base.startIndex == replacedRange.lowerBound { | ||
if replacements.isEmpty { | ||
return makeIndex(replacedRange.upperBound) | ||
} | ||
return makeIndex(replacements.startIndex) | ||
} | ||
return makeIndex(base.startIndex) | ||
} | ||
|
||
@inlinable | ||
public var endIndex: Index { | ||
if replacedRange.lowerBound != base.endIndex || replacements.isEmpty { | ||
return makeIndex(base.endIndex) | ||
} | ||
return makeIndex(replacements.endIndex) | ||
} | ||
|
||
@inlinable | ||
public var count: Int { | ||
base.distance(from: base.startIndex, to: replacedRange.lowerBound) | ||
+ replacements.count | ||
+ base.distance(from: replacedRange.upperBound, to: base.endIndex) | ||
} | ||
|
||
@inlinable | ||
public func index(after i: Index) -> Index { | ||
switch i.wrapped { | ||
case .base(var baseIndex): | ||
base.formIndex(after: &baseIndex) | ||
if baseIndex == replacedRange.lowerBound { | ||
if replacements.isEmpty { | ||
return makeIndex(replacedRange.upperBound) | ||
} | ||
return makeIndex(replacements.startIndex) | ||
} | ||
return makeIndex(baseIndex) | ||
|
||
case .replacement(var replacementIndex): | ||
replacements.formIndex(after: &replacementIndex) | ||
if replacedRange.lowerBound != base.endIndex, replacementIndex == replacements.endIndex { | ||
return makeIndex(replacedRange.upperBound) | ||
} | ||
return makeIndex(replacementIndex) | ||
} | ||
} | ||
|
||
@inlinable | ||
public subscript(position: Index) -> Element { | ||
switch position.wrapped { | ||
case .base(let baseIndex): | ||
return base[baseIndex] | ||
case .replacement(let replacementIndex): | ||
return replacements[replacementIndex] | ||
} | ||
} | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,198 @@ | ||
//===----------------------------------------------------------------------===// | ||
// | ||
// This source file is part of the Swift Algorithms open source project | ||
// | ||
// Copyright (c) 2020 Apple Inc. and the Swift project authors | ||
// Licensed under Apache License v2.0 with Runtime Library Exception | ||
// | ||
// See https://swift.org/LICENSE.txt for license information | ||
// | ||
//===----------------------------------------------------------------------===// | ||
|
||
import XCTest | ||
@testable import Algorithms | ||
|
||
final class ReplaceSubrangeTests: XCTestCase { | ||
|
||
func testAppend() { | ||
|
||
// Base: non-empty | ||
// Appending: non-empty | ||
do { | ||
let base = 0..<5 | ||
let result = base.lazy.replacingSubrange(base.endIndex..<base.endIndex, with: [8, 9, 10]) | ||
XCTAssertEqualCollections(result, [0, 1, 2, 3, 4, 8, 9, 10]) | ||
IndexValidator().validate(result, expectedCount: 8) | ||
} | ||
|
||
// Base: non-empty | ||
// Appending: empty | ||
do { | ||
let base = 0..<5 | ||
let result = base.lazy.replacingSubrange(base.endIndex..<base.endIndex, with: EmptyCollection()) | ||
XCTAssertEqualCollections(result, [0, 1, 2, 3, 4]) | ||
IndexValidator().validate(result, expectedCount: 5) | ||
} | ||
|
||
// Base: empty | ||
// Appending: non-empty | ||
do { | ||
let base = EmptyCollection<Int>() | ||
let result = base.lazy.replacingSubrange(base.endIndex..<base.endIndex, with: 5..<10) | ||
XCTAssertEqualCollections(result, [5, 6, 7, 8, 9]) | ||
IndexValidator().validate(result, expectedCount: 5) | ||
} | ||
|
||
// Base: empty | ||
// Appending: empty | ||
do { | ||
let base = EmptyCollection<Int>() | ||
let result = base.lazy.replacingSubrange(base.endIndex..<base.endIndex, with: EmptyCollection()) | ||
XCTAssertEqualCollections(result, []) | ||
IndexValidator().validate(result, expectedCount: 0) | ||
} | ||
} | ||
|
||
func testPrepend() { | ||
|
||
// Base: non-empty | ||
// Prepending: non-empty | ||
do { | ||
let base = 0..<5 | ||
let result = base.lazy.replacingSubrange(base.startIndex..<base.startIndex, with: [8, 9, 10]) | ||
XCTAssertEqualCollections(result, [8, 9, 10, 0, 1, 2, 3, 4]) | ||
IndexValidator().validate(result, expectedCount: 8) | ||
} | ||
|
||
// Base: non-empty | ||
// Prepending: empty | ||
do { | ||
let base = 0..<5 | ||
let result = base.lazy.replacingSubrange(base.startIndex..<base.startIndex, with: EmptyCollection()) | ||
XCTAssertEqualCollections(result, [0, 1, 2, 3, 4]) | ||
IndexValidator().validate(result, expectedCount: 5) | ||
} | ||
|
||
// Base: empty | ||
// Prepending: non-empty | ||
do { | ||
let base = EmptyCollection<Int>() | ||
let result = base.lazy.replacingSubrange(base.startIndex..<base.startIndex, with: 5..<10) | ||
XCTAssertEqualCollections(result, [5, 6, 7, 8, 9]) | ||
IndexValidator().validate(result, expectedCount: 5) | ||
} | ||
|
||
// Base: empty | ||
// Prepending: empty | ||
do { | ||
let base = EmptyCollection<Int>() | ||
let result = base.lazy.replacingSubrange(base.startIndex..<base.startIndex, with: EmptyCollection()) | ||
XCTAssertEqualCollections(result, []) | ||
IndexValidator().validate(result, expectedCount: 0) | ||
} | ||
} | ||
|
||
func testInsert() { | ||
|
||
// Inserting: non-empty | ||
do { | ||
let base = 0..<10 | ||
let i = base.index(base.startIndex, offsetBy: 5) | ||
let result = base.lazy.replacingSubrange(i..<i, with: 20..<25) | ||
XCTAssertEqualCollections(result, [0, 1, 2, 3, 4, 20, 21, 22, 23, 24, 5, 6, 7, 8, 9]) | ||
IndexValidator().validate(result, expectedCount: 15) | ||
} | ||
|
||
// Inserting: empty | ||
do { | ||
let base = 0..<10 | ||
let i = base.index(base.startIndex, offsetBy: 5) | ||
let result = base.lazy.replacingSubrange(i..<i, with: EmptyCollection()) | ||
XCTAssertEqualCollections(result, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) | ||
IndexValidator().validate(result, expectedCount: 10) | ||
} | ||
} | ||
|
||
func testReplace() { | ||
|
||
// Location: start | ||
// Replacement: non-empty | ||
do { | ||
let base = "hello, world!" | ||
let i = base.index(base.startIndex, offsetBy: 3) | ||
let result = base.lazy.replacingSubrange(base.startIndex..<i, with: "goodbye".reversed()) | ||
XCTAssertEqualCollections(result, "eybdooglo, world!") | ||
IndexValidator().validate(result, expectedCount: 17) | ||
} | ||
|
||
// Location: start | ||
// Replacement: empty | ||
do { | ||
let base = "hello, world!" | ||
let i = base.index(base.startIndex, offsetBy: 3) | ||
let result = base.lazy.replacingSubrange(base.startIndex..<i, with: EmptyCollection()) | ||
XCTAssertEqualCollections(result, "lo, world!") | ||
IndexValidator().validate(result, expectedCount: 10) | ||
} | ||
|
||
// Location: middle | ||
// Replacement: non-empty | ||
do { | ||
let base = "hello, world!" | ||
let start = base.index(base.startIndex, offsetBy: 3) | ||
let end = base.index(start, offsetBy: 4) | ||
let result = base.lazy.replacingSubrange(start..<end, with: "goodbye".reversed()) | ||
XCTAssertEqualCollections(result, "heleybdoogworld!") | ||
IndexValidator().validate(result, expectedCount: 16) | ||
} | ||
|
||
// Location: middle | ||
// Replacement: empty | ||
do { | ||
let base = "hello, world!" | ||
let start = base.index(base.startIndex, offsetBy: 3) | ||
let end = base.index(start, offsetBy: 4) | ||
let result = base.lazy.replacingSubrange(start..<end, with: EmptyCollection()) | ||
XCTAssertEqualCollections(result, "helworld!") | ||
IndexValidator().validate(result, expectedCount: 9) | ||
} | ||
|
||
// Location: end | ||
// Replacement: non-empty | ||
do { | ||
let base = "hello, world!" | ||
let start = base.index(base.endIndex, offsetBy: -4) | ||
let result = base.lazy.replacingSubrange(start..<base.endIndex, with: "goodbye".reversed()) | ||
XCTAssertEqualCollections(result, "hello, woeybdoog") | ||
IndexValidator().validate(result, expectedCount: 16) | ||
} | ||
|
||
// Location: end | ||
// Replacement: empty | ||
do { | ||
let base = "hello, world!" | ||
let start = base.index(base.endIndex, offsetBy: -4) | ||
let result = base.lazy.replacingSubrange(start..<base.endIndex, with: EmptyCollection()) | ||
XCTAssertEqualCollections(result, "hello, wo") | ||
IndexValidator().validate(result, expectedCount: 9) | ||
} | ||
|
||
// Location: entire collection | ||
// Replacement: non-empty | ||
do { | ||
let base = "hello, world!" | ||
let result = base.lazy.replacingSubrange(base.startIndex..<base.endIndex, with: Array("blah blah blah")) | ||
XCTAssertEqualCollections(result, "blah blah blah") | ||
IndexValidator().validate(result, expectedCount: 14) | ||
} | ||
|
||
// Location: entire collection | ||
// Replacement: empty | ||
do { | ||
let base = "hello, world!" | ||
let result = base.lazy.replacingSubrange(base.startIndex..<base.endIndex, with: EmptyCollection()) | ||
XCTAssertEqualCollections(result, "") | ||
IndexValidator().validate(result, expectedCount: 0) | ||
} | ||
} | ||
} |