'Count string elements in an array in a multidimensional array

I have an array that I populate from firestore that uses a struct. Is there a way to count the number of times there is a matching string for the productName var.

This is my struct...

struct PlayerStock: Codable, Identifiable {
    @DocumentID var id: String?
    var productName: String
    var qty: Int
    var saleUID: String
    var totalPrice: Int
    var uid: String
    var unitPrice: Int
}

This is what's in my VC, I populate this from firestore and then want to count matching strings in productName

var playerStock: [PlayerStock] = []

Is there a way to do this without using a for loop?

Strings I'd like to count in productName include "smartphone" or "laptop" I want to store the matching total count as an int like this:

var smartphoneTotal = 
var laptopTotal = 
etc etc..

I've tried using filters and compact map but can't find anything that works, I think its because the array is multidimensional or because its using a dictionary?

Pretty noob here so any help appreciated!



Solution 1:[1]

If you want to use filter, something like this should work with your struct:

var laptopTotal = playerStock.filter { $0.productName == "laptop" }.count

Solution 2:[2]

First group the array by productName

let groupedProducts = Dictionary.init(grouping: playerStock, by: \.productName)

you'll get

["smartphone":[PlayerStock(..), PlayerStock(..), PlayerStock(..)],
     "laptop":[PlayerStock(..), PlayerStock(..)]

then map the values to their amount of items

.mapValues(\.count)

The result is

["smartphone":3, "laptop":2]

Solution 3:[3]

This may help

let wordsToFind = ["smartphone", "laptop"]

var foundCounts: [String: Int] = [:]

for p in playerStock {
    for word in wordsToFind {
        if p.name.contains(word) {
            foundCounts[word] = foundCounts[word, default: 0] + 1
        }
    }
}

foundCounts

If you really want a functional "no for-loops" version, and if you mean you want to find things that contain your search terms, then:

let wordsToFind = ["smartphone", "laptop"]
let founds = wordsToFind.map { word -> (String, Int) in
    playerStock.reduce(("", 0)) { partialResult, player in
        (word, partialResult.1 + (player.name.contains(word) ? 1 : 0))
    }
}

Solution 4:[4]

You could use the higher order functions filter() or reduce(). @ShawnFrank already gave an answer using filter(). (voted.)

For a small number of items, there isn't a big difference between filter() and reduce(). For large datasets, though, filter creates a second array containing all the items that match the filter criteria. Arrays are value types, so they hold copies of the entries they contain. This would increase the memory footprint needed to do the counting. (You'd have the original array and a copy containing all the matching elements in memory).

The higher order function reduce() works differently. it takes a starting value (a total starting at 0 in our case) for the result, and a closure. The closure takes the current result, and an element from the array you are parsing. At runtime, the reduce() function calls your closure over and over, passing in each element from the array you are reducing. In the first call to the closure, it sends the closure the initial value for result (a zero total, in our case.) In each subsequent call to the closure, it passes the result of the previous call. (The running total, for our implementation.) The reduce() function returns the result returned by the last call to your closure.

You can use reduce to count the number of items that match a given test without having to build a temporary array. Below is a sample implementation using reduce(). Note that I tweaked your PlayerStock type to add default values for all the properties other than productName since I don't care about those.

// Define the PlayerStock type, but use default values for everything but `productName`
struct PlayerStock: Codable, Identifiable {
    var id: String? = nil
    var productName: String
    var qty: Int = Int.random(in: 1...10)
    var saleUID: String = ""
    var totalPrice: Int = Int.random(in: 10...200)
    var uid: String = ""
    var unitPrice: Int = Int.random(in: 10...200)
}

// Create an array of test data
let players = [
    PlayerStock(productName: "smartphone"),
    PlayerStock(productName: "CD Player"),
    PlayerStock(productName: "laptop"),
    PlayerStock(productName: "CD Player"),
    PlayerStock(productName: "smartphone"),
    PlayerStock(productName: "laptop"),
    PlayerStock(productName: "smartphone"),
    PlayerStock(productName: "boom box"),
    PlayerStock(productName: "laptop"),
    PlayerStock(productName: "smartphone"),
               ]
/// This is a function that counts and returns the number of PlayerStock items who's productName property matches a the string nameToFind.
/// If you pass in printResult = true, it logs its result for debugging.
///  - Parameter nameToFind: The `productName` to search for
///  - Parameter inArray: The array of `PlayerStock` items to search
///  - Parameter printResult:  a debugging flag. If true, the function prints the count if items to the console. Defaults to `false`
///  - Returns: The number of `PlayerStock` items that have a `productName` == `nameToFind`

@discardableResult func countPlayers(nameToFind: String, inArray array: [PlayerStock], printResult: Bool = false) -> Int {
    let count = array.reduce(0, { count, item in
        item.productName == nameToFind ? count+1 : count
    })
    if printResult {
        print("Found \(count) players with productName == \(nameToFind)")
    }
    return count
}


let smartphoneCount = countPlayers(nameToFind: "smartphone", inArray: players, printResult: true)
let laptopCount = countPlayers(nameToFind: "laptop", inArray: players, printResult: true)
let cdPlayerCount = countPlayers(nameToFind: "CD Player", inArray: players, printResult: true)

This sample code produces the following output:

Found 4 players with productName == smartphone
Found 3 players with productName == laptop
Found 2 players with productName == CD Player

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2
Solution 3
Solution 4 Duncan C