Handling time zones in Elm
posted on
When programming a single page application, especially if it is designed to run within different time zones, you often use UTC on the REST interfaces to the backend and let the single page application handle the time zone calculation.
When I first tried to implement this in Elm, I first tried to do this using
the tools at hand when using the standard package for time handling: elm/time
:
On the start-up of my SPA I send a Cmd
to the Elm runtime, requesting that it
generated a SetTimezone
event with the local time zone of the user (here
).
getTimezone : Cmd Msg
getTimezone =
Task.perform SetTimezone Time.here
When we give the Elm architecture this command, we get back this message:
type Msg
= SetTimezone Time.Zone
Then I stored the provided Zone
in my model and used it within a helper
function to format timestamps:
formatTime : Zone -> Posix -> String
formatTime zone timestamp =
let
hour =
toHour zone timestamp
minute =
toMinute zone timestamp
in
(padLeft 2 '0' <| fromInt hour) ++ ":" ++ (padLeft 2 '0' <| fromInt minute)
formatTimeOfIsoTimestamp : Zone -> String -> String
formatTimeOfIsoTimestamp zone isoTimestamp =
toTime isoTimestamp |> Result.map (formatTime zone) |> Result.withDefault isoTimestamp
The problem with this approach is, that actually here
does not tell the user's
actual time zone, that would take into account the shifts between standard time
and daylight saving time, but just something generic for the current offset to
UTC. So the
time zone we get will never be Europe/Berlin
but Etc/GMT+1
or Etc/GMT+2
depending on whether we currently have standard time or daylight saving time.
The problem with that is, that you can display “current” timestamps with that zone, but it fails for example as soon as you try to show timestamps within the winter while it is still summer.
The solution to this is to use the functionality of justinmimbs/timezone-data
.
Instead of using the here
Task
as in the code above, this Elm package
provides a different task, to get the time zone of the web browser the
application is running in:
getZone
The function, that generates the Cmd
to request the time zone gets this:
getTimezone : Cmd Msg
getTimezone =
Task.attempt SetTimezone TimeZone.getZone
With this change, also the definition of the SetTimezone
message changes a
bit as the retrieval of the time zone may fail:
type Msg
= SetTimezone (Result TimeZone.Error ( String, Time.Zone ))
When we handle the SetTimezone
message we can unwrap this result and
use a default zone, if the correct zone couldn't be determined:
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case ( msg, model ) of
( SetTimezone result ) ->
let
zone = case result of
Ok ( _, timezone ) ->
timezone
_ ->
TimeZone.europe__berlin()
in
-- store the retrieved zone in the model