From d2112a801934cd7a2eda83bef735a857caf1ded4 Mon Sep 17 00:00:00 2001
From: Karl Wagner <5254025+karwa@users.noreply.github.com>
Date: Sat, 9 Sep 2023 16:34:11 +0200
Subject: [PATCH 1/6] Add a non-mutating lazy replaceSubrange

---
 Sources/Algorithms/ReplaceSubrange.swift      | 169 +++++++++++++++
 .../ReplaceSubrangeTests.swift                | 198 ++++++++++++++++++
 2 files changed, 367 insertions(+)
 create mode 100644 Sources/Algorithms/ReplaceSubrange.swift
 create mode 100644 Tests/SwiftAlgorithmsTests/ReplaceSubrangeTests.swift

diff --git a/Sources/Algorithms/ReplaceSubrange.swift b/Sources/Algorithms/ReplaceSubrange.swift
new file mode 100644
index 00000000..31d13d1d
--- /dev/null
+++ b/Sources/Algorithms/ReplaceSubrange.swift
@@ -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]
+    }
+  }
+}
+
diff --git a/Tests/SwiftAlgorithmsTests/ReplaceSubrangeTests.swift b/Tests/SwiftAlgorithmsTests/ReplaceSubrangeTests.swift
new file mode 100644
index 00000000..ca6c9934
--- /dev/null
+++ b/Tests/SwiftAlgorithmsTests/ReplaceSubrangeTests.swift
@@ -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)
+    }
+  }
+}

From ebdcf5c944283c91d9ac92e6c98dfd99dfc18881 Mon Sep 17 00:00:00 2001
From: Karl Wagner <5254025+karwa@users.noreply.github.com>
Date: Sun, 17 Sep 2023 22:16:54 +0200
Subject: [PATCH 2/6] [ReplaceSubrange] Implement BidirectionalCollection

---
 Sources/Algorithms/ReplaceSubrange.swift | 25 ++++++++++++++++++++++++
 1 file changed, 25 insertions(+)

diff --git a/Sources/Algorithms/ReplaceSubrange.swift b/Sources/Algorithms/ReplaceSubrange.swift
index 31d13d1d..46ed3426 100644
--- a/Sources/Algorithms/ReplaceSubrange.swift
+++ b/Sources/Algorithms/ReplaceSubrange.swift
@@ -167,3 +167,28 @@ extension ReplacingSubrangeCollection {
   }
 }
 
+extension ReplacingSubrangeCollection: BidirectionalCollection
+where Base: BidirectionalCollection, Replacements: BidirectionalCollection {
+
+  @inlinable
+  public func index(before i: Index) -> Index {
+    switch i.wrapped {
+    case .base(var baseIndex):
+      if baseIndex == replacedRange.upperBound {
+        if replacements.isEmpty {
+          return makeIndex(base.index(before: replacedRange.lowerBound))
+        }
+        return makeIndex(replacements.index(before: replacements.endIndex))
+      }
+      base.formIndex(before: &baseIndex)
+      return makeIndex(baseIndex)
+
+    case .replacement(var replacementIndex):
+      if replacementIndex == replacements.startIndex {
+        return makeIndex(base.index(before: replacedRange.lowerBound))
+      }
+      replacements.formIndex(before: &replacementIndex)
+      return makeIndex(replacementIndex)
+    }
+  }
+}

From 4770b04be97c94d7fc9601c81186e347800baa13 Mon Sep 17 00:00:00 2001
From: Karl Wagner <5254025+karwa@users.noreply.github.com>
Date: Sun, 17 Sep 2023 22:27:42 +0200
Subject: [PATCH 3/6] [ReplaceSubrange] Create .overlay namespace struct, add
 RRC-like convenience methods

---
 Sources/Algorithms/ReplaceSubrange.swift      | 159 +++++++++++++-----
 .../ReplaceSubrangeTests.swift                |  72 ++++----
 2 files changed, 156 insertions(+), 75 deletions(-)

