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>> {