Module Ptime

POSIX time values.

Consult the basics and a few notes and limitations.

References

POSIX time spans

type span

The type for signed picosecond precision POSIX time spans. A value of this type represent the POSIX duration between two POSIX timestamps.

module Span : sig ... end

POSIX time spans.

POSIX timestamps

type t

The type for picosecond precision POSIX timestamps in the range [min;max]. Note that POSIX timestamps, and hence values of this type, are by definition always on the UTC timeline.

val v : (int * int64) -> t

v s is of_span (Span.v s) but raise Invalid_argument if s is not in the right range. Use Span.of_d_ps and of_span to deal with untrusted input.

val epoch : t

epoch is 1970-01-01 00:00:00 UTC.

val min : t

min is 0000-01-01 00:00:00 UTC, the earliest timestamp representable by Ptime.

val max : t

max is 9999-12-31 23:59:59.999999999999 UTC, the latest timestamp representable by Ptime.

val of_span : span -> t option

of_span d is the POSIX time stamp that:

  • Happens at the POSIX span d after epoch if d is positive.
  • Happens at the POSIX span d before epoch if d is negative.

None is returned if the timestamp is not in the range [min;max].

val to_span : t -> span

to_span t is the signed POSIX span that happen between t and epoch:

  • If the number is positive t happens after epoch.
  • If the number is negative t happens before epoch.
val of_float_s : float -> t option

of_float_s d is like of_span but with d as a floating point second POSIX span d. This function is compatible with the result of Unix.gettimeofday. Decimal fractional seconds beyond 1e-12 are truncated.

val to_float_s : t -> float

to_float_s t is like to_span but returns a floating point second POSIX span.

Warning. Due to floating point inaccuracies do not expect the function to round trip with of_float_s; especially near Ptime.min and Ptime.max.

val truncate : frac_s:int -> t -> t

truncate ~frac_s t is t truncated to the frac_s decimal fractional second. Effectively this reduces precision without rounding, the timestamp remains in the second it is in. frac_s is clipped to the range [0;12].

val frac_s : t -> span

frac_s t is the (positive) fractional second duration in t.

Predicates

val equal : t -> t -> bool

equal t t' is true iff t and t' are the same timestamps.

val compare : t -> t -> int

compare t t' is a total order on timestamps that is compatible with timeline order.

val is_earlier : t -> than:t -> bool

is_earlier t ~than is true iff compare t than = -1.

val is_later : t -> than:t -> bool

is_later t than is true iff compare t than = 1.

POSIX arithmetic

WARNING. A POSIX time span is not equal to an SI second based time span, see the basics. Do not use these functions to perform calendar arithmetic or measure wall-clock durations, you will fail.

val add_span : t -> span -> t option

add_span t d is timestamp t + d, that is t with the signed POSIX span d added. None is returned if the result is not in the range [min;max].

val sub_span : t -> span -> t option

sub_span t d is the timestamp t - d, that is t with the signed POSIX span d subtracted. None is returned if the result is not in the range [min;max].

val diff : t -> t -> span

diff t t' is the signed POSIX span t - t' that happens between the timestamps t and t'.

Time zone offsets between local and UTC timelines

type tz_offset_s = int

The type for time zone offsets between local and UTC timelines in seconds. This is the signed difference in seconds between the local timeline and the UTC timeline:

tz_offset_s = local - UTC
  • A value of -3600 means that the local timeline is sixty minutes behind the UTC timeline.
  • A value of 3600 means that the local timeline is sixty minutes ahead the UTC timeline.

Date-time value conversions

A date-time represents a point on the UTC timeline by pairing a date in the proleptic Gregorian calendar and a second precision daytime in a local timeline with stated relationship to the UTC timeline.

type date = int * int * int

The type for big-endian proleptic Gregorian dates. A triple (y, m, d) with:

  • y the year from 0 to 9999. 0 denotes -1 BCE (this follows the ISO 8601 convention).
  • m is the month from 1 to 12
  • d is the day from 1 to 28, 29, 30 or 31 depending on m and y

