Nate Birkholz
Nate Birkholz

Reputation: 2809

Count number of items in an array with a specific property value

I have a Person() class:

class Person : NSObject {

    var firstName : String
    var lastName : String
    var imageFor : UIImage?
    var isManager : Bool?

    init (firstName : String, lastName: String, isManager : Bool) {
        self.firstName = firstName
        self.lastName = lastName
        self.isManager = isManager
    }
}

I have an array of Person()

var peopleArray = [Person]()

I want to count the number of people in the array who have

 isManager: true

I feel this is out there, but I can;t find it, or find the search parameters.

Thanks.

Upvotes: 28

Views: 15663

Answers (4)

Imanou Petit
Imanou Petit

Reputation: 92439

With Swift 6, you can use Array's count(where:) method if you want to count the number of elements in an array that match a given predicate. count(where:) has the following declaration:

func count<E>(where predicate: (Self.Element) throws(E) -> Bool) throws(E) -> Int where E : Error

Returns the number of elements in the sequence that satisfy the given predicate.

The following Playground sample code shows how to use count(where:):

struct Person {
    let name: String
    let isManager: Bool
}

let array = [
    Person(name: "Jane", isManager: true),
    Person(name: "Bob", isManager: false),
    Person(name: "Joe", isManager: true),
    Person(name: "Jill", isManager: true),
    Person(name: "Ted", isManager: false)
]

let managerCount = array.count(where: { (person: Person) -> Bool in
    return person.isManager
})
// let managerCount = array.count { $0.isManager } // also works
// let managerCount = array.count(where: \.isManager) // also works

print(managerCount) // prints: 3

Upvotes: 10

Mojtaba Hosseini
Mojtaba Hosseini

Reputation: 119310

Swift 6.0

You can use the following from Swift 6.0

peopleArray.count(where: \.isManager!)

Before Swift 6.0

You can get the above extension which is proposed to and accepted by the Swift team as the following:

extension Sequence {
    @inlinable
    public func count(
        where predicate: (Element) throws -> Bool
    ) rethrows -> Int {
        var count = 0
        for e in self {
            if try predicate(e) {
                count += 1
            }
        }
        return count
    }
}

⭐️ Pros:

⭕️ Cons:
  • May cause performance problems in the compiler's type checker, due to conflicts with Collection.count.
    • ✅ Workaround: Rename the count(where:) function to something else to overcome this issue.

😎 Older and simpler answer:

You just need to do what you asked (in reversed order of saying):

/* Number of -> items with specific property -> in an array */

/* count     -> .filter(\.isManager!)        -> peopleArray */

// Just reversed of what you say in English:

peopleArray.filter(\.isManager!).count

⁉️ Performance FAQ:

🔂 Q. Does this traverse twice? once to filter and once to count?

A. NO, Array<Element> conforms to RandomAccessCollection. So the count will NOT actually count the elements. It just returns the result (the endIndex) with O(1) complexity.

⚠️ Q. Does this create a new array when filtering it?

A. YES, Swift Array is copy on write, but calling filter actually manipulates the array, so you will eventually get a new array, even if you don't assign it to a new variable.

💡 Q. How to count a filtered array wihout an intermediate array?

A. Using lazy subsystem or manually reduce like:

peopleArray.lazy.filter(\.isManager!).count

📣 Conclusion

It's perfectly safe and perfomant to call lazy->filter->count here. The most important point here is no do NOT optimize prematurely because

Premature optimization is the root of all evil.

Tony Hoare


📋 Sources:
  • The original count(where:) extension on the Sequence: here
  • The original proposal motivation (using lazy, filter etc.: here
  • Community benchmark on loop, reduce and inline reduce: here
  • Swift CI performance results (about the type checker issue): here

Upvotes: 0

Michał Ciuba
Michał Ciuba

Reputation: 7944

Use filter method:

let managersCount = peopleArray.filter { (person : Person) -> Bool in
    return person.isManager!
}.count

or even simpler:

let moreCount = peopleArray.filter{ $0.isManager! }.count

Upvotes: 43

Antonio
Antonio

Reputation: 72760

You can use reduce as follows:

let count = peopleArray.reduce(0, combine: { (count: Int, instance: Person) -> Int in
    return count + (instance.isManager! ? 1 : 0) }
)

or a more compact version:

let count = peopleArray.reduce(0) { $0 + ($1.isManager! ? 1 : 0) }

reduce applies the closure (2nd parameter) to each element of the array, passing the value obtained for the previous element (or the initial value, which is the 0 value passed as its first parameter) and the current array element. In the closure you return count plus zero or one, depending on whether the isManager property is true or not.

More info about reduce and filter in the standard library reference

Upvotes: 18

Related Questions