Skip to content

Add a non-mutating lazy replaceSubrange #203

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
170 changes: 170 additions & 0 deletions Guides/Overlay.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
# Overlay

[[Source](https://github.com/apple/swift-algorithms/blob/main/Sources/Algorithms/Overlay.swift) |
[Tests](https://github.com/apple/swift-algorithms/blob/main/Tests/SwiftAlgorithmsTests/OverlayTests.swift)]

Compose collections by overlaying the elements of one collection
over an arbitrary region of another collection.

Swift offers many interesting collections, for instance:

- `Range<Int>` allows us to express the numbers in `0..<1000`
in an efficient way that does not allocate storage for each number.

- `Repeated<Int>` allows us to express, say, one thousand copies of the same value,
without allocating space for a thousand values.

- `LazyMapCollection` allows us to transform the elements of a collection on-demand,
without creating a copy of the source collection and eagerly transforming every element.

- The collections in this package, such as `.chunked`, `.cycled`, `.joined`, and `.interspersed`,
similarly compute their elements on-demand.

While these collections can be very efficient, it is difficult to compose them in to arbitrary datasets.
If we have the Range `5..<10`, and want to insert a `0` in the middle of it, we would need to allocate storage
for the entire collection, losing the benefits of `Range<Int>`. Similarly, if we have some numbers in storage
(say, in an Array) and wish to insert a contiguous range in the middle of it, we have to allocate storage
in the Array and cannot take advantage of `Range<Int>` memory efficiency.

The `OverlayCollection` allows us to form arbitrary compositions without mutating
or allocating storage for the result.

```swift
// 'numbers' is a composition of:
// - Range<Int>, and
// - CollectionOfOne<Int>

let numbers = (5..<10).overlay.inserting(0, at: 7)

for n in numbers {
// n: 5, 6, 0, 7, 8, 9
// ^
}
```

```swift
// 'numbers' is a composition of:
// - Array<Int>, and
// - Range<Int>

let rawdata = [3, 6, 1, 4, 6]
let numbers = rawdata.overlay.inserting(contentsOf: 5..<10, at: 3)

for n in numbers {
// n: 3, 6, 1, 5, 6, 7, 8, 9, 4, 6
// ^^^^^^^^^^^^^
}
```

We can also insert elements in to a `LazyMapCollection`:

```swift
enum ListItem {
case product(Product)
case callToAction
}

let products: [Product] = ...

var listItems: some Collection<ListItem> {
products
.lazy.map { ListItem.product($0) }
.overlay.inserting(.callToAction, at: min(4, products.count))
}

for item in listItems {
// item: .product(A), .product(B), .product(C), .callToAction, .product(D), ...
// ^^^^^^^^^^^^^
}
```

## Detailed Design

An `.overlay` member is added to all collections:

```swift
extension Collection {
public var overlay: OverlayCollectionNamespace<Self> { get }
}
```

This member returns a wrapper structure, `OverlayCollectionNamespace`,
which provides a similar suite of methods to the standard library's `RangeReplaceableCollection` protocol.

However, while `RangeReplaceableCollection` methods mutate the collection they are applied to,
these methods return a new `OverlayCollection` value which substitutes the specified elements on-demand.

```swift
extension OverlayCollectionNamespace {

// Multiple elements:

public func replacingSubrange<Overlay>(
_ subrange: Range<Elements.Index>, with newElements: Overlay
) -> OverlayCollection<Elements, Overlay>

public func appending<Overlay>(
contentsOf newElements: Overlay
) -> OverlayCollection<Elements, Overlay>

public func inserting<Overlay>(
contentsOf newElements: Overlay, at position: Elements.Index
) -> OverlayCollection<Elements, Overlay>

public func removingSubrange(
_ subrange: Range<Elements.Index>
) -> OverlayCollection<Elements, EmptyCollection<Elements.Element>>

// Single elements:

public func appending(
_ element: Elements.Element
) -> OverlayCollection<Elements, CollectionOfOne<Elements.Element>>

public func inserting(
_ element: Elements.Element, at position: Elements.Index
) -> OverlayCollection<Elements, CollectionOfOne<Elements.Element>>

public func removing(
at position: Elements.Index
) -> OverlayCollection<Elements, EmptyCollection<Elements.Element>>

}
```

`OverlayCollection` conforms to `BidirectionalCollection` when both the base and overlay collections conform.

### Conditional Overlays

In order to allow overlays to be applied conditionally, another function is added to all collections:

```swift
extension Collection {

public func overlay<Overlay>(
if condition: Bool,
_ makeOverlay: (OverlayCollectionNamespace<Self>) -> OverlayCollection<Self, Overlay>
) -> OverlayCollection<Self, Overlay>

}
```

If the `condition` parameter is `true`, the `makeOverlay` closure is invoked to apply the desired overlay.
If `condition` is `false`, the closure is not invoked, and the function returns a no-op overlay,
containing the same elements as the base collection.

This allows overlays to be applied conditionally while still being usable as opaque return types:

```swift
func getNumbers(shouldInsert: Bool) -> some Collection<Int> {
(5..<10).overlay(if: shouldInsert) { $0.inserting(0, at: 7) }
}

for n in getNumbers(shouldInsert: true) {
// n: 5, 6, 0, 7, 8, 9
}

for n in getNumbers(shouldInsert: false) {
// n: 5, 6, 7, 8, 9
}
```
329 changes: 329 additions & 0 deletions Sources/Algorithms/Overlay.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,329 @@
//===----------------------------------------------------------------------===//
//
// 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
//
//===----------------------------------------------------------------------===//

/// A namespace for methods which return composed collections,
/// formed by replacing a region of a base collection
/// with another collection of elements.
///
/// Access the namespace via the `.overlay` member, available on all collections:
///
/// ```swift
/// let base = 0..<5
/// for n in base.overlay.inserting(42, at: 2) {
/// print(n)
/// }
/// // Prints: 0, 1, 42, 2, 3, 4
/// ```
///
public struct OverlayCollectionNamespace<Elements: Collection> {

public let elements: Elements

@inlinable
internal init(elements: Elements) {
self.elements = elements
}
}

extension Collection {

/// A namespace for methods which return composed collections,
/// formed by replacing a region of this collection
/// with another collection of elements.
///
@inlinable
public var overlay: OverlayCollectionNamespace<Self> {
OverlayCollectionNamespace(elements: self)
}

/// If `condition` is true, returns an `OverlayCollection` by applying the given closure.
/// Otherwise, returns an `OverlayCollection` containing the same elements as this collection.
///
/// The following example takes an array of products, lazily wraps them in a `ListItem` enum,
/// and conditionally inserts a call-to-action element if `showCallToAction` is true.
///
/// ```swift
/// var listItems: some Collection<ListItem> {
/// let products: [Product] = ...
/// return products
/// .lazy.map {
/// ListItem.product($0)
/// }
/// .overlay(if: showCallToAction) {
/// $0.inserting(.callToAction, at: min(4, $0.elements.count))
/// }
/// }
/// ```
///
@inlinable
public func overlay<Overlay>(
if condition: Bool, _ makeOverlay: (OverlayCollectionNamespace<Self>) -> OverlayCollection<Self, Overlay>
) -> OverlayCollection<Self, Overlay> {
if condition {
return makeOverlay(overlay)
} else {
return OverlayCollection(base: self, overlay: nil, replacedRange: startIndex..<startIndex)
}
}
}

extension OverlayCollectionNamespace {

@inlinable
public func replacingSubrange<Overlay>(
_ subrange: Range<Elements.Index>, with newElements: Overlay
) -> OverlayCollection<Elements, Overlay> {
OverlayCollection(base: elements, overlay: newElements, replacedRange: subrange)
}

@inlinable
public func appending<Overlay>(
contentsOf newElements: Overlay
) -> OverlayCollection<Elements, Overlay> {
replacingSubrange(elements.endIndex..<elements.endIndex, with: newElements)
}

@inlinable
public func inserting<Overlay>(
contentsOf newElements: Overlay, at position: Elements.Index
) -> OverlayCollection<Elements, Overlay> {
replacingSubrange(position..<position, with: newElements)
}

@inlinable
public func removingSubrange(
_ subrange: Range<Elements.Index>
) -> OverlayCollection<Elements, EmptyCollection<Elements.Element>> {
replacingSubrange(subrange, with: EmptyCollection())
}

@inlinable
public func appending(
_ element: Elements.Element
) -> OverlayCollection<Elements, CollectionOfOne<Elements.Element>> {
appending(contentsOf: CollectionOfOne(element))
}

@inlinable
public func inserting(
_ element: Elements.Element, at position: Elements.Index
) -> OverlayCollection<Elements, CollectionOfOne<Elements.Element>> {
inserting(contentsOf: CollectionOfOne(element), at: position)
}

@inlinable
public func removing(
at position: Elements.Index
) -> OverlayCollection<Elements, EmptyCollection<Elements.Element>> {
removingSubrange(position..<elements.index(after: position))
}
}

/// A composed collections, formed by replacing a region of a base collection
/// with another collection of elements.
///
/// To create an OverlayCollection, use the methods in the ``OverlayCollectionNamespace``
/// namespace:
///
/// ```swift
/// let base = 0..<5
/// for n in base.overlay.inserting(42, at: 2) {
/// print(n)
/// }
/// // Prints: 0, 1, 42, 2, 3, 4
/// ```
///
public struct OverlayCollection<Base, Overlay>
where Base: Collection, Overlay: Collection, Base.Element == Overlay.Element {

@usableFromInline
internal var base: Base

@usableFromInline
internal var overlay: Optional<Overlay>

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

@inlinable
internal init(base: Base, overlay: Overlay?, replacedRange: Range<Base.Index>) {
self.base = base
self.overlay = overlay
self.replacedRange = replacedRange
}
}

extension OverlayCollection: Collection {

public typealias Element = Base.Element

public struct Index: Comparable {

@usableFromInline
internal enum Wrapped {
case base(Base.Index)
case overlay(Overlay.Index)
}

/// The underlying base/overlay index.
///
@usableFromInline
internal var wrapped: Wrapped

/// The base index at which the overlay starts -- i.e. `replacedRange.lowerBound`
///
@usableFromInline
internal var startOfReplacedRange: Base.Index

@inlinable
internal init(wrapped: Wrapped, startOfReplacedRange: Base.Index) {
self.wrapped = wrapped
self.startOfReplacedRange = startOfReplacedRange
}

@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 (.overlay(let unwrappedLeft), .overlay(let unwrappedRight)):
return unwrappedLeft < unwrappedRight
case (.base(let unwrappedLeft), .overlay(_)):
return unwrappedLeft < lhs.startOfReplacedRange
case (.overlay(_), .base(let unwrappedRight)):
return !(unwrappedRight < lhs.startOfReplacedRange)
}
}

@inlinable
public static func == (lhs: Self, rhs: Self) -> Bool {
// No need to check 'startOfReplacedRange', 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 (.overlay(let unwrappedLeft), .overlay(let unwrappedRight)):
return unwrappedLeft == unwrappedRight
default:
return false
}
}
}
}

