///|
fn RRuleSet::snapshot(self : RRuleSet) -> RRuleSet {
{
rules: self.rules.copy(),
dates: self.dates.copy(),
exrules: self.exrules.copy(),
exdates: self.exdates.copy(),
cache: None,
}
}
///|
fn RRuleSet::invalidate_cache(self : RRuleSet) -> Unit {
self.cache = recurrence_cache(self.cache_enabled())
}
///|
fn merge_recurrence_sources(sources : Array[Iter[DateTime]]) -> Iter[DateTime] {
let heads : Array[DateTime?] = sources.map(_ => None)
let advance = sources.map(_ => true)
let mut finished = false
Iter::new(fn() {
if finished {
return None
}
let mut first : DateTime? = None
let mut instant : Int64? = None
for index in 0.. {
let key = rruleset_instant(value)
if instant is None || key < instant.unwrap() {
first = Some(value)
instant = Some(key)
}
}
None => ()
}
}
match instant {
None => {
finished = true
None
}
Some(key) => {
for index in 0.. Iter[DateTime] {
rruleset_unique_sorted(
values.map(value => { instant: rruleset_instant(value), value, }),
)
.iter()
.map(candidate => candidate.value)
}
///|
fn RRuleSet::stream(
self : RRuleSet,
lower : DateTime?,
inc : Bool,
) -> Iter[DateTime] {
let sources = self.rules.map(rule => {
match lower {
Some(value) => rule.iter_after(value, inc~)
None => rule.iter()
}
})
sources.push(
explicit_recurrence_dates(self.dates).filter(candidate => {
match lower {
Some(value) =>
if inc {
rruleset_instant(candidate) >= rruleset_instant(value)
} else {
rruleset_instant(candidate) > rruleset_instant(value)
}
None => true
}
}),
)
let inclusions = merge_recurrence_sources(sources)
let mut exclusions : Iter[DateTime]? = None
let mut excluded : DateTime? = None
Iter::new(fn() {
while true {
match inclusions.next() {
None => return None
Some(value) => {
if exclusions is None {
let sources = self.exrules.map(rule => {
rule.iter_after(value, inc=true)
})
sources.push(explicit_recurrence_dates(self.exdates))
exclusions = Some(merge_recurrence_sources(sources))
excluded = exclusions.unwrap().next()
}
let instant = rruleset_instant(value)
while excluded is Some(previous) &&
rruleset_instant(previous) < instant {
excluded = exclusions.unwrap().next()
}
if excluded is Some(previous) && rruleset_instant(previous) == instant {
continue
}
return Some(value)
}
}
}
None
})
}
///|
/// Return whether this set caches a shared occurrence prefix.
pub fn RRuleSet::cache_enabled(self : RRuleSet) -> Bool {
self.cache is Some(_)
}
///|
/// Lazily merge inclusion sources, deduplicate exact UTC instants, and apply
/// exclusions. Each reader snapshots the set's sources when created. Adding a
/// source invalidates future cached queries; existing readers keep their snapshot.
/// No eager array cap is imposed on this native Iter.
///
/// ```mbt check
/// test {
/// let set = @date.rruleset(cache=true)
/// set.rrule(@date.rrule(@date.Daily, @date.datetime(2024, 1, 1), count=3))
/// set.exdate(@date.datetime(2024, 1, 2))
/// assert_eq(set.iter().to_array().map(value => value.day()), [1, 3])
/// }
/// ```
pub fn RRuleSet::iter(self : RRuleSet) -> Iter[DateTime] {
let snapshot = self.snapshot()
match self.cache {
Some(cache) => cache.iterator(() => snapshot.stream(None, false))
None => snapshot.stream(None, false)
}
}
///|
/// Lazily yield set occurrences after `value`. Nonpositive count gives an
/// empty iterator; omitting count consumes through the sources' natural end.
pub fn RRuleSet::iter_after(
self : RRuleSet,
value : DateTime,
count? : Int32,
inc~ : Bool = false,
) -> Iter[DateTime] {
if count is Some(limit) && limit <= 0 {
return Iter::empty()
}
let target = rruleset_instant(value)
let values = match self.cache {
Some(_) =>
self
.iter()
.filter(candidate => {
if inc {
rruleset_instant(candidate) >= target
} else {
rruleset_instant(candidate) > target
}
})
None => self.snapshot().stream(Some(value), inc)
}
match count {
Some(limit) => values.take(limit)
None => values
}
}