We can understand why our program is currently displaying
‘Just’ followed by a large number
by looking again at our view
function:
view : Model -> Html Msg
view model =
case model.time of
Nothing ->
text "Loading…"
Just _ ->
text (toString model.time)
Since model.time
no longer contains Nothing
,
the second branch of the case
is now generating our view.
That branch says
to convert the value of model.time
to a String
and display it as HTML text.
Remember, model.time
isn’t just a Time
value; it’s a Maybe Time
,
which means that its value is either Nothing
or it’s Just Time
(a Time
value tagged with the Just
constructor).
So the word ‘Just’ in the output is just that tag,
and the large number must be the Time
.
Indeed, if you refresh the page repeatedly,
you’ll see the large number gets larger each time!
We can use pattern matching
in our case
statement again
to extract just the Time
from model.time
’s Just Time
value.
Replace the underscore
(which tells Elm we aren’t interested in the value)
in Just _
with a variable name:
Just time ->
text (toString model.time)
With the unwrapped Time
value in time
,
we can now display that instead of model.time
:
Just time ->
text (toString time)
Now our program displays just the large number
that represents the current time.
The documentation for the Time
type reveals that
Time
is just an alias for Float
– a floating point number.
In fact, a Time
is just
a number of milliseconds since midnight on 1 January, 1970 UTC.
We can use some of the other functions in the Time
module
to convert such a value in to the hours, minutes and seconds
we need for our clock.
Let’s write a function to do this.
If you’re feeling confident,
go ahead and try writing it yourself before you read on.
Here’s what a reasonable first attempt might look like:
toClock : Time -> String
toClock time =
let
hours =
Time.inHours time
minutes =
Time.inMinutes time
seconds =
Time.inSeconds time
in
hours ++ ":" ++ minutes ++ ":" ++ seconds
Here we’ve used a let
block
to calculate the hours
, minutes
and seconds
separately,
then we’ve tried to return these
in a single String
.
If we try to run this,
the compiler reminds us that ++
only works with String
s
and we’ve given it a Float
.
We can use toString
to
perform the necessary conversions:
toClock : Time -> String
toClock time =
let
hours =
toString (Time.inHours time)
minutes =
toString (Time.inMinutes time)
seconds =
toString (Time.inSeconds time)
in
hours ++ ":" ++ minutes ++ ":" ++ seconds
This will run,
but the numbers aren’t quite what we’re after.
First of all,
because the Time
functions return
floating point numbers (Float
) and not integers (Int
),
our values are fractional hours, minutes and seconds
(with digits after the decimal point).
We can round these down with floor
:
toClock : Time -> String
toClock time =
let
hours =
toString (floor (Time.inHours time))
minutes =
toString (floor (Time.inMinutes time))
seconds =
toString (floor (Time.inSeconds time))
in
hours ++ ":" ++ minutes ++ ":" ++ seconds
Note: Documentation for built-in functions like
floor
andtoString
may be found in theBasics
module.
And then to get just
the number of hours in the current day,
the number of minutes in the current hour,
and the number of seconds in the current minute,
we can use the %
operator (remainder):
toClock : Time -> String
toClock time =
let
hours =
toString (floor (Time.inHours time) % 24)
minutes =
toString (floor (Time.inMinutes time) % 60)
seconds =
toString (floor (Time.inSeconds time) % 60)
in
hours ++ ":" ++ minutes ++ ":" ++ seconds
If you run the program now,
you’ll probably notice that our Time
value
actually represents the current UTC time,
not the time in your local time zone.
If we were actually trying to build a boring, old clock
(and not a totally awesome stopwatch)
we could use Elm’s Date
module
to perform a Date.now
task to fetch a current Date
(which like JavaScript’s Date.now
returns a local date and time).
The current UTC time is all we need for our stopwatch, though.
Finally, we want to format each of the three numbers
with a leading zero if it happens to be a single digit.
We can use String.padLeft
for this:
toClock : Time -> String
toClock time =
let
hours =
String.padLeft 2 '0' (toString (floor (Time.inHours time) % 24))
minutes =
String.padLeft 2 '0' (toString (floor (Time.inMinutes time) % 60))
seconds =
String.padLeft 2 '0' (toString (floor (Time.inSeconds time) % 60))
in
hours ++ ":" ++ minutes ++ ":" ++ seconds
If you’re starting to find those three expressions
difficult to read with all those parentheses,
congratulations – you’re a human being (not a compiler)!
We can use Elm’s pipeline operator (|>
) to
make these expressions much more readable.
hours =
Time.inHours time
|> floor
|> (\hours -> hours % 24)
|> toString
|> String.padLeft 2 '0'
|>
passes the value on its left into the function on its right.
This lets us build a complex expression as
a series of transformations.
We start with Time.inHours time
(the number of hours in time
, as a Float
)
then we pass that value as an argument to floor
to round it down to the nearest integer.
We now have a whole number of hours, as an Int
.
To get the number of hours in the current day,
we want the value of the expression hours % 24
.
To use this expression as a function in our pipeline,
we write an anonymous function
(just like a JavaScript arrow function)
that takes an hours
argument and returns hours % 24
.
In Elm, we write this as \hours -> hours % 24
.
Next, we convert our Int
to a String
with toString
,
and finally we pad it with a leading zero if necessary
with String.padLeft 2 '0'
.
Note that String.padLeft
takes three arguments,
so the expression String.padLeft 2 '0'
(with just two arguments)
acts as a function that takes a single argument
(the value provided by the pipeline operator).
This technique of giving a function only some of its arguments
is called partial application,
and it’s especially handy when used within a pipeline like this.
Go ahead and repeat the same pattern for minutes and seconds. There should be no surprises here, so see if you can do it on your own before you refer to the finished function:
toClock : Time -> String
toClock time =
let
hours =
Time.inHours time
|> floor
|> (\hours -> hours % 24)
|> toString
|> String.padLeft 2 '0'
minutes =
Time.inMinutes time
|> floor
|> (\minutes -> minutes % 60)
|> toString
|> String.padLeft 2 '0'
seconds =
Time.inSeconds time
|> floor
|> (\seconds -> seconds % 60)
|> toString
|> String.padLeft 2 '0'
in
hours ++ ":" ++ minutes ++ ":" ++ seconds
Note: These three pipelines are very similar. You might like to try to find a way to eliminate the repitition, say by writing a
toClockComponent
function. You might decide that you prefer the clarity of this version, though, and that’s fine too.
Our clock’s looking pretty good now, but if we’re going to turn it into a stopwatch, we’ll need to learn to make it count up!