I'm currently writing a web-based vocabulary trainer in Elm. This requires sorting a list of words by a custom comparator.
The type I want to sort is:
type alias Word =
{ id: Int
, sourceWord: String
, targetWord: String
, numTries: Int
, numCorrect: Int
, createdAt: Maybe Date -- might be empty, therefore wrapped in Maybe
, lastAskedAt: Maybe Date -- might be empty, therefore wrapped in Maybe
}
type alias WordList = List (Word)
My rules for comparison are (in descending order of importance):
The best approach I could come up with is this:
compareWords: Word -> Word -> Basics.Order
compareWords w1 w2 =
let
dateToComparable d = Date.Format.format "%Y-%m-%d" d
orderNumCorrect = compare w1.numCorrect w2.numCorrect
orderNumTries = compare w2.numTries w1.numTries -- switch ordering to sort descending
orderLastAskedAt = case (w1.lastAskedAt, w2.lastAskedAt) of
(Just a1, Just a2) -> compare (dateToComparable a1) (dateToComparable a2)
(Nothing, Just _) -> Basics.LT
(Just _, Nothing) -> Basics.GT
(Nothing, Nothing) -> Basics.EQ
orderCreatedAt = case (w2.createdAt, w1.createdAt) of -- switch ordering to sort descending
(Just a1, Just a2) -> compare (dateToComparable a1) (dateToComparable a2)
(Nothing, Just _) -> Basics.LT
(Just _, Nothing) -> Basics.GT
(Nothing, Nothing) -> Basics.EQ
in
case orderNumCorrect of
Basics.EQ -> case orderNumTries of
Basics.EQ -> case orderLastAskedAt of
Basics.EQ -> orderCreatedAt
_ -> orderLastAskedAt
_ -> orderNumTries
_ -> orderNumCorrect
which I don't like for a number of reasons:
Date.Format.format
(from mgold/elm-date-format) to compare Date values (since Date apparently is not comparable
)Is there a more elegant / Elm-ish way to achieve what I want?
Update + solution
As @"Zimm i48" suggested in their most excellent answer, here's a much shorter version that uses the elm-ordering package:
dateToComparable : Maybe Date -> Time
dateToComparable =
Maybe.map Date.toTime >> Maybe.withDefault 0
compareWords : Ordering Word
compareWords =
Ordering.byField .numCorrect
|> Ordering.breakTiesWith (Ordering.byField (.numTries >> negate))
|> Ordering.breakTiesWith (Ordering.byField (.lastAskedAt >> dateToComparable))
|> Ordering.breakTiesWith
(Ordering.byField (.createdAt >> dateToComparable >> negate))
A more Elm-ish way of doing this kind of things is compositionally, thanks to the |>
operator.
The elm-ordering library provides the primitives that you need to do this kind of things, especially the Ordering.byField
and Ordering.breakTiesWith
functions.
As for the dates, my advice would be to use Date.toTime
(the resulting values are comparable).
Bonus: full implementation of your ordering function available for testing here: https://runelm.io/c/xoz. You can see it's much simpler and more readable than yours...
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With