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

Add ends(with:) #224

Open
wants to merge 1 commit 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
37 changes: 37 additions & 0 deletions Guides/EndsWith.md
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.
84 changes: 84 additions & 0 deletions Sources/Algorithms/EndsWith.swift
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"
Comment on lines +25 to +29
Copy link
Member

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?

Copy link
Contributor Author

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 should starts(with:) to match.

Copy link
Contributor Author

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.

///
/// 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>(
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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>(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

with possibleSuffix: PossibleSuffix,
by areEquivalent: (Element, PossibleSuffix.Element) throws -> Bool
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since equivalence is dictated by areEquivalent does this need to be constrained to element is Equatable(BidirectionalCollection where Element: Equatable)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

) rethrows -> Bool {
try self.reversed().starts(with: possibleSuffix.reversed(), by: areEquivalent)
}
}

96 changes: 96 additions & 0 deletions Tests/SwiftAlgorithmsTests/EndsWithTests.swift
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
}
}