events: iCalendar import + occurrence-exact round-trip + 19 tests
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 38s
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 38s
ical.sx parses VEVENT/VCALENDAR text back into events (ev/ical-lines->event, ev/parse-vcalendar): DTSTART/DURATION/RRULE (ordinal BYDAY, BYMONTHDAY, UNTIL/ COUNT/INTERVAL) + EXDATE/RDATE. Round-trip is occurrence-exact — export->import expands to the identical occurrence set. Completes bidirectional interop. 360/360 green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -189,3 +189,153 @@
|
||||
|
||||
;; Render content lines to the on-the-wire iCalendar text (CRLF-separated).
|
||||
(define ev/ical-render (fn (lines) (ev-ical-join lines "\r\n")))
|
||||
|
||||
;; ---- import (parse VEVENT/VCALENDAR back into events) ----
|
||||
;; Inverse of the export above: parse iCalendar content lines into event dicts
|
||||
;; (ev-event-full shape). Capacity is not an iCal property, so imported events
|
||||
;; default to capacity 0 — set it after import if needed.
|
||||
|
||||
;; "20260601T180000Z" -> UTC epoch-minutes.
|
||||
(define
|
||||
ev-ical-parse-dt
|
||||
(fn
|
||||
(s)
|
||||
(ev-dt
|
||||
(string->number (substring s 0 4))
|
||||
(string->number (substring s 4 6))
|
||||
(string->number (substring s 6 8))
|
||||
(string->number (substring s 9 11))
|
||||
(string->number (substring s 11 13)))))
|
||||
|
||||
;; "30M" / "" -> minutes.
|
||||
(define
|
||||
ev-ical-parse-min
|
||||
(fn
|
||||
(s)
|
||||
(if (= (string-length s) 0) 0 (string->number (first (split s "M"))))))
|
||||
|
||||
;; "PT1H30M" / "PT1H" / "PT30M" -> minutes.
|
||||
(define
|
||||
ev-ical-parse-duration
|
||||
(fn
|
||||
(s)
|
||||
(let
|
||||
((body (substring s 2 (string-length s))))
|
||||
(let
|
||||
((hparts (split body "H")))
|
||||
(if
|
||||
(> (len hparts) 1)
|
||||
(+ (* 60 (string->number (first hparts))) (ev-ical-parse-min (first (rest hparts))))
|
||||
(ev-ical-parse-min body))))))
|
||||
|
||||
(define
|
||||
ev-ical-wd->num
|
||||
(fn
|
||||
(tok)
|
||||
(cond
|
||||
((= tok "MO") 0)
|
||||
((= tok "TU") 1)
|
||||
((= tok "WE") 2)
|
||||
((= tok "TH") 3)
|
||||
((= tok "FR") 4)
|
||||
((= tok "SA") 5)
|
||||
((= tok "SU") 6)
|
||||
(else 0))))
|
||||
|
||||
;; "MO" -> 0 ; "2TU" -> {:ord 2 :wd 1} ; "-1FR" -> {:ord -1 :wd 4}
|
||||
(define
|
||||
ev-ical-parse-byday-token
|
||||
(fn
|
||||
(tok)
|
||||
(let
|
||||
((n (string-length tok)))
|
||||
(if
|
||||
(= n 2)
|
||||
(ev-ical-wd->num tok)
|
||||
{:ord (string->number (substring tok 0 (- n 2)))
|
||||
:wd (ev-ical-wd->num (substring tok (- n 2) n))}))))
|
||||
|
||||
(define
|
||||
ev-ical-parse-freq
|
||||
(fn
|
||||
(v)
|
||||
(cond
|
||||
((= v "DAILY") :daily)
|
||||
((= v "WEEKLY") :weekly)
|
||||
((= v "MONTHLY") :monthly)
|
||||
(else :daily))))
|
||||
|
||||
;; "FREQ=WEEKLY;INTERVAL=2;UNTIL=...;BYDAY=MO,WE" -> rrule dict.
|
||||
(define
|
||||
ev-ical-parse-rrule
|
||||
(fn
|
||||
(val)
|
||||
(let
|
||||
((rr {}))
|
||||
(begin
|
||||
(for-each
|
||||
(fn
|
||||
(p)
|
||||
(let
|
||||
((kv (split p "=")))
|
||||
(let
|
||||
((k (first kv)) (v (first (rest kv))))
|
||||
(cond
|
||||
((= k "FREQ") (dict-set! rr :freq (ev-ical-parse-freq v)))
|
||||
((= k "INTERVAL") (dict-set! rr :interval (string->number v)))
|
||||
((= k "COUNT") (dict-set! rr :count (string->number v)))
|
||||
((= k "UNTIL") (dict-set! rr :until (ev-ical-parse-dt v)))
|
||||
((= k "BYDAY") (dict-set! rr :byday (map ev-ical-parse-byday-token (split v ","))))
|
||||
((= k "BYMONTHDAY") (dict-set! rr :bymonthday (map string->number (split v ","))))
|
||||
(else nil)))))
|
||||
(split val ";"))
|
||||
rr))))
|
||||
|
||||
;; Parse a VEVENT's content lines into an event dict.
|
||||
(define
|
||||
ev/ical-lines->event
|
||||
(fn
|
||||
(lines)
|
||||
(let
|
||||
((ev {:capacity 0 :rrule nil}) (exd (list)) (rd (list)))
|
||||
(begin
|
||||
(for-each
|
||||
(fn
|
||||
(line)
|
||||
(let
|
||||
((kv (split line ":")))
|
||||
(when
|
||||
(> (len kv) 1)
|
||||
(let
|
||||
((k (first kv)) (v (first (rest kv))))
|
||||
(cond
|
||||
((= k "UID") (dict-set! ev :id (string->symbol v)))
|
||||
((= k "DTSTART") (dict-set! ev :dtstart (ev-ical-parse-dt v)))
|
||||
((= k "DURATION") (dict-set! ev :duration (ev-ical-parse-duration v)))
|
||||
((= k "RRULE") (dict-set! ev :rrule (ev-ical-parse-rrule v)))
|
||||
((= k "EXDATE") (set! exd (map ev-ical-parse-dt (split v ","))))
|
||||
((= k "RDATE") (set! rd (map ev-ical-parse-dt (split v ","))))
|
||||
(else nil))))))
|
||||
lines)
|
||||
(dict-set! ev :exdate exd)
|
||||
(dict-set! ev :rdate rd)
|
||||
ev))))
|
||||
|
||||
;; Split a VCALENDAR line list into per-VEVENT line groups.
|
||||
(define
|
||||
ev-ical-group-vevents
|
||||
(fn
|
||||
(lines cur in acc)
|
||||
(cond
|
||||
((empty? lines) acc)
|
||||
((= (first lines) "BEGIN:VEVENT") (ev-ical-group-vevents (rest lines) (list) true acc))
|
||||
((= (first lines) "END:VEVENT") (ev-ical-group-vevents (rest lines) (list) false (append acc (list cur))))
|
||||
(in (ev-ical-group-vevents (rest lines) (append cur (list (first lines))) true acc))
|
||||
(else (ev-ical-group-vevents (rest lines) cur false acc)))))
|
||||
|
||||
;; Parse a VCALENDAR line list into a list of events.
|
||||
(define
|
||||
ev/parse-vcalendar
|
||||
(fn
|
||||
(lines)
|
||||
(map ev/ical-lines->event (ev-ical-group-vevents lines (list) false (list)))))
|
||||
|
||||
Reference in New Issue
Block a user