VEvergarden
VEvergarden

Reputation: 113

Turn array of object into cummulative dictionary Swift

I have structure called person

struct person{
   var name : String
   var score: Int
}

Then I create an array called let people : [person] = [person("a", 1), person("a", 3), person("b", 5)]

As you can see, there are two objects with the same name "a" here.

Now I would like to turn this one into a cumulative dictionary, that shows the total score of each person. In this case dict = {"a": 4 (3+1), "b": 5}

I know I'm violating the OO design rules. Thanks

Upvotes: 0

Views: 63

Answers (3)

Jeffery Thomas
Jeffery Thomas

Reputation: 42598

I'm assuming you are new to Swift. Let me walk you though a few iterations of this.

The most basic is the straight forward approach.

var dict = [String: Int]()

for person in people {
    if let score = dict[person.name] {
        // If the person already has a score in dict, then add this new score to it.
        dict[person.name] = score + person.score
    } else {
        // If this is the first score for this person, then add the person to dict.
        dict[person.name] = person.score
    }
}

Next, we will used subscript(_:default:) operator to combine the two parts of the if conditional.

var dict = [String: Int]()

for person in people {
    // Add the new score to the person's existing score. If there is no existing
    // score, then default to 0 and add the new score.
    dict[person.name, default: 0] += person.score
}

Finally, use reduce(into:_:) to get rid of the for loop (see no raw loops segment of C++ Seasoning)

// Reduce people into a dictionary of names and cumulative scores.
let dict = people.reduce(into: [String: Int]()) { result, person in
    result[person.name, default: 0] += person.score
}

Upvotes: 0

Alexander
Alexander

Reputation: 63399

There's a fundamental modelling issue here. Your struct person doesn't actually model a person. It models something like a RoundResult.

I would refactor this by making a Player that truly models a person, (with only fields like name: String), and make a RoundResult that contains a winner: Player and a score: Score.

struct Player: Hashable { // Perhaps should be a class, if names aren't unique.
    let name: String
}

struct RoundResult {
    let winner: Player
    let score: Int
}

let playerA = Player(name: "a")
let playerB = Player(name: "b")

let roundResults = [
    RoundResult(winner: playerA, score: 1),
    RoundResult(winner: playerA, score: 3),
    RoundResult(winner: playerB, score: 5),
]

let scoresByPlayer = Dictionary(grouping: roundResults, by: \.winner)
    .mapValues { roundResults -> Int in
        let scores = roundResults.lazy.map(\.score)
        return scores.reduce(0, +)
    }

print(scoresByPlayer)

From here, you can add a score variable on player, which actually models the players score, not just a single sliver of it from a single round/game/match/whatever

Upvotes: 1

New Dev
New Dev

Reputation: 49590

You can achieve this in two steps: 1) group into a dictionary, and 2) sum scores of the grouping:

// grouping = { "a": [person("a", 1), person("a", 2)], "b": [person("b": 3)]
let grouping = Dictionary.init(grouping: people, by: { person in person.name })

let dict = grouping.mapValues { group in 
   group.reduce(0, { sum, person in sum + person.score })
}

Or in its shorter, but more cryptic form:

let d = Dictionary.init(grouping: people, by: { $0.name })
                  .mapValues{ $0.reduce(0, $0 + $1.score ) }

Upvotes: 0

Related Questions