You have a constant for what the scoring is
scoring :: [Int]
scoring = [25, 18, 15, 12, 10, 8, 6, 4, 2, 1]
Then you need a way for pairing up a driver with the score they got. Whenever you’re pairing two things in Haskell, the canonical choice is to use a tuple. The easiest way to construct a list of tuples from two lists is the zip
function:
zip :: [a] -> [b] -> [(a, b)]
And in this case can be used to assign scores for a race:
assignScores :: [String] -> [(String, Int)]
assignScores race = zip race scoring
Now, we need a way to total up the scores for a driver for each race. We want to be able to turn something like
[("Bob", 12), ("Joe", 10), ("Bob", 18), ("Joe", 25)]
into
[("Bob", 30), ("Joe", 35)]
The easiest way would be to make a single list of all the scores for all the races
assignAllScores :: [[String]] -> [(String, Int)]
assignAllScores races = concatMap assignScores races
Then we can use sortBy
from Data.List
to get all the same names next to each other
sortBy :: (a -> a -> Ordering) -> [a] -> [a]
compare :: Ord a => a -> a -> Ordering
sortByDriver :: [(String, Int)] -> [(String, Int)]
sortByDriver races = sortBy (\(n1, s1) (n2, s2) -> compare n1 n2) races
Then we can use groupBy
(also from Data.List
) to group them all by name
groupBy :: (a -> a -> Bool) -> [a] -> [[a]]
groupByDriver :: [(String, Int)] -> [[(String, Int)]]
groupByDriver races = groupBy (\(n1, s1) (n2, s2) -> n1 == n2) races
But this gives us a list like
[[("Bob", 12), ("Bob", 18)], [("Joe", 10), ("Joe", 25)]]
We now need a way to convert this into the form
[("Bob", [12, 18]), ("Joe", [10, 25])]
where all the scores are aggregated back into a list, and we don’t repeat the names at all. This is left as an exercise.
aggregateScores :: [[(String, Int)]] -> [(String, [Int])]
Then we can finally calculate the sum of these scores
sumScores :: [(String, [Int])] -> [(String, Int)]
sumScores races = map (\(name, scores) -> (name, sum scores)) races
Then finally we can sort by the scores to get everyone in order
sortByScore :: [(String, Int)] -> [(String, Int)]
sortByScore races = sortBy (\(n1, s1) (n2, s2) -> compare s2 s1) races
Notice that I have compare s2 s1
instead of compare s1 s2
, this means it will be sorted in descending order instead of ascending order.
The last step is to strip out the scores, now we have our list of drivers in order from winner to loser
removeScores :: [(String, Int)] -> [String]
removeScores races = map fst races
So to combine everything together into one function
f1Results :: [[String]] -> [String]
f1Results races =
removeScores $
sortByScore $
sumScores $
aggregateScores $
groupByDriver $
sortByDriver $
assignAllScores races
There are several tricks that can make this code shorter, namely Data.Function.on
, Data.Ord.comparing
, and a fun operator from Control.Arrow
. Don’t turn this in as homework, I just wanted to show an alternative that uses less code.
import Data.List
import Data.Function (on)
import Data.Ord (comparing)
scoring :: [Int]
scoring = [25, 18, 15, 12, 10, 8, 6, 4, 2, 1]
f1Results :: [[String]] -> [String]
f1Results =
map fst . sortBy (on (flip compare) snd) .
map ((head *** sum) . unzip) .
groupBy (on (==) fst) . sortBy (comparing fst) .
concatMap (`zip` scoring)
Or using Data.Map
:
import Data.Map (assocs, fromListWith)
import Data.List
import Data.Ord (comparing)
scoring :: [Int]
scoring = [25, 18, 15, 12, 10, 8, 6, 4, 2, 1]
f1Results :: [[String]] -> [String]
f1Results =
reverse . map fst . sortBy (comparing snd) .
assocs . fromListWith (+) .
concatMap (`zip` scoring)
8
solved F1 Results with Haskell