Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to compare multiple fields in Elm?

Tags:

comparison

elm

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

  • number of correct guesses (asc)
  • number overall guesses (desc)
  • when word was last asked (asc)
  • when word was added (desc)

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:

  • it's ugly as hell
  • it requires me to use 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))
like image 752
Frank Schmitt Avatar asked Jan 08 '17 17:01

Frank Schmitt


1 Answers

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...

like image 57
Zimm i48 Avatar answered Sep 25 '22 17:09

Zimm i48