Skip to content

Commit

Permalink
Add contains(countIn:where:) and related functions to Sequence
Browse files Browse the repository at this point in the history
  • Loading branch information
mdznr committed Jan 15, 2021
1 parent 5b7993c commit 77d9b3e
Show file tree
Hide file tree
Showing 3 changed files with 498 additions and 0 deletions.
87 changes: 87 additions & 0 deletions Guides/ContainsCountWhere.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# Contains Count Where

[[Source](https://github.com/apple/swift-algorithms/blob/main/Sources/Algorithms/ContainsCountWhere.swift) |
[Tests](https://github.com/apple/swift-algorithms/blob/main/Tests/SwiftAlgorithmsTests/ContainsCountWhereTests.swift)]

Returns whether or not a sequence has a particular number of elements matching a given criteria.

If you need to compare the count of a filtered sequence, using this method can give you a performance boost over filtering the entire collection, then comparing its count.

```swift
let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
print(numbers.contains(atLeast: 2, where: { $0.isMultiple(of: 3) }))
// prints "true"
```

These functions can return for _some_ infinite sequences with _some_ predicates whereas `filter(_:)` followed by `count` can’t ever do that, resulting in an infinite loop. For example, finding if there are more than 500 prime numbers with four digits (base 10). Note that there are 1,061 prime numbers with four digits, significantly more than 500.

```swift
// NOTE: Replace `primes` with a real infinite prime number `Sequence`.
let primes = [2, 3, 5, 7, 11, 13, 17, 19, 23,
print(primes.contains(moreThan: 500, where: { String($0).count == 4 }))
// prints "true"
```

## Detailed Design

A function named `contains(countIn:where:)` added as an extension to `Sequence`:

```swift
extension Sequence {
public func contains<R: RangeExpression>(
countIn rangeExpression: R,
where predicate: (Element) throws -> Bool
) rethrows -> Bool where R.Bound: FixedWidthInteger
}
```

Five small wrapper functions added to make working with different ranges easier and more readable at the call-site:

```swift
extension Sequence {
public func contains(
exactly exactCount: Int,
where predicate: (Element) throws -> Bool
) rethrows -> Bool

public func contains(
atLeast minimumCount: Int,
where predicate: (Element) throws -> Bool
) rethrows -> Bool

public func contains(
moreThan minimumCount: Int,
where predicate: (Element) throws -> Bool
) rethrows -> Bool

public func contains(
lessThan maximumCount: Int,
where predicate: (Element) throws -> Bool
) rethrows -> Bool

public func contains(
lessThanOrEqualTo maximumCount: Int,
where predicate: (Element) throws -> Bool
) rethrows -> Bool
}
```

### Complexity

These methods are all O(_n_) in the worst case, but often return much earlier than that.

### Naming

The naming of this function is based off of the `contains(where:)` function on `Sequence` in the standard library. While the standard library function only checks for a non-zero count, these functions can check for any count.

### Comparison with other languages

Many languages have functions like Swift’s [`count(where:)`](https://github.com/apple/swift/pull/16099) function.<sup>[1](#footnote1)</sup> While these functions are useful when needing a complete count, they do not return early when simply needing to do a comparison on the count.

**C++:** The `<algorithm>` library’s [`count_if`](https://www.cplusplus.com/reference/algorithm/count_if/)

**Ruby:** [`count{|item|block}`](https://ruby-doc.org/core-1.9.3/Array.html#method-i-count)

----

<a name="footnote1">1</a>: [Temporarily removed](https://github.com/apple/swift/pull/22289#issue-249472009)
251 changes: 251 additions & 0 deletions Sources/Algorithms/ContainsCountWhere.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
//===----------------------------------------------------------------------===//
//
// 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
//
//===----------------------------------------------------------------------===//

//===----------------------------------------------------------------------===//
// contains(countIn:where:)
//===----------------------------------------------------------------------===//

extension Sequence {
/// Returns whether or not the number of elements of the sequence that satisfy
/// the given predicate fall within a given range.
///
/// The following example determines if there are multiple (at least two)
/// types of animals with “lion” in its name:
///
/// let animals = [
/// "mountain lion",
/// "lion",
/// "snow leopard",
/// "leopard",
/// "tiger",
/// "panther",
/// "jaguar"
/// ]
/// print(animals.contains(countIn: 2..., where: { $0.contains("lion") }))
/// // prints "true"
///
/// - Parameters:
/// - rangeExpression: The range of acceptable counts
/// - predicate: A closure that takes an element as its argument and returns
/// a Boolean value indicating whether the element should be included in the
/// count.
/// - Returns: Whether or not the number of elements in the sequence that
/// satisfy the given predicate is within a given range
/// - Complexity: Worst case O(*n*), where *n* is the number of elements.
@inlinable
public func contains<R: RangeExpression>(
countIn rangeExpression: R,
where predicate: (Element) throws -> Bool
) rethrows -> Bool where R.Bound: FixedWidthInteger {
let range = rangeExpression.relative(to: R.Bound.zero..<R.Bound.max)

// If the upper bound is less than the max value, iteration can stop once it
// reaches the range’s upper bound and return `false`, since the bounds have
// been exceeded.
// Otherwise, treat the range as unbounded. As soon as the count reaches the
// range’s lower bound, iteration can stop and return `true`.
let threshold: R.Bound
let thresholdReturn: Bool
if range.upperBound < R.Bound.max {
threshold = range.upperBound
thresholdReturn = false
} else {
threshold = range.lowerBound
thresholdReturn = true
}

var count: R.Bound = .zero
for element in self {
if try predicate(element) {
count += 1

// Return early if we’ve reached the threshold.
if count >= threshold {
return thresholdReturn
}
}
}

return range.contains(count)
}
}

//===----------------------------------------------------------------------===//
// contains(exactly:where:)
// contains(atLeast:where:)
// contains(moreThan:where:)
// contains(lessThan:where:)
// contains(lessThanOrEqualTo:where:)
//===----------------------------------------------------------------------===//

extension Sequence {
/// Returns whether or not an exact number of elements of the sequence satisfy
/// the given predicate.
///
/// The following example determines if there are exactly two bears:
///
/// let animals = [
/// "bear",
/// "fox",
/// "bear",
/// "squirrel",
/// "bear",
/// "moose",
/// "squirrel",
/// "elk"
/// ]
/// print(animals.contains(exactly: 2, where: { $0 == "bear" }))
/// // prints "false"
///
/// Using `contains(exactly:where:)` is faster than using `filter(where:)` and
/// comparing its `count` using `==` because this function can return early,
/// without needing to iterating through all elements to get an exact count.
/// If, and as soon as, the count exceeds 2, it returns `false`.
///
/// - Parameter exactCount: The exact number to expect
/// - Parameter predicate: A closure that takes an element as its argument and
/// returns a Boolean value indicating whether the element should be included
/// in the count.
/// - Returns: Whether or not exactly `exactCount` number of elements in the
/// sequence passed `predicate`
/// - Complexity: Worst case O(*n*), where *n* is the number of elements.
@inlinable
public func contains(
exactly exactCount: Int,
where predicate: (Element) throws -> Bool
) rethrows -> Bool {
return try self.contains(countIn: exactCount...exactCount, where: predicate)
}

/// Returns whether or not at least a given number of elements of the sequence
/// satisfy the given predicate.
///
/// The following example determines if there are at least two numbers that
/// are a multiple of 3:
///
/// let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
/// print(numbers.contains(atLeast: 2, where: { $0.isMultiple(of: 3) }))
/// // prints "true"
///
/// Using `contains(atLeast:where:)` is faster than using `filter(where:)` and
/// comparing its `count` using `>=` because this function can return early,
/// without needing to iterating through all elements to get an exact count.
/// If, and as soon as, the count reaches 2, it returns `true`.
///
/// - Parameter minimumCount: The minimum number to count before returning
/// - Parameter predicate: A closure that takes an element as its argument and
/// returns a Boolean value indicating whether the element should be included
/// in the count.
/// - Returns: Whether or not at least `minimumCount` number of elements in
/// the sequence passed `predicate`
/// - Complexity: Worst case O(*n*), where *n* is the number of elements.
@inlinable
public func contains(
atLeast minimumCount: Int,
where predicate: (Element) throws -> Bool
) rethrows -> Bool {
return try self.contains(countIn: minimumCount..., where: predicate)
}

/// Returns whether or not more than a given number of elements of the
/// sequence satisfy the given predicate.
///
/// The following example determines if there are more than two numbers that
/// are a multiple of 3:
///
/// let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
/// print(numbers.contains(moreThan: 2, where: { $0.isMultiple(of: 3) }))
/// // prints "true"
///
/// Using `contains(moreThan:where:)` is faster than using `filter(where:)`
/// and comparing its `count` using `>` because this function can return
/// early, without needing to iterating through all elements to get an exact
/// count. If, and as soon as, the count reaches 2, it returns `true`.
///
/// - Parameter minimumCount: The minimum number to count before returning
/// - Parameter predicate: A closure that takes an element as its argument and
/// returns a Boolean value indicating whether the element should be included
/// in the count.
/// - Returns: Whether or not more than `minimumCount` number of elements in
/// the sequence passed `predicate`
/// - Complexity: Worst case O(*n*), where *n* is the number of elements.
@inlinable
public func contains(
moreThan minimumCount: Int,
where predicate: (Element) throws -> Bool
) rethrows -> Bool {
return try self.contains(countIn: (minimumCount + 1)..., where: predicate)
}

/// Returns whether or not fewer than a given number of elements of the
/// sequence satisfy the given predicate.
///
/// The following example determines if there are fewer than five numbers in
/// the sequence that are multiples of 10:
///
/// let numbers = [1, 2, 5, 10, 20, 50, 100, 1, 1, 5, 2]
/// print(numbers.contains(lessThan: 5, where: { $0.isMultiple(of: 10) }))
/// // prints "true"
///
/// Using `contains(moreThan:where:)` is faster than using `filter(where:)`
/// and comparing its `count` using `>` because this function can return
/// early, without needing to iterating through all elements to get an exact
/// count. If, and as soon as, the count reaches 2, it returns `true`.
///
/// - Parameter maximumCount: The maximum number to count before returning
/// - Parameter predicate: A closure that takes an element as its argument and
/// returns a Boolean value indicating whether the element should be included
/// in the count.
/// - Returns: Whether or not less than `maximumCount` number of elements in
/// the sequence passed `predicate`
/// - Complexity: Worst case O(*n*), where *n* is the number of elements.
@inlinable
public func contains(
lessThan maximumCount: Int,
where predicate: (Element) throws -> Bool
) rethrows -> Bool {
return try self.contains(countIn: ..<maximumCount, where: predicate)
}

/// Returns whether or not the number of elements of the sequence that satisfy
/// the given predicate is less than or equal to a given number.
///
/// The following example determines if there are less than or equal to five
/// numbers in the sequence that are multiples of 10:
///
/// let numbers = [1, 2, 5, 10, 20, 50, 100, 1000, 1, 1, 5, 2]
/// print(numbers.contains(lessThanOrEqualTo: 5, where: {
/// $0.isMultiple(of: 10)
/// }))
/// // prints "true"
///
/// Using `contains(lessThanOrEqualTo:where:)` is faster than using
/// `filter(where:)` and comparing its `count` with `>` because this function
/// can return early, without needing to iterating through all elements to get
/// an exact count. If, and as soon as, the count exceeds `maximumCount`,
/// it returns `false`.
///
/// - Parameter maximumCount: The maximum number to count before returning
/// - Parameter predicate: A closure that takes an element as its argument and
/// returns a Boolean value indicating whether the element should be included
/// in the count.
/// - Returns: Whether or not the number of elements that pass `predicate` is
/// less than or equal to `maximumCount`
/// the sequence passed `predicate`
/// - Complexity: Worst case O(*n*), where *n* is the number of elements.
@inlinable
public func contains(
lessThanOrEqualTo maximumCount: Int,
where predicate: (Element) throws -> Bool
) rethrows -> Bool {
return try self.contains(countIn: ...maximumCount, where: predicate)
}
}
Loading

0 comments on commit 77d9b3e

Please sign in to comment.