Add ends(with:) to BidirectionalCollection
- Proposal: SE-NNNN
- Author: Mateus Rodrigues
Introduction
The standard library offers starts(with:) for checking whether a sequence begins with the elements of another sequence. There is no counterpart for testing whether a collection ends with another collection. This proposal adds ends(with:) to BidirectionalCollection, adopting the API that have been shipping in swift-algorithms.
Motivation
Checking a suffix is as common as checking a prefix and NSPredicate even exposes the operation as ENDSWITH, alongsideSTARTWITH.
Today, callers either depend on swift-algorithms or write the check by hand. Moving the operation into the standard library makes it discoverable next to starts(with:) and removes the need for a package dependency for a single comparison.
The change also unblocks ends(with:) support in Foundation's Predicate, which builds its supported sequence operations on the standard library. See the related proposal here.
Proposed solution
Add an ends(with:) method to BidirectionalCollection, plus an overload for elements that are Equatable, mirroring the existing starts(with:) and starts(with:by:) methods:
let name = "Johnson"
name.ends(with: "son") // true
[1, 2, 3].ends(with: [2, 3]) // true
[1, 2, 3].ends(with: [2, 3, 4]) // false
Detailed design
The implementation comes direct from https://github.com/apple/swift-algorithms/blob/main/Sources/Algorithms/EndsWith.swift:
extension BidirectionalCollection {
/// Returns a Boolean value indicating whether the final elements of the
/// collection are equivalent to the elements in another collection, using
/// the given predicate as the equivalence test.
///
/// The predicate must be an *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)
///
/// - Parameters:
/// - possibleSuffix: A collection to compare to this collection.
/// - areEquivalent: A predicate that returns `true` if its two arguments
/// are equivalent; otherwise, `false`.
/// - Returns: `true` if the final elements of the collection are equivalent
/// to the elements of `possibleSuffix`; otherwise, `false`. If
/// `possibleSuffix` has no elements, the return value is `true`.
///
/// - Complexity: O(*m*), where *m* is the lesser of the length of the
/// collection and the length of `possibleSuffix`.
@inlinable
public func ends<PossibleSuffix: BidirectionalCollection>(
with possibleSuffix: PossibleSuffix,
by areEquivalent: (Element, PossibleSuffix.Element) throws -> Bool
) rethrows -> Bool {
try self
.reversed()
.starts(with: possibleSuffix.reversed(), by: areEquivalent)
}
}
extension BidirectionalCollection where Element: Equatable {
/// Returns a Boolean value indicating whether the final elements of the
/// collection are the same as the elements in another collection.
///
/// This example tests whether one countable range ends with the elements
/// of another countable range.
///
/// let a = 8...10
/// let b = 1...10
///
/// print(b.ends(with: a))
/// // Prints "true"
///
/// Passing a collection with no elements as `possibleSuffix` always results
/// in `true`.
///
/// print(b.ends(with: []))
/// // Prints "true"
///
/// - Parameter possibleSuffix: A collection to compare to this collection.
/// - Returns: `true` if the final elements of the collection are the same
/// as the elements of `possibleSuffix`; otherwise, `false`. If
/// `possibleSuffix` has no elements, the return value is `true`.
///
/// - Complexity: O(*m*), where *m* is the lesser of the length of the
/// collection and the length of `possibleSuffix`.
@inlinable
public func ends<PossibleSuffix: BidirectionalCollection>(
with possibleSuffix: PossibleSuffix
) -> Bool where PossibleSuffix.Element == Element {
self.ends(with: possibleSuffix, by: ==)
}
}
Source compatibility
Both ends(with:) and ends(with:by:) are new methods within the Standard Library; adding them is a source compatible change. Developers who currently import these methods from swift-algorithms (or define their own implementation with the same name), the imported/custom methods will still work, due to the shadowing rule for Standard Library names.
ABI compatibility
This proposal is purely additive to the Standard Library's ABI; the addition does not break any existing binary.
Alternatives considered
Move the whole set of counterparts from swift-algorithms
A small set of algorithms in swift-algorithms are near counterparts of existing APIs in the standard library:
| Swift Standard Library | Swift Algorithm |
|---|---|
| starts(with:) | ends(with:) |
| prefix(while:) | suffix(while:) |
| trimmingPrefix(while:) | trimmingSuffix(while:) |
This proposal describes ends(with:) only, but does not rule out adding the remaining algorithms, either as part of this proposal or in follow-ups.