Fundamentally, what you're trying to achieve is a mapping between one set of values into another. Dictionary has a function for this, Dictionary.mapValues(_:)
, specifically for mapping values only (keeping them under the same keys).
let appRatings = [
"Calendar Pro": [1, 5, 5, 4, 2, 1, 5, 4],
"The Messenger": [5, 4, 2, 5, 4, 1, 1, 2],
"Socialise": [2, 1, 2, 2, 1, 2, 4, 2]
]
let avgAppRatings = appRatings.mapValues { allRatings in
return computeAverage(of: allRatings) // Dummy function we'll implement later
}
So now, it's a matter of figuring out how to average all the numbers in an Array. Luckily, this is very easy:
We need to sum all the ratings
We can easily achieve this with a reduce
expression. StWe'll reduce all numbers by simply adding them into the accumulator, which will start with 0
allRatings.reduce(0, { accumulator, rating in accumulator + rate })
From here, we can notice that the closure, { accumulator, rating in accumulator + rate }
has type (Int, Int) -> Int
, and just adds the numbers together. Well hey, that's exactly what +
does! We can just use it directly:
allRatings.reduce(0, +)
We need to divide the ratings by the number of ratings
- There's a catch here. In order for the average to be of any use, it can't be truncated to a mere
Int.
So we need both the sum and the count to be converted to Double
first.
You need to guard against empty arrays, whose count will be 0, resulting in Double.infinity
.
Putting it all together, we get:
let appRatings = [
"Calendar Pro": [1, 5, 5, 4, 2, 1, 5, 4],
"The Messenger": [5, 4, 2, 5, 4, 1, 1, 2],
"Socialise": [2, 1, 2, 2, 1, 2, 4, 2]
]
let avgAppRatings = appRatings.mapValues { allRatings in
if allRatings.isEmpty { return nil }
return Double(allRatings.reduce(0, +)) / Double(allRatings.count)
}
Add in some nice printing logic:
extension Dictionary {
var toDictionaryLiteralString: String {
return """
[
\t\(self.map { k, v in "\(k): \(v)" }.joined(separator: "\n\t"))
]
"""
}
}
... and boom:
print(avgAppRatings.toDictionaryLiteralString)
/* prints:
[
Socialise: 2.0
The Messenger: 3.0
Calendar Pro: 3.375
]
*/
Comments on your attempt
You had some questions as to why your attempt didn't work:
func calculate( appName: String, ratings : [Int]) -> (String: Int ) {
var avg = ratings.reduce(0,$0+$1)/ratings.count
return appName: sum/avg
}
$0+$1
isn't within a closure ({ }
), as it needs to be.
appName: sum/avg
isn't valid Swift.
- The variable
sum
doesn't exist.
avg
is a var
variable, even though it's never mutated. It should be a let
constant.
- You're doing integer devision, which doesn't support decimals. You'll need to convert your sum and count into a floating point type, like
Double
, first.
A fixed version might look like:
func calculateAverage(of numbers: [Int]) -> Double {
let sum = Double(ratings.reduce(0, +))
let count = Double(numbers.count)
return sum / count
}
To make a function that processes your whole dictionary, incoroprating my solution above, you might write a function like:
func calculateAveragesRatings(of appRatings: [String: [Int]]) -> [String: Double?] {
return appRatings.mapValues { allRatings in
if allRatings.isEmpty { return nil }
return Double(allRatings.reduce(0, +)) / Double(allRatings.count)
}
}