TimedescTime 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_date_time_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_date_time_result or max_of_local_date_time_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, 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.make. To construct ISO week date and ISO ordinal date, we can use Date.ISO_week_date.make and Date.ISO_ord.make respectively.
We have similar set of accessors for accessing values of Date.t, such as Date.year, Date.iso_week, 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.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_date_time_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_date_time_result or max_of_local_date_time_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_date_time_result : 'a local_date_time_result -> 'aFor min_of_local_date_time_result x
x = `Single a, yields a,x = `Ambiguous (a, b), yields a,val max_of_local_date_time_result : 'a local_date_time_result -> 'aFor max_of_local_date_time_result x
x = `Single a, yields a,x = `Ambiguous (a, b), yields b,val equal_local_date_time_result :
('a -> 'a -> bool) ->
'a local_date_time_result ->
'a local_date_time_result ->
boolmodule Span : sig ... endtype timestamp = Span.tDefinition of timestamp throughout the library follows the "seconds since unix epoch" definition
Implementation of:
module Ym : sig ... endmodule ISO_week : sig ... endImplementation of date in:
Date.Ymd)Date.ISO_week_date)Date.ISO_ord)module Date : sig ... endImplementation of time of day with nanosecond precision
module Time : sig ... endImplementation of time zone which uses IANA time zone database underneath
module Time_zone : sig ... endImplementation 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_date_time_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 errorval string_of_error : error -> stringval 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) Stdlib.resultConstructs 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.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 ->
tval 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) Stdlib.resultConstructs 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 ->
tval of_date_and_time :
?tz:Time_zone.t ->
Date.t ->
Time.t ->
(t, error) Stdlib.resultSee make for details.
val of_date_and_time_exn : ?tz:Time_zone.t -> Date.t -> Time.t -> tval of_date_and_time_unambiguous :
?tz:Time_zone.t ->
offset_from_utc:Span.t ->
Date.t ->
Time.t ->
(t, error) Stdlib.resultSee make_unambiguous for details.
val of_date_and_time_unambiguous_exn :
?tz:Time_zone.t ->
offset_from_utc:Span.t ->
Date.t ->
Time.t ->
tval ymd_date : t -> Date.Ymd.viewval iso_week_date : t -> Date.ISO_week_date.viewval iso_ord_date : t -> Date.ISO_ord.viewval year : t -> intval month : t -> intval day : t -> intval iso_week : t -> ISO_week.tval iso_year : t -> intval day_of_year : t -> intval hour : t -> intval minute : t -> intval second : t -> intval ns : t -> intval is_leap_second : t -> boolval tz : t -> Time_zone.tval offset_from_utc : t -> Span.t local_date_time_resultval to_timestamp : t -> timestamp local_date_time_resultto_timestamp loses information about leap second
val to_timestamp_float_s : t -> float local_date_time_resultReturns timestamp in seconds, fraction represent
val to_timestamp_float_s_single : t -> floatval of_timestamp : ?tz_of_date_time:Time_zone.t -> timestamp -> t optionval of_timestamp_exn : ?tz_of_date_time:Time_zone.t -> timestamp -> tval of_timestamp_float_s : ?tz_of_date_time:Time_zone.t -> float -> t optionval of_timestamp_float_s_exn : ?tz_of_date_time:Time_zone.t -> float -> tCompare based on ordering of min_of_local_date_time_result @@ to_timestamp _
Warning: compare_chrono_min x y = 0 does not imply equal x y
Compare based on ordering of max_of_local_date_time_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 : tval max_val : tval now : ?tz_of_date_time:Time_zone.t -> unit -> texception Date_time_cannot_deduce_offset_from_utc of tval pp : ?format:string -> unit -> Stdlib.Format.formatter -> t -> unitPretty-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'/'x' controls the casing
{mon:Xx*} Full month name (e.g. January), casing of first 'X'/'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, e.g. {mon:X})
{day:cX} Month day (e.g. 1) character 'c' before 'X' is used for padding
(leave out character for no padding, e.g. {day:X})
{wday:Xxx} Abbreviated weekday name (e.g. Sun), the casing of 'X'/'x' controls the casing
{wday:Xx*} Full weekday name (e.g. Sunday), casing of first 'X'/'x' controls casing of first letter,
casing of second 'X'/'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, e.g. {hour:X})
{12hour:cX} Hour in 12-hour format, character 'c' before 'X' determines padding
(leave out character for no padding, e.g. {12hour:X})
{am/pm:XX} AM/PM indicator, the casing of 'X'/'x' controls the casing
{am/pm:x.x.} Same as above, but with periods, e.g. "a.m."
{min:cX} Minute, character 'c' before 'X' determines padding
(leave out character for no padding, e.g. {min:X})
{sec:cX} Second, character 'c' before 'X' determines padding
(leave out character for no padding, e.g. {sec:X})
{ns} Nanosecond
{sec-frac:cN} Fraction of second
Character c is used as the decimal separator and is mandatory
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 calculatedval to_string : ?format:string -> t -> stringString conversion using pp.
val pp_rfc3339 : ?frac_s:int -> unit -> Stdlib.Format.formatter -> t -> unitPretty-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 : Stdlib.Format.formatter -> t -> unitval pp_rfc3339_micro : Stdlib.Format.formatter -> t -> unitval pp_rfc3339_nano : Stdlib.Format.formatter -> t -> unitval to_rfc3339 : ?frac_s:int -> t -> stringString conversion using pp_rfc3339.
val to_rfc3339_milli : t -> stringval to_rfc3339_micro : t -> stringval to_rfc3339_nano : t -> stringval pp_iso8601 : ?frac_s:int -> unit -> Stdlib.Format.formatter -> t -> unitAlias to pp_rfc3339
val pp_iso8601_milli : Stdlib.Format.formatter -> t -> unitval pp_iso8601_micro : Stdlib.Format.formatter -> t -> unitval pp_iso8601_nano : Stdlib.Format.formatter -> t -> unitval to_iso8601 : ?frac_s:int -> t -> stringAlias to to_rfc3339
val to_iso8601_milli : t -> stringval to_iso8601_micro : t -> stringval to_iso8601_nano : t -> stringval pp_rfc9110 : Stdlib.Format.formatter -> t -> unitWarning: Subsecond value is truncated
val to_rfc9110 : t -> stringWarning: Subsecond value is truncated
val pp_http : Stdlib.Format.formatter -> t -> unitAlias to pp_rfc9110
val to_http : t -> stringAlias to to_rfc9110
val of_iso8601 : string -> (t, string) Stdlib.resultParses 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 -> tval of_rfc9110 : string -> (t, string) Stdlib.resultParses RFC9110/RFC5322 (HTTP) date time.
More specifically, parses the following permissively:
Weekday is not checked to be correct.
val of_rfc9110_exn : string -> tval of_http : string -> (t, string) Stdlib.resultAlias to of_rfc9110
val of_http_exn : string -> tAlias to of_rfc9110_exn
module Timestamp : sig ... endTimestamp specific functions
module Interval : sig ... endmodule Zoneless : sig ... endmodule ISO_week_date_time : sig ... endmodule ISO_ord_date_time : sig ... endmodule Time_zone_info : sig ... endmodule Utils : sig ... end