extension OverlayCollection {

@inlinable
internal func makeIndex(_ position: Base.Index) -> Index {
Index(wrapped: .base(position), startOfReplacedRange: replacedRange.lowerBound)
}

@inlinable
internal func makeIndex(_ position: Overlay.Index) -> Index {
Index(wrapped: .overlay(position), startOfReplacedRange: replacedRange.lowerBound)
}

@inlinable
public var startIndex: Index {
if let overlay = overlay, base.startIndex == replacedRange.lowerBound {
if overlay.isEmpty {
return makeIndex(replacedRange.upperBound)
}
return makeIndex(overlay.startIndex)
}
return makeIndex(base.startIndex)
}

@inlinable
public var endIndex: Index {
guard let overlay = overlay else {
return makeIndex(base.endIndex)
}
if replacedRange.lowerBound != base.endIndex || overlay.isEmpty {
return makeIndex(base.endIndex)
}
return makeIndex(overlay.endIndex)
}

@inlinable
public var count: Int {
guard let overlay = overlay else {
return base.count
}
return base.distance(from: base.startIndex, to: replacedRange.lowerBound)
+ overlay.count
+ base.distance(from: replacedRange.upperBound, to: base.endIndex)
}

@inlinable
public var isEmpty: Bool {
return replacedRange.lowerBound == base.startIndex
&& replacedRange.upperBound == base.endIndex
&& (overlay?.isEmpty ?? true)
}

@inlinable
public func index(after i: Index) -> Index {
switch i.wrapped {
case .base(var baseIndex):
base.formIndex(after: &baseIndex)
if let overlay = overlay, baseIndex == replacedRange.lowerBound {
if overlay.isEmpty {
return makeIndex(replacedRange.upperBound)
}
return makeIndex(overlay.startIndex)
}
return makeIndex(baseIndex)

case .overlay(var overlayIndex):
overlay!.formIndex(after: &overlayIndex)
if replacedRange.lowerBound != base.endIndex, overlayIndex == overlay!.endIndex {
return makeIndex(replacedRange.upperBound)
}
return makeIndex(overlayIndex)
}
}

@inlinable
public subscript(position: Index) -> Element {
switch position.wrapped {
case .base(let baseIndex):
return base[baseIndex]
case .overlay(let overlayIndex):
return overlay![overlayIndex]
}
}
}