A date is said to be valid iff the values (y, m, d) are in the range mentioned above and represent an existing date in the proleptic Gregorian calendar.

type time = (int * int * int) * tz_offset_s

The type for daytimes on a local timeline. Pairs a triple (hh, mm, ss) denoting the time on the local timeline and a tz_offset stating the relationship of the local timeline to the UTC timeline.

The (hh, mm, ss) components are understood and constrainted as follows:

  • hh is the hour from 0 to 23.
  • mm is the minute from 0 to 59.
  • ss is the seconds from 0 to 60. 60 may happen whenever a leap second is added.

A time value is said to be valid iff the values (hh, mm, ss) are in the ranges mentioned above.

Date and time

val of_date_time : (date * time) -> t option

of_date_time dt is the POSIX timestamp corresponding to date-time dt or None if dt has an invalid date, invalid time or the date-time is not in the range [min;max].

Leap seconds. Any date-time with a seconds value of 60, hence representing a leap second addition, is mapped to the date-time that happens 1 second later. Any date-time with a seconds value of 59 is mapped to the POSIX timestamp that represents this instant, if a leap second was subtracted at that point, this is the POSIX timestamp that represents this inexisting instant. See the basics.

val to_date_time : ?tz_offset_s:tz_offset_s -> t -> date * time

to_date_time ~tz_offset_s t is the date-time of the timestamp t.

tz_offset_s hints the time zone offset used for the resulting daytime component (defaults to 0, i.e. UTC). The offset is not honoured and fallbacks to 0 in case the resulting date-time rendering of the timestamp would yield an invalid date. This means that you should always interpret the resulting time component with the time zone offset it is paired with in the result and not assume it will be the one you gave to the function. Note that for real-world time zone offsets the fallback to 0 will only happen around Ptime.min and Ptime.max. Formally the fallback occurs whenever add_span t (Span.of_int_s tz_offset_s) is None.

Leap seconds. No POSIX timestamp can represent a date-time with a leap second added, hence this function will never return a date-time with a 60 seconds value. This function does return inexisting UTC date-times with 59 seconds whenever a leap second is subtracted since POSIX timestamps do represent them. See the basics.

Subsecond precision. POSIX timestamps with subsecond precision are floored, i.e. the date-time always has the second mentioned in the timestamp.

Date

val of_date : ?tz_offset_s:tz_offset_s -> date -> t option

of_date d is of_date_time (d, ((00, 00, 00), tz_offset_s)). tz_offset_s defaults to 0, i.e. UTC.

val to_date : ?tz_offset_s:tz_offset_s -> t -> date

to_date t is fst (to_date_time ?tz_offset_s t).

Year

val of_year : ?tz_offset_s:tz_offset_s -> int -> t option

of_year y is of_date ?tz_offset_s (y, 01, 01).

val to_year : ?tz_offset_s:tz_offset_s -> t -> int

to_year t is the first component of (to_date ?tz_offset_s t)) but more efficient.

Week days

