From d3dde3302f0539718a2b682d9762b04d1632c3d8 Mon Sep 17 00:00:00 2001 From: Daryle Walker Date: Mon, 26 Oct 2020 15:02:46 -0400 Subject: [PATCH 1/4] Add method for the first divergence between sequences Add a method to sequences that takes another sequence and an equivalence predicate to return the corresponding elements of where the sequences initially don't match. If at least one sequence runs out before a mismatch, its corresponding return value is NIL. Add an overload that defaults comparison to the standard equality operator. --- CHANGELOG.md | 5 +- Guides/FirstDelta.md | 47 ++++++++++ README.md | 4 + Sources/Algorithms/FirstDelta.swift | 90 +++++++++++++++++++ .../FirstDeltaTests.swift | 77 ++++++++++++++++ 5 files changed, 222 insertions(+), 1 deletion(-) create mode 100644 Guides/FirstDelta.md create mode 100644 Sources/Algorithms/FirstDelta.swift create mode 100644 Tests/SwiftAlgorithmsTests/FirstDeltaTests.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index 84f0a457..8f7e77e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,10 @@ 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. --- diff --git a/Guides/FirstDelta.md b/Guides/FirstDelta.md new file mode 100644 index 00000000..26decda6 --- /dev/null +++ b/Guides/FirstDelta.md @@ -0,0 +1,47 @@ +# First Delta + +[[Source](../Sources/Algorithms/FirstDelta.swift) | + [Tests](../Tests/SwiftAlgorithmsTests/FirstDeltaTests.swift)] + +Methods for finding the first place that two sequences differ. + +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 +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 +} +``` + +### 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..d41e94a4 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,10 @@ 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. + #### 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..ae46f454 --- /dev/null +++ b/Sources/Algorithms/FirstDelta.swift @@ -0,0 +1,90 @@ +//===----------------------------------------------------------------------===// +// +// 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: ==) + } +} diff --git a/Tests/SwiftAlgorithmsTests/FirstDeltaTests.swift b/Tests/SwiftAlgorithmsTests/FirstDeltaTests.swift new file mode 100644 index 00000000..5390b271 --- /dev/null +++ b/Tests/SwiftAlgorithmsTests/FirstDeltaTests.swift @@ -0,0 +1,77 @@ +//===----------------------------------------------------------------------===// +// +// 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` 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) + } + + /// 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) + } + + /// 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) + } + + /// 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) + } + + /// 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) + } + + /// 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) + } +} From 1b89a8271b173a1674a76361ba748d85cb0bc3f5 Mon Sep 17 00:00:00 2001 From: Daryle Walker Date: Mon, 26 Oct 2020 20:09:36 -0400 Subject: [PATCH 2/4] Add method for the first divergence between collections Add a method to collections that takes another collection and an equivalence predicate to return the indexes for the corresponding elements of where the collections initially don't match. If at least one collection runs out before a mismatch, its corresponding return value is its source's past-the-end index. Add an overload that defaults comparison to the standard equality operator. --- CHANGELOG.md | 4 +- Guides/FirstDelta.md | 17 +++- README.md | 1 + Sources/Algorithms/FirstDelta.swift | 81 +++++++++++++++++++ .../FirstDeltaTests.swift | 35 +++++++- 5 files changed, 135 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f7e77e1..4774cd80 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,9 @@ package updates, you can specify your package dependency using ### Additions - The `firstDelta(against: by:)` and `firstDelta(against:)` methods have been - added. They find where two sequences start to differ. + added. They find where two sequences start to differ. The + `diverges(from: by:)` and `diverges(from:)` methods are the analogs for + collections. --- diff --git a/Guides/FirstDelta.md b/Guides/FirstDelta.md index 26decda6..4def799d 100644 --- a/Guides/FirstDelta.md +++ b/Guides/FirstDelta.md @@ -3,7 +3,8 @@ [[Source](../Sources/Algorithms/FirstDelta.swift) | [Tests](../Tests/SwiftAlgorithmsTests/FirstDeltaTests.swift)] -Methods for finding the first place that two sequences differ. +Methods for finding the first place that two sequences differ. There are +variants for when both sequences are collections. The methods for finding the differences in sequences can be viewed as the common core operation for the Standard Library's `elementsEqual(_: by:)` and @@ -14,6 +15,7 @@ common core operation for the Standard Library's `elementsEqual(_: by:)` and ## Detailed Design The element-returning methods are declared as extensions to `Sequence`. The +index-returning methods are declared as extensions to `Collection`. 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`. @@ -31,6 +33,19 @@ extension Sequence where Element: Equatable { 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 +} ``` ### Complexity diff --git a/README.md b/README.md index d41e94a4..5a9f04cf 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ Read more about the package, and the intent behind it, in the [announcement on s #### 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. #### Other useful operations diff --git a/Sources/Algorithms/FirstDelta.swift b/Sources/Algorithms/FirstDelta.swift index ae46f454..bb217e80 100644 --- a/Sources/Algorithms/FirstDelta.swift +++ b/Sources/Algorithms/FirstDelta.swift @@ -88,3 +88,84 @@ extension Sequence where Element: Equatable { 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 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`. + /// - 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: ==) + } +} diff --git a/Tests/SwiftAlgorithmsTests/FirstDeltaTests.swift b/Tests/SwiftAlgorithmsTests/FirstDeltaTests.swift index 5390b271..052603a8 100644 --- a/Tests/SwiftAlgorithmsTests/FirstDeltaTests.swift +++ b/Tests/SwiftAlgorithmsTests/FirstDeltaTests.swift @@ -12,7 +12,7 @@ import XCTest import Algorithms -/// Unit tests for the `firstDelta` methods. +/// Unit tests for the `firstDelta` and `diverges` methods. final class FirstDeltaTests: XCTestCase { /// Check two empty sequences. func testEmptyVsEmpty() { @@ -20,6 +20,10 @@ final class FirstDeltaTests: XCTestCase { 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) } /// Check an empty sequence and a non-empty one. @@ -32,6 +36,13 @@ final class FirstDeltaTests: XCTestCase { 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) } /// Check identical non-empty sequences. @@ -45,6 +56,13 @@ final class FirstDeltaTests: XCTestCase { 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) } /// Check a non-empty sequence and its prefix. @@ -57,6 +75,13 @@ final class FirstDeltaTests: XCTestCase { 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. @@ -65,6 +90,10 @@ final class FirstDeltaTests: XCTestCase { 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. @@ -73,5 +102,9 @@ final class FirstDeltaTests: XCTestCase { 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) } } From aac070578104ecae99d92d17242afc3917eeaad6 Mon Sep 17 00:00:00 2001 From: Daryle Walker Date: Mon, 26 Oct 2020 21:14:05 -0400 Subject: [PATCH 3/4] Fix copy & paste error --- Sources/Algorithms/FirstDelta.swift | 5 ----- 1 file changed, 5 deletions(-) diff --git a/Sources/Algorithms/FirstDelta.swift b/Sources/Algorithms/FirstDelta.swift index bb217e80..328503e6 100644 --- a/Sources/Algorithms/FirstDelta.swift +++ b/Sources/Algorithms/FirstDelta.swift @@ -151,11 +151,6 @@ extension Collection where Element: Equatable { /// /// - Parameters: /// - possibleMirror: A collection to compare to this collection. - /// - 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`. /// - Returns: A two-element tuple `(x, y)` where *x* and *y* are the largest /// indices such that `self[.. Date: Mon, 26 Oct 2020 22:03:29 -0400 Subject: [PATCH 4/4] Add method for the last convergence between collections Add a method to bidirectional collections that takes another bidirectional collection and an equivalence predicate to return the indexes for the corresponding elements of where the collections permanently start matching. If at least one collection runs out before a backwards-searching mismatch, its corresponding return value is its source's starting index. Add an overload that defaults comparison to the standard equality operator. --- CHANGELOG.md | 3 +- Guides/FirstDelta.md | 24 +++++- README.md | 1 + Sources/Algorithms/FirstDelta.swift | 74 +++++++++++++++++++ .../FirstDeltaTests.swift | 40 +++++++++- 5 files changed, 136 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4774cd80..85462fab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,7 +17,8 @@ package updates, you can specify your package dependency using - 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. + 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 index 4def799d..5a4116ce 100644 --- a/Guides/FirstDelta.md +++ b/Guides/FirstDelta.md @@ -4,7 +4,9 @@ [Tests](../Tests/SwiftAlgorithmsTests/FirstDeltaTests.swift)] Methods for finding the first place that two sequences differ. There are -variants for when both sequences are collections. +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 @@ -16,9 +18,10 @@ common core operation for the Standard Library's `elementsEqual(_: by:)` and The element-returning methods are declared as extensions to `Sequence`. The index-returning methods are declared as extensions to `Collection`. 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`. +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 { @@ -46,6 +49,19 @@ extension Collection where Element: Equatable { 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 diff --git a/README.md b/README.md index 5a9f04cf..3ece9543 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,7 @@ Read more about the package, and the intent behind it, in the [announcement on s - [`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 diff --git a/Sources/Algorithms/FirstDelta.swift b/Sources/Algorithms/FirstDelta.swift index 328503e6..ce33f601 100644 --- a/Sources/Algorithms/FirstDelta.swift +++ b/Sources/Algorithms/FirstDelta.swift @@ -164,3 +164,77 @@ extension Collection where Element: Equatable { 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 index 052603a8..dc8550e8 100644 --- a/Tests/SwiftAlgorithmsTests/FirstDeltaTests.swift +++ b/Tests/SwiftAlgorithmsTests/FirstDeltaTests.swift @@ -12,7 +12,7 @@ import XCTest import Algorithms -/// Unit tests for the `firstDelta` and `diverges` methods. +/// Unit tests for the `firstDelta`, `diverges`, and `converges` methods. final class FirstDeltaTests: XCTestCase { /// Check two empty sequences. func testEmptyVsEmpty() { @@ -24,6 +24,10 @@ final class FirstDeltaTests: XCTestCase { 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. @@ -43,6 +47,13 @@ final class FirstDeltaTests: XCTestCase { 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. @@ -63,6 +74,13 @@ final class FirstDeltaTests: XCTestCase { 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. @@ -106,5 +124,25 @@ final class FirstDeltaTests: XCTestCase { 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) } }