extension OverlayCollection: BidirectionalCollection
where Base: BidirectionalCollection, Overlay: BidirectionalCollection {

@inlinable
public func index(before i: Index) -> Index {
switch i.wrapped {
case .base(var baseIndex):
if let overlay = overlay, baseIndex == replacedRange.upperBound {
if overlay.isEmpty {
return makeIndex(base.index(before: replacedRange.lowerBound))
}
return makeIndex(overlay.index(before: overlay.endIndex))
}
base.formIndex(before: &baseIndex)
return makeIndex(baseIndex)

case .overlay(var overlayIndex):
if overlayIndex == overlay!.startIndex {
return makeIndex(base.index(before: replacedRange.lowerBound))
}
overlay!.formIndex(before: &overlayIndex)
return makeIndex(overlayIndex)
}
}
}
343 changes: 343 additions & 0 deletions Tests/SwiftAlgorithmsTests/OverlayTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,343 @@
//===----------------------------------------------------------------------===//
//
// 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() {

func _performAppendTest<Base, Overlay>(
base: Base, appending newElements: Overlay,
_ checkResult: (OverlayCollection<Base, Overlay>) -> Void
) {
checkResult(base.overlay.appending(contentsOf: newElements))

checkResult(base.overlay.inserting(contentsOf: newElements, at: base.endIndex))

checkResult(base.overlay.replacingSubrange(base.endIndex..<base.endIndex, with: newElements))
}

