-
Notifications
You must be signed in to change notification settings - Fork 440
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
Add ends(with:)
#224
base: main
Are you sure you want to change the base?
Add ends(with:)
#224
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
# EndsWith | ||
|
||
[[Source](https://github.com/apple/swift-algorithms/blob/main/Sources/Algorithms/EndsWith.swift) | | ||
[Tests](https://github.com/apple/swift-algorithms/blob/main/Tests/SwiftAlgorithmsTests/EndsWithTests.swift)] | ||
|
||
This function checks whether the final elements of the one collection are the same as the elements in another collection. | ||
``` | ||
## Detailed Design | ||
The `ends(with:)` and `ends(with:by:)` functions are added as methods on an extension of | ||
`BidirectionalCollection`. | ||
```swift | ||
extension BidirectionalCollection { | ||
public func ends<PossibleSuffix: BidirectionalCollection>( | ||
with possibleSuffix: PossibleSuffix | ||
) -> Bool where PossibleSuffix.Element == Element | ||
public func ends<PossibleSuffix: BidirectionalCollection>( | ||
with possibleSuffix: PossibleSuffix, | ||
by areEquivalent: (Element, PossibleSuffix.Element) throws -> Bool | ||
) rethrows -> Bool | ||
} | ||
``` | ||
|
||
This method requires `BidirectionalCollection` for being able to traverse back from the end of the collection. It also requires the `possibleSuffix` to be `BidirectionalCollection`, because it too needs to be traverse backwards, to compare its elements against `self` from back to front. | ||
|
||
### Complexity | ||
|
||
O(*m*), where *m* is the lesser of the length of the collection and the length of `possibleSuffix`. | ||
|
||
### Naming | ||
|
||
The function's name resembles that of an existing Swift function | ||
`starts(with:)`, which performs same operation however in the forward direction | ||
of the collection. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
//===----------------------------------------------------------------------===// | ||
// | ||
// 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 | ||
// | ||
//===----------------------------------------------------------------------===// | ||
|
||
//===----------------------------------------------------------------------===// | ||
// EndsWith | ||
//===----------------------------------------------------------------------===// | ||
|
||
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 or an empty collection 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 initial 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>( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
with possibleSuffix: PossibleSuffix | ||
) -> Bool where PossibleSuffix.Element == Element { | ||
return self.ends(with: possibleSuffix, by: ==) | ||
} | ||
} | ||
|
||
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 initial 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>( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
with possibleSuffix: PossibleSuffix, | ||
by areEquivalent: (Element, PossibleSuffix.Element) throws -> Bool | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since equivalence is dictated by There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice catch! I moved this overload into its own unconstrained extension. |
||
) rethrows -> Bool { | ||
try self.reversed().starts(with: possibleSuffix.reversed(), by: areEquivalent) | ||
} | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
//===----------------------------------------------------------------------===// | ||
// | ||
// 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 EndsWithTests: XCTestCase { | ||
amomchilov marked this conversation as resolved.
Show resolved
Hide resolved
|
||
func testEndsWithCorrectSuffix() { | ||
let a = 8...10 | ||
let b = 1...10 | ||
|
||
XCTAssertTrue(b.ends(with: a)) | ||
} | ||
|
||
func testDoesntEndWithWrongSuffix() { | ||
let a = 8...9 | ||
let b = 1...10 | ||
|
||
XCTAssertFalse(b.ends(with: a)) | ||
} | ||
|
||
func testDoesntEndWithTooLongSuffix() { | ||
XCTAssertFalse((2...5).ends(with: (1...10))) | ||
} | ||
|
||
func testEndsWithEmpty() { | ||
let a = 8...10 | ||
let empty = [Int]() | ||
XCTAssertTrue(a.ends(with: empty)) | ||
} | ||
|
||
func testEmptyEndsWithEmpty() { | ||
let empty = [Int]() | ||
XCTAssertTrue(empty.ends(with: empty)) | ||
} | ||
|
||
func testEmptyDoesNotEndWithNonempty() { | ||
XCTAssertFalse([].ends(with: 1...10)) | ||
} | ||
} | ||
|
||
final class EndsWithNonEquatableTests: XCTestCase { | ||
func testEndsWithCorrectSuffix() { | ||
let a = nonEq(8...10) | ||
let b = nonEq(1...10) | ||
|
||
XCTAssertTrue(b.ends(with: a, by: areEquivalent)) | ||
} | ||
|
||
func testDoesntEndWithWrongSuffix() { | ||
let a = nonEq(8...9) | ||
let b = nonEq(1...10) | ||
|
||
XCTAssertFalse(b.ends(with: a, by: areEquivalent)) | ||
} | ||
|
||
func testDoesntEndWithTooLongSuffix() { | ||
XCTAssertFalse(nonEq(2...5).ends(with: nonEq(1...10), by: areEquivalent)) | ||
} | ||
|
||
func testEndsWithEmpty() { | ||
let a = nonEq(8...10) | ||
let empty = [NotEquatable<Int>]() | ||
XCTAssertTrue(a.ends(with: empty, by: areEquivalent)) | ||
} | ||
|
||
func testEmptyEndsWithEmpty() { | ||
let empty = [NotEquatable<Int>]() | ||
XCTAssertTrue(empty.ends(with: empty, by: areEquivalent)) | ||
} | ||
|
||
func testEmptyDoesNotEndWithNonempty() { | ||
XCTAssertFalse([].ends(with: nonEq(1...10), by: areEquivalent)) | ||
} | ||
|
||
private func nonEq(_ range: ClosedRange<Int>) -> Array<NotEquatable<Int>> { | ||
range.map(NotEquatable.init) | ||
} | ||
|
||
private func areEquivalent<T: Equatable>(lhs: NotEquatable<T>, rhs: NotEquatable<T>) -> Bool { | ||
lhs.value == rhs.value | ||
} | ||
|
||
private struct NotEquatable<T> { | ||
let value: T | ||
} | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we use a string-based example here, instead of a range?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@natecook1000 I copied this doc from
starts(with:)
and modified it. I think if we change this, we shouldstarts(with:)
to match.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm good with either Arrays or Strings. Perhaps ranges are a bad example, given that some readers might confuse
..<
and...
. We can pick a more self-evident example that doesn't need to require knowledge of that distinction.