Skip to content

Commit

Permalink
Add a non-mutating lazy replaceSubrange
Browse files Browse the repository at this point in the history
  • Loading branch information
karwa committed Sep 9, 2023
1 parent fc8fdfd commit 799bc7c
Show file tree
Hide file tree
Showing 2 changed files with 351 additions and 0 deletions.
171 changes: 171 additions & 0 deletions Sources/Algorithms/ReplaceSubrange.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
//===----------------------------------------------------------------------===//
//
// 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 replacedRange.lowerBound > base.startIndex {
return makeIndex(base.startIndex)
}
if replacements.isEmpty {
return makeIndex(replacedRange.upperBound)
}
return makeIndex(replacements.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 !replacedRange.isEmpty, replacements.isEmpty {
return makeIndex(replacedRange.upperBound)
}
if !replacements.isEmpty {
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]
}
}
}

180 changes: 180 additions & 0 deletions Tests/SwiftAlgorithmsTests/ReplaceSubrangeTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
//===----------------------------------------------------------------------===//
//
// 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)
}
}
}

0 comments on commit 799bc7c

Please sign in to comment.