Add 'single' and 'single(where:)' to Sequence

Very often when dealing with sequences or collections in Swift, there's a situation where I am expecting a collection (or filter clause) to contain a single result. Luckily, it's really simple to get a single item from a collection where you 'know' there is only one result:

let item = collection.first!  

...but, that isn't really my intent. I want the only item from the collection, and I want to know if there were more, because that would mean I have a bug:

guard collection.count == 1 else { 
    fatalError("Expected one item, but the collection contained \(collection.count)") 
}
let item = collection.first!

...so very often I end up writing an extension to do something similar. Extensions to do this are simple to write, but:

  • I end up needing them in just about any project that uses collections
  • more importantly, having it available in the standard library promotes code that is safe and clear in intent

This is a pitch to add single and single(where:) to Sequence - both asserting that the resulting sequence contains one and only one value, and returning that value. If the sequence is empty, it could either throw and have an overload to return nil, or follow first's convention and return nil by default. However, if the collection contains more than one result, it should always throw.

The name is obviously inspired by the same methods from LINQ, but I think they work well in Swift, too.

single could also be extended to take a filter predicate, similar to first(where:):

let item = collection.single { $0.iAmTheOneYouWant }

Does anyone else think this would be worth adding?

Could this be a new CollectionOfOne initializer?

extension CollectionOfOne {
  public init?<S: Sequence>(_ sequence: S) where S.Element == Element
}
2 Likes

In our project, we have a similar function, as well as functions asTuple and asTriple, which we found useful in some cases.

Not sure how useful this is in general, though.

Instead of incorporating element(s) dereference into the method, I think it may be better to segregate that to a separate step, and make the check pure:

extension Collection {

    /**
     Returns whether the count is exactly the given amount.

     Testing goes through the minimal amount of index iteration when possible.

     - Parameter n: The trial element-count to check against.

     - Returns: `n == .count`.

     - Complexity: O(1) for collections conforming to `RandomAccessCollection`, O(m) otherwise, where *m* is the smaller of `.count` and `n`.
     */
    func isCount(exactly n: Int) -> Bool {
        guard n >= 0 else { return false }

        return index(startIndex, offsetBy: n, limitedBy: endIndex) == endIndex
    }

}

var aa = Array<Int>()
aa.isCount(exactly: 0)   // true
aa.isCount(exactly: -1)  // false
aa.isCount(exactly: +1)  // false

aa = [1, 2, 3]
aa.isCount(exactly: -1)  // false
aa.isCount(exactly: 0)   // false
aa.isCount(exactly: 1)   // false
aa.isCount(exactly: 2)   // false
aa.isCount(exactly: 3)   // true
aa.isCount(exactly: 4)   // true
aa.isCount(exactly: 5)   // true

As far as I know, a 1-element test cannot be optimized to be better than a general n-element version.

A filtering version has to stop at the end of the collection or the (n + 1)th find, whichever comes first. This is nastier since we don't know where in the collection the determining index is; the performance could be O(m).

extension Sequence {

    /**
     Returns whether the count of elements matching a given predicate is exactly the given amount.

     Testing goes through the minimal amount of iteration when possible.

     - Precondition: This sequence should be finite.

     - Parameter n: The trial element-count, post-filtering, to check against.
     - Parameter isIncluded: A closure that takes an element of the sequence as its argument and returns a Boolean value indicating whether the element should be included in the processed count.

     - Returns: The same as `.filter(isIncluded).isCount(exactly: n)`.

     - Throws: Whatever `isIncluded` may throw on calls.

     - Complexity: At most O(m), where *m* is the number of elements in this sequence.
     */
    func isFilteredCount(exactly n: Int, _ isIncluded: (Element) throws -> Bool) rethrows -> Bool {
        guard n >= 0 else { return false }

        var iterator = makeIterator(), remainingMatches = n
        while let e = iterator.next() {
            if try isIncluded(e) {
                remainingMatches -= 1
                if remainingMatches < 0 {
                    return false
                }
            }
        }
        return remainingMatches == 0
    }

}

let aa = [1, 2, 3, 4, 5, 6]
aa.isFilteredCount(exactly: -1, { $0 % 2 == 0 })  // false
print(aa.isFilteredCount(exactly: 0, { $0 % 2 == 0 }))   // false
print(aa.isFilteredCount(exactly: 1, { $0 % 2 == 0 }))   // false
print(aa.isFilteredCount(exactly: 2, { $0 % 2 == 0 }))   // false
print(aa.isFilteredCount(exactly: 3, { $0 % 2 == 0 }))   // true
print(aa.isFilteredCount(exactly: 4, { $0 % 2 == 0 }))   // false
print(aa.isFilteredCount(exactly: 5, { $0 % 2 == 0 }))   // false
print(aa.isFilteredCount(exactly: 6, { $0 % 2 == 0 }))   // false
print(aa.isFilteredCount(exactly: 7, { $0 % 2 == 0 }))   // false
print()
aa.isFilteredCount(exactly: -1, { $0 > 10 })  // false
print(aa.isFilteredCount(exactly: 0, { $0 > 10 }))   // true
print(aa.isFilteredCount(exactly: 1, { $0 > 10 }))   // false
print(aa.isFilteredCount(exactly: 2, { $0 > 10 }))   // false
print(aa.isFilteredCount(exactly: 3, { $0 > 10 }))   // false
print(aa.isFilteredCount(exactly: 4, { $0 > 10 }))   // false
print(aa.isFilteredCount(exactly: 5, { $0 > 10 }))   // false
print(aa.isFilteredCount(exactly: 6, { $0 > 10 }))   // false
print(aa.isFilteredCount(exactly: 7, { $0 > 10 }))   // false

and I included an Equatable variant:

extension Sequence where Element: Equatable {

    /**
     Returns whether the count of elements equaling a value is exactly the given amount.

     Testing goes through the minimal amount of iteration when possible.

     - Precondition: This sequence should be finite.

     - Parameter n: The trial element-count, post-filtering, to check against.
     - Parameter x: The element value to check against.

     - Returns: The same as `.filter { $0 == x }.isCount(exactly: n)`.

     - Complexity: At most O(m), where *m* is the number of elements in this sequence.
     */
    func isMatchedCount(exactly n: Int, ofValue x: Element) -> Bool {
        return isFilteredCount(exactly: n, { $0 == x })
    }

}

print()
print(aa.isMatchedCount(exactly: 1, ofValue: 2))
print(aa.isMatchedCount(exactly: 0, ofValue: 2))
print(aa.isMatchedCount(exactly: -1, ofValue: 2))
print(aa.isMatchedCount(exactly: 1, ofValue: 5))
print(aa.isMatchedCount(exactly: 0, ofValue: 5))
print(aa.isMatchedCount(exactly: 2, ofValue: 5))
print(aa.isMatchedCount(exactly: 1, ofValue: 8))
print(aa.isMatchedCount(exactly: 0, ofValue: 8))
print(aa.isMatchedCount(exactly: 2, ofValue: 8))