A Schedule.t
describes a (potentially repeating) schedule by selecting
a subset of all seconds using the set operations in t
. For example:
every 5 min after the hour :
Mins [ 5 ]
9am to 10am every day :
Between (Time.Ofday.create ~hr:9 (), Time.Ofday.create ~hr:10 ())
Every weekday at 3pm:
And [ Weekdays [ Mon; Tue; Wed; Thu; Fri ] ; At [ Time.Ofday.create ~hr:15 () ] ]
On the 15th of every month at midnight:
And [ Days [ 15 ] ; At [ Time.Ofday.start_of_day ] ]
9:30 am on weekends, and 5 am on weekdays
Or [ And [ Weekdays [ Sat; Sun ] ; At [ Time.Ofday.create ~hr:9 ~min:30 () ] ] ; And [ Weekdays [ Mon; Tue; Wed; Thu; Fri ] ; At [ Time.Ofday.create ~hr:5 () ] ] ]
On top of this selection language there are two labeling branches of the variant that are important.
In_zone (zone, t)
expresses that all of t should be evaluated relative to the time
zone given.
Tag (tag, t)
tags anything matching t with tag
.
Combining these we can express something complex like the on-call groups across three offices:
let weekdays = Weekdays Day_of_week.weekdays in
let working_hours = Between Time.Ofday.((create ~hr:8 (), create ~hr:18 ())) in
let working_schedule = And [ weekdays; working_hours ] in
let offices =
let (!!) = Time.Zone.find_exn in
Location.Abbrev.([
tot, !!"America/New_York"
; hkg, !!"Asia/Hong_Kong"
; ldn, !!"Europe/London" ])
in
List.map offices ~f:(fun (office, zone) ->
In_zone (zone, Tag (office, working_schedule)))
after which we can use the tags
function to extract the groups on call at any moment.
Schedules are expressed in terms of wall clock time, and as such have interesting behavior around daylight savings time boundaries. There are two circumstances that might affect a schedule. The first is a repeated time, which occurs when time jumps back (e.g. 2:30 may happen twice in one day). The second is a skipped time, which occurs when time jumps forward by an hour.
In both cases Schedule
does the naive thing. If the time happens twice and is
included in the schedule it is included twice. If it never happens Schedule
makes
no special attempt to artificially include it.
these phantom types are concrete and exposed to help the compiler understand
that zoned and unzoned cannot be the same type (which it could not know if they
were abstract), which helps it infer the injectivity of the type t
below.
In_zone
: see the discussion under Zones and Tags aboveTag
: see the discussion under Zones and Tags aboveAnd
, Or
, Not
: correspond to the set operations intersection, union, and
complement.If_then_else (A, B, C)
: corresponds to (A && B) || (NOT A && C), useful for dealing
with schedules that change during certain times of the year (holidays, etc.)At
: the exact times given on every dayShift
: shifts an entire schedule forward or backwards by a known span. (e.g:Shift ((sec 3.), Secs[10]) = Secs [13]
Shift ((sec (-3.)), Secs[10]) = Secs [7]
)Between
: the contiguous range between the start and end times given on every daySecs
: the exact seconds given during every hour of every dayMins
: all seconds in the minutes given during every hour of every dayHours
: all seconds in the hours given on every dayWeekdays
: all seconds in the days given on every weekDays
: all seconds in the days given, every monthWeeks
: all seconds in the weeks given (numbered ISO 8601), every yearMonths
: all seconds in the months given, every yearOn
: all seconds in the exact dates givenBefore
: all seconds before the given boundary (inclusive or exclusive)After
: all seconds after the given boundary (inclusive or exclusive)Always
: the set of all secondsNever
: the empty set'a
indicates whether the schedule currently has an established zone.
'b
is the type of the tag used in this schedule. In many cases it can be
unspecified. See tags
for more.
Between (a, b)
is the empty set if a > b.
Items that take int list
s silently ignore int
s outside of the viable
range. E.g. Days [32]
will never occur.
module Inclusive_exclusive : sig ... end
type ('a, 'b) t
=
| In_zone : Core__.Import_time.Time.Zone.t * (unzoned, 'b) t ‑> (zoned, 'b) t |
| Tag : 'b * ('a, 'b) t ‑> ('a, 'b) t |
| And : ('a, 'b) t list ‑> ('a, 'b) t |
| Or : ('a, 'b) t list ‑> ('a, 'b) t |
| Not : ('a, 'b) t ‑> ('a, 'b) t |
| If_then_else : (('a, 'b) t * ('a, 'b) t * ('a, 'b) t) ‑> ('a, 'b) t |
| Shift : Core__.Import_time.Time.Span.t * ('a, 'b) t ‑> ('a, 'b) t |
| Between : (Inclusive_exclusive.t * Core__.Import_time.Time.Ofday.t) * (Inclusive_exclusive.t * Core__.Import_time.Time.Ofday.t) ‑> (unzoned, 'b) t |
| At : Core__.Import_time.Time.Ofday.t list ‑> (unzoned, 'b) t |
| Secs : int list ‑> (unzoned, 'b) t |
| Mins : int list ‑> (unzoned, 'b) t |
| Hours : int list ‑> (unzoned, 'b) t |
| Weekdays : Core__.Import.Day_of_week.t list ‑> (unzoned, 'b) t |
| Days : int list ‑> (unzoned, 'b) t |
| Weeks : int list ‑> (unzoned, 'b) t |
| Months : Core__.Import.Month.t list ‑> (unzoned, 'b) t |
| On : Core__.Import.Date.t list ‑> (unzoned, 'b) t |
| Before : (Inclusive_exclusive.t * (Core__.Import.Date.t * Core__.Import_time.Time.Ofday.t)) ‑> (unzoned, 'b) t |
| After : (Inclusive_exclusive.t * (Core__.Import.Date.t * Core__.Import_time.Time.Ofday.t)) ‑> (unzoned, 'b) t |
| Always : ('a, 'b) t |
| Never : ('a, 'b) t |
module Stable : sig ... end
val includes : (zoned, 'b) t ‑> Core__.Import_time.Time.t ‑> bool
includes t time
is true if the second represented by time
falls within the
schedule t
.
val tags : (zoned, 'tag) t ‑> Core__.Import_time.Time.t ‑> [ `Not_included | `Included of 'tag list ]
tags t time = `Not_included
iff not (includes t time)
. Otherwise, tags t time
= `Included lst
, where lst
includes all tags of a schedule such that includes t'
time
is true where t'
is a tagged branch of the schedule. E.g. for some t
equal
to Tag some_tag t'
, tags t time
will return some_tag
if and only if includes t'
time
returns true. For a more interesting use case, consider the per-office on-call
schedule example given in the beginning of this module. Note that a schdeule may have
no tags, and therefore, lst
can be empty.
val all_tags : (zoned, 'tag) t ‑> tag_comparator:('tag, 'cmp) Core__.Import.Comparator.t ‑> ('tag, 'cmp) Core__.Import.Set.t
val fold_tags : (zoned, 'tag) t ‑> init:'m ‑> f:('m ‑> 'tag ‑> 'm) ‑> Core__.Import_time.Time.t ‑> 'm option
fold_tags t ~init ~f time
is nearly behaviorally equivalent to (but more efficient
than) List.fold ~init ~f (tags t time)
, with the exception that it returns None
if
includes t time
is false. It is important that f
be pure, as its results may be
discarded.
Return a sequence of schedule changes over time that will never end.
If your schedules ends, you will continue to receive `No_change_until_at_least with increasing times forever.
The return type indicates whether includes t start_time
is true and
delivers a sequence of subsequent changes over time.
The times returned by the sequence are strictly increasing and never less
than start_time
. That is, `No_change_until_at_least x
can never be
followed by `Enter x
, only by (at least) `Enter (x + 1s)
.
if emit
is set to Transitions_and_tag_changes
then all changes in tags
will be present in the resulting sequence. Otherwise only the tags in effect
when a schedule is entered are available.
The `In_range | `Out_of_range
flag in `No_change_until_at_least
indicates whether the covered range is entirely within, or outside of the
time covered by the schedule and is only there to help with bookkeeping for
the caller. `In_range | `Out_of_range
will never disagree with what
could be inferred from the `Enter
and `Leave
events.
The sequence takes care to do only a small amount of work between each
element, so that pulling the next element of the sequence is always cheap.
This is the primary motivation behind including `No_change_until_at_least
.
The Time.t
returned by `No_change_until_at_least is guaranteed to be a reasonable
amount of time in the future (at least 1 hour).
module Event : sig ... end
type ('tag, 'a) emit
=
| Transitions : ('tag, [ Event.no_change | 'tag Event.transition ]) emit |
| Transitions_and_tag_changes : ('tag ‑> 'tag ‑> bool) ‑> ('tag, [ Event.no_change | 'tag Event.transition | 'tag Event.tag_change ]) emit |
in Transitions_and_tag_changes
equality for the tag type must be given
val to_endless_sequence : (zoned, 'tag) t ‑> start_time:Core__.Import_time.Time.t ‑> emit:('tag, 'a) emit ‑> [ `Started_in_range of 'tag list * 'a Core__.Import.Sequence.t | `Started_out_of_range of 'a Core__.Import.Sequence.t ]
val next_enter_between : (zoned, 'tag) t ‑> Core__.Import_time.Time.t ‑> Core__.Import_time.Time.t ‑> Core__.Import_time.Time.t option
next_enter_between t start end
The given start
end
range is inclusive on both
ends. This function is useful for one-off events during the run of a program.
If you want to track changes to a schedule over time it is better to call
to_endless_sequence
val next_leave_between : (zoned, 'tag) t ‑> Core__.Import_time.Time.t ‑> Core__.Import_time.Time.t ‑> Core__.Import_time.Time.t option
as next_enter_between
but for leave events