diff --git a/Sources/Algorithms/ReplaceSubrange.swift b/Sources/Algorithms/ReplaceSubrange.swift
index 46ed3426..058090ce 100644
--- a/Sources/Algorithms/ReplaceSubrange.swift
+++ b/Sources/Algorithms/ReplaceSubrange.swift
@@ -9,37 +9,114 @@
 //
 //===----------------------------------------------------------------------===//
 
-extension LazyCollection {
+/// A namespace for methods which overlay a collection of elements
+/// over a region of a base collection.
+///
+/// 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> {
+
+  @usableFromInline
+  internal var elements: Elements
+
+  @inlinable
+  internal init(elements: Elements) {
+    self.elements = elements
+  }
+}
+
+extension Collection {
+
+  /// A namespace for methods which overlay another collection of elements
+  /// over a region of this collection.
+  ///
+  @inlinable
+  public var overlay: OverlayCollectionNamespace<Self> {
+    OverlayCollectionNamespace(elements: self)
+  }
+}
+
+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 replacingSubrange<Replacements>(
-    _ subrange: Range<Index>, with newElements: Replacements
-  ) -> ReplacingSubrangeCollection<Base, Replacements> {
-    ReplacingSubrangeCollection(base: elements, replacements: newElements, replacedRange: subrange)
+  public func removing(
+    at position: Elements.Index
+  ) -> OverlayCollection<Elements, EmptyCollection<Elements.Element>> {
+    removingSubrange(position..<position)
   }
 }
 
