diff --git a/Sources/PenguinStructures/Concatenation.swift b/Sources/PenguinStructures/Concatenation.swift new file mode 100644 index 00000000..41842ba6 --- /dev/null +++ b/Sources/PenguinStructures/Concatenation.swift @@ -0,0 +1,203 @@ +// Copyright 2020 Penguin Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + + +/// A collection that is all the elements of one collection followed by all the elements of a second +/// collection. +public struct Concatenation: Collection +where First.Element == Second.Element { + /// The elements in `self`. + public typealias Element = First.Element + /// The collection whose elements appear first. + public var first: First + /// The collection whose elements appear second. + public var second: Second + + /// Concatenates `first` with `second`. + public init(_ first: First, _ second: Second) { + self.first = first + self.second = second + } + + /// A position in a `Concatenation`. + public struct Index: Comparable { + /// A position into one of the two underlying collections. + @usableFromInline + var position: Either + + /// Creates a new index into the first underlying collection. + @usableFromInline + internal init(first i: First.Index) { + self.position = .a(i) + } + + /// Creates a new index into the first underlying collection. + @usableFromInline + internal init(second i: Second.Index) { + self.position = .b(i) + } + + /// Returns `true` iff `lhs` precedes `rhs`. + public static func < (lhs: Self, rhs: Self) -> Bool { + return lhs.position < rhs.position + } + } + + /// The position of the first element, or `endIndex` if `self.isEmpty` + @inlinable + public var startIndex: Index { + if !first.isEmpty { return Index(first: first.startIndex) } + return Index(second: second.startIndex) + } + + /// The collection’s “past the last” position—that is, the position one greater than the last + /// valid subscript argument. + @inlinable + public var endIndex: Index { Index(second: second.endIndex) } + + /// Returns the next index after `i`. + public func index(after i: Index) -> Index { + switch i.position { + case .a(let index): + let newIndex = first.index(after: index) + guard newIndex != first.endIndex else { return Index(second: second.startIndex) } + return Index(first: newIndex) + case .b(let index): + return Index(second: second.index(after: index)) + } + } + + /// Accesses the element at `i`. + @inlinable + public subscript(i: Index) -> Element { + switch i.position { + case .a(let index): return first[index] + case .b(let index): return second[index] + } + } + + /// The number of elements in `self`. + @inlinable + public var count: Int { first.count + second.count } + + /// True iff `self` contains no elements. + @inlinable + public var isEmpty: Bool { first.isEmpty && second.isEmpty } + + /// Returns the distance between two indices. + @inlinable + public func distance(from start: Index, to end: Index) -> Int { + switch (start.position, end.position) { + case (.a(let start), .a(let end)): + return first.distance(from: start, to: end) + case (.a(let start), .b(let end)): + return first.distance(from: start, to: first.endIndex) + second.distance(from: second.startIndex, to: end) + case (.b(let start), .a(let end)): + return second.distance(from: start, to: second.startIndex) + first.distance(from: first.endIndex, to: end) + case (.b(let start), .b(let end)): + return second.distance(from: start, to: end) + } + } +} + +extension Concatenation: BidirectionalCollection +where First: BidirectionalCollection, Second: BidirectionalCollection { + /// Returns the next position before `i`. + @inlinable + public func index(before i: Index) -> Index { + switch i.position { + case .a(let index): return Index(first: first.index(before: index)) + case .b(let index): + if index == second.startIndex { + return Index(first: first.index(before: first.endIndex)) + } + return Index(second: second.index(before: index)) + } + } +} + +extension Concatenation: RandomAccessCollection + where First: RandomAccessCollection, Second: RandomAccessCollection +{ + @inlinable + public func index(_ i: Index, offsetBy n: Int) -> Index { + if n == 0 { return i } + if n < 0 { return offsetBackward(i, by: n) } + return offsetForward(i, by: n) + } + + @usableFromInline + func offsetForward(_ i: Index, by n: Int) -> Index { + switch i.position { + case .a(let index): + let d = first.distance(from: index, to: first.endIndex) + if n < d { + return Index(first: first.index(index, offsetBy: n)) + } else { + return Index(second: second.index(second.startIndex, offsetBy: n - d)) + } + case .b(let index): + return Index(second: second.index(index, offsetBy: n)) + } + } + + @usableFromInline + func offsetBackward(_ i: Index, by n: Int) -> Index { + switch i.position { + case .a(let index): + return Index(first: first.index(index, offsetBy: n)) + case .b(let index): + let d = second.distance(from: second.startIndex, to: index) + if -n <= d { + return Index(second: second.index(index, offsetBy: n)) + } else { + return Index(first: first.index(first.endIndex, offsetBy: n + d)) + } + } + } +} + +extension Concatenation: MutableCollection + where First: MutableCollection, Second: MutableCollection +{ + /// Accesses the element at `i`. + @inlinable + public subscript(i: Index) -> Element { + get { + switch i.position { + case .a(let index): return first[index] + case .b(let index): return second[index] + } + } + set { + switch i.position { + case .a(let index): first[index] = newValue + case .b(let index): second[index] = newValue + } + } + } +} + + +extension Collection { + /// Returns a collection containing the elements of `self` followed by the elements of `other`. + /// + /// - Complexity: O(1) + @inlinable + public func concatenated(to other: Other) -> Concatenation + where Other.Element == Element + { + return Concatenation(self, other) + } +} diff --git a/Tests/PenguinStructuresTests/ConcatenationTests.swift b/Tests/PenguinStructuresTests/ConcatenationTests.swift new file mode 100644 index 00000000..9f560ec9 --- /dev/null +++ b/Tests/PenguinStructuresTests/ConcatenationTests.swift @@ -0,0 +1,108 @@ +// Copyright 2020 Penguin Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import PenguinStructures +import XCTest + +final class ConcatenationTests: XCTestCase { + + func testInit() { + XCTAssert(Concatenation(0..<10, 10..<20).elementsEqual(0..<20)) + } + + func testConcatenated() { + XCTAssert((0..<10).concatenated(to: 10..<20).elementsEqual(0..<20)) + } + + func testConditionalConformances() { + let a3 = 0..<10, b3 = 10..<20, concatenated = 0..<20 + let a2 = AnyBidirectionalCollection(a3), b2 = AnyBidirectionalCollection(b3) + let a1 = AnyCollection(a3), b1 = AnyCollection(b3) + let j11 = a1.concatenated(to: b1) + XCTAssertFalse(j11.isBidirectional) + j11.checkCollectionSemantics(expectedValues: concatenated) + + let j12 = a1.concatenated(to: b2) + XCTAssertFalse(j12.isBidirectional) + j12.checkCollectionSemantics(expectedValues: concatenated) + + let j13 = a1.concatenated(to: b3) + XCTAssertFalse(j13.isBidirectional) + j13.checkCollectionSemantics(expectedValues: concatenated) + + let j21 = a2.concatenated(to: b1) + XCTAssertFalse(j21.isBidirectional) + j21.checkCollectionSemantics(expectedValues: concatenated) + + let j22 = a2.concatenated(to: b2) + XCTAssert(j22.isBidirectional) + XCTAssertFalse(j22.isRandomAccess) + j22.checkBidirectionalCollectionSemantics(expectedValues: concatenated) + + let j23 = a2.concatenated(to: b3) + XCTAssert(j23.isBidirectional) + XCTAssertFalse(j23.isRandomAccess) + j23.checkBidirectionalCollectionSemantics(expectedValues: concatenated) + + let j31 = a3.concatenated(to: b1) + XCTAssertFalse(j31.isBidirectional) + j31.checkCollectionSemantics(expectedValues: concatenated) + + let j32 = a3.concatenated(to: b2) + XCTAssert(j32.isBidirectional) + XCTAssertFalse(j32.isRandomAccess) + j32.checkBidirectionalCollectionSemantics(expectedValues: concatenated) + + let j33 = a3.concatenated(to: b3) + XCTAssert(j33.isRandomAccess) + j33.checkRandomAccessCollectionSemantics(expectedValues: concatenated) + } + + func testConcatenateSetToArray() { + let s = Set(["1", "2", "3"]) + let c = s.concatenated(to: ["10", "11", "12"]) + c.checkCollectionSemantics(expectedValues: Array(s) + ["10", "11", "12"]) + } + + func testConcatenateRanges() { + let c = (0..<3).concatenated(to: 3...6) + c.checkRandomAccessCollectionSemantics(expectedValues: 0...6) + } + + func testConcatenateEmptyPrefix() { + let c = (0..<0).concatenated(to: [1, 2, 3]) + c.checkRandomAccessCollectionSemantics(expectedValues: 1...3) + } + + func testConcatenateEmptySuffix() { + let c = (1...3).concatenated(to: 1000..<1000) + c.checkRandomAccessCollectionSemantics(expectedValues: [1, 2, 3]) + } + + func testMutableCollection() { + let a = Array(0..<10), b = Array(10..<20) + var j = a.concatenated(to: b) + j.checkMutableCollectionSemantics(source: 20..<40) + } + + static var allTests = [ + ("testInit", testInit), + ("testConditionalConformances", testConditionalConformances), + ("testConcatenateSetToArray", testConcatenateSetToArray), + ("testConcatenateRanges", testConcatenateRanges), + ("testConcatenateEmptyPrefix", testConcatenateEmptyPrefix), + ("testConcatenateEmptySuffix", testConcatenateEmptySuffix), + ("testMutableCollection", testMutableCollection), + ] +} diff --git a/Tests/PenguinStructuresTests/XCTestManifests.swift b/Tests/PenguinStructuresTests/XCTestManifests.swift index 0523eddd..dc738b64 100644 --- a/Tests/PenguinStructuresTests/XCTestManifests.swift +++ b/Tests/PenguinStructuresTests/XCTestManifests.swift @@ -23,6 +23,7 @@ import XCTest testCase(ArrayStorageExtensionTests.allTests), testCase(ArrayStorageTests.allTests), testCase(CollectionAlgorithmTests.allTests), + testCase(ConcatenationTests.allTests), testCase(DequeTests.allTests), testCase(DoubleEndedBufferTests.allTests), testCase(EitherCollectionTests.allTests),