Civil.DateTime

A date and a time of day, with or without an offset from UTC.

This type holds two kinds of value, and the difference between them is the reason the module exists:

  • A local date-time, 1979-05-27T07:32:00, is a reading from a wall clock and a calendar. It is not a moment in time. Nobody can say when it happened without knowing where it happened.
  • An offset date-time, 1979-05-27T07:32:00-07:00, is a moment. The offset says how far the clock was from UTC, which is enough to pin the reading to one instant. toPosix works on offset date-times and not on local ones.

RFC 3339 and TOML both make this distinction, and both keep the offset as it was written rather than converting everything to UTC. So does this module: an offset date-time remembers -07:00, and Z, +00:00 and -00:00 stay distinct (see Civil.Offset).

What is not here

Turning a local date-time into a moment. That needs a time zone, and a time zone needs the IANA time zone database, which is far larger than this package and belongs in one of its own. toPosix works on offset date-times only and returns Nothing for local ones.

The examples

The examples below assume that Civil.Date, Civil.Time and Civil.Offset are imported as Date, Time and Offset, and that core's Time is imported as CoreTime. That is how this module itself imports them.

type DateTime

A date and a time, with or without an offset.

== compares the values as written, not the moments they name. A local date-time never equals an offset one, and 07:32:00Z and 07:32:00+00:00 are two different values, just as Z and +00:00 are in Civil.Offset. To ask whether two offset date-times name the same moment, compare the results of toPosix.

Creating

local : Date -> Time -> DateTime

A date-time with no offset: a wall clock reading, not a moment.

Maybe.map2 DateTime.local
    (Date.fromString "1979-05-27")
    (Time.fromString "07:32")
    |> Maybe.map DateTime.toString
--> Just "1979-05-27T07:32:00"
offset : Date -> Time -> Offset -> DateTime

A date-time with an offset: a moment.

Maybe.map3 DateTime.offset
    (Date.fromString "1979-05-27")
    (Time.fromString "07:32")
    (Offset.fromString "-07:00")
    |> Maybe.map DateTime.toString
--> Just "1979-05-27T07:32:00-07:00"

Reading

date : DateTime -> Date

The date part.

time : DateTime -> Time

The time part.

offsetOf : DateTime -> Maybe Offset

The offset, if there is one.

DateTime.fromString "1979-05-27T07:32:00"
    |> Maybe.map DateTime.offsetOf
--> Just Nothing
isLocal : DateTime -> Bool

Whether this is a local date-time, one with no offset.

Instants

toPosix : DateTime -> Maybe Posix

The moment an offset date-time names, as a Posix from Gren's core Time module.

DateTime.fromString "1970-01-01T00:00:00Z"
    |> Maybe.andThen DateTime.toPosix
    |> Maybe.map CoreTime.posixToMillis
--> Just 0

Nothing for a local date-time, which does not name a moment. The fraction is truncated to whole milliseconds, which is all a Posix can hold.

A leap second is counted as the following second, since Posix has no way to represent one. Because of that, and because an offset on the first or last day of the calendar can reach past it, the result can be a moment outside years 1 to 9999. Posix has no such limit, so the moment is returned, but fromPosix returns Nothing for it.

fromPosix : Posix -> Maybe DateTime

The UTC date-time of a moment, written with a Z offset.

CoreTime.millisToPosix 0
    |> DateTime.fromPosix
    |> Maybe.map DateTime.toString
--> Just "1970-01-01T00:00:00Z"

Nothing for a moment outside years 1 to 9999. The result is always in UTC. Showing a moment in some other time zone needs the time zone database, which this package does not include.

Strings

fromString : String -> Maybe DateTime

Read an RFC 3339 date-time, with or without an offset.

DateTime.fromString "1979-05-27 07:32Z"
    |> Maybe.map DateTime.toString
--> Just "1979-05-27T07:32:00Z"

The date and the time may be separated by T, t, or a space, and the offset may be Z, z, or signed hours and minutes. Everything else is as strict as Civil.Date.fromString and Civil.Time.fromString: every field has exactly the number of digits shown, and nothing may follow the time or the offset.

toString : DateTime -> String

Write an RFC 3339 date-time: T between the date and the time, seconds always present, and the offset as it was written, if there is one.

DateTime.fromString "1987-07-05t17:45:00z"
    |> Maybe.map DateTime.toString
--> Just "1987-07-05T17:45:00Z"