diff --git a/CHANGELOG.md b/CHANGELOG.md index 70b59ea4..b8acad4e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,7 @@ package updates, you can specify your package dependency using ## [Unreleased] -*No changes yet.* +-`adjacentPairs()` lazily iterates over tuples of adjacent elements of a sequence. --- diff --git a/Guides/AdjacentPairs.md b/Guides/AdjacentPairs.md new file mode 100644 index 00000000..1b790eb6 --- /dev/null +++ b/Guides/AdjacentPairs.md @@ -0,0 +1,52 @@ +# AdjacentPairs + +[[Source](https://github.com/apple/swift-algorithms/blob/main/Sources/Algorithms/AdjacentPairs.swift) | + [Tests](https://github.com/apple/swift-algorithms/blob/main/Tests/SwiftAlgorithmsTests/AdjacentPairsTests.swift)] + +Lazily iterates over tuples of adjacent elements. + +This operation is available for any sequence by calling the `adjacentPairs()` method. + +```swift +let numbers = (1...5) +let pairs = numbers.adjacentPairs() +// Array(pairs) == [(1, 2), (2, 3), (3, 4), (4, 5)] +``` + +## Detailed Design + +The `adjacentPairs()` method is declared as a `Sequence` extension returning `AdjacentPairsSequence` and as a `Collection` extension returning `AdjacentPairsCollection`. + +```swift +extension Sequence { + public func adjacentPairs() -> AdjacentPairsSequence +} +``` + +```swift +extension Collection { + public func adjacentPairs() -> AdjacentPairsCollection +} +``` + +The `AdjacentPairsSequence` type is a sequence, and the `AdjacentPairsCollection` type is a collection with conditional conformance to `BidirectionalCollection` and `RandomAccessCollection` when the underlying collection conforms. + +### Complexity + +Calling `adjacentPairs` is an O(1) operation. + +### Naming + +This method is named for clarity while remaining agnostic to any particular domain of programming. In natural language processing, this operation is akin to computing a list of bigrams; however, this algorithm is not specific to this use case. + +[naming]: https://forums.swift.org/t/naming-of-chained-with/40999/ + +### Comparison with other languages + +This function is often written as a `zip` of a sequence together with itself, minus its first element. + +**Haskell:** This operation is spelled ``s `zip` tail s``. + +**Python:** Python users may write `zip(s, s[1:])` for a list with at least one element. For natural language processing, the `nltk` package offers a `bigrams` function akin to this method. + + Note that in Swift, the spelling `zip(s, s.dropFirst())` is undefined behavior for a single-pass sequence `s`. diff --git a/README.md b/README.md index 638ba8cd..a5d17f52 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,7 @@ Read more about the package, and the intent behind it, in the [announcement on s #### Other useful operations +- [`adjacentPairs()`](https://github.com/apple/swift-algorithms/blob/main/Guides/AdjacentPairs.md): Lazily iterates over tuples of adjacent elements. - [`chunked(by:)`, `chunked(on:)`, `chunks(ofCount:)`](https://github.com/apple/swift-algorithms/blob/main/Guides/Chunked.md): Eager and lazy operations that break a collection into chunks based on either a binary predicate or when the result of a projection changes or chunks of a given count. - [`indexed()`](https://github.com/apple/swift-algorithms/blob/main/Guides/Indexed.md): Iterate over tuples of a collection's indices and elements. - [`interspersed(with:)`](https://github.com/apple/swift-algorithms/blob/main/Guides/Intersperse.md): Place a value between every two elements of a sequence. diff --git a/Sources/Algorithms/AdjacentPairs.swift b/Sources/Algorithms/AdjacentPairs.swift new file mode 100644 index 00000000..ff4901c6 --- /dev/null +++ b/Sources/Algorithms/AdjacentPairs.swift @@ -0,0 +1,271 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +extension Sequence { + /// Creates a sequence of adjacent pairs of elements from this sequence. + /// + /// In the `AdjacentPairsSequence` returned by this method, the elements of + /// the *i*th pair are the *i*th and *(i+1)*th elements of the underlying + /// sequence. + /// The following example uses the `adjacentPairs()` method to iterate over + /// adjacent pairs of integers: + /// + /// for pair in (1...5).adjacentPairs() { + /// print(pair) + /// } + /// // Prints "(1, 2)" + /// // Prints "(2, 3)" + /// // Prints "(3, 4)" + /// // Prints "(4, 5)" + @inlinable + public func adjacentPairs() -> AdjacentPairsSequence { + AdjacentPairsSequence(base: self) + } +} + +extension Collection { + /// A collection of adjacent pairs of elements built from an underlying collection. + /// + /// In an `AdjacentPairsCollection`, the elements of the *i*th pair are the *i*th + /// and *(i+1)*th elements of the underlying sequence. The following example + /// uses the `adjacentPairs()` method to iterate over adjacent pairs of + /// integers: + /// ``` + /// for pair in (1...5).adjacentPairs() { + /// print(pair) + /// } + /// // Prints "(1, 2)" + /// // Prints "(2, 3)" + /// // Prints "(3, 4)" + /// // Prints "(4, 5)" + /// ``` + @inlinable + public func adjacentPairs() -> AdjacentPairsCollection { + AdjacentPairsCollection(base: self) + } +} + +/// A sequence of adjacent pairs of elements built from an underlying sequence. +/// +/// In an `AdjacentPairsSequence`, the elements of the *i*th pair are the *i*th +/// and *(i+1)*th elements of the underlying sequence. The following example +/// uses the `adjacentPairs()` method to iterate over adjacent pairs of +/// integers: +/// ``` +/// for pair in (1...5).adjacentPairs() { +/// print(pair) +/// } +/// // Prints "(1, 2)" +/// // Prints "(2, 3)" +/// // Prints "(3, 4)" +/// // Prints "(4, 5)" +/// ``` +public struct AdjacentPairsSequence { + @usableFromInline + internal let base: Base + + /// Creates an instance that makes pairs of adjacent elements from `base`. + @inlinable + internal init(base: Base) { + self.base = base + } +} + +extension AdjacentPairsSequence { + public struct Iterator { + @usableFromInline + internal var base: Base.Iterator + + @usableFromInline + internal var previousElement: Base.Element? + + @inlinable + internal init(base: Base.Iterator) { + self.base = base + } + } +} + +extension AdjacentPairsSequence.Iterator: IteratorProtocol { + public typealias Element = (Base.Element, Base.Element) + + @inlinable + public mutating func next() -> Element? { + if previousElement == nil { + previousElement = base.next() + } + + guard let previous = previousElement, let next = base.next() else { + return nil + } + + previousElement = next + return (previous, next) + } +} + +extension AdjacentPairsSequence: Sequence { + @inlinable + public func makeIterator() -> Iterator { + Iterator(base: base.makeIterator()) + } + + @inlinable + public var underestimatedCount: Int { + Swift.max(0, base.underestimatedCount - 1) + } +} + +/// A collection of adjacent pairs of elements built from an underlying collection. +/// +/// In an `AdjacentPairsCollection`, the elements of the *i*th pair are the *i*th +/// and *(i+1)*th elements of the underlying sequence. The following example +/// uses the `adjacentPairs()` method to iterate over adjacent pairs of +/// integers: +/// ``` +/// for pair in (1...5).adjacentPairs() { +/// print(pair) +/// } +/// // Prints "(1, 2)" +/// // Prints "(2, 3)" +/// // Prints "(3, 4)" +/// // Prints "(4, 5)" +/// ``` +public struct AdjacentPairsCollection { + @usableFromInline + internal let base: Base + + public let startIndex: Index + + @inlinable + internal init(base: Base) { + self.base = base + + // Precompute `startIndex` to ensure O(1) behavior, + // avoiding indexing past `endIndex` + let start = base.startIndex + let end = base.endIndex + let second = start == end ? start : base.index(after: start) + self.startIndex = Index(first: start, second: second) + } +} + +extension AdjacentPairsCollection { + public typealias Iterator = AdjacentPairsSequence.Iterator + + @inlinable + public func makeIterator() -> Iterator { + Iterator(base: base.makeIterator()) + } +} + +extension AdjacentPairsCollection { + public struct Index: Comparable { + @usableFromInline + internal var first: Base.Index + + @usableFromInline + internal var second: Base.Index + + @inlinable + internal init(first: Base.Index, second: Base.Index) { + self.first = first + self.second = second + } + + @inlinable + public static func < (lhs: Index, rhs: Index) -> Bool { + (lhs.first, lhs.second) < (rhs.first, rhs.second) + } + } +} + +extension AdjacentPairsCollection: Collection { + @inlinable + public var endIndex: Index { + switch base.endIndex { + case startIndex.first, startIndex.second: + return startIndex + case let end: + return Index(first: end, second: end) + } + } + + @inlinable + public subscript(position: Index) -> (Base.Element, Base.Element) { + (base[position.first], base[position.second]) + } + + @inlinable + public func index(after i: Index) -> Index { + let next = base.index(after: i.second) + return next == base.endIndex + ? endIndex + : Index(first: i.second, second: next) + } + + @inlinable + public func index(_ i: Index, offsetBy distance: Int) -> Index { + if distance == 0 { + return i + } else if distance > 0 { + let firstOffsetIndex = base.index(i.first, offsetBy: distance) + let secondOffsetIndex = base.index(after: firstOffsetIndex) + return secondOffsetIndex == base.endIndex + ? endIndex + : Index(first: firstOffsetIndex, second: secondOffsetIndex) + } else { + return i == endIndex + ? Index(first: base.index(i.first, offsetBy: distance - 1), + second: base.index(i.first, offsetBy: distance)) + : Index(first: base.index(i.first, offsetBy: distance), + second: i.first) + } + } + + @inlinable + public func distance(from start: Index, to end: Index) -> Int { + let offset: Int + switch (start.first, end.first) { + case (base.endIndex, base.endIndex): + return 0 + case (base.endIndex, _): + offset = +1 + case (_, base.endIndex): + offset = -1 + default: + offset = 0 + } + + return base.distance(from: start.first, to: end.first) + offset + } + + @inlinable + public var count: Int { + Swift.max(0, base.count - 1) + } +} + +extension AdjacentPairsCollection: BidirectionalCollection + where Base: BidirectionalCollection +{ + @inlinable + public func index(before i: Index) -> Index { + i == endIndex + ? Index(first: base.index(i.first, offsetBy: -2), + second: base.index(before: i.first)) + : Index(first: base.index(before: i.first), + second: i.first) + } +} + +extension AdjacentPairsCollection: RandomAccessCollection + where Base: RandomAccessCollection {} diff --git a/Tests/SwiftAlgorithmsTests/AdjacentPairsTests.swift b/Tests/SwiftAlgorithmsTests/AdjacentPairsTests.swift new file mode 100644 index 00000000..3226027d --- /dev/null +++ b/Tests/SwiftAlgorithmsTests/AdjacentPairsTests.swift @@ -0,0 +1,98 @@ +//===----------------------------------------------------------------------===// +// +// 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 +import Algorithms + +final class AdjacentPairsTests: XCTestCase { + func testZeroElements() { + let pairs = (0..<0).adjacentPairs() + XCTAssertEqual(pairs.startIndex, pairs.endIndex) + XCTAssert(Array(pairs) == []) + } + + func testOneElement() { + let pairs = (0..<1).adjacentPairs() + XCTAssertEqual(pairs.startIndex, pairs.endIndex) + XCTAssert(Array(pairs) == []) + } + + func testTwoElements() { + let pairs = (0..<2).adjacentPairs() + XCTAssert(Array(pairs) == [(0, 1)]) + } + + func testThreeElements() { + let pairs = (0..<3).adjacentPairs() + XCTAssert(Array(pairs) == [(0, 1), (1, 2)]) + } + + func testFourElements() { + let pairs = (0..<4).adjacentPairs() + XCTAssert(Array(pairs) == [(0, 1), (1, 2), (2, 3)]) + } + + func testForwardIndexing() { + let pairs = (1...5).adjacentPairs() + let expected = [(1, 2), (2, 3), (3, 4), (4, 5)] + var index = pairs.startIndex + for iteration in expected.indices { + XCTAssert(pairs[index] == expected[iteration]) + pairs.formIndex(after: &index) + } + XCTAssertEqual(index, pairs.endIndex) + } + + func testBackwardIndexing() { + let pairs = (1...5).adjacentPairs() + let expected = [(4, 5), (3, 4), (2, 3), (1, 2)] + var index = pairs.endIndex + for iteration in expected.indices { + pairs.formIndex(before: &index) + XCTAssert(pairs[index] == expected[iteration]) + } + XCTAssertEqual(index, pairs.startIndex) + } + + func testIndexDistance() { + let pairSequences = (0...4).map { (0..<$0).adjacentPairs() } + + for pairs in pairSequences { + for index in pairs.indices.dropLast() { + let next = pairs.index(after: index) + XCTAssertEqual(pairs.distance(from: index, to: next), 1) + } + + XCTAssertEqual(pairs.distance(from: pairs.startIndex, to: pairs.endIndex), pairs.count) + XCTAssertEqual(pairs.distance(from: pairs.endIndex, to: pairs.startIndex), -pairs.count) + } + } + + func testIndexOffsetBy() { + let pairSequences = (0...4).map { (0..<$0).adjacentPairs() } + + for pairs in pairSequences { + for index in pairs.indices.dropLast() { + let next = pairs.index(after: index) + XCTAssertEqual(pairs.index(index, offsetBy: 1), next) + } + + XCTAssertEqual(pairs.index(pairs.startIndex, offsetBy: pairs.count), pairs.endIndex) + XCTAssertEqual(pairs.index(pairs.endIndex, offsetBy: -pairs.count), pairs.startIndex) + } + } +} + +extension Collection { + fileprivate static func == (lhs: Self, rhs: Self) -> Bool where Element == (L, R) { + lhs.count == rhs.count && zip(lhs, rhs).allSatisfy(==) + } +}