From 4b779b8b3b3a1e36de40c3fb74e0d3292e0386ea Mon Sep 17 00:00:00 2001 From: Tim Vermeulen Date: Tue, 1 Jun 2021 17:14:34 +0200 Subject: [PATCH] Add `joined(by:)` (#138) --- Guides/Joined.md | 102 ++++ Sources/Algorithms/EitherSequence.swift | 200 +++++++ Sources/Algorithms/FlattenCollection.swift | 306 +++++++++++ Sources/Algorithms/Intersperse.swift | 455 +++++++++++++++- Sources/Algorithms/Joined.swift | 495 ++++++++++++++++++ .../IntersperseTests.swift | 29 +- Tests/SwiftAlgorithmsTests/JoinedTests.swift | 125 +++++ 7 files changed, 1692 insertions(+), 20 deletions(-) create mode 100644 Guides/Joined.md create mode 100644 Sources/Algorithms/EitherSequence.swift create mode 100644 Sources/Algorithms/FlattenCollection.swift create mode 100644 Sources/Algorithms/Joined.swift create mode 100644 Tests/SwiftAlgorithmsTests/JoinedTests.swift diff --git a/Guides/Joined.md b/Guides/Joined.md new file mode 100644 index 00000000..9b7a629d --- /dev/null +++ b/Guides/Joined.md @@ -0,0 +1,102 @@ +# Joined + +[[Source](https://github.com/apple/swift-algorithms/blob/main/Sources/Algorithms/Joined.swift) | + [Tests](https://github.com/apple/swift-algorithms/blob/main/Tests/SwiftAlgorithmsTests/JoinedTests.swift)] + +Concatenate a sequence of sequences, inserting a separator between each element. + +The separator can be either a single element or a sequence of elements, and it +can optionally depend on the sequences right before and after it by returning it +from a closure: + +```swift +for number in [[1], [2, 3], [4, 5, 6]].joined(by: 100) { + print(number) +} +// 1, 100, 2, 3, 100, 4, 5, 6 + +for number in [[10], [20, 30], [40, 50, 60]].joined(by: { [$0.count, $1.count] }) { + print(number) +} +// 10, 1, 2, 20, 30, 2, 3, 40, 50, 60 +``` + +## Detailed Design + +The versions that take a closure are executed eagerly and are defined on +`Sequence`: + +```swift +extension Sequence where Element: Sequence { + public func joined( + by separator: (Element, Element) throws -> Element.Element + ) rethrows -> [Element.Element] + + public func joined( + by separator: (Element, Element) throws -> Separator + ) rethrows -> [Element.Element] + where Separator: Sequence, Separator.Element == Element.Element +} +``` + +The versions that do not take a closure are defined on both `Sequence` and +`Collection` because the resulting collections need to precompute their start +index to ensure O(1) access: + +```swift +extension Sequence where Element: Sequence { + public func joined(by separator: Element.Element) + -> JoinedBySequence> + + public func joined( + by separator: Separator + ) -> JoinedBySequence + where Separator: Collection, Separator.Element == Element.Element +} + +extension Collection where Element: Sequence { + public func joined(by separator: Element.Element) + -> JoinedByCollection> + + public func joined( + by separator: Separator + ) -> JoinedByCollection + where Separator: Collection, Separator.Element == Element.Element +} +``` + +Note that the sequence separator of the closure-less version defined on +`Sequence` is required to be a `Collection`, because a plain `Sequence` cannot in +general be iterated over multiple times. + +The closure-based versions also have lazy variants that are defined on both +`LazySequenceProtocol` and `LazyCollectionProtocol` for the same reason as +explained above: + +```swift +extension LazySequenceProtocol where Element: Sequence { + public func joined( + by separator: @escaping (Element, Element) -> Element.Element + ) -> JoinedByClosureSequence> + + public func joined( + by separator: @escaping (Element, Element) -> Separator + ) -> JoinedByClosureSequence +} + +extension LazyCollectionProtocol where Element: Collection { + public func joined( + by separator: @escaping (Element, Element) -> Element.Element + ) -> JoinedByClosureCollection> + + public func joined( + by separator: @escaping (Element, Element) -> Separator + ) -> JoinedByClosureCollection +} +``` + +`JoinedBySequence`, `JoinedByClosureSequence`, `JoinedByCollection`, and +`JoinedByClosureCollection` conform to `LazySequenceProtocol` when the base +sequence conforms. `JoinedByCollection` and `JoinedByClosureCollection` also +conform to `LazyCollectionProtocol` and `BidirectionalCollection` when the base +collection conforms. diff --git a/Sources/Algorithms/EitherSequence.swift b/Sources/Algorithms/EitherSequence.swift new file mode 100644 index 00000000..d28c4a10 --- /dev/null +++ b/Sources/Algorithms/EitherSequence.swift @@ -0,0 +1,200 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Algorithms open source project +// +// Copyright (c) 2021 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 +// +//===----------------------------------------------------------------------===// + +//===----------------------------------------------------------------------===// +// Either +//===----------------------------------------------------------------------===// + +/// A general-purpose sum type. +@usableFromInline +internal enum Either { + case left(Left) + case right(Right) +} + +extension Either: Equatable where Left: Equatable, Right: Equatable { + @usableFromInline + internal static func == (lhs: Self, rhs: Self) -> Bool { + switch (lhs, rhs) { + case let (.left(lhs), .left(rhs)): + return lhs == rhs + case let (.right(lhs), .right(rhs)): + return lhs == rhs + case (.left, .right), (.right, .left): + return false + } + } +} + +extension Either: Comparable where Left: Comparable, Right: Comparable { + @usableFromInline + internal static func < (lhs: Self, rhs: Self) -> Bool { + switch (lhs, rhs) { + case let (.left(lhs), .left(rhs)): + return lhs < rhs + case let (.right(lhs), .right(rhs)): + return lhs < rhs + case (.left, .right): + return true + case (.right, .left): + return false + } + } +} + +//===----------------------------------------------------------------------===// +// EitherSequence +//===----------------------------------------------------------------------===// + +/// A sequence that has one of the two specified types. +@usableFromInline +internal enum EitherSequence + where Left.Element == Right.Element +{ + case left(Left) + case right(Right) +} + +extension EitherSequence: Sequence { + @usableFromInline + internal struct Iterator: IteratorProtocol { + @usableFromInline + internal var left: Left.Iterator? + + @usableFromInline + internal var right: Right.Iterator? + + @inlinable + internal mutating func next() -> Left.Element? { + left?.next() ?? right?.next() + } + } + + @usableFromInline + internal func makeIterator() -> Iterator { + switch self { + case .left(let left): + return Iterator(left: left.makeIterator(), right: nil) + case .right(let right): + return Iterator(left: nil, right: right.makeIterator()) + } + } +} + +extension EitherSequence: Collection + where Left: Collection, Right: Collection, Left.Element == Right.Element +{ + @usableFromInline + internal typealias Index = Either + + @inlinable + internal var startIndex: Index { + switch self { + case .left(let s): + return .left(s.startIndex) + case .right(let s): + return .right(s.startIndex) + } + } + + @inlinable + internal var endIndex: Index { + switch self { + case .left(let s): + return .left(s.endIndex) + case .right(let s): + return .right(s.endIndex) + } + } + + @inlinable + internal subscript(position: Index) -> Element { + switch (self, position) { + case let (.left(s), .left(i)): + return s[i] + case let (.right(s), .right(i)): + return s[i] + default: + fatalError() + } + } + + @inlinable + internal func index(after i: Index) -> Index { + switch (self,i) { + case let (.left(s), .left(i)): + return .left(s.index(after: i)) + case let (.right(s), .right(i)): + return .right(s.index(after: i)) + default: + fatalError() + } + } + + @inlinable + internal func index( + _ i: Index, + offsetBy distance: Int, + limitedBy limit: Index + ) -> Index? { + switch (self, i, limit) { + case let (.left(s), .left(i), .left(limit)): + return s.index(i, offsetBy: distance, limitedBy: limit).map { .left($0) } + case let (.right(s), .right(i), .right(limit)): + return s.index(i, offsetBy: distance, limitedBy: limit).map { .right($0) } + default: + fatalError() + } + } + + @inlinable + internal func index(_ i: Index, offsetBy distance: Int) -> Index { + switch (self, i) { + case let (.left(s), .left(i)): + return .left(s.index(i, offsetBy: distance)) + case let (.right(s), .right(i)): + return .right(s.index(i, offsetBy: distance)) + default: + fatalError() + } + } + + @inlinable + internal func distance(from start: Index, to end: Index) -> Int { + switch (self, start, end) { + case let (.left(s), .left(i), .left(j)): + return s.distance(from: i, to: j) + case let (.right(s), .right(i), .right(j)): + return s.distance(from: i, to: j) + default: + fatalError() + } + } +} + +extension EitherSequence: BidirectionalCollection + where Left: BidirectionalCollection, Right: BidirectionalCollection +{ + @inlinable + internal func index(before i: Index) -> Index { + switch (self, i) { + case let (.left(s), .left(i)): + return .left(s.index(before: i)) + case let (.right(s), .right(i)): + return .right(s.index(before: i)) + default: + fatalError() + } + } +} + +extension EitherSequence: RandomAccessCollection + where Left: RandomAccessCollection, Right: RandomAccessCollection {} diff --git a/Sources/Algorithms/FlattenCollection.swift b/Sources/Algorithms/FlattenCollection.swift new file mode 100644 index 00000000..d4b6f29b --- /dev/null +++ b/Sources/Algorithms/FlattenCollection.swift @@ -0,0 +1,306 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Algorithms open source project +// +// Copyright (c) 2021 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 +// +//===----------------------------------------------------------------------===// + +/// A collection consisting of all the elements contained in a collection of +/// collections. +@usableFromInline +internal struct FlattenCollection where Base.Element: Collection { + @usableFromInline + internal let base: Base + + @usableFromInline + internal let indexOfFirstNonEmptyElement: Base.Index + + @inlinable + internal init(base: Base) { + self.base = base + self.indexOfFirstNonEmptyElement = base.endOfPrefix(while: { $0.isEmpty }) + } +} + +extension FlattenCollection: Collection { + @usableFromInline + internal struct Index: Comparable { + @usableFromInline + internal let outer: Base.Index + + @usableFromInline + internal let inner: Base.Element.Index? + + @inlinable + init(outer: Base.Index, inner: Base.Element.Index?) { + self.outer = outer + self.inner = inner + } + + @inlinable + internal static func < (lhs: Self, rhs: Self) -> Bool { + guard lhs.outer == rhs.outer else { return lhs.outer < rhs.outer } + return lhs.inner == nil ? false : lhs.inner! < rhs.inner! + } + } + + @inlinable + internal var startIndex: Index { + let outer = indexOfFirstNonEmptyElement + let inner = outer == base.endIndex ? nil : base[outer].startIndex + return Index(outer: outer, inner: inner) + } + + @inlinable + internal var endIndex: Index { + Index(outer: base.endIndex, inner: nil) + } + + /// Forms an index from a pair of base indices, normalizing + /// `(i, base2.endIndex)` to `(base1.index(after: i), base2.startIndex)` if + /// necessary. + @inlinable + internal func normalizeIndex( + outer: Base.Index, + inner: Base.Element.Index + ) -> Index { + if inner == base[outer].endIndex { + let outer = base[base.index(after: outer)...] + .endOfPrefix(while: { $0.isEmpty }) + let inner = outer == base.endIndex ? nil : base[outer].startIndex + return Index(outer: outer, inner: inner) + } else { + return Index(outer: outer, inner: inner) + } + } + + @inlinable + internal func index(after index: Index) -> Index { + let element = base[index.outer] + let nextInner = element.index(after: index.inner!) + return normalizeIndex(outer: index.outer, inner: nextInner) + } + + @inlinable + internal subscript(position: Index) -> Base.Element.Element { + base[position.outer][position.inner!] + } + + @inlinable + internal func distance(from start: Index, to end: Index) -> Int { + guard start.outer <= end.outer + else { return -distance(from: end, to: start) } + guard let startInner = start.inner + else { return 0 } + guard start.outer != end.outer + else { return base[start.outer].distance(from: startInner, to: end.inner!) } + + let firstPart = base[start.outer][startInner...].count + let middlePart = base[start.outer.. Index { + guard distance != 0 else { return index } + + return distance > 0 + ? offsetForward(index, by: distance) + : offsetBackward(index, by: -distance) + } + + @inlinable + internal func index( + _ index: Index, + offsetBy distance: Int, + limitedBy limit: Index + ) -> Index? { + guard distance != 0 else { return index } + + if distance > 0 { + return limit >= index + ? offsetForward(index, by: distance, limitedBy: limit) + : offsetForward(index, by: distance) + } else { + return limit <= index + ? offsetBackward(index, by: -distance, limitedBy: limit) + : offsetBackward(index, by: -distance) + } + } + + @inlinable + internal func offsetForward(_ i: Index, by distance: Int) -> Index { + guard let index = offsetForward(i, by: distance, limitedBy: endIndex) + else { fatalError("Index is out of bounds") } + return index + } + + @inlinable + internal func offsetBackward(_ i: Index, by distance: Int) -> Index { + guard let index = offsetBackward(i, by: distance, limitedBy: startIndex) + else { fatalError("Index is out of bounds") } + return index + } + + @inlinable + internal func offsetForward( + _ index: Index, by distance: Int, limitedBy limit: Index + ) -> Index? { + assert(distance > 0) + assert(limit >= index) + + if index.outer == limit.outer { + if let indexInner = index.inner, let limitInner = limit.inner { + return base[index.outer] + .index(indexInner, offsetBy: distance, limitedBy: limitInner) + .map { inner in Index(outer: index.outer, inner: inner) } + } else { + // `index` and `limit` are both `endIndex` + return nil + } + } + + // `index <= limit` and `index.outer != limit.outer`, so `index != endIndex` + let indexInner = index.inner! + let element = base[index.outer] + + if let inner = element.index( + indexInner, + offsetBy: distance, + limitedBy: element.endIndex + ) { + return normalizeIndex(outer: index.outer, inner: inner) + } + + var remainder = distance - element[indexInner...].count + var outer = base.index(after: index.outer) + + while outer != limit.outer { + let element = base[outer] + + if let inner = element.index( + element.startIndex, + offsetBy: remainder, + limitedBy: element.endIndex + ) { + return normalizeIndex(outer: outer, inner: inner) + } + + remainder -= element.count + base.formIndex(after: &outer) + } + + if let limitInner = limit.inner { + let element = base[outer] + return element.index(element.startIndex, offsetBy: remainder, limitedBy: limitInner) + .map { inner in Index(outer: outer, inner: inner) } + } else { + return nil + } + } + + @inlinable + internal func offsetBackward( + _ index: Index, by distance: Int, limitedBy limit: Index + ) -> Index? { + assert(distance > 0) + assert(limit <= index) + + if index.outer == limit.outer { + if let indexInner = index.inner, let limitInner = limit.inner { + return base[index.outer] + .index(indexInner, offsetBy: -distance, limitedBy: limitInner) + .map { inner in Index(outer: index.outer, inner: inner) } + } else { + // `index` and `limit` are both `endIndex` + return nil + } + } + + var remainder = distance + + if let indexInner = index.inner { + let element = base[index.outer] + + if let inner = element.index( + indexInner, + offsetBy: -remainder, + limitedBy: element.startIndex + ) { + return Index(outer: index.outer, inner: inner) + } + + remainder -= element[.. Index { + if let inner = index.inner { + let element = base[index.outer] + + if inner != element.startIndex { + let previousInner = element.index(before: inner) + return Index(outer: index.outer, inner: previousInner) + } + } + + let previousOuter = base[.. FlattenCollection { + FlattenCollection(base: self) + } +} diff --git a/Sources/Algorithms/Intersperse.swift b/Sources/Algorithms/Intersperse.swift index 165881bf..3fa804d0 100644 --- a/Sources/Algorithms/Intersperse.swift +++ b/Sources/Algorithms/Intersperse.swift @@ -9,6 +9,10 @@ // //===----------------------------------------------------------------------===// +//===----------------------------------------------------------------------===// +// Intersperse +//===----------------------------------------------------------------------===// + /// A sequence that presents the elements of a base sequence of elements /// with a separator between each of those elements. public struct Intersperse { @@ -81,7 +85,7 @@ extension Intersperse: Collection where Base: Collection { /// A position in an `Intersperse` collection. public struct Index: Comparable { @usableFromInline - enum Representation: Equatable { + internal enum Representation: Equatable { case element(Base.Index) case separator(next: Base.Index) } @@ -90,7 +94,7 @@ extension Intersperse: Collection where Base: Collection { internal let representation: Representation @inlinable - init(representation: Representation) { + internal init(representation: Representation) { self.representation = representation } @@ -107,12 +111,12 @@ extension Intersperse: Collection where Base: Collection { } @inlinable - static func element(_ index: Base.Index) -> Self { + internal static func element(_ index: Base.Index) -> Self { Self(representation: .element(index)) } @inlinable - static func separator(next: Base.Index) -> Self { + internal static func separator(next: Base.Index) -> Self { Self(representation: .separator(next: next)) } } @@ -146,21 +150,6 @@ extension Intersperse: Collection where Base: Collection { } } - @inlinable - public func index(_ i: Index, offsetBy distance: Int) -> Index { - switch (i.representation, distance.isMultiple(of: 2)) { - case (let .element(index), true): - return .element(base.index(index, offsetBy: distance / 2)) - case (let .element(index), false): - return .separator(next: base.index(index, offsetBy: (distance + 1) / 2)) - case (let .separator(next: index), true): - return .separator(next: base.index(index, offsetBy: distance / 2)) - case (let .separator(next: index), false): - return .element(base.index(index, offsetBy: (distance - 1) / 2)) - } - } - - // TODO: Implement index(_:offsetBy:limitedBy:) @inlinable public func distance(from start: Index, to end: Index) -> Int { switch (start.representation, end.representation) { @@ -173,6 +162,98 @@ extension Intersperse: Collection where Base: Collection { return 2 * base.distance(from: start, to: end) } } + + @inlinable + public func index(_ index: Index, offsetBy distance: Int) -> Index { + distance >= 0 + ? offsetForward(index, by: distance) + : offsetBackward(index, by: -distance) + } + + @inlinable + public func index( + _ index: Index, + offsetBy distance: Int, + limitedBy limit: Index + ) -> Index? { + if distance >= 0 { + return limit >= index + ? offsetForward(index, by: distance, limitedBy: limit) + : offsetForward(index, by: distance) + } else { + return limit <= index + ? offsetBackward(index, by: -distance, limitedBy: limit) + : offsetBackward(index, by: -distance) + } + } + + @inlinable + internal func offsetForward(_ i: Index, by distance: Int) -> Index { + guard let index = offsetForward(i, by: distance, limitedBy: endIndex) + else { fatalError("Index is out of bounds") } + return index + } + + @inlinable + internal func offsetBackward(_ i: Index, by distance: Int) -> Index { + guard let index = offsetBackward(i, by: distance, limitedBy: startIndex) + else { fatalError("Index is out of bounds") } + return index + } + + @inlinable + internal func offsetForward( + _ index: Index, by distance: Int, limitedBy limit: Index + ) -> Index? { + assert(distance >= 0) + assert(limit >= index) + + switch (index.representation, limit.representation, distance.isMultiple(of: 2)) { + case let (.element(index), .element(limit), true), + let (.separator(next: index), .element(limit), false): + return base.index(index, offsetBy: distance / 2, limitedBy: limit) + .map { .element($0) } + + case let (.element(index), .element(limit), false), + let (.element(index), .separator(next: limit), false), + let (.separator(next: index), .element(limit), true), + let (.separator(next: index), .separator(next: limit), true): + return base.index(index, offsetBy: (distance + 1) / 2, limitedBy: limit) + .map { .separator(next: $0) } + + case let (.element(index), .separator(next: limit), true), + let (.separator(next: index), .separator(next: limit), false): + return base.index(index, offsetBy: distance / 2, limitedBy: limit) + .flatMap { $0 == limit ? nil : .element($0) } + } + } + + @inlinable + internal func offsetBackward( + _ index: Index, by distance: Int, limitedBy limit: Index + ) -> Index? { + assert(distance >= 0) + assert(limit <= index) + + switch (index.representation, limit.representation, distance.isMultiple(of: 2)) { + case let (.element(index), .element(limit), true), + let (.element(index), .separator(next: limit), true), + let (.separator(next: index), .element(limit), false), + let (.separator(next: index), .separator(next: limit), false): + return base.index(index, offsetBy: -((distance + 1) / 2), limitedBy: limit) + .map { .element($0) } + + case let (.element(index), .separator(next: limit), false), + let (.separator(next: index), .separator(next: limit), true): + return base.index(index, offsetBy: -(distance / 2), limitedBy: limit) + .map { .separator(next: $0) } + + case let (.element(index), .element(limit), false), + let (.separator(next: index), .element(limit), true): + return base.index(index, offsetBy: -(distance / 2), limitedBy: limit) + .flatMap { $0 == limit ? nil : .separator(next: $0) } + } + } } extension Intersperse: BidirectionalCollection @@ -199,6 +280,315 @@ extension Intersperse: LazySequenceProtocol extension Intersperse: LazyCollectionProtocol where Base: LazyCollectionProtocol {} +//===----------------------------------------------------------------------===// +// InterspersedMap +//===----------------------------------------------------------------------===// + + +/// A sequence over the results of applying a closure to the sequence's +/// elements, with a separator that separates each pair of adjacent transformed +/// values. +@usableFromInline +internal struct InterspersedMap { + @usableFromInline + internal let base: Base + + @usableFromInline + internal let transform: (Base.Element) -> Result + + @usableFromInline + internal let separator: (Base.Element, Base.Element) -> Result +} + +extension InterspersedMap: Sequence { + @usableFromInline + internal struct Iterator: IteratorProtocol { + @usableFromInline + internal var base: Base.Iterator + + @usableFromInline + internal let transform: (Base.Element) -> Result + + @usableFromInline + internal let separator: (Base.Element, Base.Element) -> Result + + @usableFromInline + internal var state = State.start + + @inlinable + internal init( + base: Base.Iterator, + transform: @escaping (Base.Element) -> Result, + separator: @escaping (Base.Element, Base.Element) -> Result + ) { + self.base = base + self.transform = transform + self.separator = separator + } + + @usableFromInline + internal enum State { + case start + case element(Base.Element) + case separator(previous: Base.Element) + } + + @inlinable + internal mutating func next() -> Result? { + switch state { + case .start: + guard let first = base.next() else { return nil } + state = .separator(previous: first) + return transform(first) + case .separator(let previous): + guard let next = base.next() else { return nil } + state = .element(next) + return separator(previous, next) + case .element(let element): + state = .separator(previous: element) + return transform(element) + } + } + } + + @inlinable + internal func makeIterator() -> Iterator { + Iterator( + base: base.makeIterator(), + transform: transform, + separator: separator) + } +} + +extension InterspersedMap: Collection where Base: Collection { + @usableFromInline + internal struct Index: Comparable { + @usableFromInline + internal enum Representation: Equatable { + case element(Base.Index) + case separator(previous: Base.Index, next: Base.Index) + } + + @usableFromInline + internal let representation: Representation + + @inlinable + internal init(representation: Representation) { + self.representation = representation + } + + @inlinable + internal static func element(_ index: Base.Index) -> Self { + Self(representation: .element(index)) + } + + @inlinable + internal static func separator(previous: Base.Index, next: Base.Index) -> Self { + Self(representation: .separator(previous: previous, next: next)) + } + + @inlinable + internal static func == (lhs: Self, rhs: Self) -> Bool { + switch (lhs.representation, rhs.representation) { + case let (.element(lhs), .element(rhs)), + let (.separator(_, next: lhs), .separator(_, next: rhs)): + return lhs == rhs + case (.element, .separator), (.separator, .element): + return false + } + } + + @inlinable + internal static func < (lhs: Self, rhs: Self) -> Bool { + switch (lhs.representation, rhs.representation) { + case let (.element(lhs), .element(rhs)), + let (.separator(_, next: lhs), .separator(_, next: rhs)), + let (.element(lhs), .separator(_, next: rhs)), + let (.separator(previous: lhs, _), .element(rhs)): + return lhs < rhs + } + } + } + + @inlinable + internal var startIndex: Index { + base.isEmpty ? endIndex : .element(base.startIndex) + } + + @inlinable + internal var endIndex: Index { + .separator(previous: base.endIndex, next: base.endIndex) + } + + @inlinable + internal func index(after index: Index) -> Index { + switch index.representation { + case .element(let index): + let next = base.index(after: index) + return .separator(previous: index, next: next) + case .separator(_, let next): + return .element(next) + } + } + + @inlinable + internal subscript(position: Index) -> Result { + switch position.representation { + case .element(let index): + return transform(base[index]) + case let .separator(previous, next): + return separator(base[previous], base[next]) + } + } + + @inlinable + internal func distance(from start: Index, to end: Index) -> Int { + switch (start.representation, end.representation) { + case let (.element(lhs), .element(rhs)), + let (.separator(_, next: lhs), .separator(_, next: rhs)): + return 2 * base.distance(from: lhs, to: rhs) + case let (.element(lhs), .separator(_, next: rhs)): + return 2 * base.distance(from: lhs, to: rhs) - 1 + case let (.separator(_, next: lhs), .element(rhs)): + return 2 * base.distance(from: lhs, to: rhs) + 1 + } + } + + @inlinable + internal func index(_ index: Index, offsetBy distance: Int) -> Index { + guard distance != 0 else { return index } + + return distance > 0 + ? offsetForward(index, by: distance) + : offsetBackward(index, by: -distance) + } + + @inlinable + internal func index( + _ index: Index, + offsetBy distance: Int, + limitedBy limit: Index + ) -> Index? { + guard distance != 0 else { return index } + + if distance > 0 { + return limit >= index + ? offsetForward(index, by: distance, limitedBy: limit) + : offsetForward(index, by: distance) + } else { + return limit <= index + ? offsetBackward(index, by: -distance, limitedBy: limit) + : offsetBackward(index, by: -distance) + } + } + + @inlinable + internal func offsetForward(_ i: Index, by distance: Int) -> Index { + guard let index = offsetForward(i, by: distance, limitedBy: endIndex) + else { fatalError("Index is out of bounds") } + return index + } + + @inlinable + internal func offsetBackward(_ i: Index, by distance: Int) -> Index { + guard let index = offsetBackward(i, by: distance, limitedBy: startIndex) + else { fatalError("Index is out of bounds") } + return index + } + + @inlinable + internal func offsetForward( + _ index: Index, by distance: Int, limitedBy limit: Index + ) -> Index? { + assert(distance > 0) + assert(limit >= index) + + switch (index.representation, limit.representation, distance.isMultiple(of: 2)) { + case let (.element(index), .element(limit), true), + let (.separator(_, next: index), .element(limit), false): + return base.index(index, offsetBy: distance / 2, limitedBy: limit) + .map { .element($0) } + + case let (.element(index), .element(limit), false), + let (.element(index), .separator(_, next: limit), false), + let (.separator(_, next: index), .element(limit), true), + let (.separator(_, next: index), .separator(_, next: limit), true): + return base.index(index, offsetBy: (distance - 1) / 2, limitedBy: limit) + .flatMap { + guard $0 != limit else { return nil } + let next = base.index(after: $0) + return next == base.endIndex + ? endIndex + : .separator(previous: $0, next: next) + } + + case let (.element(index), .separator(_, next: limit), true), + let (.separator(_, next: index), .separator(_, next: limit), false): + return base.index(index, offsetBy: distance / 2, limitedBy: limit) + .flatMap { $0 == limit ? nil : .element($0) } + } + } + + @inlinable + internal func offsetBackward( + _ index: Index, by distance: Int, limitedBy limit: Index + ) -> Index? { + assert(distance > 0) + assert(limit <= index) + + switch (index.representation, limit.representation, distance.isMultiple(of: 2)) { + case let (.element(index), .element(limit), true), + let (.element(index), .separator(_, next: limit), true), + let (.separator(_, next: index), .element(limit), false), + let (.separator(_, next: index), .separator(_, next: limit), false): + return base.index(index, offsetBy: -((distance + 1) / 2), limitedBy: limit) + .map { .element($0) } + + case let (.element(index), .separator(_, next: limit), false), + let (.separator(_, next: index), .separator(_, next: limit), true): + return base.index(index, offsetBy: -(distance / 2), limitedBy: limit) + .map { .separator(previous: base.index($0, offsetBy: -1), next: $0) } + + case let (.element(index), .element(limit), false), + let (.separator(_, next: index), .element(limit), true): + return base.index(index, offsetBy: -(distance / 2), limitedBy: limit) + .flatMap { + $0 == limit + ? nil + : .separator(previous: base.index($0, offsetBy: -1), next: $0) + } + } + } +} + +extension InterspersedMap: BidirectionalCollection + where Base: BidirectionalCollection +{ + @inlinable + internal func index(before index: Index) -> Index { + switch index.representation { + case .element(let index): + let previous = base.index(before: index) + return .separator(previous: previous, next: index) + case let .separator(previous, next): + let index = next == base.endIndex ? base.index(before: next) : previous + return .element(index) + } + } +} + +extension InterspersedMap.Index.Representation: Hashable + where Base.Index: Hashable {} + +extension InterspersedMap: LazySequenceProtocol + where Base: LazySequenceProtocol {} +extension InterspersedMap: LazyCollectionProtocol + where Base: LazyCollectionProtocol {} + +//===----------------------------------------------------------------------===// +// interspersed(with:) +//===----------------------------------------------------------------------===// + extension Sequence { /// Returns a sequence containing elements of this sequence with the given @@ -234,3 +624,30 @@ extension Sequence { Intersperse(base: self, separator: separator) } } + +//===----------------------------------------------------------------------===// +// lazy.interspersed(_:with:) +//===----------------------------------------------------------------------===// + +extension LazySequenceProtocol { + /// Returns a sequence over the results of applying a closure to the + /// sequence's elements, with a separator that separates each pair of adjacent + /// transformed values. + /// + /// The transformation closure lets you intersperse a sequence using a + /// separator of a different type than the original's sequence's elements. + /// Each separator is produced by a closure that is given access to the + /// two elements in the original sequence right before and after it. + /// + /// let strings = [1, 2, 2].interspersedMap(String.init, + /// with: { $0 == $1 ? " == " : " != " }) + /// print(strings.joined()) // "1 != 2 == 2" + /// + @usableFromInline + internal func interspersedMap( + _ transform: @escaping (Element) -> Result, + with separator: @escaping (Element, Element) -> Result + ) -> InterspersedMap { + InterspersedMap(base: self, transform: transform, separator: separator) + } +} diff --git a/Sources/Algorithms/Joined.swift b/Sources/Algorithms/Joined.swift new file mode 100644 index 00000000..d630a2d4 --- /dev/null +++ b/Sources/Algorithms/Joined.swift @@ -0,0 +1,495 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +//===----------------------------------------------------------------------===// +// JoinedBySequence +//===----------------------------------------------------------------------===// + +/// A sequence that presents the elements of a base sequence of sequences +/// concatenated using a given separator. +public struct JoinedBySequence + where Base.Element: Sequence, Base.Element.Element == Separator.Element +{ + @usableFromInline + internal typealias Inner = FlattenSequence< + Intersperse>>> + + @usableFromInline + internal let inner: Inner + + @inlinable + internal init(base: Base, separator: Separator) { + self.inner = base.lazy + .map(EitherSequence.left) + .interspersed(with: .right(separator)) + .joined() + } +} + +extension JoinedBySequence: Sequence { + public struct Iterator: IteratorProtocol { + @usableFromInline + internal var inner: Inner.Iterator + + @inlinable + internal init(inner: Inner.Iterator) { + self.inner = inner + } + + @inlinable + public mutating func next() -> Base.Element.Element? { + inner.next() + } + } + + @inlinable + public func makeIterator() -> Iterator { + Iterator(inner: inner.makeIterator()) + } +} + +extension JoinedBySequence: LazySequenceProtocol + where Base: LazySequenceProtocol {} + +//===----------------------------------------------------------------------===// +// JoinedByClosureSequence +//===----------------------------------------------------------------------===// + +/// A sequence that presents the elements of a base sequence of sequences +/// concatenated using a given separator that depends on the sequences right +/// before and after it. +public struct JoinedByClosureSequence + where Base.Element: Sequence, Base.Element.Element == Separator.Element +{ + @usableFromInline + internal typealias Inner = FlattenSequence< + InterspersedMap, EitherSequence>> + + @usableFromInline + internal let inner: Inner + + @inlinable + internal init( + base: Base, + separator: @escaping (Base.Element, Base.Element) -> Separator + ) { + self.inner = base.lazy + .interspersedMap( + EitherSequence.left, + with: { EitherSequence.right(separator($0, $1)) }) + .joined() + } +} + +extension JoinedByClosureSequence: Sequence { + public struct Iterator: IteratorProtocol { + @usableFromInline + internal var inner: Inner.Iterator + + @inlinable + internal init(inner: Inner.Iterator) { + self.inner = inner + } + + @inlinable + public mutating func next() -> Base.Element.Element? { + inner.next() + } + } + + @inlinable + public func makeIterator() -> Iterator { + Iterator(inner: inner.makeIterator()) + } +} + +extension JoinedByClosureSequence: LazySequenceProtocol + where Base: LazySequenceProtocol {} + +//===----------------------------------------------------------------------===// +// JoinedByCollection +//===----------------------------------------------------------------------===// + +/// A collection that presents the elements of a base collection of collections +/// concatenated using a given separator. +public struct JoinedByCollection + where Base.Element: Collection, Base.Element.Element == Separator.Element +{ + @usableFromInline + internal typealias Inner = FlattenCollection< + Intersperse>>> + + @usableFromInline + internal let inner: Inner + + @inlinable + internal init(base: Base, separator: Separator) { + self.inner = base.lazy + .map(EitherSequence.left) + .interspersed(with: .right(separator)) + .joined() + } +} + +extension JoinedByCollection: Collection { + public struct Index: Comparable { + @usableFromInline + internal let inner: Inner.Index + + @inlinable + internal init(_ inner: Inner.Index) { + self.inner = inner + } + + @inlinable + public static func == (lhs: Self, rhs: Self) -> Bool { + lhs.inner == rhs.inner + } + + @inlinable + public static func < (lhs: Self, rhs: Self) -> Bool { + lhs.inner < rhs.inner + } + } + + @inlinable + public var startIndex: Index { + Index(inner.startIndex) + } + + @inlinable + public var endIndex: Index { + Index(inner.endIndex) + } + + @inlinable + public func index(after index: Index) -> Index { + Index(inner.index(after: index.inner)) + } + + @inlinable + public subscript(position: Index) -> Base.Element.Element { + inner[position.inner] + } + + @inlinable + public func index(_ index: Index, offsetBy distance: Int) -> Index { + Index(inner.index(index.inner, offsetBy: distance)) + } + + @inlinable + public func index( + _ index: Index, + offsetBy distance: Int, + limitedBy limit: Index + ) -> Index? { + inner.index(index.inner, offsetBy: distance, limitedBy: limit.inner) + .map(Index.init) + } + + @inlinable + public func distance(from start: Index, to end: Index) -> Int { + inner.distance(from: start.inner, to: end.inner) + } +} + +extension JoinedByCollection: BidirectionalCollection + where Base: BidirectionalCollection, + Base.Element: BidirectionalCollection, + Separator: BidirectionalCollection +{ + @inlinable + public func index(before index: Index) -> Index { + Index(inner.index(before: index.inner)) + } +} + +extension JoinedByCollection: LazySequenceProtocol + where Base: LazySequenceProtocol {} +extension JoinedByCollection: LazyCollectionProtocol + where Base: LazyCollectionProtocol {} + +//===----------------------------------------------------------------------===// +// JoinedByClosureCollection +//===----------------------------------------------------------------------===// + +/// A collection that presents the elements of a base collection of collections +/// concatenated using a given separator that depends on the collections right +/// before and after it. +public struct JoinedByClosureCollection + where Base.Element: Collection, Base.Element.Element == Separator.Element +{ + @usableFromInline + internal typealias Inner = FlattenCollection< + InterspersedMap, EitherSequence>> + + @usableFromInline + internal let inner: Inner + + @inlinable + internal init( + base: Base, + separator: @escaping (Base.Element, Base.Element) -> Separator + ) { + self.inner = base.lazy + .interspersedMap( + EitherSequence.left, + with: { EitherSequence.right(separator($0, $1)) }) + .joined() + } +} + +extension JoinedByClosureCollection: Collection { + public struct Index: Comparable { + @usableFromInline + internal let inner: Inner.Index + + @inlinable + internal init(_ inner: Inner.Index) { + self.inner = inner + } + + @inlinable + public static func == (lhs: Self, rhs: Self) -> Bool { + lhs.inner == rhs.inner + } + + @inlinable + public static func < (lhs: Self, rhs: Self) -> Bool { + lhs.inner < rhs.inner + } + } + + @inlinable + public var startIndex: Index { + Index(inner.startIndex) + } + + @inlinable + public var endIndex: Index { + Index(inner.endIndex) + } + + @inlinable + public func index(after index: Index) -> Index { + Index(inner.index(after: index.inner)) + } + + @inlinable + public subscript(position: Index) -> Base.Element.Element { + inner[position.inner] + } + + @inlinable + public func index(_ index: Index, offsetBy distance: Int) -> Index { + Index(inner.index(index.inner, offsetBy: distance)) + } + + @inlinable + public func index( + _ index: Index, + offsetBy distance: Int, + limitedBy limit: Index + ) -> Index? { + inner.index(index.inner, offsetBy: distance, limitedBy: limit.inner) + .map(Index.init) + } + + @inlinable + public func distance(from start: Index, to end: Index) -> Int { + inner.distance(from: start.inner, to: end.inner) + } +} + +extension JoinedByClosureCollection: BidirectionalCollection + where Base: BidirectionalCollection, + Base.Element: BidirectionalCollection, + Separator: BidirectionalCollection +{ + @inlinable + public func index(before index: Index) -> Index { + Index(inner.index(before: index.inner)) + } +} + +extension JoinedByClosureCollection: LazySequenceProtocol + where Base: LazySequenceProtocol {} +extension JoinedByClosureCollection: LazyCollectionProtocol + where Base: LazyCollectionProtocol {} + +//===----------------------------------------------------------------------===// +// Sequence.joined(by:) +//===----------------------------------------------------------------------===// + +extension Sequence where Element: Sequence { + /// Returns the concatenation of the elements in this sequence of sequences, + /// inserting the given separator between each sequence. + /// + /// for x in [[1, 2], [3, 4], [5, 6]].joined(by: 100) { + /// print(x) + /// } + /// // 1, 2, 100, 3, 4, 100, 5, 6 + @inlinable + public func joined(by separator: Element.Element) + -> JoinedBySequence> + { + joined(by: CollectionOfOne(separator)) + } + + /// Returns the concatenation of the elements in this sequence of sequences, + /// inserting the given separator between each sequence. + /// + /// for x in [[1, 2], [3, 4], [5, 6]].joined(by: [100, 200]) { + /// print(x) + /// } + /// // 1, 2, 100, 200, 3, 4, 100, 200, 5, 6 + @inlinable + public func joined( + by separator: Separator + ) -> JoinedBySequence + where Separator: Collection, Separator.Element == Element.Element + { + JoinedBySequence(base: self, separator: separator) + } + + @inlinable + internal func _joined( + by update: (inout [Element.Element], Element, Element) throws -> Void + ) rethrows -> [Element.Element] { + var iterator = makeIterator() + guard let first = iterator.next() else { return [] } + + var result = Array(first) + var previous = first + + while let next = iterator.next() { + try update(&result, previous, next) + result.append(contentsOf: next) + previous = next + } + + return result + } + + /// Returns the concatenation of the elements in this sequence of sequences, + /// inserting the separator produced by the closure between each sequence. + /// + /// for x in [[1, 2], [3, 4], [5, 6]].joined(by: { $0.last! * $1.first! }) { + /// print(x) + /// } + /// // 1, 2, 6, 3, 4, 20, 5, 6 + @inlinable + public func joined( + by separator: (Element, Element) throws -> Element.Element + ) rethrows -> [Element.Element] { + try _joined(by: { $0.append(try separator($1, $2)) }) + } + + /// Returns the concatenation of the elements in this sequence of sequences, + /// inserting the separator produced by the closure between each sequence. + /// + /// for x in [[1, 2], [3, 4], [5, 6]].joined(by: { [100 * $0.last!, 100 * $1.first!] }) { + /// print(x) + /// } + /// // 1, 2, 200, 300, 3, 4, 400, 500, 5, 6 + @inlinable + public func joined( + by separator: (Element, Element) throws -> Separator + ) rethrows -> [Element.Element] + where Separator: Sequence, Separator.Element == Element.Element + { + try _joined(by: { $0.append(contentsOf: try separator($1, $2)) }) + } +} + +//===----------------------------------------------------------------------===// +// LazySequenceProtocol.joined(by:) +//===----------------------------------------------------------------------===// + +extension LazySequenceProtocol where Element: Sequence { + /// Returns the concatenation of the elements in this sequence of sequences, + /// inserting the separator produced by the closure between each sequence. + @inlinable + public func joined( + by separator: @escaping (Element, Element) -> Element.Element + ) -> JoinedByClosureSequence> { + joined(by: { CollectionOfOne(separator($0, $1)) }) + } + + /// Returns the concatenation of the elements in this sequence of sequences, + /// inserting the separator produced by the closure between each sequence. + @inlinable + public func joined( + by separator: @escaping (Element, Element) -> Separator + ) -> JoinedByClosureSequence { + JoinedByClosureSequence(base: self, separator: separator) + } +} + +//===----------------------------------------------------------------------===// +// Collection.joined(by:) +//===----------------------------------------------------------------------===// + +extension Collection where Element: Collection { + /// Returns the concatenation of the elements in this collection of + /// collections, inserting the given separator between each collection. + /// + /// for x in [[1, 2], [3, 4], [5, 6]].joined(by: 100) { + /// print(x) + /// } + /// // 1, 2, 100, 3, 4, 100, 5, 6 + @inlinable + public func joined(by separator: Element.Element) + -> JoinedByCollection> + { + joined(by: CollectionOfOne(separator)) + } + + /// Returns the concatenation of the elements in this collection of + /// collections, inserting the given separator between each collection. + /// + /// for x in [[1, 2], [3, 4], [5, 6]].joined(by: [100, 200]) { + /// print(x) + /// } + /// // 1, 2, 100, 200, 3, 4, 100, 200, 5, 6 + @inlinable + public func joined(by separator: Separator) + -> JoinedByCollection + { + JoinedByCollection(base: self, separator: separator) + } +} + +//===----------------------------------------------------------------------===// +// LazyCollectionProtocol.joined(by:) +//===----------------------------------------------------------------------===// + +extension LazyCollectionProtocol where Element: Collection { + /// Returns the concatenation of the elements in this collection of + /// collections, inserting the separator produced by the closure between each + /// sequence. + @inlinable + public func joined( + by separator: @escaping (Element, Element) -> Element.Element + ) -> JoinedByClosureCollection> { + joined(by: { CollectionOfOne(separator($0, $1)) }) + } + + /// Returns the concatenation of the elements in this collection of + /// collections, inserting the separator produced by the closure between each + /// sequence. + @inlinable + public func joined( + by separator: @escaping (Element, Element) -> Separator + ) -> JoinedByClosureCollection { + JoinedByClosureCollection(base: self, separator: separator) + } +} diff --git a/Tests/SwiftAlgorithmsTests/IntersperseTests.swift b/Tests/SwiftAlgorithmsTests/IntersperseTests.swift index a6e9460f..50baa698 100644 --- a/Tests/SwiftAlgorithmsTests/IntersperseTests.swift +++ b/Tests/SwiftAlgorithmsTests/IntersperseTests.swift @@ -10,7 +10,7 @@ //===----------------------------------------------------------------------===// import XCTest -import Algorithms +@testable import Algorithms final class IntersperseTests: XCTestCase { func testSequence() { @@ -62,4 +62,31 @@ final class IntersperseTests: XCTestCase { XCTAssertLazySequence((1...).prefix(0).lazy.interspersed(with: 0)) XCTAssertLazyCollection("ABCDE".lazy.interspersed(with: "-")) } + + func testInterspersedMap() { + XCTAssertEqualSequences( + (0..<0).lazy.interspersedMap({ $0 }, with: { _, _ in 100 }), + []) + + XCTAssertEqualSequences( + (0..<1).lazy.interspersedMap({ $0 }, with: { _, _ in 100 }), + [0]) + + XCTAssertEqualSequences( + (0..<5).lazy.interspersedMap({ $0 }, with: { $0 + $1 + 100 }), + [0, 101, 1, 103, 2, 105, 3, 107, 4]) + } + + func testInterspersedMapLazy() { + XCTAssertLazySequence(AnySequence([]).lazy.interspersedMap({ $0 }, with: { _, _ in 100 })) + XCTAssertLazyCollection((0..<0).lazy.interspersedMap({ $0 }, with: { _, _ in 100 })) + } + + func testInterspersedMapIndexTraversals() { + validateIndexTraversals( + (0..<0).lazy.interspersedMap({ $0 }, with: { _, _ in 100 }), + (0..<1).lazy.interspersedMap({ $0 }, with: { _, _ in 100 }), + (0..<2).lazy.interspersedMap({ $0 }, with: { _, _ in 100 }), + (0..<5).lazy.interspersedMap({ $0 }, with: { _, _ in 100 })) + } } diff --git a/Tests/SwiftAlgorithmsTests/JoinedTests.swift b/Tests/SwiftAlgorithmsTests/JoinedTests.swift new file mode 100644 index 00000000..cb70558a --- /dev/null +++ b/Tests/SwiftAlgorithmsTests/JoinedTests.swift @@ -0,0 +1,125 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Algorithms open source project +// +// Copyright (c) 2021 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 JoinedTests: XCTestCase { + let stringArrays = [ + [], + [""], + ["", ""], + ["foo"], + ["foo", "bar"], + ["", "", "foo", "", "bar", "baz", ""], + ] + + func testJoined() { + let expected = ["", "", "", "foo", "foobar", "foobarbaz"] + + for (strings, expected) in zip(stringArrays, expected) { + // regular sequence + XCTAssertEqualSequences(AnySequence(strings).joined(), expected) + + // lazy sequence + XCTAssertEqualSequences(AnySequence(strings).lazy.joined(), expected) + + // regular collection + XCTAssertEqualSequences(strings.joined(), expected) + + // lazy collection + XCTAssertEqualSequences(strings.lazy.joined() as FlattenCollection, expected) + } + } + + func testJoinedByElement() { + let separator: Character = " " + let expected = ["", "", " ", "foo", "foo bar", " foo bar baz "] + + for (strings, expected) in zip(stringArrays, expected) { + XCTAssertEqualSequences(AnySequence(strings).joined(by: separator), expected) + XCTAssertEqualSequences(AnySequence(strings).lazy.joined(by: separator), expected) + XCTAssertEqualSequences(strings.joined(by: separator), expected) + XCTAssertEqualSequences(strings.lazy.joined(by: separator), expected) + } + } + + func testJoinedBySequence() { + let separator = ", " + let expected = ["", "", ", ", "foo", "foo, bar", ", , foo, , bar, baz, "] + + for (strings, expected) in zip(stringArrays, expected) { + XCTAssertEqualSequences(AnySequence(strings).joined(by: separator), expected) + XCTAssertEqualSequences(AnySequence(strings).lazy.joined(by: separator), expected) + XCTAssertEqualSequences(strings.joined(by: separator), expected) + XCTAssertEqualSequences(strings.lazy.joined(by: separator), expected) + } + } + + func testJoinedByElementClosure() { + let separator = { (left: String, right: String) -> Character in + left.isEmpty || right.isEmpty ? " " : "-" + } + + let expected = ["", "", " ", "foo", "foo-bar", " foo bar-baz "] + + for (strings, expected) in zip(stringArrays, expected) { + XCTAssertEqualSequences(AnySequence(strings).joined(by: separator), expected) + XCTAssertEqualSequences(AnySequence(strings).lazy.joined(by: separator), expected) + XCTAssertEqualSequences(strings.joined(by: separator), expected) + XCTAssertEqualSequences(strings.lazy.joined(by: separator), expected) + } + } + + func testJoinedBySequenceClosure() { + let separator = { (left: String, right: String) in + "(\(left.count), \(right.count))" + } + + let expected = [ + "", + "", + "(0, 0)", + "foo", + "foo(3, 3)bar", + "(0, 0)(0, 3)foo(3, 0)(0, 3)bar(3, 3)baz(3, 0)" + ] + + for (strings, expected) in zip(stringArrays, expected) { + XCTAssertEqualSequences(AnySequence(strings).joined(by: separator), expected) + XCTAssertEqualSequences(AnySequence(strings).lazy.joined(by: separator), expected) + XCTAssertEqualSequences(strings.joined(by: separator), expected) + XCTAssertEqualSequences(strings.lazy.joined(by: separator), expected) + } + } + + func testJoinedLazy() { + XCTAssertLazySequence(AnySequence([[1], [2]]).lazy.joined()) + XCTAssertLazySequence(AnySequence([[1], [2]]).lazy.joined(by: 1)) + XCTAssertLazySequence(AnySequence([[1], [2]]).lazy.joined(by: { _, _ in 1 })) + XCTAssertLazyCollection([[1], [2]].lazy.joined()) + XCTAssertLazyCollection([[1], [2]].lazy.joined(by: 1)) + XCTAssertLazyCollection([[1], [2]].lazy.joined(by: { _, _ in 1 })) + } + + func testJoinedIndexTraversals() { + // the last test case takes too long to run + for strings in stringArrays.dropLast() { + validateIndexTraversals(strings.joined() as FlattenCollection) + validateIndexTraversals(strings.joined(by: ", ")) + } + + for (strings, separator) in product(stringArrays.dropLast(), ["", " ", ", "]) { + validateIndexTraversals(strings.joined(by: separator)) + validateIndexTraversals(strings.lazy.joined(by: { _, _ in separator })) + } + } +}