// Base: non-empty
// Appending: non-empty
_performAppendTest(base: 0..<5, appending: [8, 9, 10]) { result in
XCTAssertEqualCollections(result, [0, 1, 2, 3, 4, 8, 9, 10])
IndexValidator().validate(result, expectedCount: 8)
}

// Base: non-empty
// Appending: empty
_performAppendTest(base: 0..<5, appending: EmptyCollection()) { result in
XCTAssertEqualCollections(result, [0, 1, 2, 3, 4])
IndexValidator().validate(result, expectedCount: 5)
}

// Base: empty
// Appending: non-empty
_performAppendTest(base: EmptyCollection(), appending: 5..<10) { result in
XCTAssertEqualCollections(result, [5, 6, 7, 8, 9])
IndexValidator().validate(result, expectedCount: 5)
}

// Base: empty
// Appending: empty
_performAppendTest(base: EmptyCollection<Int>(), appending: EmptyCollection()) { result in
XCTAssertEqualCollections(result, [])
IndexValidator().validate(result, expectedCount: 0)
}
}

func testAppendSingle() {

// Base: empty
do {
let base = EmptyCollection<Int>()
let result = base.overlay.appending(99)
XCTAssertEqualCollections(result, [99])
IndexValidator().validate(result, expectedCount: 1)
}

// Base: non-empty
do {
let base = 2..<8
let result = base.overlay.appending(99)
XCTAssertEqualCollections(result, [2, 3, 4, 5, 6, 7, 99])
IndexValidator().validate(result, expectedCount: 7)
}
}

