Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 32s
Civil date arithmetic (Hinnant), integer epoch-minute datetimes, bounded windowed RRULE expansion (DAILY/WEEKLY with INTERVAL/COUNT/UNTIL/BYDAY), multi-event merge. Conformance harness + scoreboard wired. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
315 lines
9.1 KiB
Plaintext
315 lines
9.1 KiB
Plaintext
;; lib/events/calendar.sx — civil date arithmetic + RRULE expansion in a window.
|
|
;;
|
|
;; Datetimes are integer "epoch minutes": days-since-1970-01-01 * 1440 plus
|
|
;; minute-of-day. Ordering, window bounds, and durations are plain integer
|
|
;; arithmetic. Civil <-> day-number conversion uses Howard Hinnant's algorithm
|
|
;; (exact, branch-free, correct for the proleptic Gregorian calendar).
|
|
;;
|
|
;; RRULE expansion is the bridge to Datalog: a recurring event expands to a
|
|
;; bounded list of occurrence dicts within an explicit (win-start, win-end)
|
|
;; window. Expansion is ALWAYS windowed — an RRULE without a window is an
|
|
;; infinite computation and is never permitted. Supported subset (RFC 5545):
|
|
;; FREQ=DAILY|WEEKLY, INTERVAL, COUNT, UNTIL, BYDAY (weekly). MONTHLY and the
|
|
;; rest are deferred.
|
|
|
|
;; ---- integer helpers ----
|
|
|
|
;; Floored integer division (modulo is already floored, so the remainder
|
|
;; subtraction makes the quotient exact and floor-correct for any sign).
|
|
(define ev-floor-div (fn (a b) (quotient (- a (modulo a b)) b)))
|
|
|
|
(define ev-or (fn (x d) (if (nil? x) d x)))
|
|
|
|
;; ---- civil date core (Hinnant) ----
|
|
|
|
;; Days since 1970-01-01 for civil (y, m, d). m in [1,12], d in [1,31].
|
|
(define
|
|
ev-days-from-civil
|
|
(fn
|
|
(y0 m d)
|
|
(let
|
|
((y (if (<= m 2) (- y0 1) y0)))
|
|
(let
|
|
((era (ev-floor-div (if (>= y 0) y (- y 399)) 400)))
|
|
(let
|
|
((yoe (- y (* era 400)))
|
|
(doy
|
|
(+
|
|
(ev-floor-div
|
|
(+
|
|
(*
|
|
153
|
|
(+ m (if (> m 2) -3 9)))
|
|
2)
|
|
5)
|
|
(- d 1))))
|
|
(let
|
|
((doe (+ (* yoe 365) (ev-floor-div yoe 4) (- (ev-floor-div yoe 100)) doy)))
|
|
(+ (* era 146097) doe -719468)))))))
|
|
|
|
;; Civil (y m d) list from a day-number.
|
|
(define
|
|
ev-civil-from-days
|
|
(fn
|
|
(z0)
|
|
(let
|
|
((z (+ z0 719468)))
|
|
(let
|
|
((era (ev-floor-div (if (>= z 0) z (- z 146096)) 146097)))
|
|
(let
|
|
((doe (- z (* era 146097))))
|
|
(let
|
|
((yoe (ev-floor-div (+ (- doe (ev-floor-div doe 1460)) (ev-floor-div doe 36524) (- (ev-floor-div doe 146096))) 365)))
|
|
(let
|
|
((y (+ yoe (* era 400)))
|
|
(doy
|
|
(-
|
|
doe
|
|
(+
|
|
(* 365 yoe)
|
|
(ev-floor-div yoe 4)
|
|
(- (ev-floor-div yoe 100))))))
|
|
(let
|
|
((mp (ev-floor-div (+ (* 5 doy) 2) 153)))
|
|
(let
|
|
((d (+ (- doy (ev-floor-div (+ (* 153 mp) 2) 5)) 1))
|
|
(m
|
|
(if
|
|
(< mp 10)
|
|
(+ mp 3)
|
|
(- mp 9))))
|
|
(list (if (<= m 2) (+ y 1) y) m d))))))))))
|
|
|
|
;; Weekday of a day-number: 0=Mon .. 6=Sun (1970-01-01 is Thursday = 3).
|
|
(define ev-weekday-of-days (fn (z) (modulo (+ z 3) 7)))
|
|
|
|
;; ---- datetime (epoch minutes) ----
|
|
|
|
(define
|
|
ev-dt
|
|
(fn
|
|
(y m d hh mm)
|
|
(+ (* (ev-days-from-civil y m d) 1440) (* hh 60) mm)))
|
|
|
|
(define ev-date (fn (y m d) (ev-dt y m d 0 0)))
|
|
|
|
(define ev-dt->days (fn (t) (ev-floor-div t 1440)))
|
|
|
|
(define ev-dt->civil (fn (t) (ev-civil-from-days (ev-dt->days t))))
|
|
|
|
(define ev-dt-weekday (fn (t) (ev-weekday-of-days (ev-dt->days t))))
|
|
|
|
(define ev-dt-tod (fn (t) (modulo t 1440)))
|
|
|
|
;; ---- event + occurrence constructors ----
|
|
|
|
;; rrule is nil (single event) or a dict:
|
|
;; {:freq :daily|:weekly :interval N :count N|nil :until DT|nil
|
|
;; :byday (list 0 2 4)|nil} ; byday weekday numbers, 0=Mon
|
|
(define ev-event (fn (id dtstart duration rrule capacity) {:duration duration :id id :dtstart dtstart :capacity capacity :rrule rrule}))
|
|
|
|
(define ev-occ (fn (id start dur) {:id id :start start :end (+ start dur)}))
|
|
|
|
;; ---- DAILY expansion ----
|
|
;; occ starts at dtstart; n counts every generated occurrence (window-
|
|
;; independent, so COUNT/UNTIL bound the rule, not the view). Emits only
|
|
;; occurrences inside [win-start, win-end].
|
|
(define
|
|
ev-daily-loop
|
|
(fn
|
|
(id occ duration step count until dtstart win-start win-end acc n)
|
|
(cond
|
|
((> occ win-end) acc)
|
|
((and (not (nil? count)) (>= n count)) acc)
|
|
((and (not (nil? until)) (> occ until)) acc)
|
|
(else
|
|
(begin
|
|
(when (>= occ win-start) (append! acc (ev-occ id occ duration)))
|
|
(ev-daily-loop
|
|
id
|
|
(+ occ step)
|
|
duration
|
|
step
|
|
count
|
|
until
|
|
dtstart
|
|
win-start
|
|
win-end
|
|
acc
|
|
(+ n 1)))))))
|
|
|
|
;; ---- WEEKLY expansion ----
|
|
;; Iterate week by week from the Monday of dtstart's week. Within each active
|
|
;; week emit each BYDAY (sorted) whose datetime is >= dtstart and inside the
|
|
;; window. n counts every generated occurrence >= dtstart for COUNT.
|
|
|
|
(define
|
|
ev-week0-days
|
|
(fn (dtstart) (- (ev-dt->days dtstart) (ev-dt-weekday dtstart))))
|
|
|
|
(define
|
|
ev-byday-default
|
|
(fn
|
|
(byday dtstart)
|
|
(if (nil? byday) (list (ev-dt-weekday dtstart)) (sort byday))))
|
|
|
|
(define
|
|
ev-weekly-bd-loop
|
|
(fn
|
|
(id week-days tod duration bd count until dtstart win-start win-end acc n)
|
|
(if
|
|
(empty? bd)
|
|
n
|
|
(let
|
|
((wd (first bd)))
|
|
(let
|
|
((occ (+ (* (+ week-days wd) 1440) tod)))
|
|
(let
|
|
((generates? (and (>= occ dtstart) (or (nil? until) (<= occ until)) (or (nil? count) (< n count)))))
|
|
(begin
|
|
(when
|
|
(and generates? (>= occ win-start) (<= occ win-end))
|
|
(append! acc (ev-occ id occ duration)))
|
|
(ev-weekly-bd-loop
|
|
id
|
|
week-days
|
|
tod
|
|
duration
|
|
(rest bd)
|
|
count
|
|
until
|
|
dtstart
|
|
win-start
|
|
win-end
|
|
acc
|
|
(if generates? (+ n 1) n)))))))))
|
|
|
|
(define
|
|
ev-weekly-loop
|
|
(fn
|
|
(id
|
|
week-days
|
|
tod
|
|
duration
|
|
week-step
|
|
bd
|
|
count
|
|
until
|
|
dtstart
|
|
win-start
|
|
win-end
|
|
acc
|
|
n)
|
|
(let
|
|
((week-start-dt (* week-days 1440)))
|
|
(cond
|
|
((> week-start-dt win-end) acc)
|
|
((and (not (nil? count)) (>= n count)) acc)
|
|
(else
|
|
(let
|
|
((n2 (ev-weekly-bd-loop id week-days tod duration bd count until dtstart win-start win-end acc n)))
|
|
(ev-weekly-loop
|
|
id
|
|
(+ week-days week-step)
|
|
tod
|
|
duration
|
|
week-step
|
|
bd
|
|
count
|
|
until
|
|
dtstart
|
|
win-start
|
|
win-end
|
|
acc
|
|
n2)))))))
|
|
|
|
;; ---- top-level expansion ----
|
|
;; Returns a list of occurrence dicts {:id :start :end} within the window.
|
|
(define
|
|
ev-expand
|
|
(fn
|
|
(event win-start win-end)
|
|
(let
|
|
((id (get event :id))
|
|
(dtstart (get event :dtstart))
|
|
(duration (get event :duration))
|
|
(rrule (get event :rrule)))
|
|
(if
|
|
(nil? rrule)
|
|
(if
|
|
(and (>= dtstart win-start) (<= dtstart win-end))
|
|
(list (ev-occ id dtstart duration))
|
|
(list))
|
|
(let
|
|
((freq (get rrule :freq))
|
|
(interval (ev-or (get rrule :interval) 1))
|
|
(count (get rrule :count))
|
|
(until (get rrule :until))
|
|
(byday (get rrule :byday))
|
|
(acc (list)))
|
|
(begin
|
|
(cond
|
|
((= freq :daily)
|
|
(ev-daily-loop
|
|
id
|
|
dtstart
|
|
duration
|
|
(* interval 1440)
|
|
count
|
|
until
|
|
dtstart
|
|
win-start
|
|
win-end
|
|
acc
|
|
0))
|
|
((= freq :weekly)
|
|
(ev-weekly-loop
|
|
id
|
|
(ev-week0-days dtstart)
|
|
(ev-dt-tod dtstart)
|
|
duration
|
|
(* interval 7)
|
|
(ev-byday-default byday dtstart)
|
|
count
|
|
until
|
|
dtstart
|
|
win-start
|
|
win-end
|
|
acc
|
|
0))
|
|
(else (error (str "ev-expand: unsupported freq: " freq))))
|
|
acc))))))
|
|
|
|
;; ---- multi-event expansion (sorted by start) ----
|
|
|
|
;; Insertion of one occurrence into a start-ascending list.
|
|
(define
|
|
ev-occ-insert
|
|
(fn
|
|
(o sorted)
|
|
(cond
|
|
((empty? sorted) (list o))
|
|
((<= (get o :start) (get (first sorted) :start)) (cons o sorted))
|
|
(else (cons (first sorted) (ev-occ-insert o (rest sorted)))))))
|
|
|
|
(define
|
|
ev-sort-occs
|
|
(fn (occs) (reduce (fn (acc o) (ev-occ-insert o acc)) (list) occs)))
|
|
|
|
;; Expand many events into one occurrence list, ascending by start.
|
|
(define
|
|
ev-expand-all
|
|
(fn
|
|
(events win-start win-end)
|
|
(let
|
|
((acc (list)))
|
|
(begin
|
|
(for-each
|
|
(fn
|
|
(ev)
|
|
(for-each
|
|
(fn (o) (append! acc o))
|
|
(ev-expand ev win-start win-end)))
|
|
events)
|
|
(ev-sort-occs acc)))))
|