diff --git a/CHANGELOG.md b/CHANGELOG.md index 84f0a457..85462fab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,13 @@ package updates, you can specify your package dependency using ## [Unreleased] -*No changes yet.* +### Additions + +- The `firstDelta(against: by:)` and `firstDelta(against:)` methods have been + added. They find where two sequences start to differ. The + `diverges(from: by:)` and `diverges(from:)` methods are the analogs for + collections. The `converges(with: by:)` and `converges(with:)` flip the + search to identify where two bidirectional collections start to be the same. --- diff --git a/Guides/FirstDelta.md b/Guides/FirstDelta.md new file mode 100644 index 00000000..5a4116ce --- /dev/null +++ b/Guides/FirstDelta.md @@ -0,0 +1,78 @@ +# First Delta + +[[Source](../Sources/Algorithms/FirstDelta.swift) | + [Tests](../Tests/SwiftAlgorithmsTests/FirstDeltaTests.swift)] + +Methods for finding the first place that two sequences differ. There are +variants for when both sequences are collections. For bidirectional +collections, there are related methods for finding where two sources become the +same. + +The methods for finding the differences in sequences can be viewed as the +common core operation for the Standard Library's `elementsEqual(_: by:)` and +`starts(with: by:)` methods. + +(To-do: expand on this.) + +## Detailed Design + +The element-returning methods are declared as extensions to `Sequence`. The +index-returning methods are declared as extensions to `Collection`. The +suffix-targeting methods are declared as extensions to +`BidirectionalCollection`. The overloads that default comparisons to the +standard equality operator are constrained to when the sources share the same +`Element` type and said type conforms to `Equatable`. + +```swift +extension Sequence { + func firstDelta( + against possibleMirror: PossibleMirror, + by areEquivalent: (Element, PossibleMirror.Element) throws -> Bool + ) rethrows -> (Element?, PossibleMirror.Element?) +} + +extension Sequence where Element: Equatable { + func firstDelta( + against possibleMirror: PossibleMirror + ) -> (Element?, Element?) where PossibleMirror.Element == Element +} + +extension Collection { + func diverges( + from possibleMirror: PossibleMirror, + by areEquivalent: (Element, PossibleMirror.Element) throws -> Bool + ) rethrows -> (Index, PossibleMirror.Index) +} + +extension Collection where Element: Equatable { + func diverges( + from possibleMirror: PossibleMirror + ) -> (Index, PossibleMirror.Index) where PossibleMirror.Element == Element +} + +extension BidirectionalCollection { + func converges( + with possibleMirror: PossibleMirror, + by areEquivalent: (Element, PossibleMirror.Element) throws -> Bool + ) rethrows -> (Index, PossibleMirror.Index) +} + +extension BidirectionalCollection where Element: Equatable { + func converges( + with possibleMirror: PossibleMirror + ) -> (Index, PossibleMirror.Index) where PossibleMirror.Element == Element +} +``` + +### Complexity + +All of these methods have to walk the entirety of both sources until +corresponding non-matches are found, so they work in O(_n_) operations, where +_n_ is the length of the shorter source. + +### Comparison with other languages + +**C++:** The `` library defines the `mismatch` function, which has +similar semantics to `firstDelta`. + +(To-do: add other languages.) diff --git a/README.md b/README.md index f73b351c..3ece9543 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,12 @@ Read more about the package, and the intent behind it, in the [announcement on s - [`randomStableSample(count:)`, `randomStableSample(count:using:)`](https://github.com/apple/swift-algorithms/blob/main/Guides/RandomSampling.md): Randomly selects a specific number of elements from a collection, preserving their original relative order. - [`uniqued()`, `uniqued(on:)`](https://github.com/apple/swift-algorithms/blob/main/Guides/Unique.md): The unique elements of a collection, preserving their order. +#### Comparisons + +- [`firstDelta(against: by:)`, `firstDelta(against:)`](./Guides/FirstDelta.md): Finds the first corresponding elements of two sequences after their common prefix. +- [`diverges(from: by:)`, `diverges(from:)`](./Guides/FirstDelta.md): Finds the past-the-end indexes for two collections' common prefix. +- [`converges(with: by:)`, `converges(with:)`](./Guides/FirstDelta.md): Finds the starting indexes for two (bidirectional) collections' common suffix. + #### Other useful operations - [`chunked(by:)`, `chunked(on:)`](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. diff --git a/Sources/Algorithms/FirstDelta.swift b/Sources/Algorithms/FirstDelta.swift new file mode 100644 index 00000000..ce33f601 --- /dev/null +++ b/Sources/Algorithms/FirstDelta.swift @@ -0,0 +1,240 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +//===----------------------------------------------------------------------===// +// firstDelta(against: by:) +//===----------------------------------------------------------------------===// + +extension Sequence { + /// Returns the first non-matching element pair found when comparing this + /// sequence to the given sequence element-wise, using the given predicate as + /// the equivalence test. + /// + /// The predicate must be a *equivalence relation* over the elements. That + /// is, for any elements `a`, `b`, and `c`, the following conditions must + /// hold: + /// + /// - `areEquivalent(a, a)` is always `true`. (Reflexivity) + /// - `areEquivalent(a, b)` implies `areEquivalent(b, a)`. (Symmetry) + /// - If `areEquivalent(a, b)` and `areEquivalent(b, c)` are both `true`, then + /// `areEquivalent(a, c)` is also `true`. (Transitivity) + /// + /// If one sequence is a proper prefix of the other, its corresponding member + /// in the emitted result will be `nil`. If the two sequences are equivalent, + /// both members of the emitted result will be `nil`. + /// + /// - Parameters: + /// - possibleMirror: A sequence to compare to this sequence. + /// - areEquivalent: A predicate that returns `true` if its two arguments + /// are equivalent; otherwise, `false`. + /// - Returns: A two-element tuple containing, upon finding the earliest + /// diverging elements between this sequence and `possibleMirror`, those + /// differing elements. If at least one of the sequences ends before a + /// difference is found, the corresponding member of the returned tuple is + /// `nil`. + /// + /// - Complexity: O(*m*), where *m* is the lesser of the length of this + /// sequence and the length of `possibleMirror`. + public func firstDelta( + against possibleMirror: PossibleMirror, + by areEquivalent: (Element, PossibleMirror.Element) throws -> Bool + ) rethrows -> (Element?, PossibleMirror.Element?) { + var iterator1 = makeIterator(), iterator2 = possibleMirror.makeIterator() + while true { + switch (iterator1.next(), iterator2.next()) { + case let (element1?, element2?) where try areEquivalent(element1, element2): + continue + case let (next1, next2): + return (next1, next2) + } + } + } +} + +//===----------------------------------------------------------------------===// +// firstDelta(against:) +//===----------------------------------------------------------------------===// + +extension Sequence where Element: Equatable { + /// Returns the first non-equal element pair found when comparing this + /// sequence to the given sequence element-wise. + /// + /// If one sequence is a proper prefix of the other, its corresponding member + /// in the emitted result will be `nil`. If the two sequences are equal, both + /// members of the emitted result will be `nil`. + /// + /// - Parameters: + /// - possibleMirror: A sequence to compare to this sequence. + /// - Returns: A two-element tuple containing, upon finding the earliest + /// diverging elements between this sequence and `possibleMirror`, those + /// differing elements. If at least one of the sequences ends before a + /// difference is found, the corresponding member of the returned tuple is + /// `nil`. + /// + /// - Complexity: O(*m*), where *m* is the lesser of the length of this + /// sequence and the length of `possibleMirror`. + @inlinable + public func firstDelta( + against possibleMirror: PossibleMirror + ) -> (Element?, Element?) where PossibleMirror.Element == Element { + return firstDelta(against: possibleMirror, by: ==) + } +} + +//===----------------------------------------------------------------------===// +// diverges(from: by:) +//===----------------------------------------------------------------------===// + +extension Collection { + /// Finds the longest common prefix between this collection and the given + /// collection, using the given predicate as the equivalence test, returning + /// the past-the-end indexes of the respective subsequences. + /// + /// The predicate must be a *equivalence relation* over the elements. That + /// is, for any elements `a`, `b`, and `c`, the following conditions must + /// hold: + /// + /// - `areEquivalent(a, a)` is always `true`. (Reflexivity) + /// - `areEquivalent(a, b)` implies `areEquivalent(b, a)`. (Symmetry) + /// - If `areEquivalent(a, b)` and `areEquivalent(b, c)` are both `true`, then + /// `areEquivalent(a, c)` is also `true`. (Transitivity) + /// + /// If one collection is a proper prefix of the other, its corresponding + /// member in the emitted result will be its source's `endIndex`. If the two + /// collections are equivalent, both members of the emitted result will be + /// their sources' respective `endIndex`. + /// + /// - Parameters: + /// - possibleMirror: A collection to compare to this collection. + /// - areEquivalent: A predicate that returns `true` if its two arguments + /// are equivalent; otherwise, `false`. + /// - Returns: A two-element tuple `(x, y)` where *x* and *y* are the largest + /// indices such that + /// `self[..( + from possibleMirror: PossibleMirror, + by areEquivalent: (Element, PossibleMirror.Element) throws -> Bool + ) rethrows -> (Index, PossibleMirror.Index) { + let (index1, index2) = try indices.firstDelta(against: possibleMirror.indices) { + try areEquivalent(self[$0], possibleMirror[$1]) + } + return (index1 ?? endIndex, index2 ?? possibleMirror.endIndex) + } +} + +//===----------------------------------------------------------------------===// +// diverges(from:) +//===----------------------------------------------------------------------===// + +extension Collection where Element: Equatable { + /// Finds the longest common prefix between this collection and the given + /// collection, returning the past-the-end indexes of the respective + /// subsequences. + /// + /// If one collection is a proper prefix of the other, its corresponding + /// member in the emitted result will be its source's `endIndex`. If the two + /// collections are equal, both members of the emitted result will be their + /// sources' respective `endIndex`. + /// + /// - Parameters: + /// - possibleMirror: A collection to compare to this collection. + /// - Returns: A two-element tuple `(x, y)` where *x* and *y* are the largest + /// indices such that `self[..( + from possibleMirror: PossibleMirror + ) -> (Index, PossibleMirror.Index) where PossibleMirror.Element == Element { + return diverges(from: possibleMirror, by: ==) + } +} + +//===----------------------------------------------------------------------===// +// converges(with: by:) +//===----------------------------------------------------------------------===// + +extension BidirectionalCollection { + /// Finds the longest common suffix between this collection and the given + /// collection, using the given predicate as the equivalence test, returning + /// the starting indexes of the respective subsequences. + /// + /// The predicate must be a *equivalence relation* over the elements. That + /// is, for any elements `a`, `b`, and `c`, the following conditions must + /// hold: + /// + /// - `areEquivalent(a, a)` is always `true`. (Reflexivity) + /// - `areEquivalent(a, b)` implies `areEquivalent(b, a)`. (Symmetry) + /// - If `areEquivalent(a, b)` and `areEquivalent(b, c)` are both `true`, then + /// `areEquivalent(a, c)` is also `true`. (Transitivity) + /// + /// If one collection is a proper suffix of the other, its corresponding + /// member in the emitted result will be its source's `startIndex`. If the + /// two collections are equivalent, both members of the emitted result will be + /// their sources' respective `startIndex`. + /// + /// - Parameters: + /// - possibleMirror: A collection to compare to this collection. + /// - areEquivalent: A predicate that returns `true` if its two arguments + /// are equivalent; otherwise, `false`. + /// - Returns: A two-element tuple `(x, y)` where *x* and *y* are the + /// smallest indices such that + /// `self[x...].elementsEqual(possibleMirror[y...], by: areEquivalent)` is + /// `true`. Either one or both members may be its source's `startIndex`. + /// + /// - Complexity: O(*m*), where *m* is the lesser of the length of this + /// collection and the length of `possibleMirror`. + @inlinable + public func converges( + with possibleMirror: PossibleMirror, + by areEquivalent: (Element, PossibleMirror.Element) throws -> Bool + ) rethrows -> (Index, PossibleMirror.Index) { + let (reversed1, reversed2) = try reversed().diverges(from: possibleMirror.reversed(), by: areEquivalent) + return (reversed1.base, reversed2.base) + } +} + +//===----------------------------------------------------------------------===// +// converges(with:) +//===----------------------------------------------------------------------===// + +extension BidirectionalCollection where Element: Equatable { + /// Finds the longest common suffix between this collection and the given + /// collection, returning the starting indexes of the respective subsequences. + /// + /// If one collection is a proper suffix of the other, its corresponding + /// member in the emitted result will be its source's `startIndex`. If the + /// two collections are equal, both members of the emitted result will be + /// their sources' respective `startIndex`. + /// + /// - Parameters: + /// - possibleMirror: A collection to compare to this collection. + /// - Returns: A two-element tuple `(x, y)` where *x* and *y* are the + /// smallest indices such that + /// `self[x...].elementsEqual(possibleMirror[y...])` is `true`. Either one + /// or both members may be its source's `startIndex`. + /// + /// - Complexity: O(*m*), where *m* is the lesser of the length of this + /// collection and the length of `possibleMirror`. + @inlinable + public func converges( + with possibleMirror: PossibleMirror + ) -> (Index, PossibleMirror.Index) where PossibleMirror.Element == Element { + return converges(with: possibleMirror, by: ==) + } +} diff --git a/Tests/SwiftAlgorithmsTests/FirstDeltaTests.swift b/Tests/SwiftAlgorithmsTests/FirstDeltaTests.swift new file mode 100644 index 00000000..dc8550e8 --- /dev/null +++ b/Tests/SwiftAlgorithmsTests/FirstDeltaTests.swift @@ -0,0 +1,148 @@ +//===----------------------------------------------------------------------===// +// +// 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 +import Algorithms + +/// Unit tests for the `firstDelta`, `diverges`, and `converges` methods. +final class FirstDeltaTests: XCTestCase { + /// Check two empty sequences. + func testEmptyVsEmpty() { + let empty = EmptyCollection() + let dualEmptyDelta = empty.firstDelta(against: empty) + XCTAssertNil(dualEmptyDelta.0) + XCTAssertNil(dualEmptyDelta.1) + + let dualEmptyDivergence = empty.diverges(from: empty) + XCTAssertEqual(dualEmptyDivergence.0, 0) + XCTAssertEqual(dualEmptyDivergence.1, 0) + + let dualEmptyConvergence = empty.converges(with: empty) + XCTAssertEqual(dualEmptyConvergence.0, 0) + XCTAssertEqual(dualEmptyConvergence.1, 0) + } + + /// Check an empty sequence and a non-empty one. + func testExactlyOneEmpty() { + let empty = EmptyCollection(), single = CollectionOfOne(1.1) + let emptySingleDelta = empty.firstDelta(against: single) + XCTAssertNil(emptySingleDelta.0) + XCTAssertEqual(emptySingleDelta.1, 1.1) + + let singleEmptyDelta = single.firstDelta(against: empty) + XCTAssertEqual(singleEmptyDelta.0, 1.1) + XCTAssertNil(singleEmptyDelta.1) + + let emptySingleDivergence = empty.diverges(from: single), + singleEmptyDivergence = single.diverges(from: empty) + XCTAssertEqual(emptySingleDivergence.0, empty.endIndex) + XCTAssertEqual(emptySingleDivergence.1, single.startIndex) + XCTAssertEqual(singleEmptyDivergence.0, single.startIndex) + XCTAssertEqual(singleEmptyDivergence.1, empty.endIndex) + + let emptySingleConvergence = empty.converges(with: single), + singleEmptyConvergence = single.converges(with: empty) + XCTAssertEqual(emptySingleConvergence.0, empty.startIndex) + XCTAssertEqual(emptySingleConvergence.1, single.endIndex) + XCTAssertEqual(singleEmptyConvergence.0, single.endIndex) + XCTAssertEqual(singleEmptyConvergence.1, empty.startIndex) + } + + /// Check identical non-empty sequences. + func testIdenticalNonempty() { + let single = CollectionOfOne(2.2) + let dualSingleDelta = single.firstDelta(against: single) + XCTAssertNil(dualSingleDelta.0) + XCTAssertNil(dualSingleDelta.1) + + let multiple = [3.3, 4.4, 5.5, 6.6, 7.7] + let dualMultipleDelta = multiple.firstDelta(against: multiple) + XCTAssertNil(dualMultipleDelta.0) + XCTAssertNil(dualMultipleDelta.1) + + let dualSingleDivergence = single.diverges(from: single), + dualMultipleDivergence = multiple.diverges(from: multiple) + XCTAssertEqual(dualSingleDivergence.0, single.endIndex) + XCTAssertEqual(dualSingleDivergence.1, single.endIndex) + XCTAssertEqual(dualMultipleDivergence.0, multiple.endIndex) + XCTAssertEqual(dualMultipleDivergence.1, multiple.endIndex) + + let dualSingleConvergence = single.converges(with: single), + dualMultipleConvergence = multiple.converges(with: multiple) + XCTAssertEqual(dualSingleConvergence.0, single.startIndex) + XCTAssertEqual(dualSingleConvergence.1, single.startIndex) + XCTAssertEqual(dualMultipleConvergence.0, multiple.startIndex) + XCTAssertEqual(dualMultipleConvergence.1, multiple.startIndex) + } + + /// Check a non-empty sequence and its prefix. + func testPrefix() { + let short = [1.1, 2.2, 3.3], long = short + [4.4, 5.5, 6.6, 7.7] + let shortLongDelta = short.firstDelta(against: long) + XCTAssertNil(shortLongDelta.0) + XCTAssertEqual(shortLongDelta.1, 4.4) + + let longShortDelta = long.firstDelta(against: short) + XCTAssertEqual(longShortDelta.0, 4.4) + XCTAssertNil(longShortDelta.1) + + let shortLongDivergence = short.diverges(from: long), + longShortDivergence = long.diverges(from: short) + XCTAssertEqual(shortLongDivergence.0, short.endIndex) + XCTAssertEqual(shortLongDivergence.1, long.indices.dropFirst(3).first) + XCTAssertEqual(longShortDivergence.0, long.indices.dropFirst(3).first) + XCTAssertEqual(longShortDivergence.1, short.endIndex) + } + + /// Check non-empty sequences with a shared prefix. + func testSharedPrefix() { + let sample1 = [2.2, 4.4, 6.6, 8.8], sample2 = [2.2, 4.4, 8.8, 16.16] + let samples12Delta = sample1.firstDelta(against: sample2) + XCTAssertEqual(samples12Delta.0, 6.6) + XCTAssertEqual(samples12Delta.1, 8.8) + + let samples12Divergence = sample1.diverges(from: sample2) + XCTAssertEqual(samples12Divergence.0, 2) + XCTAssertEqual(samples12Divergence.1, 2) + } + + /// Check non-empty sequences with nothing in common. + func testUnrelatedNonempty() { + let sample1 = [2.2, 4.4, 6.6, 8.8], sample2 = [1.1, 3.3, 9.9, 27.27] + let samples12Delta = sample1.firstDelta(against: sample2) + XCTAssertEqual(samples12Delta.0, 2.2) + XCTAssertEqual(samples12Delta.1, 1.1) + + let samples12Divergence = sample1.diverges(from: sample2) + XCTAssertEqual(samples12Divergence.0, sample1.startIndex) + XCTAssertEqual(samples12Divergence.1, sample2.startIndex) + + let samples12Convergence = sample1.converges(with: sample2) + XCTAssertEqual(samples12Convergence.0, sample1.endIndex) + XCTAssertEqual(samples12Convergence.1, sample2.endIndex) + } + + /// Check non-empty sequences for their suffixes. + func testSuffixes() { + let short = [4.4, 5.5, 6.6, 7.7], long = [1.1, 2.2, 3.3] + short + let shortLongConvergence = short.converges(with: long), + longShortConvergence = long.converges(with: short) + XCTAssertEqual(shortLongConvergence.0, short.startIndex) + XCTAssertEqual(shortLongConvergence.1, long.indices.dropFirst(3).first) + XCTAssertEqual(longShortConvergence.0, long.indices.dropFirst(3).first) + XCTAssertEqual(longShortConvergence.1, short.startIndex) + + let sample1 = [7.7, 9.9, 11.11, 13.13], sample2 = [5.5, 7.7, 11.11, 13.13] + let samples12Convergence = sample1.converges(with: sample2) + XCTAssertEqual(samples12Convergence.0, 2) + XCTAssertEqual(samples12Convergence.1, 2) + } +}