///|
/// One effective occurrence after RRULE expansion, EXDATE removal, and
/// RECURRENCE-ID override merging.
pub(all) struct Occurrence {
uid : String
start : @model.IcalDateTime
end : @model.IcalDateTime?
summary : String?
recurrence_id : @model.IcalDateTime
} derive(Debug, Eq)
///|
pub extend Occurrence with @moonbitlang/core/debug.Debug::{to_repr}
///|
pub extend Occurrence with Eq::{not_equal, equal}
///|
/// Expand and merge one UID's VEVENT series. A cancelled override removes its
/// instance; a moved override replaces that instance; RANGE=THISANDFUTURE
/// shifts the selected instance and every later generated instance by the
/// same wall-clock delta.
pub fn expand_series(
events : Array[@model.Event],
limit? : Int = 200,
) -> Array[Occurrence] raise {
let master = find_master(events)
match master {
None => []
Some(base) =>
match base.dtstart {
None => []
Some(dtstart) => {
let generated = match base.rrule {
Some(text) => expand(parse_rule(text), dtstart, limit~)
None => [dtstart]
}
let uid = base.uid.unwrap_or("")
let index = SeriesIndex::build(events, base, uid)
let duration = match base.dtend {
Some(finish) =>
Some(finish.instant_seconds() - dtstart.instant_seconds())
None => None
}
let out : Array[Occurrence] = []
let mut future_index = 0
let mut active_future : @model.Event? = None
for slot in generated {
let key = slot.instant_seconds()
while future_index < index.future.length() &&
index.future[future_index].0 <= key {
active_future = Some(index.future[future_index].1)
future_index = future_index + 1
}
if index.exdates.contains(key) {
continue
}
match index.exact.get(key) {
Some(event) =>
if !cancelled(event) {
match event.dtstart {
Some(start) =>
out.push({
uid,
start,
end: event.dtend,
summary: event.summary,
recurrence_id: slot,
})
None => ()
}
}
None =>
match active_future {
Some(event) => {
if cancelled(event) {
continue
}
match (event.recurrence_id, event.dtstart) {
(Some(from), Some(to)) => {
let delta = to.instant_seconds() -
from.instant_seconds()
out.push({
uid,
start: slot.shift_seconds(delta),
end: shifted_end(slot, duration, delta),
summary: match event.summary {
Some(value) => Some(value)
None => base.summary
},
recurrence_id: slot,
})
}
_ => ()
}
}
None =>
out.push({
uid,
start: slot,
end: shifted_end(slot, duration, 0),
summary: base.summary,
recurrence_id: slot,
})
}
}
}
out.sort_by((a, b) => a.start.compare(b.start))
out
}
}
}
}
///|
fn find_master(events : Array[@model.Event]) -> @model.Event? {
for event in events {
if event.recurrence_id is None {
return Some(event)
}
}
None
}
///|
priv struct SeriesIndex {
exact : Map[Int64, @model.Event]
exdates : Map[Int64, Unit]
future : Array[(Int64, @model.Event)]
}
///|
fn SeriesIndex::build(
events : Array[@model.Event],
base : @model.Event,
uid : String,
) -> SeriesIndex {
let exact : Map[Int64, @model.Event] = Map([], capacity=events.length())
let future_by_slot : Map[Int64, @model.Event] = Map([])
for event in events {
if event.uid.unwrap_or("") == uid {
match event.recurrence_id {
Some(id) => {
let key = id.instant_seconds()
// The old linear lookup selected the first override at a duplicate
// RECURRENCE-ID, so preserve that deterministic wire-order rule.
if !exact.contains(key) {
exact[key] = event
}
if event.recurrence_range.map(r => r.to_upper()) ==
Some("THISANDFUTURE") &&
!future_by_slot.contains(key) {
future_by_slot[key] = event
}
}
None => ()
}
}
}
let future = future_by_slot.to_array()
future.sort_by((a, b) => a.0.compare(b.0))
let exdates : Map[Int64, Unit] = Map([], capacity=base.exdates.length())
for date in base.exdates {
exdates[date.instant_seconds()] = ()
}
{ exact, exdates, future, }
}
///|
fn cancelled(event : @model.Event) -> Bool {
event.status.map(s => s.to_upper()) == Some("CANCELLED")
}
///|
fn shifted_end(
slot : @model.IcalDateTime,
duration : Int64?,
delta : Int64,
) -> @model.IcalDateTime? raise {
match duration {
Some(seconds) => Some(slot.shift_seconds(seconds + delta))
None => None
}
}