val weekday : ?tz_offset_s:tz_offset_s -> t -> [ `Sun | `Mon | `Tue | `Wed | `Thu | `Fri | `Sat ]

weekday ~tz_offset_s t is the day in the 7-day week of timestamp t expressed in the time zone offset ts_offset_s (defaults to 0).

val weekday_num : ?tz_offset_s:tz_offset_s -> t -> int

weekday_num is like weekday but returns a weekday number, 0 is sunday, 1 is monday, …, 6 is saturday etc.

RFC 3339 timestamp conversions

type error_range = int * int

The type for error ranges, starting and ending position.

type rfc3339_error = [
  1. | `Invalid_stamp
  2. | `Eoi
  3. | `Exp_chars of char list
  4. | `Trailing_input
]

The type for RFC 3339 timestamp parsing errors. `Invalid_stamp means that either the time stamp is not in the range [min;max], or the date is invalid, or one of the fields is not in the right range.

val pp_rfc3339_error : Stdlib.Format.formatter -> rfc3339_error -> unit

pp_rfc3339_error ppf e prints an unspecified representation of e on ppf.

val rfc3339_error_to_msg : ('a, [ `RFC3339 of error_range * rfc3339_error ]) Stdlib.result -> ('a, [> `Msg of string ]) Stdlib.result

rfc3339_error_to_msg r converts RFC 3339 parse errors to error messages.

val rfc3339_string_error : ('a, [ `RFC3339 of error_range * rfc3339_error ]) Stdlib.result -> ('a, string) Stdlib.result

rfc3339_string_error r converts RFC 3339 parse errors errors to string errors.

val of_rfc3339 : ?strict:bool -> ?sub:bool -> ?start:int -> string -> (t * tz_offset_s option * int, [> `RFC3339 of error_range * rfc3339_error ]) Stdlib.result

of_rfc3339 ~strict ~sub ~start s parses an RFC 3339 date-time starting at start (defaults to 0) in s to a triple (t, tz, count) with:

  • t the POSIX timestamp (hence on the UTC timeline).
  • tz, the optional time zone offset found in the timestamp. None is returned iff the date-time satisfies the unknown local offset convention.
  • count the number of bytes read starting at start to parse the timestamp. If sub is false (default) this is always String.length s - start and Error `Trailing_input is returned if there are still bytes in s after the date-time was parsed. Use ~sub:true for allowing trailing input to exist.
  • strict if false (default) the pasring function does not error on timestamp with lowercase 'T' or 'Z' characters, or space separated date and times, and `hhmm` and `hh` timezone offsets (strict mandates hh:mm). This allows to parse a slightly larger subset of ISO 8601 than what RFC 3339 allows

Notes and limitations.

  • If start is not an index of s, Error ((start, start), `Eoi) is returned.
  • RFC 3339 allows a few degenerate (I say) timestamps with non-zero time zone offsets to be parsed at the boundaries that correspond to timestamps that cannot be expressed in UTC in RFC 3339 itself (e.g. 0000-01-01T00:00:00+00:01). The function errors on these timestamps with `Invalid_stamp as they cannot be represented in the range [min;max].
  • Leap seconds are allowed on any date-time and handled as in of_date_time
  • Fractional parts beyond the picosecond (1e-12) are truncated.
val to_rfc3339 : ?space:bool -> ?frac_s:int -> ?tz_offset_s:tz_offset_s -> t -> string

to_rfc3339_tz ~space ~frac_s ~tz_offset_s t formats the timestamp t according to a RFC 3339 date-time production with:

  • tz_offset_s hints the time zone offset to use, use 0 for UTC. The hint is ignored in the following cases: if tz_offset_s is not an integral number of minutes and its magnitude not in the range permitted by the standard, if add_span t (Span.of_int_s tz_offset_s) is None (the resulting timestamp rendering would not be RFC 3339 compliant). If either the hint is ignored or tz_offset_s is unspecified then the unknown local offset convention is used to render the time zone component.
  • frac_s, clipped to the range [0;12] specifies that exactly frac_s decimal digits of the fractional second of t are rendered (defaults to 0).
  • space if true the date and time separator is a space rather than a 'T' (not recommended but may be allowed by the protocol you are dealing with, defaults to false).
val pp_rfc3339 : ?space:bool -> ?frac_s:int -> ?tz_offset_s:tz_offset_s -> unit -> Stdlib.Format.formatter -> t -> unit

pp_rfc3339 ?space ?frac_s ?tz_offset_s () ppf t is Format.fprintf ppf "%s" (to_rfc3339 ?space ?frac_s ?tz_offset_s t).

val pp_human : ?frac_s:int -> ?tz_offset_s:tz_offset_s -> unit -> Stdlib.Format.formatter -> t -> unit

pp_human ~frac_s ~tz_offset_s () ppf t prints an unspecified, human readable, locale-independent, representation of t with:

  • tz_offset_s hints the time zone offset to use. The hint is ignored in the following cases: if tz_offset_s is not an integral number of minutes and its magnitude not in the range permitted by the standard, if add_span t (Span.of_int_s tz_offset_s) is None. If either the hint is ignored or tz_offset_s is unspecified then RFC 3339's unknown local offset convention is used to render the time zone component.
  • frac_s clipped to the range [0;12] specifies that exactly frac_s decimal digits of the fractional second of t are rendered (defaults to 0).

Note. The output of this function is similar to but not compliant with RFC 3339, it should only be used for presentation, not as a serialization format.

val pp : Stdlib.Format.formatter -> t -> unit

pp is pp_human ~tz_offset_s:0.

val dump : Stdlib.Format.formatter -> t -> unit

dump ppf t prints an unspecified raw representation of t on ppf.

Basics

POSIX time counts POSIX seconds since the epoch 1970-01-01 00:00:00 UTC. As such a POSIX timestamp is always on the UTC timeline.

POSIX time doesn't count leap seconds, so by definition it cannot represent them. One way of viewing this is that whenever a leap second is added a POSIX second lasts two SI seconds and whenever a leap second is subtracted a POSIX second lasts zero SI second.

Ptime does not provide any mean to convert the duration between two POSIX timestamps to SI seconds. The reason is that in order to accurately find this number, a leap second table is needed. However since this table may change every six months, Ptime decides not to include it so as not to potentially become incorrect every six months.

This decision has the following implications. First it should be realised that the durations mentioned by the add_span, sub_span and diff functions are expressed in POSIX seconds which may represent zero, one, or two SI seconds. For example if we add 1 second with add_span to the POSIX timestamp for 1998-12-31 23:59:59 UTC, what we get is the timestamp for 1999-01-01 00:00:00 UTC:

let get = function None -> assert false | Some v -> v
let utc d t = get @@ Ptime.of_date_time (d, (t, 0))
let t0 = utc (1998, 12, 31) (23, 59, 59)
let t1 = utc (1999, 01, 01) (00, 00, 00)
let one_s = Ptime.Span.of_int_s 1
let () = assert (Ptime.equal (get @@ Ptime.add_span t0 one_s) t1)

However since the leap second 1998-12-31 23:59:60 UTC exists, two actual SI seconds elapsed between t0 and t1. Now if we use diff to find the POSIX duration that elapsed between t0 and t1 we get one POSIX second:

let () = assert (Ptime.Span.equal (Ptime.diff t1 t0) one_s)

But still, two SI seconds elapsed between these two points in time. Note also that no value of type t can represent the UTC timetamp 1998-12-31 23:59:60 and hence Ptime.to_date_time will never return a date-time with a seconds value of 60. In fact both 1998-12-31 23:59:60 UTC and 1999-01-01 00:00:00 UTC are represented by the same timestamp:

let t2 = utc (1998, 12, 31) (23, 59, 60)
let () = assert (Ptime.equal t1 t2)

This is true of any added leap second, we map it on the first second of the next minute, thus matching the behaviour of POSIX's mktime function.

If a leap second is subtracted on a day the following occurs – 2015, as of writing this never happened. Let YYYY-06-30 23:59:58 be the instant a leap second is subtracted, this means that the next UTC date-time, one SI second later, is YYYY-07-01 00:00:00. However if we diff the two instants:

let y = 9999 (* hypothetical year were this happens *)
let t0 = utc (y, 06, 30) (23, 59, 58)
let t1 = utc (y, 07, 01) (00, 00, 00)
let two_s = Ptime.Span.of_int_s 2
let () = assert (Ptime.Span.equal (Ptime.diff t1 t0) two_s)

We get two POSIX seconds, but only one SI second elapsed between these two points in time. It should also be noted that POSIX time will represent a point that never existed in time namely YYYY-06-30 23:59:59, the POSIX second with 0 SI second duration and that Ptime.to_date_time will return a date-time value for this timestamp even though it never existed:

let t2 = utc (y, 06, 30) (23, 59, 59)
let () = assert (Ptime.equal (get @@ Ptime.add_span t0 one_s) t2)

Notes and limitations

The following points should be taken into account