Civil.Time

A time of day, with no date and no time zone.

17:45:00 is a time of day. It is not a moment in time and it is not a duration. It comes around once a day everywhere, at a different moment in each place, and on the day a time zone changes its clocks it can come around twice or not at all. It is a reading on a clock: what RFC 3339 calls a partial-time, and what someone means when they write down when a shop opens.

Seconds are optional, fractions are unlimited

TOML 1.1 makes the seconds field optional, as ISO 8601 does and RFC 3339 does not. This module follows TOML: 13:37 parses, and is written back as 13:37:00, because the written form always has all three fields. Neither standard limits the number of digits after the decimal point, and neither does this module.

The fraction is kept as text, however many digits it has, so nothing is rounded away. Trailing zeroes are dropped, so that == compares the time rather than the spelling: 10:32:00.5 and 10:32:00.50 are the same value. If you need the original spelling, keep the source text; a value cannot hold both.

Leap seconds

A second of 60 is accepted, because RFC 3339 allows it and a timestamp that records a leap second is valid. It is stored and written back as 60. This module does no arithmetic that would need to decide what a leap second means: secondOfDay returns 86400 for 23:59:60, one past the last ordinary second of the day.

The 60 is checked as a field on its own, not as part of an HH:MM:SS that has to read 23:59:60. 12:30:60 is a time here, and secondOfDay gives it 45060. That is RFC 3339's grammar, which allows 60 in the seconds field and leaves the question of whether a leap second was really inserted at that instant to whoever knows. It has to: a leap second falls at 23:59:60 UTC, which is some other wall clock at every other offset, so the same second is 1990-12-31T15:59:60-08:00 in California.

type Time

A time of day.

Creating

fromParts :
{ hour : Int
, minute : Int
, second : Int
, fraction : String
}
-> Maybe Time

A time from its parts. The fraction is the digits after the decimal point, without the point itself, or "" for no fraction.

Time.fromParts { hour = 10, minute = 32, second = 0, fraction = "555" }
    |> Maybe.map Time.toString
--> Just "10:32:00.555"

Nothing if the hour is outside 0 to 23, the minute outside 0 to 59, the second outside 0 to 60, or the fraction is anything but digits.

Trailing zeroes are dropped from the fraction, so a fraction of "5550" gives the same value as "555".

The fraction is a String and not an Int because its digits are positional and a number loses that. "5" is half a second and "05" is a twentieth of one, but both are the number 5; only the text says where after the point the digits start.

Time.fromParts { hour = 10, minute = 32, second = 0, fraction = "05" }
    |> Maybe.map Time.toString
--> Just "10:32:00.05"

A number would also have to fix a unit and a precision, milliseconds or nanoseconds or some other, and the number of digits here is not limited, so everything past that unit would be rounded away. Text keeps every digit that was written. millisecond and nanosecond are there for when a number is what you want.

midnight : Time

00:00:00.

Reading

hour : Time -> Int

The hour, 0 to 23.

minute : Time -> Int

The minute, 0 to 59.

second : Time -> Int

The second, 0 to 60. Sixty is a leap second.

fraction : Time -> String

The digits after the decimal point, without the point, with trailing zeroes already removed. "" when there is no fraction.

Time.fromString "10:32:00.5500" |> Maybe.map Time.fraction
--> Just "55"
millisecond : Time -> Int

The fraction as whole milliseconds, truncated.

Time.fromString "10:32:00.5559" |> Maybe.map Time.millisecond
--> Just 555
nanosecond : Time -> Int

The fraction as whole nanoseconds, truncated. Digits past the ninth are dropped; fraction keeps them all.

Time.fromString "10:32:00.5" |> Maybe.map Time.nanosecond
--> Just 500000000
secondOfDay : Time -> Int

Seconds since midnight, ignoring the fraction.

Time.fromString "01:00:00" |> Maybe.map Time.secondOfDay
--> Just 3600

A leap second gives 86400, one past the last ordinary second of the day. See the note on leap seconds at the top of this module.

Strings

fromString : String -> Maybe Time

Read an HH:MM, HH:MM:SS, or HH:MM:SS.fff time.

Time.fromString "13:37" |> Maybe.map Time.toString
--> Just "13:37:00"

Each field has to be exactly two digits, the fraction at least one, and the whole string has to be the time. All of these are Nothing:

Time.fromString "1:37:00"    --> Nothing
Time.fromString "13:37:00."  --> Nothing
Time.fromString "133700"     --> Nothing
Time.fromString "13:37:00Z"  --> Nothing
toString : Time -> String

Write an HH:MM:SS time, with .fff after it when there is a fraction.

Time.fromString "13:37" |> Maybe.map Time.toString
--> Just "13:37:00"

The seconds are always written, even when the value was read from text that had none. The fraction is written without its trailing zeroes.

toStringWithPlaces : Int -> Time -> String

Write an HH:MM:SS time with the fraction padded or truncated to a fixed number of places.

Time.fromString "13:37" |> Maybe.map (Time.toStringWithPlaces 3)
--> Just "13:37:00.000"

Time.fromString "10:32:00.5" |> Maybe.map (Time.toStringWithPlaces 3)
--> Just "10:32:00.500"

With zero places, no decimal point is written. Use this when the receiver requires a fixed number of places, such as exactly three for milliseconds. Otherwise use toString.