func testPrepend() {

func _performPrependTest<Base, Overlay>(
base: Base, prepending newElements: Overlay,
_ checkResult: (OverlayCollection<Base, Overlay>) -> Void
) {
checkResult(base.overlay.inserting(contentsOf: newElements, at: base.startIndex))

checkResult(base.overlay.replacingSubrange(base.startIndex..<base.startIndex, with: newElements))
}

// Base: non-empty
// Prepending: non-empty
_performPrependTest(base: 0..<5, prepending: [8, 9, 10]) { result in
XCTAssertEqualCollections(result, [8, 9, 10, 0, 1, 2, 3, 4])
IndexValidator().validate(result, expectedCount: 8)
}

// Base: non-empty
// Prepending: empty
_performPrependTest(base: 0..<5, prepending: EmptyCollection()) { result in
XCTAssertEqualCollections(result, [0, 1, 2, 3, 4])
IndexValidator().validate(result, expectedCount: 5)
}

// Base: empty
// Prepending: non-empty
_performPrependTest(base: EmptyCollection(), prepending: 5..<10) { result in
XCTAssertEqualCollections(result, [5, 6, 7, 8, 9])
IndexValidator().validate(result, expectedCount: 5)
}

// Base: empty
// Prepending: empty
_performPrependTest(base: EmptyCollection<Int>(), prepending: EmptyCollection()) { result in
XCTAssertEqualCollections(result, [])
IndexValidator().validate(result, expectedCount: 0)
}
}

func testPrependSingle() {

// Base: empty
do {
let base = EmptyCollection<Int>()
let result = base.overlay.inserting(99, at: base.startIndex)
XCTAssertEqualCollections(result, [99])
IndexValidator().validate(result, expectedCount: 1)
}

// Base: non-empty
do {
let base = 2..<8
let result = base.overlay.inserting(99, at: base.startIndex)
XCTAssertEqualCollections(result, [99, 2, 3, 4, 5, 6, 7])
IndexValidator().validate(result, expectedCount: 7)
}
}

