Library
Module
Module type
Parameter
Class
Class type
Time description and manipulations
Timedesc provides utilities to describe points of time, and properly handle calendar and time zone information.
Suppose we want to get the time right now, we can simply do Timedesc.now ()
. But what if we want to get the time right now in a different time zone? Say New York? Then we can simply do:
Timedesc.now ~tz_of_date_time:(Timedesc.Time_zone.make_exn "America/New_York") ()
.
And if we want to construct a date time from scratch, we can use constructors such as make
, with similar time zone specification:
Timedesc.make ~tz:(Timedesc.Time_zone.make_exn "Australia/Sydney") ~year:2021 ~month:5 ~day:30 ~hour:14 ~minute:10 ~second:0 ()
.
Since we deal with timestamps quite frequently, lets have a look at how Timedesc also makes working with them easier. Suppose we receive a timestamp similar to the result returned by Unix.gettimeofday
, i.e. seconds since unix epoch in float
, we can digest it in myriad ways. If we just want to construct a date time out of it, then we can use of_timestamp_float_s
. If we want to get it into the representation used in Timedesc, say to perform arithmetic operations over it etc, then we can use Timestamp.of_float_s
. But in either case, we can always swap back and forth via to_timestamp
and of_timestamp
.
In general it is better to use Timestamp as much as possible, unless you require a precision higher than nanosecond. This is because floating point is a lossy representation - if you convert a date time to floating point and back, you may not get the same date time back (i.e. it may not round trip). Also, performing arithmetic operations over floating points can introduce more and more errors, and it is advisable to use the arithmetic functions provided in Span
or Timestamp
.
To access the values of date time, we can use the constructors such as year
, month
, day
, hour
.
By now, one nicety should be obvious: you don't have to worry about what is the time zone offset at when and where - Timedesc takes care of that for you properly! All you have to do is to make a time zone following the *nix naming convention. However, even though we follow the same naming convention, we don't actually rely on the OS time zone database, and our code will run fine on any platform.
To see what time zones Timedesc supports during run time, we can refer to Time_zone.available_time_zones
. Alternatively, for a text file containing all the supported time zones by default, refer to available-time-zones.txt
in the repository.
If you are aware of DST: Yes, Timedesc takes care of that for you properly as well - Timedesc does not allow you to construct a date time that does not exist for the particular time zone, and any ambiguity is made explicit as return type via local_result
.
This does mean Timedesc does not "resolve" the result into one of the possibilities arbitrarily, and you need to resolve the ambiguity yourself. If such a coercion is desirable, however, then you can use either min_of_local_result
or max_of_local_result
.
Timedesc offers both machine-friendly and human-friendly ways of dealing with spans.
For the machine-friendly side, functions in the top level of Span
provide efficient constructions and arithmetic operations.
For the human-friendly side, Span.For_human
provides functions which work at a level closer to human language. For instance, we say things like "2 hours and 15 minutes" quite frequently, to represent this as Span.t
, we can do:
Timedesc.Span.For_human.make_exn ~hours:2 ~minutes:15 ()
And in the case of fractional descriptions, such as "1.5 hours", we can do:
Timedesc.Span.For_human.make_frac_exn ~hours:1.5 ()
Finally, to access the human friendly "view", we can use Span.For_human.view
.
Ptime is a (very) commonly used package in projects due to being very portable, and robust. However, it lacks certain features which Timedesc provides, such as first class support for time zones, support for different date systems. As such one may wish to use both Ptime and Timedesc, especially if Ptime is already being used for a particular project.
To facilitate such use of both Ptime and Timedesc, utilities for converting to and from Ptime types are available as:
Note that Timedesc only supports nanosecond precision, while Ptime supports picosecond precision. If subnanosecond precision is a concern for you, then the above functions are not suitable.
Occasionally, we receive date times which carry both the time zone and the exact offset from UTC. Naturally we can discard the time zone since the offset alone suffices in deducing the precise timestamp. However, we can actually ask Timedesc to digest both via make_unambiguous
, which checks the offset against the time zone record to make sure it is actually a possible offset.
Other than Gregorian calendar, Timedesc also supports ISO week date and ISO ordinal date.
To construct date time in the alternative systems, we can use constructors such as ISO_week_date_time.make
and ISO_ord_date_time.make
.
Then to access the representation in the alternative date systems, we can use accessors such as iso_week_year
, iso_week
, and day_of_year
.
Sometimes we are only interested in the date component rather than both date and time. We can use Date
module in this case.
To construct a Gregorian calendar date, we can use Date.Ymd_date.make
. To construct ISO week date and ISO ordinal date, we can use Date.ISO_week_date.make
and Date.ISO_ord_date
respectively.
We have similar set of accessors for accessing values of Date.t
, such as Date.year
, Date.iso_week_year
, Date.day_of_year
.
To obtain a "view" (in a manner similar to the human-friendly "view" from Span.For_human
), we can use Date.ISO_week_date.view
and Date.ISO_ord_date.view
.
It is tempting to think that a time zone maps cleanly to a constant offset, and indeed we may define time zone as such, e.g. UTC+1, UTC-10, but this is far from what we mean in everyday context.
Very often, what we consider to be time zone actually represents a table which records what offset to use in which period, which we index/refer to by geographical names like "Europe/Paris", "Australia/Sydney". These tables are defined by governmental bodies, and attributes of the table, such as offset of any particular period, start and end of any particular period, may not show any observable pattern.
Thus it is not uncommon to see date time errors arising from attempts of applying some formulas universally, which might work well for a lot of cases in contemporary time periods, but fail for some combinations.
We make explicit of above explanation by considering "Europe/Paris" as an example, which observes a common form of transition called Daylight Saving Time (DST).
When DST starts (usually in March), the clocks "jump forward" by 1 hour, usually jumping from 2am to 3am, leading 2am to 3am (exclusive) to become non-existent.
Indeed we can observe the lack of continuity of Europe/Paris timeline below (UTC timeline is always continuous):
Mar UTC -------------|------------- 1am Europe/Paris -------------|------------- 2am 3am (+1) (+2)
Paris time zone offset also changes from UTC+1 to UTC+2.
When DST ends (usually in Oct), clocks "jump backward" by 1 hour, usually jumping from 3am to 2am, leading to 2am to 3am (exclusive) becoming duplicated:
Oct UTC -------------|------------- 1am Europe/Paris -------------|------------- 3am 2am (+2) (+1)
Paris time zone offset also changes from UTC+2 to UTC+1.
Another way of looking at above is when DST is in effect, Paris observes UTC+2, and UTC+1 otherwise:
|-------------DST on------------| |---DST off--| |---DST off--| Mar Oct UTC -------------|------------- ... -------------|------------- 1am 1am Europe/Paris -------------|------------- ... -------------|------------- 2am 3am 3am 2am (+1) (+2) (+2) (+1)
This start and end of the DST on and off periods, along with the corresponding offsets, form the basis of the table we mentioned above.
We highlight some critical cases in practice, and how Timedesc behaves and how it may differ from other libraries.
Take year 2021 for example, DST starts on 2021 Mar 28 for Paris, causing clocks to jump from 2am to 3am. Pick any intermediate point, say 2:30am, we yield an undefined date time. In this case, Timedesc refuses the construction of such t
in make
etc, while some libraries coerce the result into 3:30am.
And DST ends on 2021 Oct 31, causing clocks to jump from 3am to 2am. Say we pick 2:30am again, we are actually pointing at two time points (there are two 2:30am) unless we make an explicit selection between the first or second occurance. Whenever ambiguity of this form is a possiblity for the result of a function, say to_timestamp
, Timedesc uses local_result
variant type, of which `Single _
indicates lack of ambiguity for the particular result, and `Ambiguous _
indicates the result is ambiguous.
Some other libraries coerce the ambiguous result into one of the two possible choices (which exact one may not be guaranteed). If user wishes to do similar coercions, they may use min_of_local_result
or max_of_local_result
.
For constructions, make
yields a possibly ambiguous construction, while make_unambiguous
yields an unambiguous construction. In general, if you are provided with the exact offset to UTC, then make_unambiguous
is the better choice.
Result for when a local date time may be involved, e.g. using a date time with no precise time zone offset attached.
`Single
is yielded when the date time maps to exactly one 'a
. This happens when date time carries an accurate offset, or when the date time is not affected by any offset shifts (thus an accurate offset can be inferred).`Ambiguous
is yielded when date time maps to more than one (exactly two) 'a
. This happens when DST ends and "goes back an hour" for instance.val min_of_local_result : 'a local_result -> 'a
For min_of_local_result x
x = `Single a
, yields a
,x = `Ambiguous (a, b)
, yields a
,val max_of_local_result : 'a local_result -> 'a
For max_of_local_result x
x = `Single a
, yields a
,x = `Ambiguous (a, b)
, yields b
,val equal_local_result :
eq:('a -> 'a -> bool) ->
'a local_result ->
'a local_result ->
bool
module Span : sig ... end
type timestamp = Span.t
Definition of timestamp throughout the library follows the "seconds since unix epoch" definition
Implementation of date in:
Date.Ymd_date
)Date.ISO_week_date
)Date.ISO_ord_date
)module Date : sig ... end
Implementation of time of day with nanosecond precision
module Time : sig ... end
Implementation of time zone which uses IANA time zone database underneath
module Time_zone : sig ... end
Implementation of time zone aware date time in:
ISO_week_date_time
)ISO_ord_date_time
)This is the main type, and represents a point in the local timeline with respect to the residing time zone. Conceptually a triple of "date", "time" (or "time of day"), and time zone.
A t
always maps to at least one point on the UTC timeline, and make
fails if this is not the case. t
may also map to two points on the UTC timeline in the case of DST and without an unambiguous offset, however.
In the ambiguous case, functions which return _ local_result
will yield an `Ambiguous _
value, and `Single _
otherwise.
ns
may be >= 10^9
to represent leap second, but always remains < 2 * 10^9
.
s
is always >= 0
and < 60
, even when second 60 is used during construction. In other words, second 60 is represented via ns
field.
type error = [
| `Does_not_exist
| `Invalid_year of int
| `Invalid_month of int
| `Invalid_day of int
| `Invalid_hour of int
| `Invalid_minute of int
| `Invalid_second of int
| `Invalid_s_frac of float
| `Invalid_ns of int
| `Invalid_tz_info of string option * Span.t
]
exception Error_exn of error
val string_of_error : error -> string
val make :
?tz:Time_zone.t ->
?ns:int ->
?s_frac:float ->
year:int ->
month:int ->
day:int ->
hour:int ->
minute:int ->
second:int ->
unit ->
(t, error) result
Constructs a date time providing only a time zone (defaults to local time zone).
A precise offset is inferred if possible.
Note that this may yield a ambiguous date time if the time zone has varying offsets, causing a local date time to appear twice, e.g. countries with DST.
See make_unambiguous
for the more precise construction.
See Date.Ymd_date.make
for error handling of date specification.
See Time.make
for error handling of time of day specification.
val make_exn :
?tz:Time_zone.t ->
?ns:int ->
?s_frac:float ->
year:int ->
month:int ->
day:int ->
hour:int ->
minute:int ->
second:int ->
unit ->
t
val make_unambiguous :
?tz:Time_zone.t ->
?ns:int ->
?s_frac:float ->
year:int ->
month:int ->
day:int ->
hour:int ->
minute:int ->
second:int ->
offset_from_utc:Span.t ->
unit ->
(t, error) result
Constructs a date time providing time zone offset (offset from UTC), and optionally a time zone. As an example, for "UTC+1", you would give a duration of positive 1 hour for offset_from_utc
.
Subsecond value of offset_from_utc
is ignored.
Nanosecond used is the addition of ns
and s_frac * 10^9
.
If a time zone is provided, then offset_from_utc
is checked against the time zone record, and returns Error `Invalid_tz_info
if offset_from_utc
is not a possible offset for the particular date time in said time zone.
Otherwise same leap second handling and error handling as make
.
val make_unambiguous_exn :
?tz:Time_zone.t ->
?ns:int ->
?s_frac:float ->
year:int ->
month:int ->
day:int ->
hour:int ->
minute:int ->
second:int ->
offset_from_utc:Span.t ->
unit ->
t
val ymd_date : t -> Date.Ymd_date.view
val iso_week_date : t -> Date.ISO_week_date.view
val iso_ord_date : t -> Date.ISO_ord_date.view
val year : t -> int
val month : t -> int
val day : t -> int
val iso_week_year : t -> int
val iso_week : t -> int
val day_of_year : t -> int
val hour : t -> int
val minute : t -> int
val second : t -> int
val ns : t -> int
val is_leap_second : t -> bool
val tz : t -> Time_zone.t
val offset_from_utc : t -> Span.t local_result
val to_timestamp : t -> timestamp local_result
to_timestamp
loses information about leap second
val to_timestamp_float_s : t -> float local_result
Returns timestamp in seconds, fraction represent
val to_timestamp_float_s_single : t -> float
val of_timestamp : ?tz_of_date_time:Time_zone.t -> timestamp -> t option
val of_timestamp_exn : ?tz_of_date_time:Time_zone.t -> timestamp -> t
val of_timestamp_float_s : ?tz_of_date_time:Time_zone.t -> float -> t option
val of_timestamp_float_s_exn : ?tz_of_date_time:Time_zone.t -> float -> t
Compare based on ordering of min_of_local_result @@ to_timestamp _
Warning: compare_chrono_min x y = 0
does not imply equal x y
Compare based on ordering of max_of_local_result @@ to_timestamp _
Warning: compare_chrono_max x y = 0
does not imply equal x y
Structural comparison, compare_struct x y = 0
implies equal x y
Ordering does not correspond to chronological ordering
val min_val : t
val max_val : t
val now : ?tz_of_date_time:Time_zone.t -> unit -> t
exception Date_time_cannot_deduce_offset_from_utc of t
val pp : ?format:string -> unit -> Format.formatter -> t -> unit
Pretty printing for date time.
Default format string:
{year} {mon:Xxx} {day:0X} {hour:0X}:{min:0X}:{sec:0X}{sec-frac:.} \ {tzoff-sign}{tzoff-hour:0X}:{tzoff-min:0X}:{tzoff-sec:0X}
Format string specification:
{{ literal { {year} year {mon:Xxx} abbreviated month name (e.g. Jan), casing of 'x' controls the casing {mon:Xx*} full month name (e.g. January), casing of first 'x' controls casing of first letter, casing of second 'x' controls casing of following letters {mon:cX} month in number form (e.g. 01) character 'c' before 'X' is used for padding (leave out character for no padding) {day:cX} month day (e.g. 1) character 'c' before 'X' is used for padding (leave out character for no padding) {wday:Xxx} abbreviated weekday name (e.g. Sun), the casing of 'x' controls the casing {wday:Xx*} full weekday name (e.g. Sunday), casing of first 'x' controls casing of first letter, casing of second 'x' controls casing of following letters {hour:cX} hour in 24-hour format, character 'c' before 'X' determines padding (leave out character for no padding) {12hour:cX} hour in 12-hour format, character 'c' before 'X' determines padding (leave out character for no padding) {min:cX} minute, character 'c' before 'X' determines padding (leave out character for no padding) {sec:cX} second, character 'c' before 'X' determines padding (leave out character for no padding) {ns} nanosecond {sec-frac:cN} fraction of second character c is used as the decimal separator N determines the number of digits to take after decimal separator if N is not specified, then the smallest number of digits required after decimal separator for a lossless representation is used result is truncated to said number of digits {tzoff-sign} time zone offset sign ('+' or '-') raises Date_time_cannot_deduce_offset_from_utc if time zone offset cannot be calculated {tzoff-hour:cX} time zone offset hour, follows same padding rule as "{hour:cX}" raises Date_time_cannot_deduce_offset_from_utc if time zone offset cannot be calculated {tzoff-min:cX} time zone offset minute, follows same padding rule as "{min:cX}" raises Date_time_cannot_deduce_offset_from_utc if time zone offset cannot be calculated {tzoff-sec:cX} time zone offset second, follows same padding rule as "{sec:cX}" raises Date_time_cannot_deduce_offset_from_utc if time zone offset cannot be calculated
val to_string : ?format:string -> t -> string option
String conversion using pp
.
Returns None
instead of raising exception when time zone offset cannot be deduced but required by the format string
val pp_rfc3339 : ?frac_s:int -> unit -> Format.formatter -> t -> unit
Pretty prints according to RFC3339, e.g. 2020-01-20T13:00:00.0001+10
.
frac_s
defaults to as many digits as required for a lossless representation.
val pp_rfc3339_milli : Format.formatter -> t -> unit
val pp_rfc3339_micro : Format.formatter -> t -> unit
val pp_rfc3339_nano : Format.formatter -> t -> unit
val to_rfc3339 : ?frac_s:int -> t -> string option
String conversion using pp_rfc3339
.
Returns None
if time zone offset cannot be deduced instead of raising exception.
val to_rfc3339_milli : t -> string option
val to_rfc3339_micro : t -> string option
val to_rfc3339_nano : t -> string option
Parses a subset of ISO8601, up to 9 fractional digits for second (nanosecond precision).
If more than 9 fractional digits are provided, then only the first 9 digits are used, i.e. no rounding.
val of_iso8601_exn : string -> t
val to_sexp_string : t -> string
val pp_sexp : Format.formatter -> t -> unit
module Timestamp : sig ... end
Timestamp specific functions
module Interval : sig ... end
module Zoneless : sig ... end
module ISO_week_date_time : sig ... end
module ISO_ord_date_time : sig ... end
module Time_zone_info : sig ... end
Time zone information that can be attached to date time like data
module Utils : sig ... end