Source file recurrence.ml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
let leap_year y =
match y mod 4 = 0, y mod 100 = 0, y mod 400 = 0 with
| false, _, _ -> false
| true, false, _ -> true
| true, true, false -> false
| true, true, true -> true
let days_in_month year = function
| 1 -> 31
| 2 when leap_year year -> 29
| 2 -> 28
| 3 -> 31
| 4 -> 30
| 5 -> 31
| 6 -> 30
| 7 -> 31
| 8 -> 31
| 9 -> 30
| 10 -> 31
| 11 -> 30
| 12 -> 31
| _ -> assert false
let add_years amount (y, m, d) = (y + amount, m, d)
let sub_years amount (y, m, d) = (y - amount, m, d)
let add_months amount (y, m, d) =
let rec inc_y (ny, nm, nd) month =
if month > 12
then inc_y (add_years 1 (ny, nm, nd)) (month - 12)
else (ny, month, nd)
in
let m' = m + amount in
inc_y (y, m, d) m'
let sub_months amount (y, m, d) =
let rec dec_y (ny, nm, nd) month =
if month < 1
then dec_y (sub_years 1 (ny, nm, nd)) (month + 12)
else (ny, month, nd)
in
let m' = m - amount in
dec_y (y, m, d) m'
let add_days amount (y, m, d) =
let rec inc_m (ny, nm, nd) days =
let md = days_in_month ny nm in
if days > md
then inc_m (add_months 1 (ny, nm, nd)) (days - md)
else (ny, nm, days)
in
let d' = d + amount in
inc_m (y, m, d) d'
let sub_days amount (y, m, d) =
let rec dec_m (ny, nm, nd) days =
if days < 1
then
let (ny', nm', nd') = sub_months 1 (ny, nm, nd) in
let md = days_in_month ny' nm' in
dec_m (ny', nm', nd') (days + md)
else (ny, nm, days)
in
let d' = d - amount in
dec_m (y, m, d) d'
let add_weeks amount date = add_days (7 * amount) date
let find_opt f xs =
match
List.filter (function None -> false | Some _ -> true)
(List.map f xs)
with
| [] -> None
| [ Some x ] -> Some x
| _ -> invalid_arg "wrong"
let wd_is_weekday wd wd' = match wd, wd' with
| `Monday, `Monday | `Tuesday, `Tuesday | `Wednesday, `Wednesday
| `Thursday, `Thursday | `Friday, `Friday | `Saturday, `Saturday
| `Sunday, `Sunday -> true
| _ -> false
let days_in_year y = if leap_year y then 366 else 365
let days_since_start_of_year (y, m, d) =
let rec md = function
| 0 -> 0
| n -> days_in_month y n + md (pred n)
in
md (pred m) + d
let days_until_end_of_year (y, m, d) =
let rec md = function
| 12 -> 31 - d
| n -> days_in_month y m + md (succ n)
in
md m
let weekday (y, m, d) =
let d1_to_date = pred @@ days_since_start_of_year (y, m, d) in
let epoch_to_d1 =
let rec go = function
| 1969 -> 0
| x -> days_in_year x + go (pred x)
in
go (pred y)
in
match (epoch_to_d1 + d1_to_date) mod 7 with
| 0 -> `Thursday
| 1 -> `Friday
| 2 -> `Saturday
| 3 -> `Sunday
| 4 -> `Monday
| 5 -> `Tuesday
| 6 -> `Wednesday
| _ -> invalid_arg "bad input for weekday"
let wd = function
| `Sunday -> 0
| `Monday -> 1
| `Tuesday -> 2
| `Wednesday -> 3
| `Thursday -> 4
| `Friday -> 5
| `Saturday -> 6
let w1d1_offset year =
let wd = wd (weekday (year, 01, 01)) in
(11 - wd) mod 7 - 3
let w1d1 year =
let off = w1d1_offset year
and date = (year, 01, 01)
in
if off < 0
then sub_days (abs off) date
else add_days off date
let rec week_number (y, m, d) =
let days = pred @@ days_since_start_of_year (y, m, d)
and off = w1d1_offset y
in
let ndays = days - off in
if ndays < 0
then week_number (pred y, 12, 31)
else
let next_off = w1d1_offset (succ y) in
if next_off < 0 && days_until_end_of_year (y, m, d) + next_off <= 0
then (succ y, 1)
else (y, ndays / 7 + 1)
let weeks y =
let off = w1d1_offset (succ y) in
let last_day = (y, 12, 31) in
let last =
if off >= 0
then add_days off last_day
else sub_days (abs (pred off)) last_day
in
snd (week_number last)
let monthday_matches (y, m, d) n =
if d = n
then true
else if n < 0
then d = days_in_month y m + succ n
else false
let weekno_matches date wn =
let y, week = week_number date in
if week = wn
then true
else if wn < 0
then week = weeks y + succ wn
else false
let yearday_matches (y, m, d) n =
let count = days_since_start_of_year (y, m, d) in
if count = n
then true
else if n < 0
then count = days_in_year y + succ n
else false
let weekday_matches (y, m, d) (x, wd) =
let weekday = weekday (y, m, d) in
if wd_is_weekday weekday wd
then
let n = succ (pred d / 7) in
match x with
| 0 -> true
| x ->
if x > 0
then n = x
else
let total = n + (days_in_month y m - d) / 7 in
n = total + succ x
else false
let yearly_weekday_matches (y, m, d) (x, wd) =
let weekday = weekday (y, m, d) in
if wd_is_weekday weekday wd
then
let n =
let d = days_since_start_of_year (y, m, d) in
succ (d / 7)
in
match x with
| 0 -> true
| x ->
if x > 0
then n = x
else
let total = n + (days_in_year y - n) / 7 in
n = total + succ x
else false
let is_occurence s_date freq (bymonth, byweekno, byyearday, bymonthday, byday) =
match freq with
| `Daily ->
let (y, m, d) = s_date in
let is_bymonth = match bymonth with
| None -> true
| Some ms -> List.mem m ms
in
let is_bymonthday () = match bymonthday with
| None -> true
| Some ds -> List.exists (monthday_matches (y, m, d)) ds
in
let is_byday () = match byday with
| None -> true
| Some ds ->
let weekday = weekday (y, m, d) in
List.exists (fun (_, wk') -> wd_is_weekday weekday wk') ds
in
is_bymonth && is_bymonthday () && is_byday ()
| `Weekly ->
let (y, m, d) = s_date in
let is_bymonth = match bymonth with
| None -> true
| Some ms -> List.mem m ms
in
let is_byday () = match byday with
| None -> true
| Some ds ->
let weekday = weekday (y, m, d) in
List.exists (fun (_, wk') -> wd_is_weekday weekday wk') ds
in
is_bymonth && is_byday ()
| `Monthly ->
let (y, m, d) = s_date in
let is_bymonth = match bymonth with
| None -> true
| Some ms -> List.mem m ms
in
let is_bymonthday () = match bymonthday with
| None -> true
| Some md -> List.exists (monthday_matches (y, m, d)) md
in
let is_byday () = match byday with
| None -> true
| Some wd -> List.exists (weekday_matches (y, m, d)) wd
in
is_bymonth && is_bymonthday () && is_byday ()
| `Yearly ->
let (y, m, d) = s_date in
let is_bymonth = match bymonth with
| None -> true
| Some ms -> List.mem m ms
in
let is_byweekno () = match byweekno with
| None -> true
| Some wn -> List.exists (weekno_matches (y, m, d)) wn
in
let is_byyearday () = match byyearday with
| None -> true
| Some yd -> List.exists (yearday_matches (y, m, d)) yd
in
let is_bymonthday () = match bymonthday with
| None -> true
| Some md -> List.exists (monthday_matches (y, m, d)) md
in
let is_byday () = match byday, bymonth with
| None, _ -> true
| Some wd, None -> List.exists (yearly_weekday_matches (y, m, d)) wd
| Some wd, Some _ -> List.exists (weekday_matches (y, m, d)) wd
in
is_bymonth && is_byweekno () && is_byyearday () && is_bymonthday () && is_byday ()
| `Hourly | `Minutely | `Secondly -> invalid_arg "We don't support hourly, minutely or secondly for event frequencies."
let filter_bysetpos bysetpos set =
match bysetpos with
| None -> set
| Some p ->
let l = List.length set in
let positions = List.map (fun i -> if i < 0 then l + i else pred i) p |>
List.sort_uniq compare in
List.map (List.nth set) positions
let compare_dates (y, m, d) (y', m', d') = match compare y y' with
| 0 -> begin match compare m m' with
| 0 -> compare d d'
| x -> x
end
| x -> x
let after_start start set =
List.filter (fun d -> compare_dates d start >= 0) set
type gen_state = {
mutable next_interval : Ptime.t ;
mutable set : Ptime.t list ;
next_recurrence_set : Ptime.t -> Ptime.t * Ptime.t list
}
let init_rr next_interval freq interval filters bysetpos wkst =
let next_recurrence_set start =
let s_date, s_time = Ptime.to_date_time start in
let start_set, end_set, start_next_set =
let start_set, advance_by_freq = match freq with
| `Daily -> s_date, add_days
| `Weekly -> let rec weekstart d = if wd_is_weekday wkst (weekday d) then d, add_weeks else weekstart (sub_days 1 d) in weekstart s_date
| `Monthly -> let (y, m, _) = s_date in (y, m, 1), add_months
| `Yearly -> let (y, _, _) = s_date in (y, 1, 1), add_years
| _ -> assert false
in
let interval' = match interval with None -> 1 | Some x -> x in
start_set, advance_by_freq 1 start_set, advance_by_freq interval' start_set
in
let in_set x = compare_dates start_set x <= 0 && compare_dates x end_set < 0 in
let rec next_elem d =
if in_set d
then let d' = add_days 1 d in
if is_occurence d freq filters
then d :: next_elem d'
else next_elem d'
else []
in
let set = next_elem start_set in
let set' = filter_bysetpos bysetpos set in
let set'' = after_start s_date set' in
let to_ptime t = match Ptime.of_date_time (t, s_time) with
| None -> assert false
| Some x -> x in
to_ptime start_next_set, List.map to_ptime set''
in
{ next_interval ; set = [] ; next_recurrence_set }
let rec next_rr g =
match g.set with
| [] ->
let next_interval', rr_set = g.next_recurrence_set g.next_interval in
g.next_interval <- next_interval' ;
g.set <- rr_set ;
next_rr g
| hd :: tl ->
g.set <- tl ;
hd
type count = {
mutable count : int ;
f : gen_state ;
}
let init_count count f = { count ; f }
let next_count g =
if g.count = 0
then None
else begin
g.count <- pred g.count ;
Some (next_rr g.f)
end
type until = {
until : Ptime.t ;
f : gen_state
}
let init_until until f = { until ; f }
let next_until g =
let candidate = next_rr g.f in
if Ptime.is_earlier ~than:g.until candidate
then Some candidate
else None
let add_missing_filters recurs freq start =
let s_date, _s_time = Ptime.to_date_time start in
let bymonth = find_opt (function `Bymonth x -> Some x | _ -> None) recurs
and byweekno = find_opt (function `Byweek x -> Some x | _ -> None) recurs
and byyearday = find_opt (function `Byyearday x -> Some x | _ -> None) recurs
and bymonthday = find_opt (function `Bymonthday x -> Some x | _ -> None) recurs
and byday = find_opt (function `Byday x -> Some x | _ -> None) recurs
in
let bymonth, bymonthday = match freq, byday, byyearday, bymonth, bymonthday with
| `Yearly, None, None, None, None ->
let (_, m, d) = s_date in
Some [ m ], Some [ d ]
| `Yearly, None, None, Some _, None ->
let (_, _, d) = s_date in
bymonth, Some [ d ]
| _ -> bymonth, bymonthday
in
let bymonthday = match freq, byday, bymonthday, byyearday with
| `Monthly, None, None, None -> let (_, _, d) = s_date in Some [ d ]
| _ -> bymonthday
in
let byday = match freq, byday with
| `Weekly, None -> Some [ (0, weekday s_date) ]
| _ -> byday
in
(bymonth, byweekno, byyearday, bymonthday, byday)
let new_gen start recurrence =
let (freq, count_or_until, interval, recurs) = recurrence in
let filters = add_missing_filters recurs freq start
and bysetpos = find_opt (function `Bysetposday x -> Some x | _ -> None) recurs
and wkst = find_opt (function `Weekday x -> Some x | _ -> None) recurs in
let wkst = match wkst with None -> `Monday | Some x -> x in
let gen_event = init_rr start freq interval filters bysetpos wkst in
match count_or_until with
| Some (`Count n) ->
let gen_count = init_count n gen_event in
(fun () -> next_count gen_count)
| Some (`Until (`Utc ts))
| Some (`Until (`Local ts)) ->
let gen_until = init_until ts gen_event in
(fun () -> next_until gen_until)
| _ ->
(fun () -> Some (next_rr gen_event))