func testInsert() {

// Inserting: non-empty
do {
let base = 0..<10
let i = base.index(base.startIndex, offsetBy: 5)
let result = base.overlay.inserting(contentsOf: 20..<25, at: i)
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.overlay.inserting(contentsOf: EmptyCollection(), at: i)
XCTAssertEqualCollections(result, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
IndexValidator().validate(result, expectedCount: 10)
}
}

func testInsertSingle() {

let base = 2..<8
let result = base.overlay.inserting(99, at: base.index(base.startIndex, offsetBy: 3))
XCTAssertEqualCollections(result, [2, 3, 4, 99, 5, 6, 7])
IndexValidator().validate(result, expectedCount: 7)
}

func testReplace() {

// Location: anchored to start
// Replacement: non-empty
do {
let base = "hello, world!"
let i = base.index(base.startIndex, offsetBy: 3)
let result = base.overlay.replacingSubrange(base.startIndex..<i, with: "goodbye".reversed())
XCTAssertEqualCollections(result, "eybdooglo, world!")
IndexValidator().validate(result, expectedCount: 17)
}

// Location: anchored to start
// Replacement: empty
do {
let base = "hello, world!"
let i = base.index(base.startIndex, offsetBy: 3)
let result = base.overlay.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.overlay.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.overlay.replacingSubrange(start..<end, with: EmptyCollection())
XCTAssertEqualCollections(result, "helworld!")
IndexValidator().validate(result, expectedCount: 9)
}

// Location: anchored to end
// Replacement: non-empty
do {
let base = "hello, world!"
let start = base.index(base.endIndex, offsetBy: -4)
let result = base.overlay.replacingSubrange(start..<base.endIndex, with: "goodbye".reversed())
XCTAssertEqualCollections(result, "hello, woeybdoog")
IndexValidator().validate(result, expectedCount: 16)
}

// Location: anchored to end
// Replacement: empty
do {
let base = "hello, world!"
let start = base.index(base.endIndex, offsetBy: -4)
let result = base.overlay.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.overlay.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.overlay.replacingSubrange(base.startIndex..<base.endIndex, with: EmptyCollection())
XCTAssertEqualCollections(result, "")
IndexValidator().validate(result, expectedCount: 0)
}
}

func testRemove() {

// Location: anchored to start
do {
let base = "hello, world!"
let i = base.index(base.startIndex, offsetBy: 3)
let result = base.overlay.removingSubrange(base.startIndex..<i)
XCTAssertEqualCollections(result, "lo, world!")
IndexValidator().validate(result, expectedCount: 10)
}

// Location: middle
do {
let base = "hello, world!"
let start = base.index(base.startIndex, offsetBy: 3)
let end = base.index(start, offsetBy: 4)
let result = base.overlay.removingSubrange(start..<end)
XCTAssertEqualCollections(result, "helworld!")
IndexValidator().validate(result, expectedCount: 9)
}

// Location: anchored to end
do {
let base = "hello, world!"
let start = base.index(base.endIndex, offsetBy: -4)
let result = base.overlay.removingSubrange(start..<base.endIndex)
XCTAssertEqualCollections(result, "hello, wo")
IndexValidator().validate(result, expectedCount: 9)
}

// Location: entire collection
do {
let base = "hello, world!"
let result = base.overlay.removingSubrange(base.startIndex..<base.endIndex)
XCTAssertEqualCollections(result, "")
IndexValidator().validate(result, expectedCount: 0)
}
}

func testRemoveSingle() {

// Location: start
do {
let base = "hello, world!"
let result = base.overlay.removing(at: base.startIndex)
XCTAssertEqualCollections(result, "ello, world!")
IndexValidator().validate(result, expectedCount: 12)
}

// Location: middle
do {
let base = "hello, world!"
let i = base.index(base.startIndex, offsetBy: 3)
let result = base.overlay.removing(at: i)
XCTAssertEqualCollections(result, "helo, world!")
IndexValidator().validate(result, expectedCount: 12)
}

// Location: end
do {
let base = "hello, world!"
let i = base.index(before: base.endIndex)
let result = base.overlay.removing(at: i)
XCTAssertEqualCollections(result, "hello, world")
IndexValidator().validate(result, expectedCount: 12)
}

// Location: entire collection
do {
let base = "x"
let result = base.overlay.removing(at: base.startIndex)
XCTAssertEqualCollections(result, "")
IndexValidator().validate(result, expectedCount: 0)
}
}

func testConditionalReplacement() {

func getNumbers(shouldInsert: Bool) -> OverlayCollection<Range<Int>, CollectionOfOne<Int>> {
(0..<5).overlay(if: shouldInsert) { $0.inserting(42, at: 2) }
}

do {
let result = getNumbers(shouldInsert: true)
XCTAssertEqualCollections(result, [0, 1, 42, 2, 3, 4])
IndexValidator().validate(result, expectedCount: 6)
}

do {
let result = getNumbers(shouldInsert: false)
XCTAssertEqualCollections(result, [0, 1, 2, 3, 4])
IndexValidator().validate(result, expectedCount: 5)
}
}
}