iosarraysswiftnsarray

How to count occurrences of an element in a Swift array?


I've seen a few examples of this but all of those seem to rely on knowing which element you want to count the occurrences of. My array is generated dynamically so I have no way of knowing which element I want to count the occurrences of (I want to count the occurrences of all of them). Can anyone advise?

EDIT:

Perhaps I should have been clearer, the array will contain multiple different strings (e.g. ["FOO", "FOO", "BAR", "FOOBAR"]

How can I count the occurrences of foo, bar and foobar without knowing what they are in advance?


Solution

  • Swift 3 and Swift 2:

    You can use a dictionary of type [String: Int] to build up counts for each of the items in your [String]:

    let arr = ["FOO", "FOO", "BAR", "FOOBAR"]
    var counts: [String: Int] = [:]
    
    for item in arr {
        counts[item] = (counts[item] ?? 0) + 1
    }
    
    print(counts)  // "[BAR: 1, FOOBAR: 1, FOO: 2]"
    
    for (key, value) in counts {
        print("\(key) occurs \(value) time(s)")
    }
    

    output:

    BAR occurs 1 time(s)
    FOOBAR occurs 1 time(s)
    FOO occurs 2 time(s)
    

    Swift 4:

    Swift 4 introduces (SE-0165) the ability to include a default value with a dictionary lookup, and the resulting value can be mutated with operations such as += and -=, so:

    counts[item] = (counts[item] ?? 0) + 1
    

    becomes:

    counts[item, default: 0] += 1
    

    That makes it easy to do the counting operation in one concise line using forEach:

    let arr = ["FOO", "FOO", "BAR", "FOOBAR"]
    var counts: [String: Int] = [:]
    
    arr.forEach { counts[$0, default: 0] += 1 }
    
    print(counts)  // "["FOOBAR": 1, "FOO": 2, "BAR": 1]"
    

    Swift 4: reduce(into:_:)

    Swift 4 introduces a new version of reduce that uses an inout variable to accumulate the results. Using that, the creation of the counts truly becomes a single line:

    let arr = ["FOO", "FOO", "BAR", "FOOBAR"]
    let counts = arr.reduce(into: [:]) { counts, word in counts[word, default: 0] += 1 }
    
    print(counts)  // ["BAR": 1, "FOOBAR": 1, "FOO": 2]
    

    Or using the default parameters:

    let counts = arr.reduce(into: [:]) { $0[$1, default: 0] += 1 }
    

    Finally you can make this an extension of Sequence so that it can be called on any Sequence containing Hashable items including Array, ArraySlice, String, and String.SubSequence:

    extension Sequence where Element: Hashable {
        var histogram: [Element: Int] {
            return self.reduce(into: [:]) { counts, elem in counts[elem, default: 0] += 1 }
        }
    }
    

    This idea was borrowed from this question although I changed it to a computed property. Thanks to @LeoDabus for the suggestion of extending Sequence instead of Array to pick up additional types.

    Examples:

    print("abacab".histogram)
    
    ["a": 3, "b": 2, "c": 1]
    
    print("Hello World!".suffix(6).histogram)
    
    ["l": 1, "!": 1, "d": 1, "o": 1, "W": 1, "r": 1]
    
    print([1,2,3,2,1].histogram)
    
    [2: 2, 3: 1, 1: 2]
    
    print([1,2,3,2,1,2,1,3,4,5].prefix(8).histogram)
    
    [1: 3, 2: 3, 3: 2]
    
    print(stride(from: 1, through: 10, by: 2).histogram)
    
    [1: 1, 3: 1, 5: 1, 7: 1, 9: 1]