Time

Now we are going to make a digital clock. (Analog will be an exercise!)

So far we have focused on commands. With the HTTP and randomness examples, we commanded Elm to do specific work immediately, but that is sort of a weird pattern for a clock. We always want to know the current time. This is where subscriptions come in!

Start by clicking the blue “Edit” button and looking through the code a bit in the online editor.

Edit

  1. import Browser
  2. import Html exposing (..)
  3. import Task
  4. import Time
  5. -- MAIN
  6. main =
  7. Browser.element
  8. { init = init
  9. , view = view
  10. , update = update
  11. , subscriptions = subscriptions
  12. }
  13. -- MODEL
  14. type alias Model =
  15. { zone : Time.Zone
  16. , time : Time.Posix
  17. }
  18. init : () -> (Model, Cmd Msg)
  19. init _ =
  20. ( Model Time.utc (Time.millisToPosix 0)
  21. , Task.perform AdjustTimeZone Time.here
  22. )
  23. -- UPDATE
  24. type Msg
  25. = Tick Time.Posix
  26. | AdjustTimeZone Time.Zone
  27. update : Msg -> Model -> (Model, Cmd Msg)
  28. update msg model =
  29. case msg of
  30. Tick newTime ->
  31. ( { model | time = newTime }
  32. , Cmd.none
  33. )
  34. AdjustTimeZone newZone ->
  35. ( { model | zone = newZone }
  36. , Cmd.none
  37. )
  38. -- SUBSCRIPTIONS
  39. subscriptions : Model -> Sub Msg
  40. subscriptions model =
  41. Time.every 1000 Tick
  42. -- VIEW
  43. view : Model -> Html Msg
  44. view model =
  45. let
  46. hour = String.fromInt (Time.toHour model.zone model.time)
  47. minute = String.fromInt (Time.toMinute model.zone model.time)
  48. second = String.fromInt (Time.toSecond model.zone model.time)
  49. in
  50. h1 [] [ text (hour ++ ":" ++ minute ++ ":" ++ second) ]

The new stuff is all coming from the elm/time package. Let’s go through these parts!

Time.Posix and Time.Zone

To work with time successfully in programming, we need three different concepts:

  • Human Time — This is what you see on clocks (8am) or on calendars (May 3rd). Great! But if my phone call is at 8am in Boston, what time is it for my friend in Vancouver? If it is at 8am in Tokyo, is that even the same day in New York? (No!) So between time zones based on ever-changing political boundaries and inconsistent use of daylight saving time, human time should basically never be stored in your Model or database! It is only for display!

  • POSIX Time — With POSIX time, it does not matter where you live or what time of year it is. It is just the number of seconds elapsed since some arbitrary moment (in 1970). Everywhere you go on Earth, POSIX time is the same.

  • Time Zones — A “time zone” is a bunch of data that allows you to turn POSIX time into human time. This is not just UTC-7 or UTC+3 though! Time zones are way more complicated than a simple offset! Every time Florida switches to DST forever or Samoa switches from UTC-11 to UTC+13, some poor soul adds a note to the IANA time zone database. That database is loaded onto every computer, and between POSIX time and all the corner cases in the database, we can figure out human times!

So to show a human being a time, you must always know Time.Posix and Time.Zone. That is it! So all that “human time” stuff is for the view function, not the Model. In fact, you can see that in our view:

  1. view : Model -> Html Msg
  2. view model =
  3. let
  4. hour = String.fromInt (Time.toHour model.zone model.time)
  5. minute = String.fromInt (Time.toMinute model.zone model.time)
  6. second = String.fromInt (Time.toSecond model.zone model.time)
  7. in
  8. h1 [] [ text (hour ++ ":" ++ minute ++ ":" ++ second) ]

The Time.toHour function takes Time.Zone and Time.Posix gives us back an Int from 0 to 23 indicating what hour it is in your time zone.

There is a lot more info about handling times in the README of elm/time. Definitely read it before doing more with time! Especially if you are working with scheduling, calendars, etc.

subscriptions

Okay, well how should we get our Time.Posix though? With a subscription!

  1. subscriptions : Model -> Sub Msg
  2. subscriptions model =
  3. Time.every 1000 Tick

We are using the Time.every function:

  1. every : Float -> (Time.Posix -> msg) -> Sub msg

It takes two arguments:

  1. A time interval in milliseconds. We said 1000 which means every second. But we could also say 60 * 1000 for every minute, or 5 * 60 * 1000 for every five minutes.
  2. A function that turns the current time into a Msg. So every second, the current time is going to turn into a Tick <time> for our update function.

That is the basic pattern of any subscription. You give some configuration, and you describe how to produce Msg values. Not too bad!

Task.perform

Getting Time.Zone is a bit trickier. Our program created a command with:

  1. Task.perform AdjustTimeZone Time.here

Reading through the Task docs is the best way to understand that line. The docs are written to actually explain the new concepts, and I think it would be too much of a digression to include a worse version of that info here. The point is just that we command the runtime to give us the Time.Zone wherever the code is running.

Exercises:

  • Add a button to pause the clock, turning the Time.every subscription off.
  • Make the digital clock look nicer. Maybe add some style attributes.
  • Use elm/svg to make an analog clock with a red second hand!