-public struct ReplacingSubrangeCollection<Base, Replacements>
-where Base: Collection, Replacements: Collection, Base.Element == Replacements.Element {
+public struct OverlayCollection<Base, Overlay>
+where Base: Collection, Overlay: Collection, Base.Element == Overlay.Element {
 
   @usableFromInline
   internal var base: Base
 
   @usableFromInline
-  internal var replacements: Replacements
+  internal var overlay: Overlay
 
   @usableFromInline
   internal var replacedRange: Range<Base.Index>
 
   @inlinable
-  internal init(base: Base, replacements: Replacements, replacedRange: Range<Base.Index>) {
+  internal init(base: Base, overlay: Overlay, replacedRange: Range<Base.Index>) {
     self.base = base
-    self.replacements = replacements
+    self.overlay = overlay
     self.replacedRange = replacedRange
   }
 }
 
-extension ReplacingSubrangeCollection: Collection {
+extension OverlayCollection: Collection {
 
   public typealias Element = Base.Element
 
@@ -48,10 +125,10 @@ extension ReplacingSubrangeCollection: Collection {
     @usableFromInline
     internal enum Wrapped {
       case base(Base.Index)
-      case replacement(Replacements.Index)
+      case overlay(Overlay.Index)
     }
 
-    /// The underlying base/replacements index.
+    /// The underlying base/overlay index.
     ///
     @usableFromInline
     internal var wrapped: Wrapped
@@ -72,11 +149,11 @@ extension ReplacingSubrangeCollection: Collection {
       switch (lhs.wrapped, rhs.wrapped) {
       case (.base(let unwrappedLeft), .base(let unwrappedRight)):
         return unwrappedLeft < unwrappedRight
-      case (.replacement(let unwrappedLeft), .replacement(let unwrappedRight)):
+      case (.overlay(let unwrappedLeft), .overlay(let unwrappedRight)):
         return unwrappedLeft < unwrappedRight
-      case (.base(let unwrappedLeft), .replacement(_)):
+      case (.base(let unwrappedLeft), .overlay(_)):
         return unwrappedLeft < lhs.replacedRange.lowerBound
-      case (.replacement(_), .base(let unwrappedRight)):
+      case (.overlay(_), .base(let unwrappedRight)):
         return !(unwrappedRight < lhs.replacedRange.lowerBound)
       }
     }
@@ -87,7 +164,7 @@ extension ReplacingSubrangeCollection: Collection {
       switch (lhs.wrapped, rhs.wrapped) {
       case (.base(let unwrappedLeft), .base(let unwrappedRight)):
         return unwrappedLeft == unwrappedRight
-      case (.replacement(let unwrappedLeft), .replacement(let unwrappedRight)):
+      case (.overlay(let unwrappedLeft), .overlay(let unwrappedRight)):
         return unwrappedLeft == unwrappedRight
       default:
         return false
@@ -96,7 +173,7 @@ extension ReplacingSubrangeCollection: Collection {
   }
 }
 
-extension ReplacingSubrangeCollection {
+extension OverlayCollection {
 
   @inlinable
   internal func makeIndex(_ position: Base.Index) -> Index {
@@ -104,33 +181,33 @@ extension ReplacingSubrangeCollection {
   }
 
   @inlinable
-  internal func makeIndex(_ position: Replacements.Index) -> Index {
-    Index(wrapped: .replacement(position), replacedRange: replacedRange)
+  internal func makeIndex(_ position: Overlay.Index) -> Index {
+    Index(wrapped: .overlay(position), replacedRange: replacedRange)
   }
 
   @inlinable
   public var startIndex: Index {
     if base.startIndex == replacedRange.lowerBound {
-      if replacements.isEmpty {
+      if overlay.isEmpty {
         return makeIndex(replacedRange.upperBound)
       }
-      return makeIndex(replacements.startIndex)
+      return makeIndex(overlay.startIndex)
     }
     return makeIndex(base.startIndex)
   }
 
   @inlinable
   public var endIndex: Index {
-    if replacedRange.lowerBound != base.endIndex || replacements.isEmpty {
+    if replacedRange.lowerBound != base.endIndex || overlay.isEmpty {
       return makeIndex(base.endIndex)
     }
-    return makeIndex(replacements.endIndex)
+    return makeIndex(overlay.endIndex)
   }
 
   @inlinable
   public var count: Int {
     base.distance(from: base.startIndex, to: replacedRange.lowerBound)
-    + replacements.count
+    + overlay.count
     + base.distance(from: replacedRange.upperBound, to: base.endIndex)
   }
 
@@ -140,19 +217,19 @@ extension ReplacingSubrangeCollection {
     case .base(var baseIndex):
       base.formIndex(after: &baseIndex)
       if baseIndex == replacedRange.lowerBound {
-        if replacements.isEmpty {
+        if overlay.isEmpty {
           return makeIndex(replacedRange.upperBound)
         }
-        return makeIndex(replacements.startIndex)
+        return makeIndex(overlay.startIndex)
       }
       return makeIndex(baseIndex)
 
-    case .replacement(var replacementIndex):
-      replacements.formIndex(after: &replacementIndex)
-      if replacedRange.lowerBound != base.endIndex, replacementIndex == replacements.endIndex {
+    case .overlay(var overlayIndex):
+      overlay.formIndex(after: &overlayIndex)
+      if replacedRange.lowerBound != base.endIndex, overlayIndex == overlay.endIndex {
         return makeIndex(replacedRange.upperBound)
       }
-      return makeIndex(replacementIndex)
+      return makeIndex(overlayIndex)
     }
   }
 
@@ -161,34 +238,34 @@ extension ReplacingSubrangeCollection {
     switch position.wrapped {
     case .base(let baseIndex): 
       return base[baseIndex]
-    case .replacement(let replacementIndex): 
-      return replacements[replacementIndex]
+    case .overlay(let overlayIndex):
+      return overlay[overlayIndex]
     }
   }
 }
 
-extension ReplacingSubrangeCollection: BidirectionalCollection
-where Base: BidirectionalCollection, Replacements: BidirectionalCollection {
+extension OverlayCollection: BidirectionalCollection
+where Base: BidirectionalCollection, Overlay: BidirectionalCollection {
 
   @inlinable
   public func index(before i: Index) -> Index {
     switch i.wrapped {
     case .base(var baseIndex):
       if baseIndex == replacedRange.upperBound {
-        if replacements.isEmpty {
+        if overlay.isEmpty {
           return makeIndex(base.index(before: replacedRange.lowerBound))
         }
-        return makeIndex(replacements.index(before: replacements.endIndex))
+        return makeIndex(overlay.index(before: overlay.endIndex))
       }
       base.formIndex(before: &baseIndex)
       return makeIndex(baseIndex)
 
-    case .replacement(var replacementIndex):
-      if replacementIndex == replacements.startIndex {
+    case .overlay(var overlayIndex):
+      if overlayIndex == overlay.startIndex {
         return makeIndex(base.index(before: replacedRange.lowerBound))
       }
-      replacements.formIndex(before: &replacementIndex)
-      return makeIndex(replacementIndex)
+      overlay.formIndex(before: &overlayIndex)
+      return makeIndex(overlayIndex)
     }
   }
 }
diff --git a/Tests/SwiftAlgorithmsTests/ReplaceSubrangeTests.swift b/Tests/SwiftAlgorithmsTests/ReplaceSubrangeTests.swift
index ca6c9934..998b3e69 100644
--- a/Tests/SwiftAlgorithmsTests/ReplaceSubrangeTests.swift
+++ b/Tests/SwiftAlgorithmsTests/ReplaceSubrangeTests.swift
@@ -16,38 +16,41 @@ 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
-    do {
-      let base = 0..<5
-      let result = base.lazy.replacingSubrange(base.endIndex..<base.endIndex, with: [8, 9, 10])
+    _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
-    do {
-      let base = 0..<5
-      let result = base.lazy.replacingSubrange(base.endIndex..<base.endIndex, with: EmptyCollection())
+    _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
-    do {
-      let base = EmptyCollection<Int>()
-      let result = base.lazy.replacingSubrange(base.endIndex..<base.endIndex, with: 5..<10)
+    _performAppendTest(base: EmptyCollection(), appending: 5..<10) { result in
       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())
+    _performAppendTest(base: EmptyCollection<Int>(), appending: EmptyCollection()) { result in
       XCTAssertEqualCollections(result, [])
       IndexValidator().validate(result, expectedCount: 0)
     }
@@ -55,38 +58,39 @@ final class ReplaceSubrangeTests: XCTestCase {
 
   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
-    do {
-      let base = 0..<5
-      let result = base.lazy.replacingSubrange(base.startIndex..<base.startIndex, with: [8, 9, 10])
+    _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
-    do {
-      let base = 0..<5
-      let result = base.lazy.replacingSubrange(base.startIndex..<base.startIndex, with: EmptyCollection())
+    _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
-    do {
-      let base = EmptyCollection<Int>()
-      let result = base.lazy.replacingSubrange(base.startIndex..<base.startIndex, with: 5..<10)
+    _performPrependTest(base: EmptyCollection(), prepending: 5..<10) { result in
       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())
+    _performPrependTest(base: EmptyCollection<Int>(), prepending: EmptyCollection()) { result in
       XCTAssertEqualCollections(result, [])
       IndexValidator().validate(result, expectedCount: 0)
     }
@@ -98,7 +102,7 @@ final class ReplaceSubrangeTests: XCTestCase {
     do {
       let base = 0..<10
       let i = base.index(base.startIndex, offsetBy: 5)
-      let result = base.lazy.replacingSubrange(i..<i, with: 20..<25)
+      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)
     }
@@ -107,7 +111,7 @@ final class ReplaceSubrangeTests: XCTestCase {
     do {
       let base = 0..<10
       let i = base.index(base.startIndex, offsetBy: 5)
-      let result = base.lazy.replacingSubrange(i..<i, with: EmptyCollection())
+      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)
     }
@@ -120,7 +124,7 @@ final class ReplaceSubrangeTests: XCTestCase {
     do {
       let base = "hello, world!"
       let i = base.index(base.startIndex, offsetBy: 3)
-      let result = base.lazy.replacingSubrange(base.startIndex..<i, with: "goodbye".reversed())
+      let result = base.overlay.replacingSubrange(base.startIndex..<i, with: "goodbye".reversed())
       XCTAssertEqualCollections(result, "eybdooglo, world!")
       IndexValidator().validate(result, expectedCount: 17)
     }
@@ -130,7 +134,7 @@ final class ReplaceSubrangeTests: XCTestCase {
     do {
       let base = "hello, world!"
       let i = base.index(base.startIndex, offsetBy: 3)
-      let result = base.lazy.replacingSubrange(base.startIndex..<i, with: EmptyCollection())
+      let result = base.overlay.replacingSubrange(base.startIndex..<i, with: EmptyCollection())
       XCTAssertEqualCollections(result, "lo, world!")
       IndexValidator().validate(result, expectedCount: 10)
     }
@@ -141,7 +145,7 @@ final class ReplaceSubrangeTests: XCTestCase {
       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())
+      let result = base.overlay.replacingSubrange(start..<end, with: "goodbye".reversed())
       XCTAssertEqualCollections(result, "heleybdoogworld!")
       IndexValidator().validate(result, expectedCount: 16)
     }
@@ -152,7 +156,7 @@ final class ReplaceSubrangeTests: XCTestCase {
       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())
+      let result = base.overlay.replacingSubrange(start..<end, with: EmptyCollection())
       XCTAssertEqualCollections(result, "helworld!")
       IndexValidator().validate(result, expectedCount: 9)
     }
@@ -162,7 +166,7 @@ final class ReplaceSubrangeTests: XCTestCase {
     do {
       let base = "hello, world!"
       let start = base.index(base.endIndex, offsetBy: -4)
-      let result = base.lazy.replacingSubrange(start..<base.endIndex, with: "goodbye".reversed())
+      let result = base.overlay.replacingSubrange(start..<base.endIndex, with: "goodbye".reversed())
       XCTAssertEqualCollections(result, "hello, woeybdoog")
       IndexValidator().validate(result, expectedCount: 16)
     }
@@ -172,7 +176,7 @@ final class ReplaceSubrangeTests: XCTestCase {
     do {
       let base = "hello, world!"
       let start = base.index(base.endIndex, offsetBy: -4)
-      let result = base.lazy.replacingSubrange(start..<base.endIndex, with: EmptyCollection())
+      let result = base.overlay.replacingSubrange(start..<base.endIndex, with: EmptyCollection())
       XCTAssertEqualCollections(result, "hello, wo")
       IndexValidator().validate(result, expectedCount: 9)
     }
@@ -181,7 +185,7 @@ final class ReplaceSubrangeTests: XCTestCase {
     // Replacement: non-empty
     do {
       let base = "hello, world!"
-      let result = base.lazy.replacingSubrange(base.startIndex..<base.endIndex, with: Array("blah blah blah"))
+      let result = base.overlay.replacingSubrange(base.startIndex..<base.endIndex, with: Array("blah blah blah"))
       XCTAssertEqualCollections(result, "blah blah blah")
       IndexValidator().validate(result, expectedCount: 14)
     }
@@ -190,7 +194,7 @@ final class ReplaceSubrangeTests: XCTestCase {
     // Replacement: empty
     do {
       let base = "hello, world!"
-      let result = base.lazy.replacingSubrange(base.startIndex..<base.endIndex, with: EmptyCollection())
+      let result = base.overlay.replacingSubrange(base.startIndex..<base.endIndex, with: EmptyCollection())
       XCTAssertEqualCollections(result, "")
       IndexValidator().validate(result, expectedCount: 0)
     }

From 2f54c4f7a11d09cd25c04e842851ac945b1efa54 Mon Sep 17 00:00:00 2001
From: Karl Wagner <5254025+karwa@users.noreply.github.com>
Date: Mon, 18 Sep 2023 03:29:30 +0200
Subject: [PATCH 4/6] [ReplaceSubrange] Conditional replacement

---
 Sources/Algorithms/ReplaceSubrange.swift      | 85 +++++++++++++++----
 .../ReplaceSubrangeTests.swift                | 19 +++++
 2 files changed, 87 insertions(+), 17 deletions(-)

diff --git a/Sources/Algorithms/ReplaceSubrange.swift b/Sources/Algorithms/ReplaceSubrange.swift
index 058090ce..2825ae9b 100644
--- a/Sources/Algorithms/ReplaceSubrange.swift
+++ b/Sources/Algorithms/ReplaceSubrange.swift
@@ -9,8 +9,9 @@
 //
 //===----------------------------------------------------------------------===//
 
-/// A namespace for methods which overlay a collection of elements
-/// over a region of a base collection.
+/// 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:
 ///
@@ -24,8 +25,7 @@
 ///
 public struct OverlayCollectionNamespace<Elements: Collection> {
 
-  @usableFromInline
-  internal var elements: Elements
+  public let elements: Elements
 
   @inlinable
   internal init(elements: Elements) {
@@ -35,13 +35,44 @@ public struct OverlayCollectionNamespace<Elements: Collection> {
 
 extension Collection {
 
-  /// A namespace for methods which overlay another collection of elements
-  /// over a region of this 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 {
@@ -96,6 +127,20 @@ extension OverlayCollectionNamespace {
   }
 }
 
+/// 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 {
 
@@ -103,13 +148,13 @@ where Base: Collection, Overlay: Collection, Base.Element == Overlay.Element {
   internal var base: Base
 
   @usableFromInline
-  internal var overlay: Overlay
+  internal var overlay: Optional<Overlay>
 
   @usableFromInline
   internal var replacedRange: Range<Base.Index>
 
   @inlinable
-  internal init(base: Base, overlay: Overlay, replacedRange: Range<Base.Index>) {
+  internal init(base: Base, overlay: Overlay?, replacedRange: Range<Base.Index>) {
     self.base = base
     self.overlay = overlay
     self.replacedRange = replacedRange
@@ -187,7 +232,7 @@ extension OverlayCollection {
 
   @inlinable
   public var startIndex: Index {
-    if base.startIndex == replacedRange.lowerBound {
+    if let overlay = overlay, base.startIndex == replacedRange.lowerBound {
       if overlay.isEmpty {
         return makeIndex(replacedRange.upperBound)
       }
@@ -198,6 +243,9 @@ extension OverlayCollection {
 
   @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)
     }
@@ -206,7 +254,10 @@ extension OverlayCollection {
 
   @inlinable
   public var count: Int {
-    base.distance(from: base.startIndex, to: replacedRange.lowerBound)
+    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)
   }
@@ -216,7 +267,7 @@ extension OverlayCollection {
     switch i.wrapped {
     case .base(var baseIndex):
       base.formIndex(after: &baseIndex)
-      if baseIndex == replacedRange.lowerBound {
+      if let overlay = overlay, baseIndex == replacedRange.lowerBound {
         if overlay.isEmpty {
           return makeIndex(replacedRange.upperBound)
         }
@@ -225,8 +276,8 @@ extension OverlayCollection {
       return makeIndex(baseIndex)
 
     case .overlay(var overlayIndex):
-      overlay.formIndex(after: &overlayIndex)
-      if replacedRange.lowerBound != base.endIndex, overlayIndex == overlay.endIndex {
+      overlay!.formIndex(after: &overlayIndex)
+      if replacedRange.lowerBound != base.endIndex, overlayIndex == overlay!.endIndex {
         return makeIndex(replacedRange.upperBound)
       }
       return makeIndex(overlayIndex)
@@ -239,7 +290,7 @@ extension OverlayCollection {
     case .base(let baseIndex): 
       return base[baseIndex]
     case .overlay(let overlayIndex):
-      return overlay[overlayIndex]
+      return overlay![overlayIndex]
     }
   }
 }
@@ -251,7 +302,7 @@ where Base: BidirectionalCollection, Overlay: BidirectionalCollection {
   public func index(before i: Index) -> Index {
     switch i.wrapped {
     case .base(var baseIndex):
-      if baseIndex == replacedRange.upperBound {
+      if let overlay = overlay, baseIndex == replacedRange.upperBound {
         if overlay.isEmpty {
           return makeIndex(base.index(before: replacedRange.lowerBound))
         }
@@ -261,10 +312,10 @@ where Base: BidirectionalCollection, Overlay: BidirectionalCollection {
       return makeIndex(baseIndex)
 
     case .overlay(var overlayIndex):
-      if overlayIndex == overlay.startIndex {
+      if overlayIndex == overlay!.startIndex {
         return makeIndex(base.index(before: replacedRange.lowerBound))
       }
-      overlay.formIndex(before: &overlayIndex)
+      overlay!.formIndex(before: &overlayIndex)
       return makeIndex(overlayIndex)
     }
   }
diff --git a/Tests/SwiftAlgorithmsTests/ReplaceSubrangeTests.swift b/Tests/SwiftAlgorithmsTests/ReplaceSubrangeTests.swift
index 998b3e69..a1abe9d5 100644
--- a/Tests/SwiftAlgorithmsTests/ReplaceSubrangeTests.swift
+++ b/Tests/SwiftAlgorithmsTests/ReplaceSubrangeTests.swift
@@ -199,4 +199,23 @@ final class ReplaceSubrangeTests: XCTestCase {
       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)
+    }
+  }
 }

From 5a192f24cc3a0ebd68fdbe29e508b2b87ef78004 Mon Sep 17 00:00:00 2001
From: Karl Wagner <5254025+karwa@users.noreply.github.com>
Date: Mon, 18 Sep 2023 18:37:53 +0200
Subject: [PATCH 5/6] Add guide for Overlay, rename files to
 Overlay/OverlayTests

---
 Guides/Overlay.md                             | 170 ++++++++++++++++++
 .../{ReplaceSubrange.swift => Overlay.swift}  |   0
 ...SubrangeTests.swift => OverlayTests.swift} |   0
 3 files changed, 170 insertions(+)
 create mode 100644 Guides/Overlay.md
 rename Sources/Algorithms/{ReplaceSubrange.swift => Overlay.swift} (100%)
 rename Tests/SwiftAlgorithmsTests/{ReplaceSubrangeTests.swift => OverlayTests.swift} (100%)

diff --git a/Guides/Overlay.md b/Guides/Overlay.md
new file mode 100644
index 00000000..a2934654
--- /dev/null
+++ b/Guides/Overlay.md
@@ -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
+}
+``` 
diff --git a/Sources/Algorithms/ReplaceSubrange.swift b/Sources/Algorithms/Overlay.swift
similarity index 100%
rename from Sources/Algorithms/ReplaceSubrange.swift
rename to Sources/Algorithms/Overlay.swift
diff --git a/Tests/SwiftAlgorithmsTests/ReplaceSubrangeTests.swift b/Tests/SwiftAlgorithmsTests/OverlayTests.swift
similarity index 100%
rename from Tests/SwiftAlgorithmsTests/ReplaceSubrangeTests.swift
rename to Tests/SwiftAlgorithmsTests/OverlayTests.swift

From 598946f0f7b47c0fadfead8b130a17e1cd490f01 Mon Sep 17 00:00:00 2001
From: Karl Wagner <5254025+karwa@users.noreply.github.com>
Date: Thu, 26 Oct 2023 12:12:15 +0200
Subject: [PATCH 6/6] - Fixed OverlayCollectionNamespace.removing(at:) - Shrunk
 OverlayCollection.Index - Implemented Collection.isEmpty - Added tests for
 single-element append/insert/removal methods - Added separate removeSubrange
 tests

---
 Sources/Algorithms/Overlay.swift              |  27 ++--
 Tests/SwiftAlgorithmsTests/OverlayTests.swift | 130 +++++++++++++++++-
 2 files changed, 143 insertions(+), 14 deletions(-)

diff --git a/Sources/Algorithms/Overlay.swift b/Sources/Algorithms/Overlay.swift
index 2825ae9b..c42be0a7 100644
--- a/Sources/Algorithms/Overlay.swift
+++ b/Sources/Algorithms/Overlay.swift
@@ -123,7 +123,7 @@ extension OverlayCollectionNamespace {
   public func removing(
     at position: Elements.Index
   ) -> OverlayCollection<Elements, EmptyCollection<Elements.Element>> {
-    removingSubrange(position..<position)
+    removingSubrange(position..<elements.index(after: position))
   }
 }
 
@@ -178,15 +178,15 @@ extension OverlayCollection: Collection {
     @usableFromInline
     internal var wrapped: Wrapped
 
-    /// The base indices which have been replaced.
+    /// The base index at which the overlay starts -- i.e. `replacedRange.lowerBound`
     ///
     @usableFromInline
-    internal var replacedRange: Range<Base.Index>
+    internal var startOfReplacedRange: Base.Index
 
     @inlinable
-    internal init(wrapped: Wrapped, replacedRange: Range<Base.Index>) {
+    internal init(wrapped: Wrapped, startOfReplacedRange: Base.Index) {
       self.wrapped = wrapped
-      self.replacedRange = replacedRange
+      self.startOfReplacedRange = startOfReplacedRange
     }
 
     @inlinable
@@ -197,15 +197,15 @@ extension OverlayCollection: Collection {
       case (.overlay(let unwrappedLeft), .overlay(let unwrappedRight)):
         return unwrappedLeft < unwrappedRight
       case (.base(let unwrappedLeft), .overlay(_)):
-        return unwrappedLeft < lhs.replacedRange.lowerBound
+        return unwrappedLeft < lhs.startOfReplacedRange
       case (.overlay(_), .base(let unwrappedRight)):
-        return !(unwrappedRight < lhs.replacedRange.lowerBound)
+        return !(unwrappedRight < lhs.startOfReplacedRange)
       }
     }
 
     @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.
+      // 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
@@ -222,12 +222,12 @@ extension OverlayCollection {
 
   @inlinable
   internal func makeIndex(_ position: Base.Index) -> Index {
-    Index(wrapped: .base(position), replacedRange: replacedRange)
+    Index(wrapped: .base(position), startOfReplacedRange: replacedRange.lowerBound)
   }
 
   @inlinable
   internal func makeIndex(_ position: Overlay.Index) -> Index {
-    Index(wrapped: .overlay(position), replacedRange: replacedRange)
+    Index(wrapped: .overlay(position), startOfReplacedRange: replacedRange.lowerBound)
   }
 
   @inlinable
@@ -262,6 +262,13 @@ extension OverlayCollection {
     + 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 {
diff --git a/Tests/SwiftAlgorithmsTests/OverlayTests.swift b/Tests/SwiftAlgorithmsTests/OverlayTests.swift
index a1abe9d5..19c20d17 100644
--- a/Tests/SwiftAlgorithmsTests/OverlayTests.swift
+++ b/Tests/SwiftAlgorithmsTests/OverlayTests.swift
@@ -56,6 +56,25 @@ final class ReplaceSubrangeTests: XCTestCase {
     }
   }
 
+  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>(
@@ -96,6 +115,25 @@ final class ReplaceSubrangeTests: XCTestCase {
     }
   }
 
+  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
@@ -117,9 +155,17 @@ final class ReplaceSubrangeTests: XCTestCase {
     }
   }
 
+  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: start
+    // Location: anchored to start
     // Replacement: non-empty
     do {
       let base = "hello, world!"
@@ -129,7 +175,7 @@ final class ReplaceSubrangeTests: XCTestCase {
       IndexValidator().validate(result, expectedCount: 17)
     }
 
-    // Location: start
+    // Location: anchored to start
     // Replacement: empty
     do {
       let base = "hello, world!"
@@ -161,7 +207,7 @@ final class ReplaceSubrangeTests: XCTestCase {
       IndexValidator().validate(result, expectedCount: 9)
     }
 
-    // Location: end
+    // Location: anchored to end
     // Replacement: non-empty
     do {
       let base = "hello, world!"
@@ -171,7 +217,7 @@ final class ReplaceSubrangeTests: XCTestCase {
       IndexValidator().validate(result, expectedCount: 16)
     }
 
-    // Location: end
+    // Location: anchored to end
     // Replacement: empty
     do {
       let base = "hello, world!"
@@ -200,6 +246,82 @@ final class ReplaceSubrangeTests: XCTestCase {
     }
   }
 
+  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>> {