Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
45a1204
Begin adding a union-edges composite graph, but being forced into def…
saeta Jun 15, 2020
1c3882a
Augment `addEdges` to take a callback and add tests.
saeta Jun 15, 2020
055d23f
Add either tests.
saeta Jun 15, 2020
d7580c8
Implement `UnionEdgeGraph` which supports combining the edges of two …
saeta Jun 16, 2020
78a0787
Cleanups.
saeta Jun 16, 2020
303fcc6
WIP: Respond to comments
saeta Jun 19, 2020
3489977
Update Sources/PenguinStructures/ConcatenatedCollection.swift
saeta Jun 20, 2020
bbe3d2e
Apply suggestions from code review
saeta Jun 20, 2020
1240d2b
Make mapping extensible, remove UnionEdges graph combinator.
saeta Jun 20, 2020
379ede0
Merge branch 'union-edges' of github.com:saeta/penguin into union-edges
saeta Jun 20, 2020
cc98e87
Get everything compiling again.
saeta Jun 20, 2020
6ecb1dc
Merge remote-tracking branch 'origin/master' into union-edges
saeta Jun 21, 2020
da60e78
[WIP]: Intermediate commit to switch to Linux for easier debugging.
saeta Jul 4, 2020
21f5791
Finish `Concatenation` implementation to properly adhere to relevant …
saeta Jul 4, 2020
5b8e168
Merge branch 'master' into concatenation
Aug 11, 2020
973ae4b
Remove everything but Either/Concatenated.
Aug 11, 2020
4160e46
Make Either ready for prime time
Aug 11, 2020
97968dd
Eliminate concatenated collection (next PR).
Aug 11, 2020
60b675d
Bring back Concatenation<>
Aug 11, 2020
91f8d3c
Rename ConcatenatedCollectionTests => ConcatenationTests
Aug 11, 2020
40ba528
Concatenation cleanups and test completion
Aug 12, 2020
ee7f907
Add MutableCollection conformance.
Aug 12, 2020
44516a2
Merge remote-tracking branch 'origin/master' into concatenation
Aug 12, 2020
8a6e946
Remove obsolete TODO
Aug 12, 2020
4a2377a
Rename joined(to:) => concatenated(to:)
Aug 12, 2020
8d61667
Merge remote-tracking branch 'origin/concatenation' into concatenation
Aug 12, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
203 changes: 203 additions & 0 deletions Sources/PenguinStructures/Concatenation.swift
Original file line number Diff line number Diff line change
@@ -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<First: Collection, Second: Collection>: 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<First.Index, Second.Index>

/// 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<Other: Collection>(to other: Other) -> Concatenation<Self, Other>
where Other.Element == Element
{
return Concatenation(self, other)
}
}
108 changes: 108 additions & 0 deletions Tests/PenguinStructuresTests/ConcatenationTests.swift
Original file line number Diff line number Diff line change
@@ -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),
]
}
1 change: 1 addition & 0 deletions Tests/PenguinStructuresTests/XCTestManifests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down