Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

First Mismatching Elements, or Indexes Thereof #37

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

---

Expand Down
78 changes: 78 additions & 0 deletions Guides/FirstDelta.md
Original file line number Diff line number Diff line change
@@ -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<PossibleMirror: Sequence>(
against possibleMirror: PossibleMirror,
by areEquivalent: (Element, PossibleMirror.Element) throws -> Bool
) rethrows -> (Element?, PossibleMirror.Element?)
}

extension Sequence where Element: Equatable {
func firstDelta<PossibleMirror: Sequence>(
against possibleMirror: PossibleMirror
) -> (Element?, Element?) where PossibleMirror.Element == Element
}

extension Collection {
func diverges<PossibleMirror: Collection>(
from possibleMirror: PossibleMirror,
by areEquivalent: (Element, PossibleMirror.Element) throws -> Bool
) rethrows -> (Index, PossibleMirror.Index)
}

extension Collection where Element: Equatable {
func diverges<PossibleMirror: Collection>(
from possibleMirror: PossibleMirror
) -> (Index, PossibleMirror.Index) where PossibleMirror.Element == Element
}

extension BidirectionalCollection {
func converges<PossibleMirror: BidirectionalCollection>(
with possibleMirror: PossibleMirror,
by areEquivalent: (Element, PossibleMirror.Element) throws -> Bool
) rethrows -> (Index, PossibleMirror.Index)
}

extension BidirectionalCollection where Element: Equatable {
func converges<PossibleMirror: BidirectionalCollection>(
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 `<algorithm>` library defines the `mismatch` function, which has
similar semantics to `firstDelta`.

(To-do: add other languages.)
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
240 changes: 240 additions & 0 deletions Sources/Algorithms/FirstDelta.swift
Original file line number Diff line number Diff line change
@@ -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<PossibleMirror: Sequence>(
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<PossibleMirror: Sequence>(
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[..<x].elementsEqual(possibleMirror[..<y], by: areEquivalent)` is
/// `true`. Either one or both members may be its source's `endIndex`.
///
/// - Complexity: O(*m*), where *m* is the lesser of the length of this
/// collection and the length of `possibleMirror`.
@inlinable
public func diverges<PossibleMirror: Collection>(
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[..<x].elementsEqual(possibleMirror[..<y])` is
/// `true`. Either one or both members may be its source's `endIndex`.
///
/// - Complexity: O(*m*), where *m* is the lesser of the length of this
/// collection and the length of `possibleMirror`.
@inlinable
public func diverges<PossibleMirror: Collection>(
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<PossibleMirror: BidirectionalCollection>(
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<PossibleMirror: BidirectionalCollection>(
with possibleMirror: PossibleMirror
) -> (Index, PossibleMirror.Index) where PossibleMirror.Element == Element {
return converges(with: possibleMirror, by: ==)
}
}
Loading