0

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!

5
  • You can use array higher order functions for this.filter to filter the array by the product you wish and then count the elements in the array. Or reduce is another option. Commented Apr 1, 2022 at 9:03
  • Thanks do you have any code examples? Commented Apr 1, 2022 at 9:09
  • 1
    var laptopTotal = playerStock.filter { $0.productName == "laptop" }.count - something like this Commented Apr 1, 2022 at 9:47
  • Thanks @ShawnFrank that's perfect. I was so close with some of my other attempts to. Not sure I can mark yours as the correct answer Commented Apr 1, 2022 at 10:43
  • I have added my comments as an answer and happy to help. Commented Apr 1, 2022 at 10:57

4 Answers 4

4

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]
Sign up to request clarification or add additional context in comments.

5 Comments

OP is looking for counts where there is a matching string for the productName, not counts of productNames
@Shadowrun Actually this is exactly what the code does. Dictionary.grouping creates a dictionary with the product name as key and the items matching the name as value. The number of the value is the number of items which contains the product name respectively.
I took it to mean "a matching string" is one where the product name contains the substring "laptop" not the product name is exactly "laptop". Now I am not sure what the OP meant by "like to count in productName include "smartphone" or "laptop"
Ok this is great. Although the solution I had in mind was what Shawn Frank posted in the original question comments, this is exactly what I need for another part of the project
The benefit of my suggestion is that it is dynamic and the array is only parsed once.
1

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

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

Comments

0

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))
    }
}

1 Comment

I think the OP did not want to use any loops Is there a way to do this without using a for loop?
0

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

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.