- Add project scaffolding (dune, dune-project, opam, .ocamlformat) - Implement basic parsing and handling of iCalendar events - Add event predicates for common event types (all-day, timed, recurrence, exceptions) - Add transformation logic to map iCalendar events to Remind format (stub implementation) - Provide utilities for extracting event details and converting dates/times - Set up executable entrypoint and command-line interface using Cmdliner - Include Remind event type definitions and helpers
2302 lines
82 KiB
OCaml
2302 lines
82 KiB
OCaml
open Icalendar
|
||
open Utils
|
||
|
||
(*
|
||
FILE: icalendar.ml
|
||
--------------------
|
||
|
||
module Uri = struct
|
||
include Uri
|
||
let pp = pp_hum
|
||
end
|
||
|
||
module Ptime = struct
|
||
include Ptime
|
||
let equal_date (y, m, d) (y', m', d') = y = y' && m = m' && d = d'
|
||
let pp_date fmt (y, m, d) = Fmt.pf fmt "%04d-%02d-%02d" y m d
|
||
end
|
||
|
||
let positive = true
|
||
|
||
type timestamp_utc = Ptime.t [@@deriving eq, show]
|
||
type timestamp_local = Ptime.t [@@deriving eq, show]
|
||
|
||
type utc_or_timestamp_local = [
|
||
| `Utc of timestamp_utc
|
||
| `Local of timestamp_local
|
||
] [@@deriving eq, show]
|
||
|
||
type timestamp = [
|
||
utc_or_timestamp_local
|
||
| `With_tzid of timestamp_local * (bool * string)
|
||
] [@@deriving eq, show]
|
||
|
||
type date_or_datetime = [ `Datetime of timestamp | `Date of Ptime.date ] [@@deriving eq, show]
|
||
|
||
type valuetype = [
|
||
`Binary | `Boolean | `Caladdress | `Date | `Datetime | `Duration | `Float
|
||
| `Integer | `Period | `Recur | `Text | `Time | `Uri | `Utcoffset
|
||
| `Xname of (string * string) | `Ianatoken of string
|
||
] [@@deriving eq, show]
|
||
|
||
type cutype = [ `Group | `Individual | `Resource | `Room | `Unknown
|
||
| `Ianatoken of string | `Xname of string * string ] [@@deriving eq, show]
|
||
|
||
type partstat = [ `Accepted | `Completed | `Declined | `Delegated
|
||
| `In_process | `Needs_action | `Tentative
|
||
| `Ianatoken of string | `Xname of string * string ] [@@deriving eq, show]
|
||
|
||
type relationship =
|
||
[ `Parent | `Child | `Sibling |
|
||
`Ianatoken of string | `Xname of string * string ] [@@deriving eq, show]
|
||
|
||
type role = [ `Chair | `Nonparticipant | `Optparticipant | `Reqparticipant
|
||
| `Ianatoken of string | `Xname of string * string ] [@@deriving eq, show]
|
||
|
||
type fbtype = [ `Free | `Busy | `Busy_Unavailable | `Busy_Tentative | `Ianatoken of string | `Xname of string * string ] [@@deriving eq, show]
|
||
|
||
type param_value = [ `Quoted of string | `String of string ] [@@deriving eq, show]
|
||
|
||
type _ icalparameter =
|
||
| Altrep : Uri.t icalparameter
|
||
| Cn : param_value icalparameter
|
||
| Cutype : cutype icalparameter
|
||
| Delegated_from : (Uri.t list) icalparameter
|
||
| Delegated_to : (Uri.t list) icalparameter
|
||
| Dir : Uri.t icalparameter
|
||
| Encoding : [ `Base64 ] icalparameter
|
||
| Media_type : (string * string) icalparameter
|
||
| Fbtype : fbtype icalparameter
|
||
| Language : string icalparameter
|
||
| Member : (Uri.t list) icalparameter
|
||
| Partstat : partstat icalparameter
|
||
| Range : [ `Thisandfuture ] icalparameter
|
||
| Related : [ `Start | `End ] icalparameter
|
||
| Reltype : relationship icalparameter
|
||
| Role : role icalparameter
|
||
| Rsvp : bool icalparameter
|
||
| Sentby : Uri.t icalparameter
|
||
| Tzid : (bool * string) icalparameter
|
||
| Valuetype : valuetype icalparameter
|
||
| Iana_param : string -> param_value list icalparameter
|
||
| Xparam : (string * string) -> param_value list icalparameter
|
||
|
||
let equal_icalparameter : type a. a icalparameter -> a -> a -> bool
|
||
= fun k lhs_v rhs_v ->
|
||
match k with
|
||
| Altrep -> Uri.equal lhs_v rhs_v
|
||
| Cn -> equal_param_value lhs_v rhs_v
|
||
| Cutype -> equal_cutype lhs_v rhs_v
|
||
| Delegated_from -> List.for_all2 Uri.equal lhs_v rhs_v
|
||
| Delegated_to -> List.for_all2 Uri.equal lhs_v rhs_v
|
||
| Dir -> Uri.equal lhs_v rhs_v
|
||
| Encoding -> lhs_v = rhs_v
|
||
| Media_type -> String.equal (fst lhs_v) (fst rhs_v) && String.equal (snd lhs_v) (snd rhs_v)
|
||
| Fbtype -> equal_fbtype lhs_v rhs_v
|
||
| Language -> String.equal lhs_v rhs_v
|
||
| Member -> List.for_all2 Uri.equal lhs_v rhs_v
|
||
| Partstat -> equal_partstat lhs_v rhs_v
|
||
| Range -> lhs_v = rhs_v
|
||
| Related -> lhs_v = rhs_v
|
||
| Reltype -> equal_relationship lhs_v rhs_v
|
||
| Role -> equal_role lhs_v rhs_v
|
||
| Rsvp -> lhs_v = rhs_v
|
||
| Sentby -> Uri.equal lhs_v rhs_v
|
||
| Tzid -> fst lhs_v = fst rhs_v && String.equal (snd lhs_v) (snd rhs_v)
|
||
| Valuetype -> equal_valuetype lhs_v rhs_v
|
||
| Iana_param _ -> List.for_all2 equal_param_value lhs_v rhs_v
|
||
| Xparam _ -> List.for_all2 equal_param_value lhs_v rhs_v
|
||
|
||
let pp_icalparameter : type a. Format.formatter -> a icalparameter -> a -> unit
|
||
= fun fmt k v ->
|
||
match k with
|
||
| Altrep -> Format.fprintf fmt "Altrep %a" Uri.pp v
|
||
| Cn -> Format.fprintf fmt "Cn %a" pp_param_value v
|
||
| Cutype -> Format.fprintf fmt "Cutype %a" pp_cutype v
|
||
| Delegated_from -> Format.fprintf fmt "Delegated_from %a" Fmt.(list Uri.pp) v
|
||
| Delegated_to -> Format.fprintf fmt "Delegated_to %a" Fmt.(list Uri.pp) v
|
||
| Dir -> Format.fprintf fmt "Dir %a" Uri.pp v
|
||
| Encoding -> Format.fprintf fmt "Encoding base64"
|
||
| Media_type -> Format.fprintf fmt "Media_type (%s/%s)" (fst v) (snd v)
|
||
| Fbtype -> Format.fprintf fmt "Fbtype %a" pp_fbtype v
|
||
| Language -> Format.fprintf fmt "Language %s" v
|
||
| Member -> Format.fprintf fmt "Member %a" Fmt.(list Uri.pp) v
|
||
| Partstat -> Format.fprintf fmt "Partstat %a" pp_partstat v
|
||
| Range -> Format.fprintf fmt "Range thisandfuture"
|
||
| Related -> Format.fprintf fmt "Related %s" (match v with `Start -> "start" | `End -> "end")
|
||
| Reltype -> Format.fprintf fmt "Reltype %a" pp_relationship v
|
||
| Role -> Format.fprintf fmt "Role %a" pp_role v
|
||
| Rsvp -> Format.fprintf fmt "Rsvp %b" v
|
||
| Sentby -> Format.fprintf fmt "Sentby %a" Uri.pp v
|
||
| Tzid -> Format.fprintf fmt "Tzid (%b, %s)" (fst v) (snd v)
|
||
| Valuetype -> Format.fprintf fmt "Valuetype %a" pp_valuetype v
|
||
| Iana_param a -> Format.fprintf fmt "Iana_param (%s, %a)" a Fmt.(list pp_param_value) v
|
||
| Xparam (a, b) -> Format.fprintf fmt "Xparam ((%s, %s), %a)" a b Fmt.(list pp_param_value) v
|
||
|
||
module K = struct
|
||
type 'a t = 'a icalparameter
|
||
|
||
let compare : type a b. a t -> b t -> (a, b) Gmap.Order.t = fun lhs rhs ->
|
||
let open Gmap.Order in
|
||
match (lhs, rhs) with
|
||
| (Altrep, Altrep) -> Eq
|
||
| (Cn, Cn) -> Eq
|
||
| (Cutype, Cutype) -> Eq
|
||
| (Delegated_from, Delegated_from) -> Eq
|
||
| (Delegated_to, Delegated_to) -> Eq
|
||
| (Dir, Dir) -> Eq
|
||
| (Encoding, Encoding) -> Eq
|
||
| (Media_type, Media_type) -> Eq
|
||
| (Fbtype, Fbtype) -> Eq
|
||
| (Language, Language) -> Eq
|
||
| (Member, Member) -> Eq
|
||
| (Partstat, Partstat) -> Eq
|
||
| (Range, Range) -> Eq
|
||
| (Related, Related) -> Eq
|
||
| (Reltype, Reltype) -> Eq
|
||
| (Role, Role) -> Eq
|
||
| (Rsvp, Rsvp) -> Eq
|
||
| (Sentby, Sentby) -> Eq
|
||
| (Tzid, Tzid) -> Eq
|
||
| (Valuetype, Valuetype) -> Eq
|
||
| (Iana_param a, Iana_param a') ->
|
||
begin match String.compare a a' with
|
||
| 0 -> Eq
|
||
| x when x < 0 -> Lt
|
||
| _ -> Gt
|
||
end
|
||
| (Xparam (a, b), Xparam (a', b')) ->
|
||
begin match String.compare a a' with
|
||
| 0 ->
|
||
begin match String.compare b b' with
|
||
| 0 -> Eq
|
||
| y when y < 0 -> Lt
|
||
| _ -> Gt
|
||
end
|
||
| x when x < 0 -> Lt
|
||
| _ -> Gt
|
||
end
|
||
| _ ->
|
||
let to_int : type a. a icalparameter -> int =
|
||
function
|
||
| Altrep -> 0
|
||
| Cn -> 1
|
||
| Cutype -> 2
|
||
| Delegated_from -> 3
|
||
| Delegated_to -> 4
|
||
| Dir -> 5
|
||
| Encoding -> 6
|
||
| Media_type -> 7
|
||
| Fbtype -> 8
|
||
| Language -> 9
|
||
| Member -> 10
|
||
| Partstat -> 11
|
||
| Range -> 12
|
||
| Related -> 13
|
||
| Reltype -> 14
|
||
| Role -> 15
|
||
| Rsvp -> 16
|
||
| Sentby -> 17
|
||
| Tzid -> 18
|
||
| Valuetype -> 19
|
||
| Iana_param _ -> 20
|
||
| Xparam _ -> 21 in
|
||
if Stdlib.compare (to_int lhs) (to_int rhs) < 0
|
||
then Lt else Gt
|
||
end
|
||
|
||
module Params = Gmap.Make(K)
|
||
|
||
type params = Params.t
|
||
|
||
let equal_params m m' =
|
||
Params.equal { f = equal_icalparameter } m m'
|
||
|
||
let pp_params ppf m = Params.iter
|
||
(fun (Params.B (k, v)) ->
|
||
pp_icalparameter ppf k v ;
|
||
Format.pp_print_space ppf ()) m
|
||
|
||
type other_prop =
|
||
[ `Iana_prop of string * params * string
|
||
| `Xprop of (string * string) * params * string ] [@@deriving eq, show]
|
||
|
||
type cal_prop =
|
||
[ `Prodid of params * string
|
||
| `Version of params * string
|
||
| `Calscale of params * string
|
||
| `Method of params * string
|
||
| other_prop
|
||
] [@@deriving eq, show]
|
||
|
||
type weekday = [ `Friday | `Monday | `Saturday | `Sunday | `Thursday | `Tuesday | `Wednesday ] [@@deriving eq, show]
|
||
|
||
type recur = [
|
||
| `Byminute of int list
|
||
| `Byday of (int * weekday) list
|
||
| `Byhour of int list
|
||
| `Bymonth of int list
|
||
| `Bymonthday of int list
|
||
| `Bysecond of int list
|
||
| `Bysetposday of int list
|
||
| `Byweek of int list
|
||
| `Byyearday of int list
|
||
| `Weekday of weekday
|
||
] [@@deriving eq, show]
|
||
|
||
type freq = [ `Daily | `Hourly | `Minutely | `Monthly | `Secondly | `Weekly | `Yearly ] [@@deriving eq, show]
|
||
|
||
type count_or_until = [
|
||
| `Count of int
|
||
| `Until of utc_or_timestamp_local
|
||
] [@@deriving eq, show]
|
||
|
||
type interval = int [@@deriving eq, show]
|
||
|
||
type recurrence = freq * count_or_until option * interval option * recur list [@@deriving eq, show]
|
||
|
||
type class_ = [ `Public | `Private | `Confidential | `Ianatoken of string | `Xname of string * string ] [@@deriving eq, show]
|
||
|
||
type status = [ `Draft | `Final | `Cancelled |
|
||
`Needs_action | `Completed | `In_process | `Tentative | `Confirmed ] [@@deriving eq, show]
|
||
|
||
type period = timestamp * Ptime.Span.t * bool [@@deriving eq, show]
|
||
type period_utc = timestamp_utc * Ptime.Span.t * bool [@@deriving eq, show]
|
||
|
||
type dates_or_datetimes = [ `Datetimes of timestamp list | `Dates of Ptime.date list ] [@@deriving eq, show]
|
||
type dates_or_datetimes_or_periods = [ dates_or_datetimes | `Periods of period list ] [@@deriving eq, show]
|
||
|
||
type general_prop = [
|
||
| `Dtstamp of params * timestamp_utc
|
||
| `Uid of params * string
|
||
| `Dtstart of params * date_or_datetime
|
||
| `Class of params * class_
|
||
| `Created of params * timestamp_utc
|
||
| `Description of params * string
|
||
| `Geo of params * (float * float)
|
||
| `Lastmod of params * timestamp_utc
|
||
| `Location of params * string
|
||
| `Organizer of params * Uri.t
|
||
| `Priority of params * int
|
||
| `Seq of params * int
|
||
| `Status of params * status
|
||
| `Summary of params * string
|
||
| `Url of params * Uri.t
|
||
| `Recur_id of params * date_or_datetime
|
||
| `Rrule of params * recurrence
|
||
| `Duration of params * Ptime.Span.t
|
||
| `Attach of params * [ `Uri of Uri.t | `Binary of string ]
|
||
| `Attendee of params * Uri.t
|
||
| `Categories of params * string list
|
||
| `Comment of params * string
|
||
| `Contact of params * string
|
||
| `Exdate of params * dates_or_datetimes
|
||
| `Rstatus of params * ((int * int * int option) * string * string option)
|
||
| `Related of params * string
|
||
| `Resource of params * string list
|
||
| `Rdate of params * dates_or_datetimes_or_periods
|
||
] [@@deriving eq, show]
|
||
|
||
type event_prop = [
|
||
| general_prop
|
||
| `Transparency of params * [ `Transparent | `Opaque ]
|
||
| `Dtend of params * date_or_datetime
|
||
| other_prop
|
||
] [@@deriving eq, show]
|
||
|
||
type 'a alarm_struct = {
|
||
trigger : params * [ `Duration of Ptime.Span.t | `Datetime of timestamp_utc ] ;
|
||
duration_repeat: ((params * Ptime.Span.t) * (params * int )) option ;
|
||
summary : (params * string) option ;
|
||
other: other_prop list ;
|
||
special: 'a ;
|
||
} [@@deriving eq, show]
|
||
|
||
type audio_struct = {
|
||
attach: (params * [ `Uri of Uri.t | `Binary of string ]) option ;
|
||
} [@@deriving eq, show]
|
||
|
||
type display_struct = {
|
||
description : (params * string) option ;
|
||
} [@@deriving eq, show]
|
||
|
||
type email_struct = {
|
||
description : params * string ;
|
||
attendees : (params * Uri.t) list ;
|
||
attach: (params * [ `Uri of Uri.t | `Binary of string ]) option ;
|
||
} [@@deriving eq, show]
|
||
|
||
type alarm = [
|
||
| `Audio of audio_struct alarm_struct
|
||
| `Display of display_struct alarm_struct
|
||
| `Email of email_struct alarm_struct
|
||
| `None of unit alarm_struct
|
||
] [@@deriving eq, show]
|
||
|
||
type tz_prop = [
|
||
| `Dtstart_local of params * timestamp_local
|
||
| `Tzoffset_to of params * Ptime.Span.t
|
||
| `Tzoffset_from of params * Ptime.Span.t
|
||
| `Rrule of params * recurrence
|
||
| `Comment of params * string
|
||
| `Rdate of params * dates_or_datetimes_or_periods
|
||
| `Tzname of params * string
|
||
| other_prop
|
||
] [@@deriving eq, show]
|
||
|
||
type timezone_prop = [
|
||
| `Timezone_id of params * (bool * string)
|
||
| `Lastmod of params * timestamp_utc
|
||
| `Tzurl of params * Uri.t
|
||
| `Standard of tz_prop list
|
||
| `Daylight of tz_prop list
|
||
| other_prop
|
||
] [@@deriving eq, show]
|
||
|
||
type todo_prop = [
|
||
| general_prop
|
||
| `Completed of params * timestamp_utc
|
||
| `Percent of params * int
|
||
| `Due of params * date_or_datetime
|
||
| other_prop
|
||
] [@@deriving eq, show]
|
||
|
||
type journal_prop = [
|
||
| general_prop
|
||
| other_prop
|
||
] [@@deriving eq, show]
|
||
|
||
type freebusy_prop = [
|
||
| `Dtstamp of params * timestamp_utc
|
||
| `Uid of params * string
|
||
| `Contact of params * string
|
||
| `Dtstart_utc of params * timestamp_utc
|
||
| `Dtend_utc of params * timestamp_utc
|
||
| `Organizer of params * Uri.t
|
||
| `Url of params * Uri.t
|
||
| `Attendee of params * Uri.t
|
||
| `Comment of params * string
|
||
| `Freebusy of params * period_utc list
|
||
| `Rstatus of params * ((int * int * int option) * string * string option)
|
||
| other_prop
|
||
] [@@deriving eq, show]
|
||
|
||
type event = {
|
||
dtstamp : params * timestamp_utc ;
|
||
uid : params * string ;
|
||
dtstart : params * date_or_datetime ;
|
||
dtend_or_duration : [ `Duration of params * Ptime.Span.t | `Dtend of params * date_or_datetime ] option ;
|
||
rrule : (params * recurrence) option ;
|
||
props : event_prop list ;
|
||
alarms : alarm list ;
|
||
} [@@deriving eq, show]
|
||
|
||
END OF FILE: icalendar.ml
|
||
*)
|
||
|
||
(*
|
||
FILE: icalendar.mli
|
||
--------------------
|
||
|
||
type timestamp_utc = Ptime.t [@@deriving eq, show]
|
||
type timestamp_local = Ptime.t [@@deriving eq, show]
|
||
|
||
type utc_or_timestamp_local = [
|
||
| `Utc of timestamp_utc
|
||
| `Local of timestamp_local
|
||
] [@@deriving eq, show]
|
||
|
||
type timestamp = [
|
||
utc_or_timestamp_local
|
||
| `With_tzid of timestamp_local * (bool * string)
|
||
] [@@deriving eq, show]
|
||
|
||
type date_or_datetime = [ `Datetime of timestamp | `Date of Ptime.date ]
|
||
|
||
type weekday = [ `Friday | `Monday | `Saturday | `Sunday | `Thursday | `Tuesday | `Wednesday ] [@@deriving eq, show]
|
||
|
||
type recur = [
|
||
| `Byminute of int list
|
||
| `Byday of (int * weekday) list
|
||
| `Byhour of int list
|
||
| `Bymonth of int list
|
||
| `Bymonthday of int list
|
||
| `Bysecond of int list
|
||
| `Bysetposday of int list
|
||
| `Byweek of int list
|
||
| `Byyearday of int list
|
||
| `Weekday of weekday
|
||
] [@@deriving eq, show]
|
||
|
||
type freq = [ `Daily | `Hourly | `Minutely | `Monthly | `Secondly | `Weekly | `Yearly ] [@@deriving eq, show]
|
||
|
||
type count_or_until = [
|
||
| `Count of int
|
||
| `Until of utc_or_timestamp_local (* TODO date or datetime *)
|
||
] [@@deriving eq, show]
|
||
|
||
type interval = int
|
||
|
||
type recurrence = freq * count_or_until option * interval option * recur list [@@deriving eq, show]
|
||
|
||
type valuetype = [
|
||
`Binary | `Boolean | `Caladdress | `Date | `Datetime | `Duration | `Float
|
||
| `Integer | `Period | `Recur | `Text | `Time | `Uri | `Utcoffset
|
||
| `Xname of (string * string) | `Ianatoken of string
|
||
]
|
||
|
||
type cutype = [ `Group | `Individual | `Resource | `Room | `Unknown
|
||
| `Ianatoken of string | `Xname of string * string ]
|
||
|
||
type partstat = [ `Accepted | `Completed | `Declined | `Delegated
|
||
| `In_process | `Needs_action | `Tentative
|
||
| `Ianatoken of string | `Xname of string * string ]
|
||
|
||
type role = [ `Chair | `Nonparticipant | `Optparticipant | `Reqparticipant
|
||
| `Ianatoken of string | `Xname of string * string ]
|
||
|
||
type relationship =
|
||
[ `Parent | `Child | `Sibling |
|
||
`Ianatoken of string | `Xname of string * string ]
|
||
|
||
type fbtype = [ `Free | `Busy | `Busy_Unavailable | `Busy_Tentative | `Ianatoken of string | `Xname of string * string ] [@@deriving eq, show]
|
||
|
||
type param_value = [ `Quoted of string | `String of string ]
|
||
|
||
type _ icalparameter =
|
||
| Altrep : Uri.t icalparameter
|
||
| Cn : param_value icalparameter
|
||
| Cutype : cutype icalparameter
|
||
| Delegated_from : (Uri.t list) icalparameter
|
||
| Delegated_to : (Uri.t list) icalparameter
|
||
| Dir : Uri.t icalparameter
|
||
| Encoding : [ `Base64 ] icalparameter
|
||
| Media_type : (string * string) icalparameter
|
||
| Fbtype : fbtype icalparameter
|
||
| Language : string icalparameter
|
||
| Member : (Uri.t list) icalparameter
|
||
| Partstat : partstat icalparameter
|
||
| Range : [ `Thisandfuture ] icalparameter
|
||
| Related : [ `Start | `End ] icalparameter
|
||
| Reltype : relationship icalparameter
|
||
| Role : role icalparameter
|
||
| Rsvp : bool icalparameter
|
||
| Sentby : Uri.t icalparameter
|
||
| Tzid : (bool * string) icalparameter
|
||
| Valuetype : valuetype icalparameter
|
||
| Iana_param : string -> param_value list icalparameter
|
||
| Xparam : (string * string) -> param_value list icalparameter
|
||
|
||
module Params : sig include Gmap.S with type 'a key = 'a icalparameter end
|
||
|
||
type params = Params.t
|
||
|
||
type other_prop =
|
||
[ `Iana_prop of string * params * string
|
||
| `Xprop of (string * string) * params * string ] [@@deriving eq, show]
|
||
|
||
type cal_prop =
|
||
[ `Prodid of params * string
|
||
| `Version of params * string
|
||
| `Calscale of params * string
|
||
| `Method of params * string
|
||
| other_prop
|
||
]
|
||
|
||
type class_ = [ `Public | `Private | `Confidential | `Ianatoken of string | `Xname of string * string ]
|
||
|
||
type status = [ `Draft | `Final | `Cancelled |
|
||
`Needs_action | `Completed | `In_process | (* `Cancelled *)
|
||
`Tentative | `Confirmed (* | `Cancelled *) ]
|
||
|
||
type period = timestamp * Ptime.Span.t * bool
|
||
type period_utc = timestamp_utc * Ptime.Span.t * bool
|
||
|
||
type dates_or_datetimes = [ `Datetimes of timestamp list | `Dates of Ptime.date list ]
|
||
type dates_or_datetimes_or_periods = [ dates_or_datetimes | `Periods of period list ]
|
||
|
||
type general_prop = [
|
||
| `Dtstamp of params * timestamp_utc
|
||
| `Uid of params * string
|
||
| `Dtstart of params * date_or_datetime
|
||
| `Class of params * class_
|
||
| `Created of params * timestamp_utc
|
||
| `Description of params * string
|
||
| `Geo of params * (float * float)
|
||
| `Lastmod of params * timestamp_utc
|
||
| `Location of params * string
|
||
| `Organizer of params * Uri.t
|
||
| `Priority of params * int
|
||
| `Seq of params * int
|
||
| `Status of params * status
|
||
| `Summary of params * string
|
||
| `Url of params * Uri.t
|
||
| `Recur_id of params * date_or_datetime
|
||
(* TODO: Furthermore, this property MUST be specified
|
||
as a date with local time if and only if the "DTSTART" property
|
||
contained within the recurring component is specified as a date
|
||
with local time. *)
|
||
| `Rrule of params * recurrence
|
||
| `Duration of params * Ptime.Span.t
|
||
| `Attach of params * [ `Uri of Uri.t | `Binary of string ]
|
||
| `Attendee of params * Uri.t
|
||
| `Categories of params * string list
|
||
| `Comment of params * string
|
||
| `Contact of params * string
|
||
| `Exdate of params * dates_or_datetimes
|
||
| `Rstatus of params * ((int * int * int option) * string * string option)
|
||
| `Related of params * string
|
||
| `Resource of params * string list
|
||
| `Rdate of params * dates_or_datetimes_or_periods
|
||
]
|
||
|
||
type event_prop = [
|
||
| general_prop
|
||
| `Transparency of params * [ `Transparent | `Opaque ]
|
||
| `Dtend of params * date_or_datetime
|
||
(* TODO: valuetype same as DTSTART *)
|
||
| other_prop
|
||
]
|
||
|
||
type 'a alarm_struct = {
|
||
trigger : params * [ `Duration of Ptime.Span.t | `Datetime of timestamp_utc ] ;
|
||
duration_repeat: ((params * Ptime.Span.t) * (params * int )) option ;
|
||
summary : (params * string) option ;
|
||
other: other_prop list ;
|
||
special: 'a ;
|
||
}
|
||
|
||
type audio_struct = {
|
||
attach: (params * [ `Uri of Uri.t | `Binary of string ]) option ;
|
||
}
|
||
|
||
type display_struct = {
|
||
description : (params * string) option ;
|
||
}
|
||
|
||
type email_struct = {
|
||
description : params * string ;
|
||
attendees : (params * Uri.t) list ;
|
||
attach: (params * [ `Uri of Uri.t | `Binary of string ]) option ;
|
||
}
|
||
|
||
type alarm = [
|
||
| `Audio of audio_struct alarm_struct
|
||
| `Display of display_struct alarm_struct
|
||
| `Email of email_struct alarm_struct
|
||
| `None of unit alarm_struct
|
||
]
|
||
|
||
type tz_prop = [
|
||
| `Dtstart_local of params * timestamp_local
|
||
| `Tzoffset_to of params * Ptime.Span.t
|
||
| `Tzoffset_from of params * Ptime.Span.t
|
||
| `Rrule of params * recurrence
|
||
| `Comment of params * string
|
||
| `Rdate of params * dates_or_datetimes_or_periods
|
||
| `Tzname of params * string
|
||
| other_prop
|
||
]
|
||
|
||
type timezone_prop = [
|
||
| `Timezone_id of params * (bool * string)
|
||
| `Lastmod of params * timestamp_utc
|
||
| `Tzurl of params * Uri.t
|
||
| `Standard of tz_prop list
|
||
| `Daylight of tz_prop list
|
||
| other_prop
|
||
]
|
||
|
||
type todo_prop = [
|
||
| general_prop
|
||
| `Completed of params * timestamp_utc
|
||
| `Percent of params * int
|
||
| `Due of params * date_or_datetime
|
||
| other_prop
|
||
]
|
||
|
||
type journal_prop = [
|
||
| general_prop
|
||
| other_prop
|
||
]
|
||
|
||
type freebusy_prop = [
|
||
| `Dtstamp of params * timestamp_utc
|
||
| `Uid of params * string
|
||
| `Contact of params * string
|
||
| `Dtstart_utc of params * timestamp_utc
|
||
| `Dtend_utc of params * timestamp_utc
|
||
| `Organizer of params * Uri.t
|
||
| `Url of params * Uri.t
|
||
| `Attendee of params * Uri.t
|
||
| `Comment of params * string
|
||
| `Freebusy of params * period_utc list
|
||
| `Rstatus of params * ((int * int * int option) * string * string option)
|
||
| other_prop
|
||
]
|
||
|
||
type event = {
|
||
dtstamp : params * timestamp_utc ;
|
||
uid : params * string ;
|
||
dtstart : params * date_or_datetime ; (* NOTE: optional if METHOD present according to RFC 5545 *)
|
||
dtend_or_duration : [ `Duration of params * Ptime.Span.t | `Dtend of params * date_or_datetime ] option ;
|
||
rrule : (params * recurrence) option ; (* NOTE: RFC says SHOULD NOT occur more than once *)
|
||
props : event_prop list ;
|
||
alarms : alarm list ;
|
||
}
|
||
|
||
type timezone = timezone_prop list
|
||
|
||
type component = [
|
||
| `Event of event
|
||
| `Todo of todo_prop list * alarm list
|
||
| `Journal of journal_prop list
|
||
| `Freebusy of freebusy_prop list
|
||
| `Timezone of timezone
|
||
] [@@deriving show]
|
||
|
||
(*
|
||
val in_timerange : component -> (Ptime.t * bool) * (Ptime.t * bool) -> bool
|
||
*)
|
||
val component_to_ics_key : component -> string
|
||
|
||
type calendar = cal_prop list * component list
|
||
|
||
val parse_datetime: string -> (timestamp, string) result
|
||
val parse : string -> (calendar, string) result
|
||
val pp : calendar Fmt.t
|
||
val equal_calendar : calendar -> calendar -> bool
|
||
|
||
(* TODO this actually belongs to CalDAV! this is Webdav_xml module! *)
|
||
type comp = [ `Allcomp | `Comp of component_transform list ]
|
||
and prop = [ `Allprop | `Prop of (string * bool) list ]
|
||
and component_transform = string * prop * comp [@@deriving show, eq]
|
||
|
||
val to_ics : ?cr:bool -> ?filter:component_transform option -> calendar -> string
|
||
|
||
module Writer : sig
|
||
val duration_to_ics : Ptime.Span.t -> Buffer.t -> unit
|
||
val cal_prop_to_ics_key : cal_prop -> string
|
||
end
|
||
|
||
val recur_dates : Ptime.t -> recurrence -> (unit -> Ptime.t option)
|
||
|
||
val recur_events : ?recurrence_ids:event list -> event -> (unit -> event option)
|
||
|
||
val normalize_timezone : Ptime.t -> bool * String.t ->
|
||
timezone_prop list list ->
|
||
Ptime.t option
|
||
|
||
|
||
END OF FILE: icalendar.mli
|
||
*)
|
||
|
||
(*
|
||
FILE: ptime.ml
|
||
--------------------
|
||
(*---------------------------------------------------------------------------
|
||
Copyright (c) 2015 The ptime programmers. All rights reserved.
|
||
SPDX-License-Identifier: ISC
|
||
---------------------------------------------------------------------------*)
|
||
|
||
(* Julian day and proleptic Gregorian calendar date conversion.
|
||
|
||
Formulae are from the calendar FAQ:
|
||
http://www.tondering.dk/claus/cal/julperiod.php#formula
|
||
|
||
These formulae work for positive Julian days. They represent
|
||
Gegorian calendar BCE year `y` by `-(y-1)`, e.g. 2 BCE is -1, this
|
||
follows the convention of ISO 8601.
|
||
|
||
All timestamps in Ptime's [min;max] range are represented by
|
||
positive Julian days and the formulae do not overflow on 32-bit
|
||
platforms in this restricted range. *)
|
||
|
||
let jd_to_date jd =
|
||
let a = jd + 32044 in
|
||
let b = (4 * a + 3) / 146097 in
|
||
let c = a - ((146097 * b) / 4) in
|
||
let d = (4 * c + 3) / 1461 in
|
||
let e = c - ((1461 * d) / 4) in
|
||
let m = (5 * e + 2) / 153 in
|
||
let day = e - ((153 * m + 2) / 5) + 1 in
|
||
let month = m + 3 - (12 * (m / 10)) in
|
||
let year = 100 * b + d - 4800 + (m / 10) in
|
||
(year, month, day)
|
||
|
||
let jd_to_year jd = (* Same as above but only for the year *)
|
||
let a = jd + 32044 in
|
||
let b = (4 * a + 3) / 146097 in
|
||
let c = a - ((146097 * b) / 4) in
|
||
let d = (4 * c + 3) / 1461 in
|
||
let e = c - ((1461 * d) / 4) in
|
||
let m = (5 * e + 2) / 153 in
|
||
100 * b + d - 4800 + (m / 10)
|
||
|
||
let jd_of_date (year, month, day) =
|
||
let a = (14 - month) / 12 in
|
||
let y = year + 4800 - a in
|
||
let m = month + 12 * a - 3 in
|
||
day + ((153 * m) + 2)/ 5 + 365 * y +
|
||
(y / 4) - (y / 100) + (y / 400) - 32045
|
||
|
||
let jd_posix_epoch = 2_440_588 (* the Julian day of the POSIX epoch *)
|
||
let jd_ptime_min = 1_721_060 (* the Julian day of Ptime.min *)
|
||
let jd_ptime_max = 5_373_484 (* the Julian day of Ptime.max *)
|
||
|
||
(* Picosecond precision POSIX timestamps and time span representation.
|
||
|
||
POSIX timestamps and spans are represented by int * int64 pairs
|
||
with the int64 in the range [0L;86_399_999_999_999_999L]. A pair
|
||
[(d, ps)] denotes the POSIX picosecond duration [d] * 86_400e12 +
|
||
[ps].
|
||
|
||
For a timestamp this can be seen as a POSIX day count from the
|
||
epoch paired with a picosecond precision POSIX time point in that
|
||
day starting from 00:00:00.
|
||
|
||
By definition with a negative [d] the [ps] duration brings us
|
||
towards zero, *not* towards infinity:
|
||
|
||
|
||
(d * 86_400e12) (d * 86_400e12 + ps) 0
|
||
... -----+-----------------+-------------------+--------- ...
|
||
[---------------->|
|
||
ps
|
||
|
||
[d] is largely sufficent to represent all the days in Ptime's
|
||
[min;max] range on both 32-bit and 64-bit platforms. *)
|
||
|
||
type t = int * int64
|
||
|
||
let ps_count_in_ps = 1L
|
||
let ps_count_in_ns = 1_000L
|
||
let ps_count_in_100ns = 100_000L
|
||
let ps_count_in_us = 1_000_000L
|
||
let ps_count_in_100us = 100_000_000L
|
||
let ps_count_in_ms = 1_000_000_000L
|
||
let ps_count_in_100ms = 100_000_000_000L
|
||
let ps_count_in_s = 1_000_000_000_000L
|
||
let ps_count_in_min = 60_000_000_000_000L
|
||
let ps_count_in_hour = 3600_000_000_000_000L
|
||
let ps_count_in_day = 86_400_000_000_000_000L
|
||
let ps_day_max = 86_399_999_999_999_999L
|
||
|
||
let day_min = jd_ptime_min - jd_posix_epoch
|
||
let day_max = jd_ptime_max - jd_posix_epoch
|
||
|
||
let epoch = (0, 0L) (* 1970-01-01 00:00:00 UTC *)
|
||
let min = (day_min, 0L) (* 0000-01-01 00:00:00 UTC *)
|
||
let max = (day_max, ps_day_max) (* 9999-12-31 23:59:59 UTC *)
|
||
|
||
(* POSIX time spans *)
|
||
|
||
type span = t
|
||
|
||
module Span = struct
|
||
|
||
let stdlib_abs = abs
|
||
|
||
(* Arithmetic *)
|
||
|
||
let neg = function
|
||
| (d, 0L) -> (-d, 0L)
|
||
| (d, ps) -> (-(d + 1), Int64.sub ps_count_in_day ps)
|
||
|
||
let add (d0, ps0) (d1, ps1) =
|
||
let d = d0 + d1 in
|
||
let ps = Int64.add ps0 ps1 in
|
||
let ps_clamp = Int64.rem ps ps_count_in_day in
|
||
let d = d + Int64.compare ps ps_clamp in
|
||
d, ps_clamp
|
||
|
||
let sub s0 s1 = add s0 (neg s1)
|
||
let abs (d, _ as s) = if d < 0 then neg s else s
|
||
|
||
(* POSIX time spans *)
|
||
|
||
type t = span
|
||
|
||
let zero = (0, 0L)
|
||
let v (d, ps as s) =
|
||
if ps < 0L || ps > ps_day_max
|
||
then invalid_arg (Format.sprintf "illegal ptime time span: (%d,%Ld)" d ps)
|
||
else s
|
||
|
||
let of_d_ps (d, ps as s) = if ps < 0L || ps > ps_day_max then None else Some s
|
||
let unsafe_of_d_ps s = s
|
||
let unsafe_of_d_ps_option s = s
|
||
let to_d_ps s = s
|
||
|
||
let of_int_s secs =
|
||
let d = stdlib_abs secs in
|
||
let s = (d / 86_400, Int64.(mul (of_int (d mod 86_400)) ps_count_in_s)) in
|
||
if secs < 0 then neg s else s
|
||
|
||
let day_int_min = min_int / 86_400
|
||
let day_int_max = max_int / 86_400
|
||
let to_int_s (d, ps) =
|
||
if d < day_int_min || d > day_int_max then None else
|
||
let days_s = d * 86_400 in
|
||
let day_s = Int64.(to_int (div ps ps_count_in_s)) (* always positive *) in
|
||
let secs = days_s + day_s in
|
||
if secs < days_s (* positive overflow *) then None else Some secs
|
||
|
||
let min_int_float = float min_int
|
||
let max_int_float = float max_int
|
||
let of_float_s secs =
|
||
if secs <> secs (* nan *) then None else
|
||
let days = floor (secs /. 86_400.) in
|
||
if days < min_int_float || days > max_int_float then None else
|
||
let rem_s = mod_float secs 86_400. in
|
||
let rem_s = if rem_s < 0. then 86_400. +. rem_s else rem_s in
|
||
if rem_s >= 86_400. then
|
||
(* Guard against a potential overflow in the computation of [rem_s] *)
|
||
let days = days +. 1. in
|
||
if days > max_int_float then None else
|
||
Some (int_of_float days, 0L)
|
||
else
|
||
let frac_s, rem_s = modf rem_s in
|
||
let rem_ps = Int64.(mul (of_float rem_s) ps_count_in_s) in
|
||
let frac_ps = Int64.(of_float (frac_s *. 1e12)) in
|
||
Some (int_of_float days, (Int64.add rem_ps frac_ps))
|
||
|
||
let to_float_s (d, ps) =
|
||
let days_s = (float d) *. 86_400. in
|
||
let day_s = Int64.(to_float (div ps ps_count_in_s)) in
|
||
let day_rem_ps = Int64.(to_float (rem ps ps_count_in_s)) in
|
||
days_s +. day_s +. (day_rem_ps *. 1e-12)
|
||
|
||
(* Predicates *)
|
||
|
||
let equal (d0, ps0) (d1, ps1) =
|
||
(compare : int -> int -> int) d0 d1 = 0 &&
|
||
Int64.compare ps0 ps1 = 0
|
||
|
||
let compare (d0, ps0) (d1, ps1) =
|
||
let c = (compare : int -> int -> int) d0 d1 in
|
||
if c <> 0 then c else (compare : int64 -> int64 -> int) ps0 ps1
|
||
|
||
(* Rounding *)
|
||
|
||
let round_div a b = (* a >= 0 and b > 0 *)
|
||
if a = 0L then 0L else
|
||
Int64.(div (add a (div b 2L)) b)
|
||
|
||
let frac_div = [| 1_000_000_000_000L;
|
||
100_000_000_000L;
|
||
10_000_000_000L;
|
||
1_000_000_000L;
|
||
100_000_000L;
|
||
10_000_000L;
|
||
1_000_000L;
|
||
100_000L;
|
||
10_000L;
|
||
1_000L;
|
||
100L;
|
||
10L;
|
||
1L; |]
|
||
|
||
let round ~frac_s:frac (sign, _ as t) =
|
||
let frac = if frac < 0 then 0 else (if frac > 12 then 12 else frac) in
|
||
let (d, ps) = if sign < 0 then neg t else t in
|
||
let rps = Int64.mul (round_div ps frac_div.(frac)) frac_div.(frac) in
|
||
let t = if rps > ps_day_max then (d + 1, 0L) else (d, rps) in
|
||
if sign < 0 then neg t else t
|
||
|
||
let truncate ~frac_s:frac (sign, _ as t) =
|
||
let frac = if frac < 0 then 0 else (if frac > 12 then 12 else frac) in
|
||
let (d, ps) = if sign < 0 then neg t else t in
|
||
let tps = Int64.(sub ps (rem ps frac_div.(frac))) in
|
||
if sign < 0 then neg (d, tps) else (d, tps)
|
||
|
||
let truncate_down ~frac_s:frac (d, ps) =
|
||
(d, Int64.(sub ps (rem ps frac_div.(frac ))))
|
||
|
||
(* Pretty printing *)
|
||
|
||
let dump ppf (d, ps) = Format.fprintf ppf "@[<1>(%d,@,%Ld)@]" d ps
|
||
|
||
(* Warning laborious code follows. Is there a better way ? *)
|
||
|
||
let divide_ps ~carry ps hi lo =
|
||
let hi_d = Int64.(to_int (div ps hi)) in
|
||
let rem_ps = Int64.rem ps hi in
|
||
let lo_d = Int64.to_int (round_div rem_ps lo) in
|
||
if lo_d = carry then hi_d + 1, 0 else hi_d, lo_d
|
||
|
||
let pp_y_d ppf ~neg d ps = (* assert d >= 0 *)
|
||
let y, rem_d =
|
||
let max_d = max_int / 4 in
|
||
if d > max_d then (* d * 4 overflows *) d / 365, d mod 365 else
|
||
let y = (d * 4) / 1461 (* / 365.25 *) in
|
||
y, d - (y * 1461) / 4
|
||
in
|
||
let days = rem_d + Int64.to_int (round_div ps ps_count_in_day) in
|
||
let y, days = if days = 366 then y + 1, 1 else y, days in
|
||
let y = if neg then -y else y in
|
||
Format.fprintf ppf "%dy" y;
|
||
if days <> 0 then Format.fprintf ppf "%dd" days;
|
||
()
|
||
|
||
let pp_d_h ppf ~neg d ps =
|
||
let h, _ = divide_ps ~carry:1 ps ps_count_in_hour ps_count_in_hour in
|
||
let d, h = if h = 24 then d + 1, 0 else d, h in
|
||
if d = 366 then Format.fprintf ppf "%dy1d" (if neg then -1 else 1) else
|
||
if d = 365 && h >= 6
|
||
then Format.fprintf ppf "%dy" (if neg then -1 else 1) else
|
||
let d = if neg then -d else d in
|
||
Format.fprintf ppf "%dd" d;
|
||
if h <> 0 then Format.fprintf ppf "%dh" h;
|
||
()
|
||
|
||
let pp_h_m ppf ~neg ps =
|
||
let h, m = divide_ps ~carry:60 ps ps_count_in_hour ps_count_in_min in
|
||
if h = 24 then Format.fprintf ppf "%dd" (if neg then -1 else 1) else
|
||
let h = if neg then -h else h in
|
||
Format.fprintf ppf "%dh" h;
|
||
if m <> 0 then Format.fprintf ppf "%dmin" m;
|
||
()
|
||
|
||
let pp_m_s ppf ~neg ps =
|
||
let m, s = divide_ps ~carry:60 ps ps_count_in_min ps_count_in_s in
|
||
if m = 60 then Format.fprintf ppf "%dh" (if neg then -1 else 1) else
|
||
let m = if neg then -m else m in
|
||
Format.fprintf ppf "%dmin" m;
|
||
if s <> 0 then Format.fprintf ppf "%ds" s;
|
||
()
|
||
|
||
let pp_s ppf ~neg ps =
|
||
let s, ms = divide_ps ~carry:1000 ps ps_count_in_s ps_count_in_ms in
|
||
if s = 60 then Format.fprintf ppf "%dmin" (if neg then -1 else 1) else
|
||
let s = if neg then -s else s in
|
||
if ms <> 0 then Format.fprintf ppf "%d.%03ds" s ms else
|
||
Format.fprintf ppf "%ds" s
|
||
|
||
let pp_unit higher_str hi hi_str frac_limit lo ppf ~neg ps =
|
||
let pp_unit_integral ppf ~neg h =
|
||
if h = 1000
|
||
then Format.fprintf ppf "%d%s" (if neg then -1 else 1) higher_str
|
||
else Format.fprintf ppf "%d%s" (if neg then -h else h) hi_str
|
||
in
|
||
if ps < frac_limit then begin
|
||
let h, l = divide_ps ~carry:1000 ps hi lo in
|
||
if h >= 100 || l = 0 then pp_unit_integral ppf ~neg h else
|
||
let h = if neg then -h else h in
|
||
Format.fprintf ppf "%d.%03d%s" h l hi_str
|
||
end else begin
|
||
let ms, _ = divide_ps ~carry:1 ps hi hi in
|
||
pp_unit_integral ppf ~neg ms
|
||
end
|
||
|
||
let pp_ms =
|
||
pp_unit "s" ps_count_in_ms "ms" ps_count_in_100ms ps_count_in_us
|
||
|
||
let pp_us =
|
||
pp_unit "ms" ps_count_in_us "us" ps_count_in_100us ps_count_in_ns
|
||
|
||
let pp_ns =
|
||
pp_unit "us" ps_count_in_ns "ns" ps_count_in_100ns ps_count_in_ps
|
||
|
||
let pp_ps ppf ~neg ps =
|
||
let ps = Int64.to_int ps in
|
||
Format.fprintf ppf "%dps" (if neg then -ps else ps)
|
||
|
||
let pp ppf (sign, _ as s) =
|
||
let neg = sign < 0 in
|
||
match (abs s) with
|
||
| (0, ps) ->
|
||
if ps >= ps_count_in_hour then pp_h_m ppf ~neg ps else
|
||
if ps >= ps_count_in_min then pp_m_s ppf ~neg ps else
|
||
if ps >= ps_count_in_s then pp_s ppf ~neg ps else
|
||
if ps >= ps_count_in_ms then pp_ms ppf ~neg ps else
|
||
if ps >= ps_count_in_us then pp_us ppf ~neg ps else
|
||
if ps >= ps_count_in_ns then pp_ns ppf ~neg ps else
|
||
pp_ps ppf ~neg ps
|
||
| (d, ps) ->
|
||
if d > 365 then pp_y_d ppf ~neg d ps else
|
||
pp_d_h ppf ~neg d ps
|
||
end
|
||
|
||
(* POSIX timestamps *)
|
||
|
||
let v (d, ps as s) =
|
||
if (ps < 0L || ps > ps_day_max || d < day_min || d > day_max)
|
||
then invalid_arg (Format.sprintf "illegal ptime timestamp: (%d,%Ld)" d ps)
|
||
else s
|
||
|
||
let unsafe_of_d_ps s = s
|
||
|
||
let of_span (d, _ as span) =
|
||
if d < day_min || d > day_max then None else Some span
|
||
|
||
let to_span t = t
|
||
|
||
let of_float_s secs = match Span.of_float_s secs with
|
||
| None -> None
|
||
| Some d -> of_span d
|
||
|
||
let to_float_s = Span.to_float_s
|
||
|
||
let truncate = Span.truncate_down
|
||
|
||
let frac_s (_, ps) = (0, Int64.(rem ps ps_count_in_s))
|
||
|
||
(* Predicates *)
|
||
|
||
let equal = Span.equal
|
||
let compare = Span.compare
|
||
let is_earlier t ~than = compare t than = -1
|
||
let is_later t ~than = compare t than = 1
|
||
|
||
(* POSIX arithmetic *)
|
||
|
||
let add_span t d = of_span (Span.add t d)
|
||
let sub_span t d = of_span (Span.sub t d)
|
||
let diff t1 t0 = Span.sub t1 t0
|
||
|
||
(* Time zone offsets between local and UTC timelines *)
|
||
|
||
type tz_offset_s = int
|
||
|
||
(* Date-time conversion
|
||
|
||
POSIX time counts seconds since 1970-01-01 00:00:00 UTC without
|
||
counting leap seconds -- when a leap second occurs a POSIX second
|
||
can be two SI seconds or zero SI second. Hence 86400 POSIX seconds
|
||
always represent an UTC day and the translations below are accurate
|
||
without having to refer to a leap seconds table. *)
|
||
|
||
type date = (int * int * int)
|
||
type time = (int * int * int) * tz_offset_s
|
||
|
||
let max_month_day = (* max day number in a given year's month. *)
|
||
let is_leap_year y = (y mod 4 = 0) && (y mod 100 <> 0 || y mod 400 = 0) in
|
||
let mlen = [|31; 28 (* or not *); 31; 30; 31; 30; 31; 31; 30; 31; 30; 31|] in
|
||
fun y m -> if (m = 2 && is_leap_year y) then 29 else mlen.(m - 1)
|
||
|
||
let is_date_valid (y, m, d) =
|
||
0 <= y && y <= 9999 &&
|
||
1 <= m && m <= 12 &&
|
||
1 <= d && d <= max_month_day y m
|
||
|
||
let is_time_valid ((hh, mm, ss), _) =
|
||
0 <= hh && hh <= 23 &&
|
||
0 <= mm && mm <= 59 &&
|
||
0 <= ss && ss <= 60
|
||
|
||
let of_date_time (date, ((hh, mm, ss), tz_offset_s as t)) =
|
||
(* We first verify that the given date and time are Ptime-valid.
|
||
Once this has been established we find find the number of Julian
|
||
days since the epoch for the given proleptic Georgian calendar
|
||
date. This gives us the POSIX day component of the timestamp. The
|
||
remaining time fields are used to derive the picosecond precision
|
||
time in that day compensated by the time zone offset. The final
|
||
result is checked to be in Ptime's [min;max] range.
|
||
|
||
By definition POSIX timestamps cannot represent leap seconds.
|
||
With the code below any date-time with a seconds value of 60
|
||
(leap second addition) is mapped to the POSIX timestamp that
|
||
happens 1 second later which is what POSIX mktime would to. Any
|
||
formally non-existing UTC date-time with a seconds value of 59
|
||
(leap second subtraction) is mapped on the POSIX timestamp that
|
||
represents this non existing instant. *)
|
||
if not (is_date_valid date && is_time_valid t) then None else
|
||
let d = jd_of_date date - jd_posix_epoch in
|
||
let hh_ps = Int64.(mul (of_int hh) ps_count_in_hour) in
|
||
let mm_ps = Int64.(mul (of_int mm) ps_count_in_min) in
|
||
let ss_ps = Int64.(mul (of_int ss) ps_count_in_s) in
|
||
let ps = Int64.(add hh_ps (add mm_ps ss_ps)) in
|
||
sub_span (d, ps) (Span.of_int_s tz_offset_s)
|
||
|
||
let to_date_time ?(tz_offset_s = 0) t =
|
||
(* To render the timestamp in the given time zone offset we first
|
||
express the timestamp in local time and then compute the date
|
||
fields on that stamp as if it were UTC. If the local timestamp is
|
||
not in [min;max] then its date fields cannot be valid according
|
||
to the constraints guaranteed by Ptime and we fallback to UTC,
|
||
i.e. a time zone offset of 0.
|
||
|
||
We then apply the following algorithm whose description makes
|
||
sense on a POSIX timestamp (i.e. UTC) but works equally well to
|
||
render the date-time fields of a local timestamp.
|
||
|
||
We first take take the POSIX day count [d] (equivalent by
|
||
definition to an UTC day count) from the epoch, convert it to a
|
||
Julian day and use this to get the proleptic Gregorian calendar
|
||
date. The POSIX picoseconds [ps] in the day are are converted to
|
||
a daytime according to to its various units.
|
||
|
||
By definition no POSIX timestamp can represent a date-time with a
|
||
seconds value of 60 (leap second addition) and thus the function
|
||
will never return a date-time with such a value. On the other
|
||
hand it will return an inexisting UTC date-time with a seconds
|
||
value of 59 whenever a leap second is subtracted since there is a
|
||
POSIX timestamp that represents this instant. *)
|
||
let (d, ps), tz_offset_s = match add_span t (Span.of_int_s tz_offset_s) with
|
||
| None -> t, 0 (* fallback to UTC *)
|
||
| Some local -> local, tz_offset_s
|
||
in
|
||
let jd = d + jd_posix_epoch in
|
||
let date = jd_to_date jd in
|
||
let hh = Int64.(to_int (div ps ps_count_in_hour)) in
|
||
let hh_rem = Int64.rem ps ps_count_in_hour in
|
||
let mm = Int64.(to_int (div hh_rem ps_count_in_min)) in
|
||
let mm_rem = Int64.rem hh_rem ps_count_in_min in
|
||
let ss = Int64.(to_int (div mm_rem ps_count_in_s)) in
|
||
date, ((hh, mm, ss), tz_offset_s)
|
||
|
||
let of_date ?tz_offset_s:(tz = 0) date = of_date_time (date, ((00, 00, 00), tz))
|
||
let to_date ?tz_offset_s t = fst (to_date_time ?tz_offset_s t)
|
||
let of_year ?tz_offset_s y = of_date ?tz_offset_s (y, 01, 01)
|
||
let to_year ?(tz_offset_s = 0) t =
|
||
let d = match add_span t (Span.of_int_s tz_offset_s) with
|
||
| None -> fst t (* fallback to UTC *) | Some (local_d, _) -> local_d
|
||
in
|
||
jd_to_year (d + jd_posix_epoch)
|
||
|
||
|
||
type weekday = [ `Sun | `Mon | `Tue | `Wed | `Thu | `Fri | `Sat ]
|
||
|
||
let weekday_num ?(tz_offset_s = 0) t =
|
||
let (d, _) = Span.add t (Span.of_int_s tz_offset_s) in
|
||
(* N.B. in contrast to [to_date_time] we don't care if we fall outside
|
||
[min;max]. Even if it happens the result of the computation is still
|
||
correct *)
|
||
let i = (d + 4 (* Epoch, d = 0, was a thu, we want 4 for that day *)) mod 7 in
|
||
if i < 0 then 7 + i else i
|
||
|
||
let weekday =
|
||
let wday = [| `Sun; `Mon; `Tue; `Wed; `Thu; `Fri; `Sat; |] in
|
||
fun ?tz_offset_s t -> wday.(weekday_num ?tz_offset_s t)
|
||
|
||
(* RFC 3339 timestamp conversions *)
|
||
|
||
(* RFC 3339 timestamp parser *)
|
||
|
||
type error_range = int * int
|
||
type rfc3339_error =
|
||
[ `Invalid_stamp | `Eoi | `Exp_chars of char list | `Trailing_input ]
|
||
|
||
let pp_rfc3339_error ppf = function
|
||
| `Invalid_stamp -> Format.fprintf ppf "@[invalid@ time@ stamp@]"
|
||
| `Eoi -> Format.fprintf ppf "@[unexpected@ end@ of@ input@]"
|
||
| `Trailing_input -> Format.fprintf ppf "@[trailing@ input@]"
|
||
| `Exp_chars cs ->
|
||
let rec pp_chars ppf = function
|
||
| c :: cs -> Format.fprintf ppf "@ %C" c; pp_chars ppf cs
|
||
| [] -> ()
|
||
in
|
||
Format.fprintf ppf "@[expected@ a@ character@ in:%a@]" pp_chars cs
|
||
|
||
let pp_range ppf (s, e) =
|
||
if s = e then Format.pp_print_int ppf s else Format.fprintf ppf "%d-%d" s e
|
||
|
||
let _rfc3339_error_to_string (r, err) =
|
||
Format.asprintf "@[<h>%a: %a@]" pp_range r pp_rfc3339_error err
|
||
|
||
let rfc3339_string_error = function
|
||
| Ok _ as v -> v | Error (`RFC3339 e) -> Error (_rfc3339_error_to_string e)
|
||
|
||
let rfc3339_error_to_msg = function
|
||
| Ok _ as v -> v | Error (`RFC3339 e) ->
|
||
Error (`Msg (_rfc3339_error_to_string e))
|
||
|
||
exception RFC3339 of (int * int) * rfc3339_error (* Internal *)
|
||
|
||
let error r e = raise (RFC3339 (r, e))
|
||
let error_pos p e = error (p, p) e
|
||
let error_exp_digit p =
|
||
error_pos p (`Exp_chars ['0'; '1'; '2'; '3'; '4'; '5'; '6'; '7'; '8'; '9'])
|
||
|
||
let is_digit = function '0' .. '9' -> true | _ -> false
|
||
|
||
let parse_digits ~count pos max s =
|
||
let stop = pos + count - 1 in
|
||
if stop > max then error_pos max `Eoi else
|
||
let rec loop k acc =
|
||
if k > stop then acc else
|
||
if is_digit s.[k] then loop (k+1) (acc * 10 + Char.code s.[k] - 0x30) else
|
||
error_exp_digit k
|
||
in
|
||
loop pos 0
|
||
|
||
let parse_char c pos max s =
|
||
if pos > max then error_pos max `Eoi else
|
||
if s.[pos] = c then () else error_pos pos (`Exp_chars [c])
|
||
|
||
let parse_dt_sep ~strict pos max s =
|
||
let is_dt_sep = function
|
||
| 'T' -> true
|
||
| 't' | ' ' when not strict -> true
|
||
| _ -> false
|
||
in
|
||
if pos > max then error_pos max `Eoi else
|
||
if is_dt_sep s.[pos] then () else
|
||
error_pos pos (`Exp_chars (['T'] @ if strict then [] else ['t'; ' ']))
|
||
|
||
let decide_frac_or_tz ~strict pos max s =
|
||
if pos > max then error_pos max `Eoi else
|
||
match s.[pos] with
|
||
| '.' -> `Frac
|
||
| '+' | '-' | 'Z' -> `Tz
|
||
| 'z' when not strict -> `Tz
|
||
| c ->
|
||
let chars = ['.'; '+'; '-'; 'Z'] @ if strict then [] else ['z'] in
|
||
error_pos pos (`Exp_chars chars)
|
||
|
||
let parse_frac_ps pos max s =
|
||
if pos > max then error_pos max `Eoi else
|
||
if not (is_digit s.[pos]) then error_exp_digit pos else
|
||
let rec loop k acc pow =
|
||
if k > max then error_pos max `Eoi else
|
||
if not (is_digit s.[k]) then (Some acc), k else
|
||
let count = k - pos + 1 in
|
||
if count > 12 then (* truncate *) loop (k + 1) acc pow else
|
||
let pow = Int64.div pow 10L in
|
||
let acc = Int64.(add acc (mul (of_int (Char.code s.[k] - 0x30)) pow)) in
|
||
loop (k + 1) acc pow
|
||
in
|
||
loop pos 0L ps_count_in_s
|
||
|
||
let parse_tz_s ~strict pos max s =
|
||
let parse_tz_mag sign pos =
|
||
let hh_pos = pos in
|
||
let hh = parse_digits ~count:2 hh_pos max s in
|
||
let mm, mm_pos = match strict with
|
||
| true ->
|
||
let mm_pos = hh_pos + 3 in
|
||
parse_char ':' (mm_pos - 1) max s;
|
||
parse_digits ~count:2 mm_pos max s, mm_pos
|
||
| false ->
|
||
let next = hh_pos + 2 in
|
||
if next > max || not (s.[next] = ':' || is_digit s.[next])
|
||
then (0, hh_pos (* end pos of parse - 1, one is added at the end *))
|
||
else
|
||
let mm_pos = if s.[next] = ':' then hh_pos + 3 else hh_pos + 2 in
|
||
parse_digits ~count:2 mm_pos max s, mm_pos
|
||
in
|
||
if hh > 23 then error (hh_pos, hh_pos + 1) `Invalid_stamp else
|
||
if mm > 59 then error (mm_pos, mm_pos + 1) `Invalid_stamp else
|
||
let secs = hh * 3600 + mm * 60 in
|
||
let tz_s = match secs = 0 && sign = -1 with
|
||
| true -> None (* -00:00 convention *)
|
||
| false -> Some (sign * secs)
|
||
in
|
||
tz_s, mm_pos + 1
|
||
in
|
||
if pos > max then error_pos max `Eoi else
|
||
match s.[pos] with
|
||
| 'Z' -> Some 0, pos
|
||
| 'z' when not strict -> Some 0, pos
|
||
| '+' -> parse_tz_mag ( 1) (pos + 1)
|
||
| '-' -> parse_tz_mag (-1) (pos + 1)
|
||
| c ->
|
||
let chars = ['+'; '-'; 'Z'] @ if strict then [] else ['z'] in
|
||
error_pos pos (`Exp_chars chars)
|
||
|
||
let of_rfc3339 ?(strict = false) ?(sub = false) ?(start = 0) s =
|
||
try
|
||
let s_len = String.length s in
|
||
let max = s_len - 1 in
|
||
if s_len = 0 || start < 0 || start > max then error_pos start `Eoi else
|
||
let y_pos = start in
|
||
let m_pos = y_pos + 5 in
|
||
let d_pos = m_pos + 3 in
|
||
let hh_pos = d_pos + 3 in
|
||
let mm_pos = hh_pos + 3 in
|
||
let ss_pos = mm_pos + 3 in
|
||
let decide_pos = ss_pos + 2 in
|
||
let y = parse_digits ~count:4 y_pos max s in
|
||
parse_char '-' (m_pos - 1) max s;
|
||
let m = parse_digits ~count:2 m_pos max s in
|
||
parse_char '-' (d_pos - 1) max s;
|
||
let d = parse_digits ~count:2 d_pos max s in
|
||
parse_dt_sep ~strict (hh_pos - 1) max s;
|
||
let hh = parse_digits ~count:2 hh_pos max s in
|
||
parse_char ':' (mm_pos - 1) max s;
|
||
let mm = parse_digits ~count:2 mm_pos max s in
|
||
parse_char ':' (ss_pos - 1) max s;
|
||
let ss = parse_digits ~count:2 ss_pos max s in
|
||
let frac, tz_pos = match decide_frac_or_tz ~strict decide_pos max s with
|
||
| `Frac -> parse_frac_ps (decide_pos + 1) max s
|
||
| `Tz -> None, decide_pos
|
||
in
|
||
let tz_s_opt, last_pos = parse_tz_s ~strict tz_pos max s in
|
||
let tz_s = match tz_s_opt with None -> 0 | Some s -> s in
|
||
match of_date_time ((y, m, d), ((hh, mm, ss), tz_s)) with
|
||
| None -> error (start, last_pos) `Invalid_stamp
|
||
| Some t ->
|
||
let t, tz_s = match frac with
|
||
| None | Some 0L -> t, tz_s
|
||
| Some frac ->
|
||
match add_span t (0, frac) with
|
||
| None -> error (start, last_pos) `Invalid_stamp
|
||
| Some t -> t, tz_s
|
||
in
|
||
if not sub && last_pos <> max
|
||
then error_pos (last_pos + 1) `Trailing_input
|
||
else Ok (t, tz_s_opt, last_pos - start + 1)
|
||
with RFC3339 (r, e) -> Error (`RFC3339 (r, e))
|
||
|
||
(* RFC 3339 timestamp formatter *)
|
||
|
||
let rfc3339_adjust_tz_offset tz_offset_s =
|
||
(* The RFC 3339 time zone offset field is limited in expression to
|
||
the bounds below with minute precision. If the requested time
|
||
zone offset exceeds these bounds or is not an *integral* number
|
||
of minutes we simply use UTC. An alternative would be to
|
||
compensate the offset *and* the timestamp but it's more
|
||
complicated to explain and maybe more surprising to the user. *)
|
||
let min = -86340 (* -23h59 in secs *) in
|
||
let max = +86340 (* +23h59 in secs *) in
|
||
if min <= tz_offset_s && tz_offset_s <= max && tz_offset_s mod 60 = 0
|
||
then tz_offset_s, false
|
||
else 0 (* UTC *), true
|
||
|
||
let s_frac_of_ps frac ps =
|
||
Int64.(div (rem ps ps_count_in_s) Span.frac_div.(frac))
|
||
|
||
let to_rfc3339 ?(space = false) ?frac_s:(frac = 0) ?tz_offset_s (_, ps as t) =
|
||
let buf = Buffer.create 255 in
|
||
let tz_offset_s, tz_unknown = match tz_offset_s with
|
||
| Some tz -> rfc3339_adjust_tz_offset tz
|
||
| None -> 0, true
|
||
in
|
||
let (y, m, d), ((hh, ss, mm), tz_offset_s) = to_date_time ~tz_offset_s t in
|
||
let dt_sep = if space then ' ' else 'T' in
|
||
Printf.bprintf buf "%04d-%02d-%02d%c%02d:%02d:%02d" y m d dt_sep hh ss mm;
|
||
let frac = if frac < 0 then 0 else (if frac > 12 then 12 else frac) in
|
||
if frac <> 0 then Printf.bprintf buf ".%0*Ld" frac (s_frac_of_ps frac ps);
|
||
if tz_offset_s = 0 && not tz_unknown then Printf.bprintf buf "Z" else
|
||
begin
|
||
let tz_sign = if tz_offset_s < 0 || tz_unknown then '-' else '+' in
|
||
let tz_min = abs (tz_offset_s / 60) in
|
||
let tz_hh = tz_min / 60 in
|
||
let tz_mm = tz_min mod 60 in
|
||
Printf.bprintf buf "%c%02d:%02d" tz_sign tz_hh tz_mm;
|
||
end;
|
||
Buffer.contents buf
|
||
|
||
let pp_rfc3339 ?space ?frac_s ?tz_offset_s () ppf t =
|
||
Format.fprintf ppf "%s" (to_rfc3339 ?space ?frac_s ?tz_offset_s t)
|
||
|
||
(* Pretty printing *)
|
||
|
||
let pp_human ?frac_s:(frac = 0) ?tz_offset_s () ppf (_, ps as t) =
|
||
let tz_offset_s, tz_unknown = match tz_offset_s with
|
||
| Some tz -> rfc3339_adjust_tz_offset tz
|
||
| None -> 0, true
|
||
in
|
||
let (y, m, d), ((hh, ss, mm), tz_offset_s) = to_date_time ~tz_offset_s t in
|
||
Format.fprintf ppf "%04d-%02d-%02d %02d:%02d:%02d" y m d hh ss mm;
|
||
let frac = if frac < 0 then 0 else (if frac > 12 then 12 else frac) in
|
||
if frac <> 0 then Format.fprintf ppf ".%0*Ld" frac (s_frac_of_ps frac ps);
|
||
let tz_sign = if tz_offset_s < 0 || tz_unknown then '-' else '+' in
|
||
let tz_min = abs (tz_offset_s / 60) in
|
||
let tz_hh = tz_min / 60 in
|
||
let tz_mm = tz_min mod 60 in
|
||
Format.fprintf ppf " %c%02d:%02d" tz_sign tz_hh tz_mm;
|
||
()
|
||
|
||
let pp = pp_human ~tz_offset_s:0 ()
|
||
let dump = Span.dump
|
||
|
||
|
||
END OF FILE: ptime.ml
|
||
*)
|
||
|
||
(*
|
||
FILE: ptime.mli
|
||
--------------------
|
||
(*---------------------------------------------------------------------------
|
||
Copyright (c) 2015 The ptime programmers. All rights reserved.
|
||
SPDX-License-Identifier: ISC
|
||
---------------------------------------------------------------------------*)
|
||
|
||
(** POSIX time values.
|
||
|
||
Consult the {{!basics}basics} and a few {{!notes}notes
|
||
and limitations}.
|
||
|
||
{b References}
|
||
{ul
|
||
{- The Open Group. {{:http://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap04.html#tag_04_15}The Open Group Base Specifications Issue 7, section 4.15 Seconds Since the Epoch}. 2013}
|
||
{- G. Klyne et al.
|
||
{{:http://tools.ietf.org/html/rfc3339}
|
||
{e Date and Time on the Internet: Timestamps}}. RFC 3339, 2002.}} *)
|
||
|
||
(** {1:timespans 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. *)
|
||
|
||
(** POSIX time spans.
|
||
|
||
{b WARNING.} A POSIX time span is not equal to an SI second based time
|
||
span see the {{!basics}basics}. *)
|
||
module Span : sig
|
||
|
||
(** {1:spans POSIX time spans} *)
|
||
|
||
type t = span
|
||
(** The type for signed, picosecond precision, POSIX time spans. *)
|
||
|
||
val v : int * int64 -> span
|
||
(** [v s] is like {!of_d_ps}[ s] but raises [Invalid_argument] if
|
||
[s] is not in the right range. Use {!of_d_ps} to deal with
|
||
untrusted input. *)
|
||
|
||
val zero : span
|
||
(** [zero] is the neutral element of {!add}. *)
|
||
|
||
val of_d_ps : int * int64 -> span option
|
||
(** [of_d_ps (d, ps)] is a span for the signed POSIX picosecond
|
||
span [d] * 86_400e12 + [ps]. [d] is a signed number of POSIX
|
||
days and [ps] a number of picoseconds in the range
|
||
\[[0];[86_399_999_999_999_999L]\]. [None] is returned if
|
||
[ps] is not in the right range. *)
|
||
|
||
(**/**)
|
||
val unsafe_of_d_ps : int * int64 -> span
|
||
val unsafe_of_d_ps_option : (int * int64) option -> span option
|
||
(**/**)
|
||
|
||
val to_d_ps : span -> int * int64
|
||
(** [to_d_ps d] is the span [d] as a pair [(d, ps)] expressing the
|
||
POSIX picosecond span [d] * 86_400e12 + [ps] with
|
||
[ps] in the range \[[0];[86_399_999_999_999_999L]\] *)
|
||
|
||
val of_int_s : int -> span
|
||
(** [of_int_s secs] is a span from the signed integer POSIX second
|
||
span [secs]. *)
|
||
|
||
val to_int_s : span -> int option
|
||
(** [to_int_s d] is the span [d] as a signed integer POSIX second
|
||
span, if [int]'s range can represent it (note that this
|
||
depends on {!Sys.word_size}). Subsecond precision numbers are
|
||
truncated. *)
|
||
|
||
val of_float_s : float -> span option
|
||
(** [of_float_s secs] is a span from the signed floating point POSIX
|
||
second span [d]. Subpicosecond precision numbers are truncated.
|
||
|
||
[None] is returned if [secs] cannot be represented as a span.
|
||
This occurs on {!Stdlib.nan} or if the duration in POSIX
|
||
days cannot fit on an [int] (on 32-bit platforms this means the
|
||
absolute magnitude of the duration is greater than ~2'941'758
|
||
years). *)
|
||
|
||
val to_float_s : span -> float
|
||
(** [to_float_s s] is the span [d] as floating point POSIX seconds.
|
||
|
||
{b Warning.} The magnitude of [s] may not be represented exactly
|
||
by the floating point value. *)
|
||
|
||
(** {1:predicates Predicates} *)
|
||
|
||
val equal : span -> span -> bool
|
||
(** [equal d d'] is [true] iff [d] and [d'] are the same time span. *)
|
||
|
||
val compare : span -> span -> int
|
||
(** [compare d d'] is a total order on durations that is compatible
|
||
with signed time span order. *)
|
||
|
||
(** {1:arith Arithmetic}
|
||
|
||
{b Note.} The following functions rollover on overflows. *)
|
||
|
||
val neg : span -> span
|
||
(** [neg d] is the span [d] negated. *)
|
||
|
||
val add : span -> span -> span
|
||
(** [add d d'] is [d] + [d']. *)
|
||
|
||
val sub : span -> span -> span
|
||
(** [sub d d'] is [d] - [d']. *)
|
||
|
||
val abs : span -> span
|
||
(** [abs d] is the absolute value of span [d]. *)
|
||
|
||
(** {1:rounding Rounding} *)
|
||
|
||
val round : frac_s:int -> span -> span
|
||
(** [round ~frac_s t] is [t] rounded to the [frac_s] decimal
|
||
fractional second. Ties are rounded away from zero. [frac_s] is
|
||
clipped to the range \[[0];[12]\]. *)
|
||
|
||
val truncate : frac_s:int -> span -> span
|
||
(** [truncate ~frac_s t] is [t] truncated to the [frac_s] decimal
|
||
fractional second. [frac_s] is clipped to the range
|
||
\[[0];[12]\]. *)
|
||
|
||
(** {1:print Pretty printing} *)
|
||
|
||
val pp : Format.formatter -> span -> unit
|
||
(** [pp ppf d] prints an unspecified, approximative, representation of [d]
|
||
on [ppf].
|
||
|
||
The representation is not fixed-width, depends on the magnitude of [d]
|
||
and uses locale independent
|
||
{{:http://www.bipm.org/en/publications/si-brochure/chapter3.html}SI
|
||
prefixes} on seconds and
|
||
{{:http://www.bipm.org/en/publications/si-brochure/table6.html}accepted
|
||
non-SI units}. Years are counted in Julian years (365.25
|
||
SI-accepted days) as
|
||
{{:http://www.iau.org/publications/proceedings_rules/units/}defined}
|
||
by the International Astronomical Union (IUA).
|
||
|
||
The representation is approximative. In particular beyond 60
|
||
seconds it only keeps the two most significant time units and
|
||
rounds towards the infinity. The latter means that case arising,
|
||
it always {e over} approximates durations.
|
||
|
||
{b Warning} Becomes unprecise (but does not overflow) if the
|
||
absolute number of POSIX days in the time span is greater than [max_int /
|
||
4] (on 32-bit platforms this is ~735'439 years) *)
|
||
|
||
val dump : Format.formatter -> span -> unit
|
||
(** [dump ppf s] prints an unspecified raw representation of [d]
|
||
on [ppf]. *)
|
||
end
|
||
|
||
(** {1:timestamps 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:
|
||
{ul
|
||
{- Happens at the POSIX span [d] {e after} {!epoch}
|
||
if [d] is positive.}
|
||
{- Happens at the POSIX span [d] {e 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}:
|
||
{ul
|
||
{- If the number is positive [t] happens {e after} {!epoch}.}
|
||
{- If the number is negative [t] happens {e before} {!epoch}.}} *)
|
||
|
||
(**/**)
|
||
val unsafe_of_d_ps : int * int64 -> t
|
||
(**/**)
|
||
|
||
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.
|
||
|
||
{b 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]. *)
|
||
|
||
(** {1:predicates 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]. *)
|
||
|
||
(** {1:posix_arithmetic POSIX arithmetic}
|
||
|
||
{b WARNING.} A POSIX time span is not equal to an SI second based
|
||
time span, see the {{!basics}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']. *)
|
||
|
||
(** {1:tz_offset 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
|
||
]}
|
||
{ul
|
||
{- A value of [-3600] means that the local timeline is sixty minutes
|
||
{e behind} the UTC timeline.}
|
||
{- A value of [3600] means that the local timeline is sixty
|
||
minutes {e ahead} the UTC timeline.}} *)
|
||
|
||
(** {1:date_time Date-time value conversions}
|
||
|
||
A {e 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:
|
||
{ul
|
||
{- [y] the year from [0] to [9999]. [0] denotes -1 BCE
|
||
(this follows the
|
||
{{:http://www.iso.org/iso/home/standards/iso8601.htm}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 {e 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 {{!tz_offset_s}relationship} of the local timeline to
|
||
the UTC timeline.
|
||
|
||
The [(hh, mm, ss)] components are understood and constrainted as
|
||
follows:
|
||
{ul
|
||
{- [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 {e valid} iff the values [(hh, mm, ss)]
|
||
are in the ranges mentioned above. *)
|
||
|
||
(** {2:datetimes 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 {{!date}invalid date},
|
||
{{!time}invalid time} or the date-time is not in the range
|
||
\[{!min};{!max}\].
|
||
|
||
{b 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}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 {{!date}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].
|
||
|
||
{b 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}basics}.
|
||
|
||
{b Subsecond precision.} POSIX timestamps with subsecond precision
|
||
are floored, i.e. the date-time always has the second mentioned in
|
||
the timestamp. *)
|
||
|
||
(** {2:dates 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)]. *)
|
||
|
||
(** {2:years 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. *)
|
||
|
||
(** {2:weekdays Week days} *)
|
||
|
||
type weekday = [ `Sun | `Mon | `Tue | `Wed | `Thu | `Fri | `Sat ]
|
||
(** The type for the days of the 7-day week. *)
|
||
|
||
val weekday : ?tz_offset_s:tz_offset_s -> t -> weekday
|
||
(** [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. *)
|
||
|
||
(** {1:rfc3339 RFC 3339 timestamp conversions} *)
|
||
|
||
type error_range = int * int
|
||
(** The type for error ranges, starting and ending position. *)
|
||
|
||
type rfc3339_error =
|
||
[ `Invalid_stamp
|
||
| `Eoi
|
||
| `Exp_chars of char list
|
||
| `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 : 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])
|
||
result -> ('a, [> `Msg of string]) 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]) result -> ('a, string) 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]) result
|
||
(** [of_rfc3339 ~strict ~sub ~start s] parses an RFC 3339
|
||
{{:https://tools.ietf.org/html/rfc3339#section-5.6}[date-time]}
|
||
starting at [start] (defaults to [0]) in [s] to a triple [(t, tz, count)]
|
||
with:
|
||
{ul
|
||
{- [t] the POSIX timestamp (hence on the UTC timeline).}
|
||
{- [tz], the optional {{!tz_offset_s}time zone offset} found in the
|
||
timestamp. [None] is returned iff the date-time satisfies the
|
||
{{:https://tools.ietf.org/html/rfc3339#section-4.3}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}}
|
||
|
||
{b Notes and limitations.}
|
||
{ul
|
||
{- 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
|
||
{{:https://tools.ietf.org/html/rfc3339#section-5.6}[date-time]}
|
||
production with:
|
||
{ul
|
||
{- [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
|
||
{{:https://tools.ietf.org/html/rfc3339#section-4.3}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 -> 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)]. *)
|
||
|
||
(** {1:print Pretty printing} *)
|
||
|
||
val pp_human : ?frac_s:int -> ?tz_offset_s:tz_offset_s -> unit ->
|
||
Format.formatter -> t -> unit
|
||
(** [pp_human ~frac_s ~tz_offset_s () ppf t] prints an unspecified, human
|
||
readable, locale-independent, representation of [t] with:
|
||
{ul
|
||
{- [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
|
||
{{:https://tools.ietf.org/html/rfc3339#section-4.3}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]).}}
|
||
|
||
{b Note.} The output of this function is similar to but {b not}
|
||
compliant with RFC 3339, it should only be used for presentation,
|
||
not as a serialization format. *)
|
||
|
||
val pp : Format.formatter -> t -> unit
|
||
(** [pp] is [pp_human ~tz_offset_s:0]. *)
|
||
|
||
val dump : Format.formatter -> t -> unit
|
||
(** [dump ppf t] prints an unspecified raw representation of [t]
|
||
on [ppf]. *)
|
||
|
||
(** {1:basics Basics}
|
||
|
||
POSIX time counts POSIX seconds since the epoch 1970-01-01
|
||
00:00:00 UTC. As such a POSIX timestamp is {b 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
|
||
{{:http://www.ietf.org/timezones/data/leap-seconds.list}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 {e
|
||
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,
|
||
{e 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
|
||
{{:http://pubs.opengroup.org/onlinepubs/9699919799/functions/mktime.html}
|
||
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)
|
||
]}
|
||
|
||
{1:notes Notes and limitations}
|
||
|
||
The following points should be taken into account
|
||
{ul
|
||
{- {!Ptime} is not a calendar library and will never be.}
|
||
{- {!Ptime} can only represent picosecond precision timestamps in
|
||
the range \[{!Ptime.min};{!Ptime.max}\]. It is however able to
|
||
convert {e any} of these timestamps to a valid date-time or RFC
|
||
3339 timestamp.}
|
||
{- POSIX time in general is ill-suited to measure wall-clock
|
||
time spans for the following reasons.
|
||
{ul
|
||
{- POSIX time counts time in POSIX seconds. POSIX
|
||
seconds can represent 2, 1 or 0 SI seconds. [Ptime]
|
||
offers no mechanism to determine the SI duration between
|
||
two timestamps, see the {{!basics}basics}.}
|
||
{- The POSIX timestamps returned by your platform are not
|
||
monotonic: they are subject to operating system time
|
||
adjustements and can even go back in time. If you need to
|
||
measure time spans in a single program run use a monotonic
|
||
time source (e.g. {!Mtime}).}}}}
|
||
*)
|
||
|
||
END OF FILE: ptime.mli
|
||
*)
|
||
|
||
(* CASE ANALYSIS PREDICATES
|
||
- id: P01
|
||
pattern: All-day singolo
|
||
ics: "DTSTART;VALUE=DATE, opzionale DTEND=giorno+1"
|
||
remind_support: nativo
|
||
strategia: "REM <data> MSG <summary>"
|
||
snippet: 'REM 2025-12-25 MSG Natale'
|
||
priorita: Subito
|
||
|
||
- id: P02
|
||
pattern: All-day multi-giorno
|
||
ics: "DTSTART;VALUE=DATE + DTEND;VALUE=DATE esclusivo"
|
||
remind_support: nativo+accorgimenti
|
||
strategia: "espandi in eventi giornalieri; stesso SUMMARY"
|
||
snippet: 'REM 2025-08-10 THROUGH 2025-08-15 MSG Ferie # oppure espansione per-giorno'
|
||
priorita: Subito
|
||
|
||
- id: P03
|
||
pattern: Evento a orario locale
|
||
ics: "DTSTART;TZID=… + DTEND oppure DURATION"
|
||
remind_support: nativo
|
||
strategia: "REM <data> AT <hh:mm> [DURATION] MSG …"
|
||
snippet: 'REM 2025-10-05 AT 09:00 DURATION 1:00 MSG Riunione'
|
||
priorita: Subito
|
||
|
||
- id: P04
|
||
pattern: Evento a orario in UTC
|
||
ics: "DTSTART/DTEND con suffisso Z"
|
||
remind_support: nativo+accorgimenti
|
||
strategia: "converti a fuso locale prima di emettere AT"
|
||
snippet: 'REM 2025-09-03 AT 06:45 MSG Treno'
|
||
priorita: Subito
|
||
|
||
- id: P05
|
||
pattern: Ricorrenza settimanale semplice
|
||
ics: "RRULE:FREQ=WEEKLY;BYDAY=…;[UNTIL|COUNT]"
|
||
remind_support: nativo
|
||
strategia: "REM <giorni> FROM <start> [UNTIL <end>] AT <hh:mm> MSG …"
|
||
snippet: 'REM Mon Wed FROM 2025-09-01 UNTIL 2025-10-31 AT 09:00 MSG Standup'
|
||
priorita: Subito
|
||
|
||
- id: P06
|
||
pattern: Ricorrenza giornaliera semplice
|
||
ics: "RRULE:FREQ=DAILY;[UNTIL|COUNT]"
|
||
remind_support: nativo
|
||
strategia: "REM FROM <start> UNTIL <end> AT <hh:mm> EVERY 1 MSG …"
|
||
snippet: 'REM FROM 2025-10-01 UNTIL 2025-10-10 AT 08:30 MSG Daily'
|
||
priorita: Subito
|
||
|
||
- id: P07
|
||
pattern: Ricorrenza mensile per giorno fisso
|
||
ics: "RRULE:FREQ=MONTHLY;BYMONTHDAY=…"
|
||
remind_support: nativo
|
||
strategia: "REM <giorno-num> AT <hh:mm> FROM/UNTIL"
|
||
snippet: 'REM 15 AT 10:00 FROM 2025-01-01 MSG Fatture'
|
||
priorita: Dopo
|
||
|
||
- id: P08
|
||
pattern: Ricorrenza “n-esimo weekday” del mese
|
||
ics: "RRULE:FREQ=MONTHLY;BYDAY=MO;BYSETPOS=3"
|
||
remind_support: espansione
|
||
strategia: "materializza occorrenze in singoli REM o calcola in codice"
|
||
snippet: '# genera REM per ciascuna data calcolata'
|
||
priorita: Dopo
|
||
|
||
- id: P09
|
||
pattern: Ricorrenza annuale semplice
|
||
ics: "RRULE:FREQ=YEARLY;[BYMONTH][BYMONTHDAY]"
|
||
remind_support: nativo
|
||
strategia: "REM <data ricorrente> MSG …"
|
||
snippet: 'REM Jul 29 MSG Compleanno'
|
||
priorita: Dopo
|
||
|
||
- id: P10
|
||
pattern: Eccezioni
|
||
ics: "EXDATE (una o più), RDATE aggiuntive"
|
||
remind_support: nativo+accorgimenti
|
||
strategia: "usa OMIT per rimuovere date; aggiungi REM singoli per RDATE"
|
||
snippet: 'REM Mon AT 09:00 FROM 2025-09-01 UNTIL 2025-10-31 MSG Standup\nOMIT 2025-10-13'
|
||
priorita: Subito
|
||
|
||
- id: P11
|
||
pattern: Override/cancellazioni per istanza
|
||
ics: "RECURRENCE-ID con contenuto modificato o STATUS:CANCELLED"
|
||
remind_support: espansione
|
||
strategia: "OMIT la data dalla serie; aggiungi REM singolo con i campi override"
|
||
snippet: '# serie + REM specifico per l’istanza'
|
||
priorita: Subito
|
||
|
||
- id: P12
|
||
pattern: DURATION al posto di DTEND
|
||
ics: "DURATION:PT…"
|
||
remind_support: nativo
|
||
strategia: "mappa su DURATION <h:mm> in REM"
|
||
snippet: 'REM 2025-10-05 AT 14:00 DURATION 2:30 MSG Workshop'
|
||
priorita: Subito
|
||
|
||
- id: P13
|
||
pattern: Allarmi
|
||
ics: "VALARM DISPLAY/AUDIO/EMAIL; TRIGGER relativo"
|
||
remind_support: parziale
|
||
strategia: "mappa 1 allarme principale su WARN; multipli opzionali come REM duplicati HIDE"
|
||
snippet: 'REM 2025-10-05 AT 09:00 WARN 15 MSG Riunione'
|
||
priorita: Dopo
|
||
|
||
- id: P14
|
||
pattern: Fusi orari dichiarati (VTIMEZONE, TZID diversi)
|
||
ics: "VTIMEZONE + DTSTART;TZID=…"
|
||
remind_support: nativo+accorgimenti
|
||
strategia: "normalizza tutto al fuso locale del sistema prima dell’output"
|
||
snippet: '# conversione in pre-processing'
|
||
priorita: Subito
|
||
|
||
- id: P15
|
||
pattern: Partecipanti/organizzatore
|
||
ics: "ORGANIZER, ATTENDEE*, PARTSTAT…"
|
||
remind_support: non previsto
|
||
strategia: "appendi a DESCRIPTION/MSG come testo"
|
||
snippet: '# nessuna semantica in Remind'
|
||
priorita: Quando serve
|
||
|
||
- id: P16
|
||
pattern: Allegati/URL esterni
|
||
ics: "ATTACH, URL"
|
||
remind_support: non previsto
|
||
strategia: "conserva URL in coda al MSG"
|
||
snippet: '# link nel testo'
|
||
priorita: Quando serve
|
||
|
||
- id: P17
|
||
pattern: Meeting online (Google/Teams metadati)
|
||
ics: "X-GOOGLE-CONFERENCE, X-MICROSOFT-*"
|
||
remind_support: non previsto
|
||
strategia: "estrai solo URL di join nel MSG"
|
||
snippet: '# riduci al link'
|
||
priorita: Quando serve
|
||
|
||
- id: P18
|
||
pattern: Visibilità/trasparenza
|
||
ics: "CLASS, TRANSP"
|
||
remind_support: non previsto
|
||
strategia: "ignora o aggiungi prefisso [FREE]/[BUSY] nel MSG"
|
||
snippet: '# opzionale'
|
||
priorita: Ignora
|
||
|
||
- id: P19
|
||
pattern: Stato/versioning
|
||
ics: "STATUS, SEQUENCE, CREATED, LAST-MODIFIED"
|
||
remind_support: non previsto
|
||
strategia: "ignora; usa solo STATUS:CANCELLED per soppressioni"
|
||
snippet: '# già coperto in P11'
|
||
priorita: Ignora
|
||
|
||
- id: P20
|
||
pattern: Categorie/etichette
|
||
ics: "CATEGORIES:…"
|
||
remind_support: parziale
|
||
strategia: "prefisso nel MSG o uso TAG se ti serve filtrare"
|
||
snippet: 'REM 2025-10-05 AT 09:00 TAG Work MSG [Work] Riunione'
|
||
priorita: Dopo
|
||
*)
|
||
|
||
type event_description =
|
||
[ `All_day_event_single (* P01 *)
|
||
| `All_day_event_multi (* P02 *)
|
||
| `Timed_event (* P03 and P04 *)
|
||
| `Weekly_simple_recurrence (* P05 *)
|
||
| `Daily_simple_recurrence (* P06 *)
|
||
| `Exception_events (* P10 *) ]
|
||
[@@deriving show]
|
||
|
||
type predicate = Icalendar.event -> bool
|
||
|
||
let all_day_event_single ev : bool =
|
||
(* P01 *)
|
||
let _, dtstart = ev.dtstart in
|
||
match dtstart with
|
||
| `Date d -> begin
|
||
match ev.dtend_or_duration with
|
||
| None -> true
|
||
| Some (`Dtend (_, `Date end_)) -> begin
|
||
let start_dt = Ptime.of_date d |> Option.get in
|
||
let end_dt = Ptime.of_date end_ |> Option.get in
|
||
if Ptime.diff end_dt start_dt = Ptime.Span.of_int_s 86400 then true else false
|
||
end
|
||
| _ -> false
|
||
end
|
||
| _ -> begin false end
|
||
|
||
let all_day_event_multi ev : bool =
|
||
(* P02 *)
|
||
let _, dtstart = ev.dtstart in
|
||
match dtstart with
|
||
| `Date d -> begin
|
||
match ev.dtend_or_duration with
|
||
| None -> false
|
||
| Some (`Dtend (_, `Date end_)) -> begin
|
||
let start_dt = Ptime.of_date d |> Option.get in
|
||
let end_dt = Ptime.of_date end_ |> Option.get in
|
||
if Ptime.diff end_dt start_dt > Ptime.Span.of_int_s 86400 then true else false
|
||
end
|
||
| _ -> false
|
||
end
|
||
| _ -> begin false end
|
||
|
||
let timed_event ev : bool =
|
||
(* P03 and P04 *)
|
||
let _, dtstart = ev.dtstart in
|
||
let start_td = get_start ev in
|
||
let uid = get_uid ev in
|
||
match dtstart with
|
||
| `Datetime (`Local _) -> begin
|
||
Printf.printf "Local time event: %s\n" uid;
|
||
Printf.printf " Timedesc: %s\n" (start_td |> Timedesc.to_rfc3339);
|
||
true
|
||
end
|
||
| `Datetime (`Utc ts) -> begin
|
||
Printf.printf "UTC time event: %s, time: %s\n" uid (Ptime.to_rfc3339 ts);
|
||
Printf.printf " Timedesc: %s\n" (start_td |> Timedesc.to_rfc3339);
|
||
true
|
||
end
|
||
| `Datetime (`With_tzid (ts, (b, tz_name))) -> begin
|
||
Printf.printf "With TZID event: %s, TZID: (%b, %s), time: %s\n" uid b tz_name (Ptime.to_rfc3339 ts);
|
||
Printf.printf " Timedesc: %s\n" (start_td |> Timedesc.to_rfc3339);
|
||
true
|
||
end
|
||
| `Date (y, m, d) -> begin
|
||
Printf.printf "All-day event (date): %s, date: %04d-%02d-%02d\n" uid y m d;
|
||
Printf.printf " Timedesc: %s\n" (start_td |> Timedesc.to_rfc3339);
|
||
false
|
||
end
|
||
|
||
let weekly_simple_recurrence ev : bool =
|
||
(* P05 *)
|
||
let rrules = ev.rrule in
|
||
match rrules with
|
||
| None -> false
|
||
| Some (_, (`Weekly, _, _, _)) -> begin
|
||
Printf.printf " Weekly simple recurrence event\n";
|
||
true
|
||
end
|
||
| _ -> false
|
||
|
||
let daily_simple_recurrence ev : bool =
|
||
(* P06 *)
|
||
let rrules = ev.rrule in
|
||
match rrules with
|
||
| None -> false
|
||
| Some (_, (`Daily, _, _, _)) -> begin
|
||
Printf.printf " Daily simple recurrence event\n";
|
||
true
|
||
end
|
||
| _ -> false
|
||
|
||
let exception_events ev : bool =
|
||
(* P10 *)
|
||
let exdates = get_exdates ev in
|
||
let rdates = get_rdates ev in
|
||
if exdates <> [] || rdates <> []
|
||
then begin
|
||
Printf.printf " Exception event: %s\n" (get_uid ev);
|
||
true
|
||
end
|
||
else false
|
||
|
||
let all_predicates : (predicate * event_description) list =
|
||
[
|
||
(all_day_event_single, `All_day_event_single);
|
||
(all_day_event_multi, `All_day_event_multi);
|
||
(timed_event, `Timed_event);
|
||
(weekly_simple_recurrence, `Weekly_simple_recurrence);
|
||
(daily_simple_recurrence, `Daily_simple_recurrence);
|
||
(exception_events, `Exception_events);
|
||
]
|