pimdav: recurrence expansion crate for CalDAV

- New sans-IO workspace crate; calcard parses, `rrule` iterates wall time
- Expansion: DTSTART + RRULE + RDATE - EXRULE - EXDATE, overrides by
  RECURRENCE-ID only, THISANDFUTURE, DST gap/fold per RFC 5545, window
- Zones: exact IANA TZID first, guessed names checked against VTIMEZONE
  rules, custom VTIMEZONE evaluation, saturating date arithmetic
- Corpus test against recurring-ical-events over calcard's 998 fixtures;
  every deviation listed with its reason

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
AuthorKonata <konata@posteo.jp>
Date
Commitd9de2fc5a94dc7e4f03c7e967d4260736eecae38
Parentd4bdfb0
12 files changed, 2692 insertions(+), 3 deletions(-)
▾MCargo.lock
@@ -8,6 +8,19 @@ version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
[[package]]
name = "ahash"
version = "0.8.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
dependencies = [
"cfg-if",
"getrandom 0.3.4",
"once_cell",
"version_check",
"zerocopy",
]
[[package]]
name = "aho-corasick"
version = "1.1.5"
@@ -383,6 +396,20 @@ version = "1.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
[[package]]
name = "calcard"
version = "0.3.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c601473ec15a875626bce73db1a1f0fc81e9c949ca25f1463b714947c0a70a1f"
dependencies = [
"ahash",
"chrono",
"chrono-tz",
"hashify",
"mail-builder",
"mail-parser",
]
[[package]]
name = "camino"
version = "1.2.6"
@@ -421,6 +448,16 @@ dependencies = [
"windows-link",
]
[[package]]
name = "chrono-tz"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6139a8597ed92cf816dfb33f5dd6cf0bb93a6adc938f11039f371bc5bcd26c3"
dependencies = [
"chrono",
"phf",
]
[[package]]
name = "clap"
version = "4.6.7"
@@ -1299,6 +1336,18 @@ dependencies = [
"foldhash",
]
[[package]]
name = "hashify"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd1246c0e5493286aeb2dde35b1f4eb9c4ce00e628641210a5e553fc001a1f26"
dependencies = [
"indexmap",
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]]
name = "hashlink"
version = "0.12.2"
@@ -1903,6 +1952,21 @@ version = "0.4.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
[[package]]
name = "mail-builder"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccbc27cb02a58a311a404769a22f5abf63fe3173b290d80be7c534f9c7023151"
[[package]]
name = "mail-parser"
version = "0.11.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ec00bda90c6e645a54506c630c2820cd6b1890cfd2b0a169b50f74b2b8c7c86"
dependencies = [
"hashify",
]
[[package]]
name = "manyhow"
version = "0.11.4"
@@ -2226,6 +2290,34 @@ dependencies = [
"getrandom 0.4.3",
]
[[package]]
name = "phf"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "913273894cec178f401a31ec4b656318d95473527be05c0752cc41cdc32be8b7"
dependencies = [
"phf_shared",
]
[[package]]
name = "phf_shared"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06005508882fb681fd97892ecff4b7fd0fee13ef1aa569f8695dae7ab9099981"
dependencies = [
"siphasher",
]
[[package]]
name = "pimdav"
version = "0.1.0"
dependencies = [
"calcard",
"chrono",
"chrono-tz",
"rrule",
]
[[package]]
name = "pin-project"
version = "1.1.13"
@@ -2527,6 +2619,19 @@ version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
[[package]]
name = "rrule"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "720acfb4980b9d8a6a430f6d7a11933e701ebbeba5eee39cc9d8c5f932aaff74"
dependencies = [
"chrono",
"chrono-tz",
"log",
"regex",
"thiserror 2.0.21",
]
[[package]]
name = "rsqlite-vfs"
version = "0.1.1"
@@ -2950,6 +3055,12 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
[[package]]
name = "siphasher"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33f4fe9184a62d842c9ef383018f3306d8ba224fd9d836f56d7288308847c256"
[[package]]
name = "slab"
version = "0.4.12"
▾MCargo.toml
@@ -1,6 +1,6 @@
[workspace]
resolver = "2"
members = ["api-types", "server", "web"]
members = ["api-types", "pimdav", "server", "web"]
[profile.dev]
debug = "line-tables-only"
▾Mjustfile
@@ -50,9 +50,9 @@ build-cm:
run: dev-data build
./target/release/filebrowser-ng --root {{dev-root}} --db {{dev-db}} --cache {{dev-cache}}
# Server test suite (unit + API integration tests).
# Server and pimdav test suites (unit + integration tests).
test:
cargo test -p server -p api-types
cargo test -p server -p api-types -p pimdav
# Lint: formatting check + clippy with warnings denied.
lint:
▾Apimdav/Cargo.toml
@@ -0,0 +1,16 @@
[package]
name = "pimdav"
version = "0.1.0"
edition = "2024"
license = "AGPL-3.0-or-later"
[dependencies]
# Parsing and serialization only. Its recurrence expansion drops or
# duplicates instances on common client data, so `expand` replaces it.
# Pinned: tests/corpus.rs reads the fixtures of exactly this version.
calcard = { version = "=0.3.14", default-features = false }
chrono = "0.4"
chrono-tz = "0.10"
# Wall-clock RRULE iteration only. Time zones, UNTIL, overrides, RDATE and
# EXDATE are handled in `expand`.
rrule = "0.14"
▾Apimdav/src/expand.rs
@@ -0,0 +1,629 @@
//! Recurrence expansion: DTSTART + RRULE + RDATE - EXRULE - EXDATE, with
//! RECURRENCE-ID overrides applied.
use std::collections::{BTreeMap, HashMap, HashSet};
use std::ops::Range;
use calcard::common::{CalendarScale, PartialDateTime};
use calcard::icalendar::{
ICalendar, ICalendarComponent, ICalendarComponentType, ICalendarDuration, ICalendarFrequency,
ICalendarParameterName, ICalendarPeriod, ICalendarProperty, ICalendarRecurrenceRule,
ICalendarSkip, ICalendarValue,
};
use chrono::{DateTime, NaiveDateTime, NaiveTime, TimeDelta, TimeZone, Utc, Weekday};
use crate::zone::{Zone, Zones, add, add_local};
/// Occurrences one rule may generate before expansion gives up.
// ponytail: a FREQ=MINUTELY rule that started years ago hits this. Skip ahead
// for simple rules if that matters.
const MAX_OCCURRENCES: usize = 1_000_000;
#[derive(Debug, Clone, PartialEq)]
pub struct Instance {
pub start: DateTime<Utc>,
pub end: DateTime<Utc>,
/// The original start. `None` if the component does not recur.
pub recurrence_id: Option<DateTime<Utc>>,
/// Index into `ICalendar::components` of the master or override that
/// describes this instance.
pub component: usize,
}
#[derive(Debug, Default)]
pub struct Expansion {
/// Sorted by start.
pub instances: Vec<Instance>,
/// A rule reached `MAX_OCCURRENCES`, so later instances are missing.
pub truncated: bool,
}
/// The instances of all VEVENT, VTODO and VJOURNAL components that overlap
/// `window`. A zero-length instance overlaps if it starts inside.
///
/// Components are grouped by type and UID into one master and its overrides.
/// Components without DTSTART are skipped. `floating` interprets values
/// without a zone.
pub fn expand(cal: &ICalendar, window: Range<DateTime<Utc>>, floating: Zone) -> Expansion {
let zones = Zones::new(cal, floating);
let mut out = Expansion::default();
let mut groups: HashMap<(&ICalendarComponentType, &str), Vec<usize>> = HashMap::new();
for (i, c) in cal.components.iter().enumerate() {
if !matches!(
c.component_type,
ICalendarComponentType::VEvent
| ICalendarComponentType::VTodo
| ICalendarComponentType::VJournal
) {
continue;
}
match c.uid() {
Some(uid) => groups.entry((&c.component_type, uid)).or_default().push(i),
None => expand_group(cal, &zones, &[i], &window, &mut out),
}
}
for group in groups.values() {
expand_group(cal, &zones, group, &window, &mut out);
}
out.instances.sort_by_key(|i| (i.start, i.component));
out
}
/// A date or date-time value and the zone it is in.
#[derive(Debug, Clone)]
struct Stamp {
local: NaiveDateTime,
zone: Zone,
date: bool,
/// A date-time without TZID or UTC offset.
floating: bool,
}
impl Stamp {
fn utc(&self) -> DateTime<Utc> {
self.zone.to_utc(self.local)
}
}
#[derive(Debug, Clone)]
enum Length {
Exact(TimeDelta),
/// Whole days count in wall-clock time, so a day can last 23 or 25 hours.
Nominal {
days: i64,
exact: TimeDelta,
},
}
impl Length {
fn end(&self, zone: &Zone, local: NaiveDateTime, utc: DateTime<Utc>) -> DateTime<Utc> {
match self {
Length::Exact(d) => add(utc, *d),
Length::Nominal { days, exact } => add(
zone.to_utc(add_local(local, TimeDelta::days(*days))),
*exact,
),
}
}
/// An upper bound, for widening the generation window.
fn max(&self) -> TimeDelta {
match self {
Length::Exact(d) => *d,
Length::Nominal { days, exact } => TimeDelta::days(days + 1) + *exact,
}
}
}
/// DTSTART and length of one component.
#[derive(Debug, Clone)]
struct Timing {
start: Stamp,
length: Length,
component: usize,
}
impl Timing {
fn of(cal: &ICalendar, zones: &Zones, component: usize) -> Option<Self> {
let c = &cal.components[component];
let start = prop_stamp(zones, c, &ICalendarProperty::Dtstart)?;
let end = match c.component_type {
ICalendarComponentType::VTodo => ICalendarProperty::Due,
_ => ICalendarProperty::Dtend,
};
let zero = TimeDelta::zero();
let length = if let Some(end) = prop_stamp(zones, c, &end) {
if start.date && end.date {
Length::Nominal {
days: (end.local - start.local).num_days().max(0),
exact: zero,
}
} else {
Length::Exact((end.utc() - start.utc()).max(zero))
}
} else if let Some(ICalendarValue::Duration(d)) = c
.property(&ICalendarProperty::Duration)
.and_then(|e| e.values.first())
{
nominal(d)
} else if start.date {
Length::Nominal {
days: 1,
exact: zero,
}
} else {
Length::Exact(zero)
};
Some(Timing {
start,
length,
component,
})
}
fn end_at(&self, local: NaiveDateTime, utc: DateTime<Utc>) -> DateTime<Utc> {
self.length.end(&self.start.zone, local, utc)
}
/// A RECURRENCE-ID, RDATE or EXDATE value. Dates and floating times are
/// read in the zone of the series: clients write them that way.
fn utc_of(&self, s: &Stamp) -> DateTime<Utc> {
if s.date || s.floating {
self.start.zone.to_utc(s.local)
} else {
s.utc()
}
}
/// The identity of a recurrence instance. An all-day series matches by
/// date alone, whatever zone the other value names.
fn key(&self, s: &Stamp) -> i64 {
if self.start.date {
day_key(s.local)
} else {
self.utc_of(s).timestamp()
}
}
fn member_key(&self, local: NaiveDateTime, utc: DateTime<Utc>) -> i64 {
if self.start.date {
day_key(local)
} else {
utc.timestamp()
}
}
}
struct Member {
local: NaiveDateTime,
utc: DateTime<Utc>,
/// From an RDATE period.
length: Option<Length>,
}
struct Override {
sequence: i64,
recurrence_id: DateTime<Utc>,
timing: Timing,
}
fn expand_group(
cal: &ICalendar,
zones: &Zones,
group: &[usize],
window: &Range<DateTime<Utc>>,
out: &mut Expansion,
) {
let comp = |i: usize| &cal.components[i];
let sequence = |i: usize| {
comp(i)
.property(&ICalendarProperty::Sequence)
.and_then(|e| e.values.first()?.as_integer())
.unwrap_or(0)
};
let (overrides, masters): (Vec<usize>, Vec<usize>) = group
.iter()
.partition(|&&i| comp(i).has_property(&ICalendarProperty::RecurrenceId));
// Several masters for one UID are invalid. Take the latest revision,
// or the first one on a tie.
let master = masters
.into_iter()
.rev()
.max_by_key(|&i| sequence(i))
.and_then(|i| Timing::of(cal, zones, i));
// Matched by RECURRENCE-ID only. SEQUENCE picks between two overrides of
// one instance and never decides whether an override applies.
let mut exact: HashMap<i64, Override> = HashMap::new();
let mut future: Vec<(i64, DateTime<Utc>, Timing)> = Vec::new();
for i in overrides {
let Some(e) = comp(i).property(&ICalendarProperty::RecurrenceId) else {
continue;
};
let (Some(timing), Some(rid)) = (
Timing::of(cal, zones, i),
e.values
.first()
.and_then(|v| stamp(zones, v.as_partial_date_time()?, e.tz_id())),
) else {
continue;
};
let (key, rid_utc) = match &master {
Some(m) => (m.key(&rid), m.utc_of(&rid)),
None => (rid.utc().timestamp(), rid.utc()),
};
if e.parameter(&ICalendarParameterName::Range).is_some() {
future.push((key, rid_utc, timing.clone()));
}
if exact.get(&key).is_none_or(|old| sequence(i) > old.sequence) {
let sequence = sequence(i);
exact.insert(
key,
Override {
sequence,
recurrence_id: rid_utc,
timing,
},
);
}
}
future.sort_by_key(|f| f.0);
let Some(m) = master else {
push_overrides(out, window, exact.values());
return;
};
let mc = comp(m.component);
let mz = &m.start.zone;
let rules: Vec<_> = mc
.properties(&ICalendarProperty::Rrule)
.filter_map(|e| rule(e.values.first()?))
.collect();
let mut set: BTreeMap<i64, Member> = BTreeMap::new();
let mut slack = m.length.max();
// DTSTART is an instance even when it does not match the RRULE, unless
// the series ended before it.
let ended = !rules.is_empty()
&& rules.iter().all(|r| {
r.until
.as_ref()
.and_then(|u| until_local(u, &m.start))
.is_some_and(|u| u < m.start.local)
});
if !ended {
let utc = m.start.utc();
set.insert(
m.member_key(m.start.local, utc),
Member {
local: m.start.local,
utc,
length: None,
},
);
}
for e in mc.properties(&ICalendarProperty::Rdate) {
for v in &e.values {
let (s, length) = match v {
ICalendarValue::PartialDateTime(p) => {
let Some(s) = stamp(zones, p, e.tz_id()) else {
continue;
};
(s, None)
}
ICalendarValue::Period(ICalendarPeriod::Range { start, end }) => {
let (Some(s), Some(end)) =
(stamp(zones, start, e.tz_id()), stamp(zones, end, e.tz_id()))
else {
continue;
};
let d = (end.utc() - s.utc()).max(TimeDelta::zero());
(s, Some(Length::Exact(d)))
}
ICalendarValue::Period(ICalendarPeriod::Duration { start, duration }) => {
let Some(s) = stamp(zones, start, e.tz_id()) else {
continue;
};
(s, Some(nominal(duration)))
}
_ => continue,
};
let utc = m.utc_of(&s);
let local = mz.to_local(utc);
if let Some(l) = &length {
slack = slack.max(l.max());
}
set.entry(m.key(&s))
.or_insert(Member { local, utc, length });
}
}
for (_, rid, t) in &future {
slack = slack.max((t.start.utc() - *rid).abs() + t.length.max());
}
let slack = slack + TimeDelta::days(1);
let (from, to) = (add(window.start, -slack), add(window.end, slack));
let to_local = mz.to_local(to);
let mut excluded = HashSet::new();
for e in mc.properties(&ICalendarProperty::Exrule) {
let Some(rule) = e.values.first().and_then(rule) else {
continue;
};
for local in occurrences_capped(rule, &m, to_local, out) {
excluded.insert(m.member_key(local, mz.to_utc(local)));
}
}
for rule in rules {
for local in occurrences_capped(rule, &m, to_local, out) {
let utc = mz.to_utc(local);
if utc >= from {
set.entry(m.member_key(local, utc)).or_insert(Member {
local,
utc,
length: None,
});
}
}
}
// A DATE EXDATE on a DATE-TIME series removes every instance on that day.
let mut excluded_days = HashSet::new();
for e in mc.properties(&ICalendarProperty::Exdate) {
for s in e
.values
.iter()
.filter_map(|v| stamp(zones, v.as_partial_date_time()?, e.tz_id()))
{
if s.date && !m.start.date {
excluded_days.insert(s.local.date());
} else {
excluded.insert(m.key(&s));
}
}
}
// An EXDATE removes an override of that instance too. An override whose
// RECURRENCE-ID matches no instance still shows, as in clients.
push_overrides(
out,
window,
exact.iter().filter_map(|(key, o)| {
let day = mz.to_local(o.recurrence_id).date();
(!excluded.contains(key) && !excluded_days.contains(&day)).then_some(o)
}),
);
let recurs =
mc.has_property(&ICalendarProperty::Rrule) || mc.has_property(&ICalendarProperty::Rdate);
for (key, member) in &set {
if member.utc < from
|| excluded.contains(key)
|| excluded_days.contains(&member.local.date())
|| exact.contains_key(key)
{
continue;
}
let recurrence_id = recurs.then_some(member.utc);
// The latest THISANDFUTURE override before this instance moves it by
// the same offset and gives it the override's length.
let instance = match future.iter().rev().find(|f| f.0 <= *key) {
Some((_, rid, t)) => {
let start = add(member.utc, t.start.utc() - *rid);
Instance {
start,
end: t.end_at(t.start.zone.to_local(start), start),
recurrence_id,
component: t.component,
}
}
None => Instance {
start: member.utc,
end: member.length.as_ref().map_or_else(
|| m.end_at(member.local, member.utc),
|l| l.end(mz, member.local, member.utc),
),
recurrence_id,
component: m.component,
},
};
push(out, window, instance);
}
}
fn push_overrides<'a>(
out: &mut Expansion,
window: &Range<DateTime<Utc>>,
overrides: impl Iterator<Item = &'a Override>,
) {
for o in overrides {
let t = &o.timing;
let start = t.start.utc();
let end = t.end_at(t.start.local, start);
push(
out,
window,
Instance {
start,
end,
recurrence_id: Some(o.recurrence_id),
component: t.component,
},
);
}
}
fn occurrences_capped(
rule: &ICalendarRecurrenceRule,
m: &Timing,
to_local: NaiveDateTime,
out: &mut Expansion,
) -> Vec<NaiveDateTime> {
let until = match &rule.until {
Some(u) => match until_local(u, &m.start) {
Some(u) => Some(u),
None => return Vec::new(),
},
None => None,
};
let mut list: Vec<_> = occurrences(rule, m.start.local, until, to_local)
.take(MAX_OCCURRENCES + 1)
.collect();
if list.len() > MAX_OCCURRENCES {
list.pop();
out.truncated = true;
}
list
}
fn push(out: &mut Expansion, window: &Range<DateTime<Utc>>, i: Instance) {
let overlaps = if i.start == i.end {
window.contains(&i.start)
} else {
i.start < window.end && i.end > window.start
};
if overlaps {
out.instances.push(i);
}
}
/// UNTIL as wall-clock time in the zone of DTSTART. The rule iterates in wall
/// time, so a UTC UNTIL compared as-is cuts the last instance east of UTC.
fn until_local(u: &PartialDateTime, start: &Stamp) -> Option<NaiveDateTime> {
let dt = u.to_date_time()?;
Some(match dt.offset {
Some(o) => {
let utc = dt.date_time - TimeDelta::seconds(o.local_minus_utc().into());
start.zone.to_local(utc.and_utc())
}
// A DATE on a DATE-TIME series includes that whole day.
None if u.hour.is_none() && !start.date => dt.date_time.date().and_hms_opt(23, 59, 59)?,
// Floating, or a DATE on an all-day series: already wall time.
None => dt.date_time,
})
}
fn day_key(local: NaiveDateTime) -> i64 {
local.date().and_time(NaiveTime::MIN).and_utc().timestamp()
}
fn nominal(d: &ICalendarDuration) -> Length {
if d.neg {
return Length::Exact(TimeDelta::zero());
}
Length::Nominal {
days: i64::from(d.weeks) * 7 + i64::from(d.days),
exact: TimeDelta::seconds(
i64::from(d.hours) * 3600 + i64::from(d.minutes) * 60 + i64::from(d.seconds),
),
}
}
fn stamp(zones: &Zones, v: &PartialDateTime, tzid: Option<&str>) -> Option<Stamp> {
let dt = v.to_date_time()?;
let date = v.hour.is_none();
let zone = match dt.offset {
Some(o) if o.local_minus_utc() == 0 => Zone::Utc,
Some(o) => Zone::Fixed(o.local_minus_utc()),
None if date => zones.floating().clone(),
None => zones.get(tzid),
};
Some(Stamp {
local: dt.date_time,
zone,
date,
floating: !date && dt.offset.is_none() && tzid.is_none(),
})
}
fn prop_stamp(zones: &Zones, c: &ICalendarComponent, prop: &ICalendarProperty) -> Option<Stamp> {
let e = c.property(prop)?;
stamp(zones, e.values.first()?.as_partial_date_time()?, e.tz_id())
}
pub(crate) fn rule(v: &ICalendarValue) -> Option<&ICalendarRecurrenceRule> {
match v {
ICalendarValue::RecurrenceRule(r) => Some(r),
_ => None,
}
}
/// Wall-clock occurrences of `rule` from `start` up to `end`. An invalid or
/// unsupported rule yields none.
pub(crate) fn occurrences(
rule: &ICalendarRecurrenceRule,
start: NaiveDateTime,
until: Option<NaiveDateTime>,
end: NaiveDateTime,
) -> impl Iterator<Item = NaiveDateTime> {
let wall = |t: NaiveDateTime| rrule::Tz::UTC.from_utc_datetime(&t);
let freq = match rule.freq {
ICalendarFrequency::Yearly => rrule::Frequency::Yearly,
ICalendarFrequency::Monthly => rrule::Frequency::Monthly,
ICalendarFrequency::Weekly => rrule::Frequency::Weekly,
ICalendarFrequency::Daily => rrule::Frequency::Daily,
ICalendarFrequency::Hourly => rrule::Frequency::Hourly,
ICalendarFrequency::Minutely => rrule::Frequency::Minutely,
ICalendarFrequency::Secondly => rrule::Frequency::Secondly,
};
// Other calendar scales and leap months (RFC 7529) are unsupported. RFC
// 7529 allows treating such a rule as absent.
let months: Option<Vec<chrono::Month>> = rule
.bymonth
.iter()
.map(|m| (!m.is_leap()).then(|| chrono::Month::try_from(m.month()).ok())?)
.collect();
let valid = months.is_some()
&& rule
.rscale
.as_ref()
.is_none_or(|s| *s == CalendarScale::Gregorian)
&& rule.skip.is_none_or(|s| s == ICalendarSkip::Omit);
// rrule intersects plain and numbered weekdays, RFC 5545 unites them.
// Numbering every occurrence of the plain ones gives the union.
let mixed = rule.byday.iter().any(|d| d.ordwk.is_some())
&& rule.byday.iter().any(|d| d.ordwk.is_none());
let max_nth = if rule.freq == ICalendarFrequency::Monthly || !rule.bymonth.is_empty() {
5
} else {
53
};
let weekdays = rule
.byday
.iter()
.flat_map(|d| {
let wd = d.weekday.into();
match d.ordwk {
Some(n) => vec![rrule::NWeekday::Nth(n, wd)],
None if mixed => (1..=max_nth).map(|n| rrule::NWeekday::Nth(n, wd)).collect(),
None => vec![rrule::NWeekday::Every(wd)],
}
})
.collect();
let mut r = rrule::RRule::new(freq)
.interval(rule.interval.unwrap_or(1).max(1))
.week_start(rule.wkst.map_or(Weekday::Mon, Into::into))
.by_set_pos(rule.bysetpos.clone())
.by_month(&months.unwrap_or_default())
.by_month_day(rule.bymonthday.clone())
.by_year_day(rule.byyearday.clone())
.by_week_no(rule.byweekno.clone())
.by_weekday(weekdays)
.by_hour(rule.byhour.clone())
.by_minute(rule.byminute.clone())
.by_second(rule.bysecond.clone());
// COUNT and UNTIL together are invalid. UNTIL is the safer bound.
if let Some(n) = rule.count.filter(|_| until.is_none()) {
r = r.count(n);
}
if let Some(u) = until {
r = r.until(wall(u));
}
r.build(wall(start))
.ok()
.filter(|_| valid)
.map(|set| (&set).into_iter())
.into_iter()
.flatten()
.map(|t| t.naive_utc())
.take_while(move |t| *t <= end)
}
▾Apimdav/src/lib.rs
@@ -0,0 +1,7 @@
//! CalDAV and CardDAV logic without I/O: the caller loads and stores data,
//! this crate computes on it.
pub mod expand;
pub mod zone;
pub use calcard;
▾Apimdav/src/zone.rs
@@ -0,0 +1,286 @@
//! Time zones: resolving TZIDs, evaluating VTIMEZONE rules, and converting
//! wall-clock time to UTC.
use std::collections::HashMap;
use std::str::FromStr;
use std::sync::Arc;
use calcard::common::timezone::Tz as CalTz;
use calcard::icalendar::{
ICalendar, ICalendarComponent, ICalendarComponentType, ICalendarProperty,
};
use chrono::{DateTime, NaiveDate, NaiveDateTime, Offset, TimeDelta, TimeZone, Utc};
const DAY: i64 = 86_400;
/// Maximum onsets per VTIMEZONE observance.
const MAX_ONSETS: usize = 5_000;
/// A zone in which wall-clock values are interpreted.
#[derive(Debug, Clone)]
pub enum Zone {
Utc,
/// Seconds east of UTC.
Fixed(i32),
Iana(chrono_tz::Tz),
/// A VTIMEZONE whose name maps to no IANA zone.
Custom(Arc<Transitions>),
}
/// The UTC offset changes of a VTIMEZONE.
#[derive(Debug)]
pub struct Transitions {
initial: i32,
/// `(unix seconds, offset from then on)`, sorted.
list: Vec<(i64, i32)>,
}
impl Zone {
/// Seconds east of UTC at the instant `utc` (unix seconds).
pub fn offset_at(&self, utc: i64) -> i32 {
match self {
Zone::Utc => 0,
Zone::Fixed(o) => *o,
Zone::Iana(tz) => DateTime::from_timestamp(utc, 0).map_or(0, |t| {
tz.offset_from_utc_datetime(&t.naive_utc())
.fix()
.local_minus_utc()
}),
Zone::Custom(t) => match t.list.partition_point(|&(at, _)| at <= utc) {
0 => t.initial,
i => t.list[i - 1].1,
},
}
}
/// A skipped wall time takes the offset before the gap, a repeated one its
/// first occurrence (RFC 5545, 3.3.5).
pub fn to_utc(&self, local: NaiveDateTime) -> DateTime<Utc> {
let l = local.and_utc().timestamp();
// ponytail: assumes at most one offset change within a day of `local`.
let (before, after) = (self.offset_at(l - DAY), self.offset_at(l + DAY));
let (u1, u2) = (l - i64::from(before), l - i64::from(after));
let u = match (self.offset_at(u1) == before, self.offset_at(u2) == after) {
(true, true) => u1.min(u2),
(false, true) => u2,
_ => u1,
};
DateTime::from_timestamp(u, 0).unwrap_or(if u < 0 {
DateTime::<Utc>::MIN_UTC
} else {
DateTime::<Utc>::MAX_UTC
})
}
pub fn to_local(&self, utc: DateTime<Utc>) -> NaiveDateTime {
let offset = TimeDelta::seconds(self.offset_at(utc.timestamp()).into());
add_local(utc.naive_utc(), offset)
}
}
/// Saturating, since durations and time ranges come from clients.
pub(crate) fn add(t: DateTime<Utc>, d: TimeDelta) -> DateTime<Utc> {
t.checked_add_signed(d).unwrap_or(if d < TimeDelta::zero() {
DateTime::<Utc>::MIN_UTC
} else {
DateTime::<Utc>::MAX_UTC
})
}
pub(crate) fn add_local(t: NaiveDateTime, d: TimeDelta) -> NaiveDateTime {
t.checked_add_signed(d).unwrap_or(if d < TimeDelta::zero() {
NaiveDateTime::MIN
} else {
NaiveDateTime::MAX
})
}
/// The zones one iCalendar object refers to.
pub struct Zones {
by_tzid: HashMap<String, Zone>,
floating: Zone,
}
impl Zones {
/// `floating` interprets values without a zone, dates included.
pub fn new(cal: &ICalendar, floating: Zone) -> Self {
let by_tzid = cal
.components
.iter()
.filter(|c| c.component_type == ICalendarComponentType::VTimezone)
.filter_map(|c| {
let id = text(c, &ICalendarProperty::Tzid)?;
Some((id.to_string(), vtimezone(cal, c, id)?))
});
let mut map = HashMap::new();
// A repeated TZID is invalid. The first definition wins.
for (id, zone) in by_tzid {
map.entry(id).or_insert(zone);
}
Zones {
by_tzid: map,
floating,
}
}
pub fn floating(&self) -> &Zone {
&self.floating
}
/// The zone a TZID parameter names. Without one, or for an unknown name
/// without a VTIMEZONE, this is the floating zone.
pub fn get(&self, tzid: Option<&str>) -> Zone {
let Some(id) = tzid else {
return self.floating.clone();
};
self.by_tzid
.get(id)
.cloned()
.or_else(|| by_name(id))
.or_else(|| fixed_guess(id))
.unwrap_or_else(|| self.floating.clone())
}
}
/// An exact IANA name wins over the VTIMEZONE rules: clients send stale
/// rules, and the user means the place. A zone guessed from another name must
/// agree with the rules.
fn vtimezone(cal: &ICalendar, tz: &ICalendarComponent, id: &str) -> Option<Zone> {
if let Ok(tz) = chrono_tz::Tz::from_str(id.trim()) {
return Some(Zone::Iana(tz));
}
let guess = by_name(id)
.or_else(|| other_text(tz, "X-LIC-LOCATION").and_then(by_name))
.or_else(
|| match CalTz::from_ms_cdo_zone_id(other_text(tz, "X-MICROSOFT-CDO-TZID")?)? {
CalTz::Tz(tz) => Some(Zone::Iana(tz)),
_ => None,
},
);
match (
guess,
transitions(cal, tz).map(|t| Zone::Custom(Arc::new(t))),
) {
(Some(guess), Some(rules)) if agree(&guess, &rules) => Some(guess),
(_, Some(rules)) => Some(rules),
(Some(guess), None) => Some(guess),
(None, None) => fixed_guess(id),
}
}
/// Same offsets in mid-January and mid-July of recent years.
fn agree(a: &Zone, b: &Zone) -> bool {
(2020..=2022)
.flat_map(|y| [1, 7].map(|m| NaiveDate::from_ymd_opt(y, m, 15)))
.flatten()
.map(|d| d.and_time(Default::default()).and_utc().timestamp())
.all(|t| a.offset_at(t) == b.offset_at(t))
}
fn by_name(name: &str) -> Option<Zone> {
let name = name.trim();
// Vendor-prefixed IDs such as `/mozilla.org/20050126_1/Europe/Berlin`.
std::iter::once(name)
.chain(name.match_indices('/').map(|(i, _)| &name[i + 1..]))
.find_map(|n| chrono_tz::Tz::from_str(n).ok())
.or_else(|| match CalTz::from_str(name) {
Ok(CalTz::Tz(tz)) if !is_fixed(tz) => Some(tz),
_ => None,
})
.map(Zone::Iana)
}
/// A fixed offset read from a label such as `(UTC+02:00) Athens`. Ignores DST,
/// so it ranks below the VTIMEZONE rules.
fn fixed_guess(name: &str) -> Option<Zone> {
match CalTz::from_str(name) {
Ok(CalTz::Fixed(o)) => Some(Zone::Fixed(o.local_minus_utc())),
Ok(CalTz::Tz(tz)) if is_fixed(tz) => Some(Zone::Iana(tz)),
_ => None,
}
}
/// calcard turns offset labels into `Etc/GMT-2` and the like.
fn is_fixed(tz: chrono_tz::Tz) -> bool {
tz.name().starts_with("Etc/")
}
fn transitions(cal: &ICalendar, tz: &ICalendarComponent) -> Option<Transitions> {
// ponytail: rules past this year repeat the last offset.
let horizon = NaiveDate::from_ymd_opt(2200, 1, 1)?.and_time(Default::default());
let mut list = Vec::new();
for obs in tz
.component_ids
.iter()
.filter_map(|&id| cal.components.get(id as usize))
{
if !matches!(
obs.component_type,
ICalendarComponentType::Standard | ICalendarComponentType::Daylight
) {
continue;
}
let (Some(from), Some(to), Some(start)) = (
offset(obs, &ICalendarProperty::Tzoffsetfrom),
offset(obs, &ICalendarProperty::Tzoffsetto),
obs.property(&ICalendarProperty::Dtstart)
.and_then(|e| e.values.first()?.as_partial_date_time()?.to_date_time()),
) else {
continue;
};
let start = start.date_time;
let mut onsets = vec![start];
for rule in obs
.properties(&ICalendarProperty::Rrule)
.filter_map(|e| crate::expand::rule(e.values.first()?))
{
// UNTIL is UTC; onsets are wall-clock time before the change.
let until = rule.until.as_ref().and_then(|u| {
let u = u.to_date_time()?;
Some(match u.offset {
Some(o) => {
u.date_time + TimeDelta::seconds((from - o.local_minus_utc()).into())
}
None => u.date_time,
})
});
onsets.extend(crate::expand::occurrences(rule, start, until, horizon).take(MAX_ONSETS));
}
for e in obs.properties(&ICalendarProperty::Rdate) {
onsets.extend(
e.values
.iter()
.filter_map(|v| Some(v.as_partial_date_time()?.to_date_time()?.date_time)),
);
}
list.extend(
onsets
.into_iter()
.map(|l| (l.and_utc().timestamp() - i64::from(from), from, to)),
);
}
list.sort_unstable();
Some(Transitions {
initial: list.first()?.1,
list: list.into_iter().map(|(at, _, to)| (at, to)).collect(),
})
}
fn offset(c: &ICalendarComponent, prop: &ICalendarProperty) -> Option<i32> {
let v = c.property(prop)?.values.first()?.as_partial_date_time()?;
let secs = i32::from(v.tz_hour?) * 3600 + i32::from(v.tz_minute.unwrap_or(0)) * 60;
Some(if v.tz_minus { -secs } else { secs })
}
fn text<'a>(c: &'a ICalendarComponent, prop: &ICalendarProperty) -> Option<&'a str> {
c.property(prop)?.values.first()?.as_text()
}
fn other_text<'a>(c: &'a ICalendarComponent, name: &str) -> Option<&'a str> {
c.entries
.iter()
.find(|e| matches!(&e.name, ICalendarProperty::Other(n) if n.eq_ignore_ascii_case(name)))?
.values
.first()?
.as_text()
}
▾Apimdav/tests/corpus.rs
@@ -0,0 +1,137 @@
//! Differential test of `expand` over calcard's fixture corpus.
//!
//! `corpus/oracle.tsv` holds the instances recurring-ical-events computes
//! (see `corpus/oracle.py`). `corpus/deviations.tsv` overrides it where the
//! oracle fails or where this crate deliberately differs, with the reason.
//! On failure the test prints deviation lines for the files that differ.
//!
//! `PIMDAV_CORPUS_FULL=<path>` writes every start to that file, in the format
//! of `oracle.py --full`, for review.
use std::collections::HashMap;
use std::fmt::Write as _;
use std::path::PathBuf;
use chrono::{DateTime, Utc};
use pimdav::calcard::icalendar::ICalendarComponentType;
use pimdav::calcard::{Entry, Parser};
use pimdav::expand::expand;
use pimdav::zone::Zone;
/// Keep equal to the pinned calcard version in Cargo.toml.
const CALCARD: &str = "calcard-0.3.14";
fn fnv(data: &[u8]) -> String {
let mut h: u64 = 0xcbf29ce484222325;
for b in data {
h = (h ^ u64::from(*b)).wrapping_mul(0x100000001b3);
}
format!("{h:016x}")
}
fn fixtures() -> PathBuf {
let home = std::env::var_os("CARGO_HOME")
.map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from(std::env::var_os("HOME").unwrap()).join(".cargo"));
std::fs::read_dir(home.join("registry/src"))
.expect("no cargo registry")
.map(|e| e.unwrap().path().join(CALCARD).join("resources/ical"))
.find(|p| p.is_dir())
.expect("calcard sources not in the cargo registry")
}
/// `name -> (columns...)` of a tab-separated file.
fn table(name: &str) -> HashMap<String, Vec<String>> {
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests/corpus")
.join(name);
std::fs::read_to_string(path)
.unwrap()
.lines()
.filter(|l| !l.is_empty() && !l.starts_with('#'))
.map(|l| {
let mut cols = l.split('\t').map(str::to_string);
(cols.next().unwrap(), cols.collect())
})
.collect()
}
fn starts(input: &str) -> Vec<i64> {
let lo = DateTime::<Utc>::from_timestamp(0, 0).unwrap();
let hi = DateTime::<Utc>::from_timestamp(2_208_988_800, 0).unwrap();
let mut parser = Parser::new(input);
let mut out = Vec::new();
loop {
match parser.entry() {
Entry::ICalendar(cal) => {
let expansion = expand(&cal, lo..hi, Zone::Utc);
out.extend(
expansion
.instances
.iter()
.filter(|i| {
cal.components[i.component].component_type
== ICalendarComponentType::VEvent
&& i.start >= lo
&& i.start < hi
})
.map(|i| i.start.timestamp()),
);
}
Entry::Eof => break,
_ => {}
}
}
out.sort_unstable();
out
}
#[test]
fn corpus_matches_oracle() {
let dir = fixtures();
let oracle = table("oracle.tsv");
let deviations = table("deviations.tsv");
let mut names: Vec<_> = std::fs::read_dir(&dir)
.unwrap()
.map(|e| e.unwrap().file_name().into_string().unwrap())
.filter(|n| n.ends_with(".ics"))
.collect();
names.sort();
assert_eq!(
names.len(),
oracle.len(),
"fixture set changed, rerun oracle.py"
);
let mut full = String::new();
let mut failures = String::new();
for name in &names {
let raw = std::fs::read(dir.join(name)).unwrap();
let o = &oracle[name];
assert_eq!(o[0], fnv(&raw), "{name} changed, rerun oracle.py");
let input = String::from_utf8(raw.clone())
.unwrap_or_else(|_| raw.iter().map(|&b| b as char).collect());
let starts = starts(&input);
let list = starts
.iter()
.map(i64::to_string)
.collect::<Vec<_>>()
.join(",");
let (count, hash) = (starts.len(), fnv(list.as_bytes()));
writeln!(full, "{name}\t{}\tok\t{count}\t{list}", o[0]).unwrap();
let expected = match deviations.get(name) {
Some(d) => Some((d[0].as_str(), d[1].as_str())),
None if o[1] == "ok" => Some((o[2].as_str(), o[3].as_str())),
None => None,
};
if expected != Some((count.to_string().as_str(), hash.as_str())) {
let why = deviations.get(name).map_or("?", |d| d[2].as_str());
writeln!(failures, "{name}\t{count}\t{hash}\t{why}").unwrap();
}
}
if let Some(path) = std::env::var_os("PIMDAV_CORPUS_FULL") {
std::fs::write(path, full).unwrap();
}
assert!(failures.is_empty(), "differs from expectation:\n{failures}");
}
▾Apimdav/tests/corpus/deviations.tsv
@@ -0,0 +1,172 @@
# file count hash reason
000.ics 1608 7a27917255eea216 oracle system tzdata is newer than chrono-tz (BC permanent DST from 2026-11)
006.ics 90 622bf4547de03900 Windows TZID label without VTIMEZONE: we map it to IANA, oracle reads it as floating
008.ics 3 df802948ea602d1a oracle fails (BrokenCalendarProperty); agrees with calcard
009.ics 3 df802948ea602d1a oracle fails (BrokenCalendarProperty); agrees with calcard
013.ics 137 93b49bf846d98b61 oracle fails (BrokenCalendarProperty); agrees with calcard
014.ics 8 ae6153441313fbd3 oracle fails (BrokenCalendarProperty); agrees with calcard
015.ics 0 cbf29ce484222325 oracle fails (KeyError); agrees with calcard
017.ics 8 d5879dbedfe854e6 DTEND before DTSTART: we clamp the length to 0, oracle takes DTEND as start
021.ics 10 35313e431e0c562a Windows TZID label without VTIMEZONE: we map it to IANA, oracle reads it as floating
022.ics 1 65697e58d06e3dbb TZID on a DATE value: RFC 5545 forbids applying it, oracle applies it
027.ics 9 5851e9e4e8720ff2 offset label TZID without VTIMEZONE: we use the offset, oracle uses UTC
029.ics 0 cbf29ce484222325 oracle fails (KeyError); agrees with calcard
034.ics 159 b7a9ec452d882ace VALUE=DATE with a time part: we keep the date
035.ics 28 300c7f82568e2a9d TZID on a DATE value: RFC 5545 forbids applying it, oracle applies it
038.ics 1 cbc1675328dcef31 oracle fails (BrokenCalendarProperty); agrees with calcard
042.ics 1 22068e3caeb5bbf1 oracle fails (AttributeError); agrees with calcard
049.ics 0 cbf29ce484222325 oracle fails (ValueError); agrees with calcard
050.ics 0 cbf29ce484222325 oracle fails (ValueError); agrees with calcard
051.ics 1 b99149db7c366dc2 oracle fails (AttributeError); two DTSTART lines: we take the first (reviewed)
053.ics 0 cbf29ce484222325 oracle fails (KeyError); agrees with calcard
059.ics 2 7f3bbd632d25a9f9 oracle fails (BrokenCalendarProperty); agrees with calcard
061.ics 0 cbf29ce484222325 oracle fails (KeyError); agrees with calcard
062.ics 0 cbf29ce484222325 oracle fails (KeyError); agrees with calcard
064.ics 0 cbf29ce484222325 oracle fails (KeyError); agrees with calcard
082.ics 0 cbf29ce484222325 oracle fails (ValueError); agrees with calcard
083.ics 1 2de92f32be81d0cd oracle fails (KeyError); agrees with calcard
084.ics 2 5eb57ba89e47422e oracle fails (KeyError); agrees with calcard
086.ics 1 7d1059f6c35c2d76 oracle fails (ValueError); agrees with calcard
089.ics 14 fd3e864ade295a2d oracle fails (BadRuleStringFormat); UTC UNTIL equal to the last instance, custom VTIMEZONE (reviewed)
098.ics 0 cbf29ce484222325 oracle fails (KeyError); agrees with calcard
108.ics 2 f2c0b761a472ee51 oracle fails (KeyError); override without DTSTART and UID is skipped (reviewed)
110.ics 2 aacdfeabc6587a6c blank lines inside folded lines: oracle is lenient
111.ics 1 f0321b7e40bc2f34 oracle fails (AttributeError); two DTSTART lines: we take the first (reviewed)
112.ics 40 393c8a4703ea7f07 oracle fails (BrokenCalendarProperty); agrees with calcard
113.ics 0 cbf29ce484222325 oracle fails (ValueError); agrees with calcard
117.ics 4 764ebae68647fbdd oracle fails (BadRuleStringFormat); RSCALE, SKIP or leap month: rule dropped, DTSTART only (RFC 7529) (reviewed)
124.ics 0 cbf29ce484222325 oracle fails (KeyError); agrees with calcard
125.ics 0 cbf29ce484222325 oracle fails (KeyError); agrees with calcard
126.ics 0 cbf29ce484222325 oracle fails (KeyError); agrees with calcard
127.ics 0 cbf29ce484222325 oracle fails (KeyError); agrees with calcard
129.ics 0 cbf29ce484222325 oracle fails (KeyError); agrees with calcard
131.ics 0 cbf29ce484222325 oracle fails (KeyError); agrees with calcard
140.ics 0 cbf29ce484222325 oracle fails (KeyError); agrees with calcard
147.ics 0 cbf29ce484222325 oracle fails (KeyError); agrees with calcard
148.ics 42 4fbd867506f32c98 missing END lines: we parse leniently, oracle yields nothing
152.ics 0 cbf29ce484222325 oracle fails (ValueError); agrees with calcard
161.ics 20027 833644fea5dd717d oracle fails (BrokenCalendarProperty); override with higher SEQUENCE replaces the instance (reviewed)
162.ics 37 0f322c180f96334c TZID on a DATE value: RFC 5545 forbids applying it, oracle applies it
163.ics 1 51775e34bf91afe9 oracle fails (BadRuleStringFormat); unparseable UNTIL: rule dropped (reviewed)
167.ics 1938 0e0f79dd99b7c745 oracle fails (ValueError); agrees with calcard
198.ics 1 80ccc81a8af90b2b oracle fails (ValueError); agrees with calcard
210.ics 0 cbf29ce484222325 oracle fails (KeyError); agrees with calcard
217.ics 2139 8d434978b3cfcdf0 we apply EXRULE (RFC 2445), oracle ignores it
219.ics 302 5fe0b53ce6430ba9 TZID on a DATE value: RFC 5545 forbids applying it, oracle applies it
220.ics 4 b47c7c173cdf7129 oracle fails (KeyError); agrees with calcard
221.ics 49 780830fed4d2f99d TZID on a DATE value: RFC 5545 forbids applying it, oracle applies it
225.ics 0 cbf29ce484222325 oracle fails (KeyError); agrees with calcard
253.ics 6 51cdddceec67f42f oracle fails (BrokenCalendarProperty); overrides with another UID are orphans (reviewed)
266.ics 0 cbf29ce484222325 oracle fails (ValueError); agrees with calcard
267.ics 0 cbf29ce484222325 oracle fails (ValueError); agrees with calcard
270.ics 0 cbf29ce484222325 oracle fails (ValueError); agrees with calcard
272.ics 0 cbf29ce484222325 oracle fails (KeyError); agrees with calcard
298.ics 28 567209dcb2aa0274 wall times in a DST gap move to the next real instant and merge with it (RFC 5545 3.3.5)
301.ics 1 18726d5622f2988d oracle fails (KeyError); agrees with calcard
306.ics 1 1d6089d11d0a3fd2 oracle fails (BrokenCalendarProperty); huge DURATION saturates (reviewed)
310.ics 1 8bbebcd06413c5f7 TZID on a DATE value: RFC 5545 forbids applying it, oracle applies it
312.ics 1 df6850b329c4fc23 TZID on a DATE value: RFC 5545 forbids applying it, oracle applies it
322.ics 5 c3e2fc933adcf17b TZID on a DATE value: RFC 5545 forbids applying it, oracle applies it
323.ics 500 f4e7ea40c5b7d14a TZID on a DATE value: RFC 5545 forbids applying it, oracle applies it
325.ics 730 09386e54c65dfa72 TZID on a DATE value: RFC 5545 forbids applying it, oracle applies it
326.ics 12417 cd5550d301ccc9aa TZID on a DATE value: RFC 5545 forbids applying it, oracle applies it
328.ics 755 545bdffd58393f3c TZID on a DATE value: RFC 5545 forbids applying it, oracle applies it
330.ics 1 c252bfa7a8ac91a6 TZID on a DATE value: RFC 5545 forbids applying it, oracle applies it
331.ics 1 6cc1534a95ee2cfd floating DTSTART with zoned DTEND: oracle gives DTSTART the DTEND zone
401.ics 0 cbf29ce484222325 oracle fails (KeyError); agrees with calcard
403.ics 0 cbf29ce484222325 oracle fails (KeyError); agrees with calcard
407.ics 0 cbf29ce484222325 oracle fails (KeyError); agrees with calcard
408.ics 0 cbf29ce484222325 oracle fails (KeyError); agrees with calcard
468.ics 0 cbf29ce484222325 oracle fails (BrokenCalendarProperty); agrees with calcard
480.ics 1 a3e63eb3ff4476cc floating DTSTART with zoned DTEND: oracle gives DTSTART the DTEND zone
494.ics 289 85de61f244f9e485 oracle fails (BadRuleStringFormat); agrees with calcard
495.ics 2609 89eee3b7f0358a3a oracle fails (BadRuleStringFormat); mixed numbered and plain BYDAY, matches ical.js (reviewed)
497.ics 288 e54608f34424fa62 oracle fails (BadRuleStringFormat); override with higher SEQUENCE replaces the instance (reviewed)
498.ics 289 8736a96828e73352 oracle fails (BadRuleStringFormat); override with higher SEQUENCE replaces the instance (reviewed)
500.ics 1 f30cc8a60062c785 oracle fails (BadRuleStringFormat); RSCALE, SKIP or leap month: rule dropped, DTSTART only (RFC 7529) (reviewed)
504.ics 1 7410972609a7caa8 TZID on a UTC value: we use the Z, oracle uses the TZID
515.ics 1 02da48d67d9df29f oracle fails (ValueError); agrees with calcard
517.ics 5 12de74873aa0cfb7 oracle fails (BrokenCalendarProperty); agrees with calcard
523.ics 1 c1ba92fcbe57a1d5 TZID on a DATE value: RFC 5545 forbids applying it, oracle applies it
554.ics 1 b99149db7c366dc2 oracle fails (AttributeError); two DTSTART lines: we take the first (reviewed)
555.ics 0 cbf29ce484222325 oracle fails (BrokenCalendarProperty); agrees with calcard
560.ics 0 cbf29ce484222325 oracle fails (KeyError); agrees with calcard
561.ics 0 cbf29ce484222325 oracle fails (KeyError); agrees with calcard
564.ics 0 cbf29ce484222325 oracle fails (KeyError); agrees with calcard
568.ics 1 0ec50d31b5f14f4b oracle fails (BadRuleStringFormat); agrees with calcard
569.ics 0 cbf29ce484222325 oracle fails (BrokenCalendarProperty); agrees with calcard
570.ics 0 cbf29ce484222325 oracle fails (BrokenCalendarProperty); agrees with calcard
578.ics 0 cbf29ce484222325 oracle fails (KeyError); agrees with calcard
579.ics 0 cbf29ce484222325 oracle fails (KeyError); agrees with calcard
581.ics 1 37980ac3bde021ca RDATE PERIOD of DATE values: we drop it
582.ics 1 3c1ba6e24e6b99f9 RDATE PERIOD of DATE values: we drop it
588.ics 0 cbf29ce484222325 oracle fails (KeyError); agrees with calcard
590.ics 3 a59f40b8b00d9c66 oracle fails (TypeError); agrees with calcard
591.ics 0 cbf29ce484222325 oracle fails (KeyError); agrees with calcard
592.ics 0 cbf29ce484222325 oracle fails (KeyError); agrees with calcard
593.ics 0 cbf29ce484222325 oracle fails (KeyError); agrees with calcard
598.ics 0 cbf29ce484222325 oracle fails (KeyError); agrees with calcard
599.ics 0 cbf29ce484222325 oracle fails (KeyError); agrees with calcard
602.ics 0 cbf29ce484222325 oracle fails (KeyError); agrees with calcard
603.ics 0 cbf29ce484222325 oracle fails (KeyError); agrees with calcard
607.ics 1 fb52d207f4490cbd oracle fails (ValueError); agrees with calcard
608.ics 4947 b842896706966d46 oracle fails (ValueError); agrees with calcard
610.ics 1 1bf3586ab351b1ce DTEND before DTSTART: we clamp the length to 0, oracle takes DTEND as start
630.ics 1 ef9fd962f299e6b3 oracle fails (BrokenCalendarProperty); agrees with calcard
631.ics 1 cd46b4d9a7a4b941 oracle fails (KeyError); agrees with calcard
632.ics 11 b8ecf984b8ac6e89 TZID on a DATE value: RFC 5545 forbids applying it, oracle applies it
643.ics 639 80b0fdaafcc6b848 oracle fails (BrokenCalendarProperty); agrees with calcard
644.ics 639 7a0adfb99acac7d8 oracle fails (BrokenCalendarProperty); agrees with calcard
645.ics 710 a36a6620d45d3f3c oracle fails (BrokenCalendarProperty); DTSTART not matching RRULE counts as an instance (reviewed)
657.ics 2 94b7f172d05cd460 TZID on a UTC value: we use the Z, oracle uses the TZID
682.ics 1 d7e4905c8854fbfa oracle fails (ValueError); agrees with calcard
684.ics 2 98513a04bbdbb3a8 oracle fails (InvalidCalendar); agrees with calcard
689.ics 11658 602e0614375c688e oracle fails (KeyError); override carrying RRULE, custom VTIMEZONE, RDATE equal to DTSTART (reviewed)
690.ics 0 cbf29ce484222325 oracle fails (KeyError); agrees with calcard
692.ics 4 2bfa2ac075f3cd07 oracle fails (KeyError); agrees with calcard
693.ics 0 cbf29ce484222325 oracle fails (KeyError); agrees with calcard
695.ics 0 cbf29ce484222325 oracle fails (ValueError); agrees with calcard
696.ics 11658 602e0614375c688e oracle fails (KeyError); override carrying RRULE, custom VTIMEZONE, RDATE equal to DTSTART (reviewed)
698.ics 3 f8bd25199f328d3e oracle fails (BrokenCalendarProperty); agrees with calcard
699.ics 8 2aa1244be3352d58 oracle fails (KeyError); agrees with calcard
700.ics 0 cbf29ce484222325 oracle fails (ValueError); agrees with calcard
701.ics 0 cbf29ce484222325 oracle fails (ValueError); agrees with calcard
702.ics 1 51775e34bf91afe9 oracle fails (BadRuleStringFormat); agrees with calcard
710.ics 395 c165cc477c8026f9 oracle fails (BrokenCalendarProperty); two DTSTART lines: we take the first (reviewed)
712.ics 15 68cce3e686e9f36a UNTIL form differs from DTSTART: we read a DATE as the whole day and floating as wall time
734.ics 1066 02c5e6614bcc7294 oracle fails (BadRuleStringFormat); agrees with calcard
745.ics 1 bf188eee93f02fd5 DTEND before DTSTART: we clamp the length to 0, oracle takes DTEND as start
752.ics 1 74d781665f807cb5 DTEND before DTSTART: we clamp the length to 0, oracle takes DTEND as start
772.ics 3 fee1b84c6335dfe1 override with its own RRULE: we show it, oracle drops it
788.ics 194 9b53013aec9467bb UNTIL form differs from DTSTART: we read a DATE as the whole day and floating as wall time
826.ics 0 cbf29ce484222325 oracle fails (KeyError); agrees with calcard
827.ics 0 cbf29ce484222325 oracle fails (KeyError); agrees with calcard
851.ics 529 c5ac4fd42f33ce94 TZID on a DATE value: RFC 5545 forbids applying it, oracle applies it
921.ics 1 eaabc3803505ca24 TZID on a DATE value: RFC 5545 forbids applying it, oracle applies it
923.ics 9 4535d8168d054301 UNTIL form differs from DTSTART: we read a DATE as the whole day and floating as wall time
926.ics 0 cbf29ce484222325 oracle fails (ValueError); agrees with calcard
927.ics 0 cbf29ce484222325 oracle fails (ValueError); agrees with calcard
929.ics 0 cbf29ce484222325 oracle fails (ValueError); agrees with calcard
933.ics 0 cbf29ce484222325 oracle fails (ValueError); agrees with calcard
934.ics 0 cbf29ce484222325 oracle fails (ValueError); agrees with calcard
939.ics 0 cbf29ce484222325 oracle fails (ValueError); agrees with calcard
941.ics 0 cbf29ce484222325 oracle fails (ValueError); agrees with calcard
949.ics 0 cbf29ce484222325 oracle fails (ValueError); agrees with calcard
954.ics 0 cbf29ce484222325 oracle fails (ValueError); agrees with calcard
959.ics 0 cbf29ce484222325 oracle fails (ValueError); agrees with calcard
960.ics 0 cbf29ce484222325 oracle fails (ValueError); agrees with calcard
967.ics 0 cbf29ce484222325 oracle fails (ValueError); agrees with calcard
968.ics 0 cbf29ce484222325 oracle fails (ValueError); agrees with calcard
973.ics 0 cbf29ce484222325 oracle fails (ValueError); agrees with calcard
984.ics 0 cbf29ce484222325 oracle fails (ValueError); agrees with calcard
985.ics 1905 abb42e92aeec7b3b oracle fails (ValueError); DTSTART not matching RRULE counts as an instance (reviewed)
986.ics 229 dd84d31e797ff91d oracle fails (KeyError); UTC UNTIL equal to the last instance, custom VTIMEZONE (reviewed)
988.ics 48 a5b204b9ede56fe8 oracle fails (ValueError); VTIMEZONE first onset after the event: offset before it applies (reviewed)
989.ics 48 a5b204b9ede56fe8 oracle fails (ValueError); VTIMEZONE first onset after the event: offset before it applies (reviewed)
990.ics 48 a5b204b9ede56fe8 RFC 5545 example VTIMEZONEs: repeated TZID with a broken DAYLIGHT onset
991.ics 48 a5b204b9ede56fe8 oracle fails (ValueError); VTIMEZONE first onset after the event: offset before it applies (reviewed)
992.ics 3 c54c8efd55b13c89 oracle fails (BrokenCalendarProperty); agrees with calcard
993.ics 3 c54c8efd55b13c89 oracle fails (BrokenCalendarProperty); agrees with calcard
994.ics 1 fd208164eb8c65f8 oracle fails (AttributeError); override without DTSTART and UID is skipped (reviewed)
995.ics 0 cbf29ce484222325 oracle fails (ValueError); agrees with calcard
996.ics 0 cbf29ce484222325 oracle fails (ValueError); agrees with calcard
▾Apimdav/tests/corpus/oracle.py
@@ -0,0 +1,74 @@
# /// script
# dependencies = ["icalendar==7.3.0", "recurring-ical-events==3.8.2"]
# ///
"""Expected VEVENT instances for tests/corpus.rs, from recurring-ical-events.
uv run oracle.py <fixture dir> > oracle.tsv # compact, checked in
uv run oracle.py <fixture dir> --full > full.tsv # every start, for review
Conventions shared with corpus.rs: starts in [1970, 2040) as unix seconds,
floating times and dates in UTC, X-WR-TIMEZONE ignored.
"""
import datetime as dt
import os
import sys
from concurrent.futures import ProcessPoolExecutor
UTC = dt.timezone.utc
LO, HI = dt.datetime(1970, 1, 1, tzinfo=UTC), dt.datetime(2040, 1, 1, tzinfo=UTC)
def fnv(data: bytes) -> str:
h = 0xCBF29CE484222325
for b in data:
h = ((h ^ b) * 0x100000001B3) & 0xFFFFFFFFFFFFFFFF
return f"{h:016x}"
def expand(path):
# One process per file: icalendar caches TZIDs globally, so a VTIMEZONE
# from one fixture would leak into the next.
import icalendar
import recurring_ical_events
raw = open(path, "rb").read()
try:
txt = raw.decode("utf-8")
except UnicodeDecodeError:
txt = raw.decode("latin-1")
starts = []
try:
for cal in icalendar.Calendar.from_ical(txt, multiple=True):
cal.pop("X-WR-TIMEZONE", None)
for ev in recurring_ical_events.of(cal, components=["VEVENT"]).between(LO, HI):
s = ev.get("DTSTART")
if s is None:
continue
s = s.dt
if not isinstance(s, dt.datetime):
s = dt.datetime(s.year, s.month, s.day)
if s.tzinfo is None:
s = s.replace(tzinfo=UTC)
if LO <= s < HI:
starts.append(int(s.timestamp()))
status = "ok"
except Exception as e:
status = "error:" + type(e).__name__
starts.sort()
return fnv(raw), status, starts
def main():
d, full = sys.argv[1], "--full" in sys.argv
names = sorted(n for n in os.listdir(d) if n.endswith(".ics"))
with ProcessPoolExecutor(max_tasks_per_child=1) as pool:
for name, (file_hash, status, starts) in zip(
names, pool.map(expand, [os.path.join(d, n) for n in names])
):
text = ",".join(map(str, starts))
last = text if full else fnv(text.encode())
print(f"{name}\t{file_hash}\t{status}\t{len(starts)}\t{last}", flush=True)
if __name__ == "__main__":
main()
▾Apimdav/tests/corpus/oracle.tsv
@@ -0,0 +1,998 @@
000.ics 98713b0cf31da8b0 ok 1608 e7c93afa3a415cab
001.ics 557946486121fc0c ok 1 7c905f2def9e2762
002.ics bd698c848c7d9fb9 ok 1 91290548ee331c0a
003.ics 175ab09335ad4231 ok 2 463ba28337d5e4dd
004.ics bc5c37f877e21218 ok 1 0c1f9d0aa4bac819
005.ics 013548bfce136970 ok 1 701314559a03fa13
006.ics 7382eeef2cd68b90 ok 90 36cb9d930b7b0800
007.ics 510f4732a21ef60a ok 15072 cba7e3dd781d0b3c
008.ics 9997d25b6a4be839 error:BrokenCalendarProperty 0 cbf29ce484222325
009.ics 83109ff0118757b9 error:BrokenCalendarProperty 0 cbf29ce484222325
010.ics cfd4d18b7f093cc4 ok 34 f099e0741d217267
011.ics 8c4f29f0c9292843 ok 8 e31641a3d2cb74ee
012.ics e35467a44070fd7d ok 4 6c53db3e78deb5a4
013.ics 2cb6a058882e4cd0 error:BrokenCalendarProperty 0 cbf29ce484222325
014.ics acddad944a408fee error:BrokenCalendarProperty 0 cbf29ce484222325
015.ics f7b718a94a6c6cae error:KeyError 0 cbf29ce484222325
016.ics 18e73143266770f1 ok 0 cbf29ce484222325
017.ics 7f7bb2e6c810a0f9 ok 8 55fc0c2176da1064
018.ics db82b72d5bb3cbd3 ok 4 beea0237492f5726
019.ics 553dc428c5e0fe1f ok 1189 34eb5da74efbf17b
020.ics 757da876b8aa4bb0 ok 1 08b9517c4d25ffca
021.ics 7b7f2cc9463e42b4 ok 8 ccf5a419f043c36e
022.ics 2ed710cb843cc2e3 ok 1 30f5781d6b4dc9d2
023.ics a97e5866dfdc2b4d ok 5 277db23e523a0242
024.ics 1a9c9afeb3035bf9 ok 1 2d9b7fcbba24503e
025.ics 7e9bbccf7e61910a ok 3 3b32cab87fc9c752
026.ics 37bc2790ef85c790 ok 4 6c4be288416c82fa
027.ics 7a0d24f56c83eb07 ok 9 7f36b3c851e6b57c
028.ics bdbb3c127436da94 ok 1 096ebb46f10ed0cb
029.ics e01f6f9acbbf351e error:KeyError 0 cbf29ce484222325
030.ics 15cba856fc0b2850 ok 36 cf9e30ca012dc691
031.ics f43d333d413e3979 ok 26 b296cb8938b17963
032.ics fbc4039d5de49fd1 ok 47 2f976c4a701ff39a
033.ics dfec0299b3fb9622 ok 1 bb95f5f88ddcad40
034.ics e5b4765932b5f106 ok 159 11e1f7211c606fdb
035.ics aa677dbe8b26697b ok 28 388b5580e4222ac6
036.ics 46a2f8163123b2d9 ok 0 cbf29ce484222325
037.ics ed35b420290c4cde ok 1 2d9b7fcbba24503e
038.ics 24f118716657a344 error:BrokenCalendarProperty 0 cbf29ce484222325
039.ics 27647d1176e93857 ok 1 379d42b1fb2c9790
040.ics d846aa405e488775 ok 1003 9d198a97729809f5
041.ics 15e877b462dbc3ec ok 2 a4347fe53ddb5b04
042.ics 09ed794a6c3ac176 error:AttributeError 0 cbf29ce484222325
043.ics f619dc97431cacb1 ok 0 cbf29ce484222325
044.ics 9a94f6db9b30a2b5 ok 3 aa20573cb1ac7557
045.ics 07f2d36e0e1e9b3e ok 1 b44a889768787025
046.ics 3a366ec9ea5871f2 ok 2 bfc0b1d65ad55bab
047.ics 3238359e3b09e02f ok 2 4850c3bd8f3214fb
048.ics 184fec73b1ede14f ok 3 83539bbdb5558ab8
049.ics b18748972058037b error:ValueError 0 cbf29ce484222325
050.ics 100e42f167a20eda error:ValueError 0 cbf29ce484222325
051.ics da53cc66c1cf3e83 error:AttributeError 0 cbf29ce484222325
052.ics a0d71e881134bbf2 ok 0 cbf29ce484222325
053.ics a5cd2b97437a81be error:KeyError 0 cbf29ce484222325
054.ics cf34d42f7d258589 ok 97 3e03b9238fe82106
055.ics eae5cfefe3b3fb2c ok 2 7f3bbd632d25a9f9
056.ics dbacd7500f07e804 ok 3 a0fe29d3ebfe4471
057.ics 1add7455e2b5ba03 ok 26 9aca5ccb06d207f9
058.ics f3e99d2a8d948dc1 ok 1 9e001145ce04168e
059.ics be97662bc2186c60 error:BrokenCalendarProperty 0 cbf29ce484222325
060.ics d14e8b3ea1d2f643 ok 1 c242b8f9420e078f
061.ics fd20123b0ca7a717 error:KeyError 0 cbf29ce484222325
062.ics 37269c411859e47e error:KeyError 0 cbf29ce484222325
063.ics f6a5ca8cb87ab021 ok 18 9a577a950dab3b76
064.ics 420f90a53fef1600 error:KeyError 0 cbf29ce484222325
065.ics 01b27e2607e75d42 ok 1 4b8809d5b52d7076
066.ics 5a92709825c91e19 ok 0 cbf29ce484222325
067.ics 14952da279716801 ok 0 cbf29ce484222325
068.ics 1565392fcb47f87e ok 0 cbf29ce484222325
069.ics beaaed8b1f6e5975 ok 1 537ab3433ccfd523
070.ics 4c35a1b4bb49a82b ok 1 537ab3433ccfd523
071.ics d343eafba219567f ok 1 99f11f9247bd9863
072.ics 34ae540213f26b8f ok 1 864535d676957ba5
073.ics 25a7cb3ce18c511f ok 1 864535d676957ba5
074.ics 8bb8bb8d6021cdee ok 1 44fa8dffd7493bf5
075.ics 45eecb0b3a9cd5f8 ok 1 44fa8dffd7493bf5
076.ics b266843f0ae6c9db ok 1 44fa8dffd7493bf5
077.ics 99361de974416322 ok 1 4d531434b8e71813
078.ics 0420579b646c85f6 ok 1 4d531434b8e71813
079.ics a3351e52d5135024 ok 1 4d531434b8e71813
080.ics 7bdd2f29a925c3b6 ok 1 b99149db7c366dc2
081.ics 48c3038303a22dd6 ok 0 cbf29ce484222325
082.ics 13abf17adb4e7140 error:ValueError 0 cbf29ce484222325
083.ics df040e17081baef7 error:KeyError 0 cbf29ce484222325
084.ics 1c68e5ed57424a9e error:KeyError 0 cbf29ce484222325
085.ics de60ad9ec7427932 ok 3 6a7b9ad157dce283
086.ics 7e28149b9aca00e4 error:ValueError 0 cbf29ce484222325
087.ics 9d01295e7823d423 ok 1 5bd02425cdd2d6ce
088.ics 86a8c324f62015a6 ok 1 32ad5871fde0723c
089.ics daef753fe421e7bf error:BadRuleStringFormat 0 cbf29ce484222325
090.ics 8bfff5050142a31b ok 1 f0c2d695490b9b96
091.ics f0a2cbb7de57e208 ok 1 f0c2d695490b9b96
092.ics 7f1f71c29ff7f339 ok 0 cbf29ce484222325
093.ics c8ce4ef1ecae2939 ok 1 cc54b6bbab9f7029
094.ics 14b52fa8147e069e ok 0 cbf29ce484222325
095.ics 3377f797de7d4d64 ok 1 d5be9ec9d343bcf0
096.ics 2761defcdbffdb3d ok 0 cbf29ce484222325
097.ics dbd1c1cad5e2acf1 ok 0 cbf29ce484222325
098.ics 566ab9b2bb29ca9c error:KeyError 0 cbf29ce484222325
099.ics 6d737ffa55158757 ok 1 20e77b89810e665d
100.ics b5047c17c23bddc3 ok 1 cf170c1011195a79
101.ics b1a3db8b6fed9d5d ok 898 7ed4945a876436a2
102.ics 5cd265e4c842b88f ok 898 7ed4945a876436a2
103.ics 85bc646bfb5e13af ok 2 fd9de2163530ad31
104.ics 3efbc4e05b129517 ok 1 33cdabf9cbd233bf
105.ics 6cd945d8cf1c234d ok 2 fd9de2163530ad31
106.ics 0a88faabf44d2173 ok 2 fd9de2163530ad31
107.ics f768369d5dae2376 ok 1 6aeb31dbfe9f1e1d
108.ics 387f633e3c092dc0 error:KeyError 0 cbf29ce484222325
109.ics e7ab6e82805235b6 ok 4 30f88b2f39f2f9ff
110.ics 607efbf6fa2496bf ok 3 81734a59f576bfcc
111.ics 7561e6cdabfaba56 error:AttributeError 0 cbf29ce484222325
112.ics f320655a40b95a2d error:BrokenCalendarProperty 0 cbf29ce484222325
113.ics f28cd27685c41997 error:ValueError 0 cbf29ce484222325
114.ics 66316182edcb336f ok 9 6aaa4c03a6d18247
115.ics f665f5e554b714ff ok 0 cbf29ce484222325
116.ics cacb15e70c92d5de ok 1 0ed87b6b9ac7db32
117.ics ea27db0ce2ce3724 error:BadRuleStringFormat 0 cbf29ce484222325
118.ics 42527a511c0e2734 ok 0 cbf29ce484222325
119.ics 1bc620b785e48432 ok 0 cbf29ce484222325
120.ics 299832b83c9c5da8 ok 1 f9e5746e6528f95c
121.ics 3404319efe73f100 ok 1 92acbef097af17f3
122.ics 47a013072c37e533 ok 1 4b8809d5b52d7076
123.ics b85b3d2f702003d2 ok 6046 5234457b4263a240
124.ics babbdcab1229c054 error:KeyError 0 cbf29ce484222325
125.ics 293f95b414dd0dc4 error:KeyError 0 cbf29ce484222325
126.ics b8bba8b06011096d error:KeyError 0 cbf29ce484222325
127.ics f918961d97c53436 error:KeyError 0 cbf29ce484222325
128.ics 6956784f3a4c7273 ok 10 dd745ebc9adaf267
129.ics d9c0e13244cec7ec error:KeyError 0 cbf29ce484222325
130.ics c5155604168972ca ok 1 ac4b9f4380f00103
131.ics a0158076bec5dc4e error:KeyError 0 cbf29ce484222325
132.ics 65a467f65928d883 ok 0 cbf29ce484222325
133.ics c34baf7c3f807a3b ok 1 92d56cb8999bcfb6
134.ics 0034647325f9275c ok 1 7d1059f6c35c2d76
135.ics 0695ea4efd106782 ok 11 02a59c025af59f37
136.ics 0dd2a58ad4ae8241 ok 1 060b67319b9f3be3
137.ics 1bccffd07678580c ok 1 060b67319b9f3be3
138.ics acd9e6a8871889a6 ok 1 060b67319b9f3be3
139.ics 9d212148784ae35f ok 1 060b67319b9f3be3
140.ics 8f38b88bd087faf9 error:KeyError 0 cbf29ce484222325
141.ics e35bafff7984c4ba ok 0 cbf29ce484222325
142.ics 3c330244609abf43 ok 0 cbf29ce484222325
143.ics 745e656b4c7b0e78 ok 0 cbf29ce484222325
144.ics d6ac14c241795859 ok 0 cbf29ce484222325
145.ics 9ba2141965a44daf ok 1 66e36e2cb073742f
146.ics d102a94758b325db ok 1 475ba8f554e6cff7
147.ics edced0f0ec9deb88 error:KeyError 0 cbf29ce484222325
148.ics 06f6abaec3c24267 ok 0 cbf29ce484222325
149.ics 2370371d30192f1f ok 4824 8b8a4b40bb2e6aa3
150.ics 7b1784e2a0c6895b ok 182 4cbafa88b6d8c233
151.ics c668863a19cfb072 ok 1 4951d21ef2f46efd
152.ics fb740ebf74a78c0f error:ValueError 0 cbf29ce484222325
153.ics 9117278dee63ade5 ok 81 daa9b652f3cad25e
154.ics 3ea26e830b1162fc ok 1 2401f61737e03a8c
155.ics 5f33139bd5acebab ok 1 2401f61737e03a8c
156.ics cafac37703a6c2cc ok 1 017fee3b5036751d
157.ics 39c09d476131e468 ok 35 7d4dc72afbd12074
158.ics d7692c2dd60b8e48 ok 35 7d4dc72afbd12074
159.ics a5523072eceb7da5 ok 33 7516ff2adfb30be7
160.ics 35dfbd93b324eadf ok 5 68d67299fb575d66
161.ics d71f188fb1ba0045 error:BrokenCalendarProperty 0 cbf29ce484222325
162.ics d7d5f2f24226cacc ok 37 358374d7c1879ff0
163.ics 9304757721f49447 error:BadRuleStringFormat 0 cbf29ce484222325
164.ics 13354fb42424e4c0 ok 1 7f0a8b5d71cfead9
165.ics 9626a3c5378401ee ok 1 45f1224e2af8fc07
166.ics 7a0f923f97882974 ok 1 c85a9439352562a7
167.ics 7a619581166e542d error:ValueError 0 cbf29ce484222325
168.ics 7aeb8adc437fb2fe ok 1 d9eac4d1463f599e
169.ics 9547c1bf09272b1e ok 1627 cbd14668c3ec15d6
170.ics 9ef536100920f819 ok 0 cbf29ce484222325
171.ics b7715f44952010cf ok 0 cbf29ce484222325
172.ics a7019a735cfc5ae2 ok 1 7e4670bdf07af4c2
173.ics 459b52053470cf50 ok 1 7b3b728024d140c0
174.ics 6b1a097ab82abcbc ok 11 aebdeccdaba2e877
175.ics 93ce92acecfd57f0 ok 232 f3c9990a07fa34fa
176.ics 963b65c2eabbe436 ok 263 7edc64ce6c0dd56a
177.ics 1aa7cc73aa8358b1 ok 1255 fbe1bafb3aff7824
178.ics 235b1cfde186ca1a ok 30 d0862ba1f26d6b8b
179.ics 2c3ea727fac1287a ok 112 ca542f0e70966982
180.ics 1ca68790a61da62f ok 0 cbf29ce484222325
181.ics ec4d3ea2224748fb ok 58 b781c26ea675327e
182.ics ddb491d32b079ab7 ok 31 8daef0e6d471d69e
183.ics e7efc4155aa49ef5 ok 85 798655020aeaa255
184.ics 443ca34fdc589bb1 ok 5 d4db788c38347e61
185.ics b4fc726b9e8dbb82 ok 72 1ce65e09b20acdfc
186.ics dbb2a367b663ff88 ok 415 22635d351fe807f3
187.ics 36a3ee59dd9e3960 ok 15947 a0b5ce75d7602f94
188.ics ec732890c4d86ded ok 1 1c70ebb33b51c284
189.ics ef65b1b3527ca858 ok 66 df3aa98e4297ddf2
190.ics 7d362518b7029949 ok 1 271bb15cd733f929
191.ics d932a3b0c40ad1c3 ok 8039 3e6a3d734a40db7b
192.ics e99a7c65ca0bded3 ok 52 5109567b1a489382
193.ics 784417e6b7440d24 ok 846 e7ae9ca60ff3d944
194.ics dbf9c4796d9fab0b ok 207 f8de4949fb39918c
195.ics e6325ba43a63e81c ok 21 dbf05b4eb18f9ff8
196.ics 2813da4d93a9faa6 ok 1 e13e7566e58f598b
197.ics 0b03f24cb2384d2a ok 429 a1f3a71cfc731df0
198.ics 978947cbc1ade721 error:ValueError 0 cbf29ce484222325
199.ics d3a875dae8cc1c17 ok 5 9e92896db0604529
200.ics ff818f294cc6edae ok 5 26cc420c2282755e
201.ics 80ad1d1a14fe3820 ok 5 c46171955d0e3f3d
202.ics 894e65c171e0b541 ok 5 15ec5d2dc2726c90
203.ics 3834c7d9add9faa2 ok 5 15ec5d2dc2726c90
204.ics aa0dfa4251f286b6 ok 5 85499961b06e472b
205.ics 2df87c4dcd56db56 ok 1 657c5f7fb8df6021
206.ics cfc3d5e0b204b889 ok 1 590531ac0b15ce61
207.ics abee4d0b1fe1b4d4 ok 5 c6cc9c592989a14a
208.ics 723e3635a2c30198 ok 5 85499961b06e472b
209.ics 2d0d41b68309bf03 ok 265 faa5e95630f5ade3
210.ics 58ed3d5d2b518057 error:KeyError 0 cbf29ce484222325
211.ics 695b36aab3f5677a ok 2 d06cb2d28dd1e275
212.ics 3166a497556fe276 ok 0 cbf29ce484222325
213.ics 3c96394586a92860 ok 16 7d6b701793b0b8af
214.ics f70a07a84d65dd70 ok 1 81bdf11a4e682e35
215.ics 8c28772d9869713f ok 1 2b14928972809e89
216.ics 3f53f4f3b5c75c7d ok 1 ddfd12bab3dd2243
217.ics 64c1f5133f983883 ok 2210 df1c0d60312a315f
218.ics 95f514947c47896e ok 71 bae3867d5b201ec2
219.ics 026cf0623cae6c91 ok 302 ce6709ad307aa1b1
220.ics 34d2000fdf56fd5d error:KeyError 3 e31864dab920dc18
221.ics bfe96c0840eef895 ok 49 96aa9bc3c11ab0b2
222.ics 7c627fad842c3a43 ok 26 01042aa658796301
223.ics d066cfbc8a787945 ok 1297 e7566c93749a1352
224.ics 30a32d6090de9fcf ok 1297 e7566c93749a1352
225.ics 5e1e923a165590a5 error:KeyError 0 cbf29ce484222325
226.ics e90f67b90a1c6bfe ok 47417 a66961956ae0e0e7
227.ics 2a758ece1337d089 ok 1 2be50c7d64dbda34
228.ics 1c27412ee1364c2b ok 2 6c3fb4e0da17e9fa
229.ics 49fa25e1844aa851 ok 1 25e84d1a9d848652
230.ics 5d46c623fda6621d ok 1 25e84d1a9d848652
231.ics 6d8c9568ec237792 ok 1 b4ec68b8c373d660
232.ics 1d13e3c752974cc8 ok 1 51775e34bf91afe9
233.ics 1145c8cbaf6ec8d9 ok 46 1cf9b89e61758cf2
234.ics a1427234709467ea ok 0 cbf29ce484222325
235.ics 8f73cce0f88afce0 ok 0 cbf29ce484222325
236.ics bd1d749171413217 ok 0 cbf29ce484222325
237.ics 4ea30e55892f9892 ok 0 cbf29ce484222325
238.ics 04b77b466bdfb3c5 ok 0 cbf29ce484222325
239.ics 0f75d95f3acf75a6 ok 1 a51c5ec49253321a
240.ics ae53f0d519bc46b2 ok 1 a089cdbe1b590bae
241.ics 4377ef863b7782dc ok 1 475ba8f554e6cff7
242.ics 871eec0a08a278a3 ok 0 cbf29ce484222325
243.ics f79f3b5fc7956042 ok 0 cbf29ce484222325
244.ics 7cb1b2fa21a97e10 ok 1 4391843b5ab0ab63
245.ics e51fab979f39cd63 ok 182 83654498cd16782e
246.ics b7cc056e63bb16fa ok 1 c9d2d43ece5de2cd
247.ics 55997299e129017e ok 1 3a168fcee565779f
248.ics 9825c372b502634b ok 0 cbf29ce484222325
249.ics 2779400fabd98429 ok 10014 954a599ecc3418e9
250.ics 7224ac2a713b307d ok 7151 e16743dfc3d090f8
251.ics c8b7d440157e96c4 ok 1 3a168fcee565779f
252.ics 8b26f6e4f249021f ok 1 5263916a19484aed
253.ics 457d049a684bed4a error:BrokenCalendarProperty 0 cbf29ce484222325
254.ics 8e09bdd7af2a816a ok 1 3a168fcee565779f
255.ics 66f3976c2407e145 ok 36 8be3da7ee73d0549
256.ics e65fc374d7a380d4 ok 1 e7ec99b1fd8c4339
257.ics 3ceba7efe50baa5d ok 1 3a168fcee565779f
258.ics bce6420bbcbf1ea6 ok 7124 a31c6ff78d594b3e
259.ics 1b1930f74b80c1f3 ok 327 18631298aa733f5c
260.ics a18fb5839b3882bb ok 5 aa6aa8e4a3f9342d
261.ics dbab4d3152646e7a ok 1 211abbd6e57e2d3f
262.ics d9e587e1c75a6431 ok 0 cbf29ce484222325
263.ics 659324402e067d9f ok 0 cbf29ce484222325
264.ics 4736441901b468d7 ok 0 cbf29ce484222325
265.ics 8550130b352e3299 ok 0 cbf29ce484222325
266.ics 1270832bea1a443a error:ValueError 0 cbf29ce484222325
267.ics ac221042b7c67702 error:ValueError 0 cbf29ce484222325
268.ics a9d96f8e715fc5de ok 0 cbf29ce484222325
269.ics 59d74b95b4a7be03 ok 1 c40d75cabf75f8c5
270.ics 2784822789f8b3d6 error:ValueError 0 cbf29ce484222325
271.ics 4bebf9386eadb8d1 ok 0 cbf29ce484222325
272.ics 37386edc2c5354d4 error:KeyError 0 cbf29ce484222325
273.ics db33b31e6081389a ok 0 cbf29ce484222325
274.ics 607c8259362b2bb3 ok 0 cbf29ce484222325
275.ics 3e82b6eea2b39e99 ok 0 cbf29ce484222325
276.ics 2f9e903d002b83fb ok 0 cbf29ce484222325
277.ics fa62e7133f03dd0c ok 0 cbf29ce484222325
278.ics f9ad6bb4bebdd63a ok 0 cbf29ce484222325
279.ics 17137122dd4c8afa ok 0 cbf29ce484222325
280.ics bd43f0adf65513e0 ok 0 cbf29ce484222325
281.ics c7b52dae8149bf30 ok 0 cbf29ce484222325
282.ics b3c880b0b18bc94f ok 0 cbf29ce484222325
283.ics 77cf6b821cff8f9b ok 1 3c54b87eaea490cf
284.ics 88a1f392326469d5 ok 0 cbf29ce484222325
285.ics 6708c899ca56f07d ok 0 cbf29ce484222325
286.ics 21276d0cba4aa00b ok 0 cbf29ce484222325
287.ics 44174728f04dae36 ok 0 cbf29ce484222325
288.ics 2aba55a5b2b60551 ok 0 cbf29ce484222325
289.ics bfc86b43cdcdfe3a ok 0 cbf29ce484222325
290.ics ceb9d764b9050011 ok 1 06137f23428feae2
291.ics ef3db7a19dc280bf ok 1 9acbad80f9a64a9b
292.ics 5623fd9c83163bab ok 1 0f8e0a78f937a2cd
293.ics aa0dc89fb6a5a247 ok 1 7951baa4b32c372a
294.ics b316b56d2b54c4e2 ok 1 4d928e3465ff6a93
295.ics ca16403dca746306 ok 1 d61b014e6f6caf72
296.ics b2661c35bc8dd77f ok 1 1c8b2cbc3e931d64
297.ics 130a21f4921c0727 ok 5 3064f97472a34be9
298.ics 66ac137f51b98f2b ok 30 13edfc754cc3e27c
299.ics e1fdefddea166a91 ok 1 c8dd90bfddec2e8b
300.ics 0d0e2f82eadf30c6 ok 1 92ee4084fc0ba022
301.ics 0c6f5c48b410d913 error:KeyError 0 cbf29ce484222325
302.ics d536a117f9ce2ab7 ok 1 1f86cde1da2a9ffe
303.ics 510c471d5203f375 ok 0 cbf29ce484222325
304.ics a1b2d094b8f0b580 ok 1 1f86cde1da2a9ffe
305.ics 68338b50fb3bb107 ok 1 d4d7c36f7484d1b4
306.ics 3a288d7931a62ae1 error:BrokenCalendarProperty 0 cbf29ce484222325
307.ics b518ee553a04125e ok 1 1f86cde1da2a9ffe
308.ics 4f6120bb5135d615 ok 1 1f86cde1da2a9ffe
309.ics 8aea0a9a1a6033e3 ok 1374 64571c77cc3557a7
310.ics 8d50d508569aea4b ok 1 fb37d6b6ac5fca47
311.ics f557a89d08f668aa ok 1 7914aaa70f816492
312.ics 555489414b1defa4 ok 1 661512779dc0c1a2
313.ics 146a7bdaee1a4d10 ok 2 cf6fb2dca07e5a4c
314.ics c5837a5fc946ff1c ok 8219 a90b3967b8ecdde1
315.ics 354a1960d4b50ba2 ok 2 cf6fb2dca07e5a4c
316.ics 5b3c61e07be19b43 ok 2 7251e46430693ec8
317.ics 957827a9e36f72b6 ok 5 51aa1697ae56b6b6
318.ics 890ffd79e8e396ca ok 5 61df21610a8c830b
319.ics e6ce19e950220d80 ok 12417 f759a63192d698c1
320.ics 1bb39e9c8c4a4e2d ok 5 3064f97472a34be9
321.ics 38813ba00845b0cd ok 812 91a5d5b9b75b9a19
322.ics 86432fe2e810c547 ok 5 4c04d75a50fb7c20
323.ics 1e91fbde7b8527d1 ok 500 5239d4b5b8d24e34
324.ics a7fe3d52c71ccffe ok 3 257a2f203d8dbddf
325.ics cbaa8876a0ad1f60 ok 729 89e00035ec4f6c31
326.ics 32708d911faaeae1 ok 12417 70f262c4025befee
327.ics 520ba27583d2fbe8 ok 6199 4983fff04f9f8f5c
328.ics 9fe266fe2d1f8b50 ok 755 72cb6284ee90e238
329.ics 0a47911de0aa991b ok 1 2193abc27729dc69
330.ics e05f498f3444ade3 ok 1 1b5e4c290904a201
331.ics cbf705bc159509fe ok 1 776f814ef2efc16f
332.ics 4a0be25b1647e352 ok 1 776f814ef2efc16f
333.ics 4e89189ff5e8550e ok 1 734c6b374c91a09a
334.ics b39aad79359cf3fc ok 1 3fdaeebabb0d629f
335.ics 8aa85fa1ce9e56e7 ok 1 c252bfa7a8ac91a6
336.ics a75fe398ea37ce1b ok 1 1f86cde1da2a9ffe
337.ics ec727e4a78d4fdd2 ok 1 1f86cde1da2a9ffe
338.ics 08e793e5ef7c66a2 ok 27 342a0c2a5dd3b0c1
339.ics 31f6e896700d63c4 ok 1 ffdaee8b036928b3
340.ics 7a0b6bc5ca3e657d ok 2 eef0a17584ee2501
341.ics 2ba101b7e8e08139 ok 3 ce0f6cebef23120c
342.ics 45bfeb3272930887 ok 4 d24fb410646bfc76
343.ics 87ea98d355ef78e6 ok 12 28492646e85f6e0b
344.ics 6a41fab2bf86dd48 ok 2 92a4bff1a020995b
345.ics d868926b9693b74a ok 2 1b5e4e7534902028
346.ics c7dd66df1ac7a095 ok 2089 482da896e745c4fd
347.ics b7381b7fae843182 ok 2090 ed4d902baf908c9a
348.ics 646f4b7ea1b2ffa2 ok 4 014a802d2e22c238
349.ics 8cc83c94b73ee28f ok 1 f356a3db826a6638
350.ics b48d9a35f2362fc7 ok 0 cbf29ce484222325
351.ics 8ac97e5cded509d2 ok 0 cbf29ce484222325
352.ics 5417b3fe51607870 ok 0 cbf29ce484222325
353.ics da68bc43ab47a342 ok 0 cbf29ce484222325
354.ics 78c1359c9f81f854 ok 0 cbf29ce484222325
355.ics 48b894471e8f3155 ok 0 cbf29ce484222325
356.ics 460a9e34e2ae4d88 ok 0 cbf29ce484222325
357.ics c2d8db352d6e3dc7 ok 0 cbf29ce484222325
358.ics 6d6850d9f1d79f2e ok 0 cbf29ce484222325
359.ics 004dff89b9cb38ed ok 0 cbf29ce484222325
360.ics c580901e8d9b4a8c ok 0 cbf29ce484222325
361.ics eae7178b6e10e80a ok 0 cbf29ce484222325
362.ics 2897d8c455b60c0c ok 0 cbf29ce484222325
363.ics 6e1b319199105c00 ok 0 cbf29ce484222325
364.ics 6b2d03721d62bdd2 ok 1 86db0e5d66892b25
365.ics 93b902c311cf5264 ok 1 86db0e5d66892b25
366.ics 395a30d792fbee2c ok 1 cce7ef437323dc2a
367.ics 74fb84ca0bf63203 ok 1 a51c5ec49253321a
368.ics c541b37d9cff026b ok 1 a51c5ec49253321a
369.ics baf5b1510a15eb10 ok 1 a089cdbe1b590bae
370.ics a6dbe0ada56450b2 ok 1 a089cdbe1b590bae
371.ics 11b788269698a961 ok 1 475ba8f554e6cff7
372.ics ca7c698917dd082f ok 0 cbf29ce484222325
373.ics d04498c364c89eed ok 0 cbf29ce484222325
374.ics fc451b717c8b1c0c ok 0 cbf29ce484222325
375.ics 0754c98ea52a8dda ok 0 cbf29ce484222325
376.ics a3bfdc9024170120 ok 1 03243ebfab2f5425
377.ics d581024a0472da39 ok 0 cbf29ce484222325
378.ics bdf46cf5bbc90a50 ok 1 08e00249c1b34419
379.ics 9cd1f8f59901b926 ok 6 1b284829de40f083
380.ics abd8ff4a760b8451 ok 1 246cdd0761f5ee52
381.ics 912ea1a9aab5d4d0 ok 1 bee004f45c064d24
382.ics d64b386b2b5c4e3d ok 2 3281e917ead162f2
383.ics 4c5662de7608bcc8 ok 1 fb732bf211c52312
384.ics ee05cdd5d0d6413c ok 1 fb732bf211c52312
385.ics dc0e5c982df26a89 ok 5 a1ba0ce5b1cbab75
386.ics afa4398f13346954 ok 0 cbf29ce484222325
387.ics 5f45debc55020d53 ok 147 e3be08d2a89738b6
388.ics 1312a1c406d8ac53 ok 0 cbf29ce484222325
389.ics 5001d29307ae6790 ok 0 cbf29ce484222325
390.ics 0f6ab610fa2350d6 ok 0 cbf29ce484222325
391.ics 047996dca99b50c0 ok 0 cbf29ce484222325
392.ics 76460e7940998b28 ok 0 cbf29ce484222325
393.ics 7f3345822af73e1f ok 0 cbf29ce484222325
394.ics feaba78ff4b421fa ok 0 cbf29ce484222325
395.ics 7235e7de80b3a2bd ok 0 cbf29ce484222325
396.ics 6b638a60599cebae ok 0 cbf29ce484222325
397.ics 840efc846db0638e ok 0 cbf29ce484222325
398.ics 2576134ed22ccdaa ok 0 cbf29ce484222325
399.ics dccb04a7e1579307 ok 0 cbf29ce484222325
400.ics 4a84b6a284e9f1d3 ok 1 acaa9be887d82269
401.ics 65a81d5d594516e4 error:KeyError 0 cbf29ce484222325
402.ics e1e25162ab087ebd ok 1 f5d15619aa9f343a
403.ics 7cc1871571040352 error:KeyError 0 cbf29ce484222325
404.ics 408ea01198809e70 ok 0 cbf29ce484222325
405.ics d702745154daa26f ok 0 cbf29ce484222325
406.ics b7a48924d31504f4 ok 0 cbf29ce484222325
407.ics 171d269751e4222e error:KeyError 0 cbf29ce484222325
408.ics 2de404933798fe44 error:KeyError 0 cbf29ce484222325
409.ics 7abe6d0785b5af48 ok 1 103743fef3acd1c5
410.ics 9b1c8232e96b759b ok 0 cbf29ce484222325
411.ics 509ca539d0f8b743 ok 1 868a764113cb6c0a
412.ics 587bdc175d2240a6 ok 1 bbc1eb3c4daa27fb
413.ics ade3493abbfd592b ok 1 868a764113cb6c0a
414.ics 505bdf883d361b7f ok 1 868a764113cb6c0a
415.ics ce3ddd45472f4b5f ok 1 868a764113cb6c0a
416.ics 46c5f1e1fe4b3a1d ok 1 2d2b3e83024f585a
417.ics 9e6fcb0ab4ee9b49 ok 1 2d2b3e83024f585a
418.ics d6506a9ff6d8f698 ok 1 2d2b3e83024f585a
419.ics b479a0babac1574a ok 737 6737fc8c2370c005
420.ics bb01197c8d039bba ok 1031 67063734e021fe7e
421.ics 38d7ff4eb1663e4c ok 1036 dede2afa839b0042
422.ics 60b20eb2c30260e1 ok 1 868a764113cb6c0a
423.ics eb62bffccfc017d6 ok 0 cbf29ce484222325
424.ics 66341290d896fb6c ok 0 cbf29ce484222325
425.ics 2ba46e04586a1fcb ok 0 cbf29ce484222325
426.ics 4e842d97039b513c ok 0 cbf29ce484222325
427.ics 8fe1d0d3442f2624 ok 755028 b107616e4c68e387
428.ics d7a040c30088ecf6 ok 1 33fbd22315035870
429.ics b8ad2d533295acce ok 1 fe87a3c7703dde70
430.ics 31c85cf0a9a2db74 ok 1065 86fc9df1747f84ce
431.ics 8d5638ed4c71d89a ok 2 1b7f04ce451df391
432.ics fad73c3bf28227ee ok 2 5cbb786eddd7ba46
433.ics 666d4f882d7eb92c ok 2 b75d9bfe1df4aeb1
434.ics b9444faa548763dc ok 2 02c09600512ffdeb
435.ics 9f99e0c92e194e8d ok 38 28f8b059bf5072ef
436.ics 70de3e08404a195f ok 38 49689106fb3cbdcf
437.ics 7be9c70e3994ca26 ok 2 989407ad717b22c4
438.ics 3ce114a35c7aba8e ok 1 ecc5f2f4c9bf5f66
439.ics 795b0739b6fb6521 ok 1 7999a05e11540fd7
440.ics 69c1de7473cfd00b ok 38 e5bb48ffb029ebbb
441.ics dec6dff69ea83ced ok 38 c04fa11eb3d1296b
442.ics 3c6a87a808443f1f ok 38 3f30f0d62a679df0
443.ics 8a63ee2dc745762c ok 38 103c79834516b7b4
444.ics 39e788531ef434ca ok 1 c795b55eed69044e
445.ics cb646b5912102ecd ok 2 42ca8f3cd74ec070
446.ics 25c0174f024cd5e0 ok 1 9ab092ba1b2cbe32
447.ics 01d644f8bf89d2c5 ok 1 257fb25d9a8f4736
448.ics fb4156f61a5ff17e ok 36 4bca751d6ccb4192
449.ics bbbca8c3302a3ffc ok 36 13a317d29ea03196
450.ics 8036e03591ada6e3 ok 36 cd89d90c2c6abd7e
451.ics 5e94b1ae342198e5 ok 36 3635eebf161157ac
452.ics 00c4e0c6ae86244f ok 36 ee3e6254b3d781f0
453.ics 0fce1cffce28ce4b ok 36 2a08f244b0efd2f2
454.ics db3fcc3017b5b9c9 ok 1 08a00cb8dc888fdf
455.ics b4171763625937ef ok 431 7a6e68aea63feb94
456.ics 458557a30f8b1f8a ok 1 29ad2e08d862ae5d
457.ics 325e3333d055a4d1 ok 1 3b2fd22b7a65fb42
458.ics a2f95206753e7e1c ok 1 29ac3f462e035c62
459.ics 8d3ccec36eb259eb ok 0 cbf29ce484222325
460.ics d35c5ae7b2afd005 ok 0 cbf29ce484222325
461.ics f169737980e93e24 ok 0 cbf29ce484222325
462.ics 401eefcf94323c16 ok 0 cbf29ce484222325
463.ics 328b05e0c9109aae ok 0 cbf29ce484222325
464.ics 88c3c8f41e739986 ok 0 cbf29ce484222325
465.ics 02ef3bda75ae20ad ok 0 cbf29ce484222325
466.ics 3ef7a2115896a2bc ok 0 cbf29ce484222325
467.ics 05fca688188dc764 ok 0 cbf29ce484222325
468.ics 96faca6800974811 error:BrokenCalendarProperty 0 cbf29ce484222325
469.ics e42e5b2ef736b51f ok 0 cbf29ce484222325
470.ics e5835123ee7a8b11 ok 1 3d95a249ebcf5189
471.ics 9d45edd0dfef085b ok 1 f1465a126091d972
472.ics af748d2f93726a77 ok 1 f1465a126091d972
473.ics 966833f4348ba94d ok 1 f1465a126091d972
474.ics ab5a40761c44b75f ok 1 47d6e3d74a4debb9
475.ics 79cd24beb5b795e2 ok 1 47d6e3d74a4debb9
476.ics 8386a552b9ef6f7a ok 1 47d6e3d74a4debb9
477.ics 8e905099eeb337ac ok 1 47d6e3d74a4debb9
478.ics c75085c526bebae2 ok 1 b3005c04e9b3820e
479.ics 307522eaa3f19ae5 ok 1 ecb2d7a29da8ac82
480.ics 25671b6c5143cc4d ok 1 bbdf2ad51d3b790c
481.ics afaf1bac830f37b5 ok 1 f1465a126091d972
482.ics ea761b05f03866b4 ok 1 7a43d82dd57d59f2
483.ics f3898dcfd3701ead ok 1 f1465a126091d972
484.ics b1eb1a3597072047 ok 1 f1465a126091d972
485.ics 9f170e5e72ce1585 ok 1 f1465a126091d972
486.ics a1d640dd4f6b5ec4 ok 1 f1465a126091d972
487.ics 8abbab19ed2a4bdc ok 1 f1465a126091d972
488.ics 6e213add0de5dac0 ok 1 f1465a126091d972
489.ics 9a5c5859b52659a8 ok 1 f1465a126091d972
490.ics 93965e548bbeac15 ok 1 f1465a126091d972
491.ics 93cba98da59e3c91 ok 1 f1465a126091d972
492.ics a11b0ed0e8186514 ok 1 f1465a126091d972
493.ics f01c5b53ad20ce75 ok 1 47d6e3d74a4debb9
494.ics 19971fc7bff018a7 error:BadRuleStringFormat 0 cbf29ce484222325
495.ics e89060ba94e1f22a error:BadRuleStringFormat 0 cbf29ce484222325
496.ics 4386ec2660f2dfa1 ok 10 986041f44885520e
497.ics 1bb9346781a40ac5 error:BadRuleStringFormat 0 cbf29ce484222325
498.ics 0ae4f77620ed93bc error:BadRuleStringFormat 0 cbf29ce484222325
499.ics f9748b8d210d8b3a ok 1 47d6e3d74a4debb9
500.ics e0b7930e4237dd72 error:BadRuleStringFormat 0 cbf29ce484222325
501.ics 9a0fe7027c4bcb63 ok 3 5e8870a3795886e7
502.ics 73a44088dcbbee7e ok 1 47d6e3d74a4debb9
503.ics 19fc01a654103f1e ok 1 f1465a126091d972
504.ics 09e66a57e0ec95cd ok 1 bc259b46b1de49c0
505.ics 0b407bbb336979ba ok 1 25b001b6773aca17
506.ics fe2ec7e119c40021 ok 1 25b001b6773aca17
507.ics abfc3a0e51599ffe ok 1 030530e610819cd3
508.ics d16369614d9ceec0 ok 1 030530e610819cd3
509.ics 4219886712f1610d ok 10 4618eedf3cd634e7
510.ics 7015345be01ac4ab ok 2 c46216cbe5139596
511.ics bd114031379df6e2 ok 1 a8ca5bc886b3a25c
512.ics f03f8b638bbec302 ok 1 34307a1171eadcde
513.ics 951d3c4526f6153b ok 1 34307a1171eadcde
514.ics 6fd70c84359810f0 ok 1 02da48d67d9df29f
515.ics f71cce794553c70f error:ValueError 0 cbf29ce484222325
516.ics 77a4c1cf833c02d4 ok 0 cbf29ce484222325
517.ics 804d00ee0d6c1f1f error:BrokenCalendarProperty 0 cbf29ce484222325
518.ics 91a0be2c5f908df1 ok 2 8d4042745646dc3c
519.ics 7456a6a2b283c16e ok 1130 9ac4fc7f1036a118
520.ics dbfe1a6a9660b37b ok 17 230d981e803fba5a
521.ics 36f2ccd021551723 ok 7 74cef3249ede9632
522.ics 60d92914c5fd8511 ok 4 e159edc63ab01325
523.ics b729571f782b0399 ok 1 9f82af098e45ea3a
524.ics 3cfebd509e406f96 ok 1190 b88f274ab2c6fd56
525.ics 4cc550d6ae4e7f28 ok 1 6c3a67c01570eda2
526.ics 2026237f4e2ada02 ok 1 6c3a67c01570eda2
527.ics ba34640f110e6234 ok 1 de0d27fe2828426d
528.ics 23a430264eef7874 ok 322 04c47d2a7e9cec8f
529.ics 4c5b997d6b89327f ok 1 6c3a67c01570eda2
530.ics c96f310d4cda1eea ok 1 de0d27fe2828426d
531.ics 9611da1a344ec3ca ok 737 4b37f9536e46cee4
532.ics 256fa91bd5836627 ok 1 6c3a67c01570eda2
533.ics bc77498d56e9c20a ok 1 6c3a67c01570eda2
534.ics 00371c537e8e5650 ok 1 de0d27fe2828426d
535.ics 0f2b71bf277b32c1 ok 1 6c3a67c01570eda2
536.ics c94b1ad9fd9661e0 ok 3 9699a1bc21b4c750
537.ics b269380167b3a924 ok 329 b67824486726dc97
538.ics e2ebafbc78d892da ok 1 55336e9560b1aab6
539.ics 24ec06242f67e328 ok 14 98446047366d5f0a
540.ics bfa28324dcaf0577 ok 1 d2317806e4bb6f82
541.ics c0cc620398e9d393 ok 10371 e0289b9ee36530cc
542.ics c01aa080828ae5e3 ok 10371 e0289b9ee36530cc
543.ics 037a04737df21d72 ok 1 29a37ed8d3a09c16
544.ics 4ba4751b376ccdf1 ok 11698 3ae162fb85b6074f
545.ics 5f7ee367c93f740c ok 1 2fcf1e1f68924c94
546.ics e30b75abced43051 ok 1 2fcf1e1f68924c94
547.ics 76ea54368affdfb1 ok 98 5bba0b1d45ba2353
548.ics 2f49b635f2d52e03 ok 98 5bba0b1d45ba2353
549.ics fa04e5e4610b56e0 ok 1 7cbd44c64b1a3bf8
550.ics fc1674fcb9141e1b ok 18092 aca5b269758362d0
551.ics 6718bdf3d84a0538 ok 1 50311b702b4a531d
552.ics 57d113b154829296 ok 15 4aa9a44698134f76
553.ics 0cf01b5dd112eeb3 ok 1 038b22ef6529c981
554.ics ff0a75a54986ac17 error:AttributeError 0 cbf29ce484222325
555.ics af73b1b7f71208fc error:BrokenCalendarProperty 0 cbf29ce484222325
556.ics 1c221085638b4571 ok 0 cbf29ce484222325
557.ics 2ca7351267cc1ddf ok 1 9970734c58210e73
558.ics 5aa24981cb7d7bfa ok 2 9f74b0ab752d23d9
559.ics 8b112196755a45a6 ok 1 9c476352c55209cf
560.ics 5c6cd46ecb8e6cc4 error:KeyError 0 cbf29ce484222325
561.ics 7673eee31a35503e error:KeyError 0 cbf29ce484222325
562.ics b84fde2bd79c93af ok 1 0ec50d31b5f14f4b
563.ics 1f4c4703635e655b ok 0 cbf29ce484222325
564.ics 8f38c27c20d2027c error:KeyError 0 cbf29ce484222325
565.ics bc2cdf875da9586e ok 1 0ec50d31b5f14f4b
566.ics 7237ff03893a3ff0 ok 10 9ac9ef5e2ad08ef8
567.ics 89735fd9433a9233 ok 0 cbf29ce484222325
568.ics eec25d0de39b8bb1 error:BadRuleStringFormat 0 cbf29ce484222325
569.ics 60020b6f4316c07e error:BrokenCalendarProperty 0 cbf29ce484222325
570.ics 011f39d09eeaad5e error:BrokenCalendarProperty 0 cbf29ce484222325
571.ics 02f994b802c67caf ok 1 0ec50d31b5f14f4b
572.ics e39bd7637de1835a ok 1 b54151085483e20f
573.ics 42a2186d9720ddcb ok 1 69814599ecf11e5e
574.ics 795a9e7cd192d0fb ok 0 cbf29ce484222325
575.ics 2d95f3f68c705049 ok 1 192521434ae8a49a
576.ics 2001cc86e9ec5343 ok 3 55ac0eeafac3a215
577.ics 29ab39dda92362fb ok 1 e87b83f08bb90337
578.ics 2618e9105afa159f error:KeyError 0 cbf29ce484222325
579.ics 5859d6c1d533445d error:KeyError 0 cbf29ce484222325
580.ics fddb736e3b312d39 ok 0 cbf29ce484222325
581.ics 8b4316e1158c313d ok 2 1c3eea5403880b46
582.ics 4504ade9b852e971 ok 2 929e4066db270a79
583.ics 9f3fa6fa90d65210 ok 1 a154aba2c5bf9b7b
584.ics 7606288c783e3e71 ok 3 7088847fdb1e322d
585.ics 5410b6cd30908fc7 ok 1 2fa990dcb0ddd477
586.ics 2e908ff3acde1d17 ok 1 bf802123a5cbde25
587.ics 57d536e8c6cfb664 ok 0 cbf29ce484222325
588.ics 2eb1984a2d05e1c1 error:KeyError 0 cbf29ce484222325
589.ics 84563358c2f285b1 ok 1 e0519fb25dc14698
590.ics a01e320031afe59c error:TypeError 0 cbf29ce484222325
591.ics 28338ad68524d374 error:KeyError 0 cbf29ce484222325
592.ics 1e9513aad1176a3b error:KeyError 0 cbf29ce484222325
593.ics 5768c3a72a34543f error:KeyError 0 cbf29ce484222325
594.ics 22edb72f365a9fe8 ok 1 3c54b87eaea490cf
595.ics 6baaaf14ba0ec13b ok 6 899249ff9479e5cd
596.ics 7995c972d13640c7 ok 0 cbf29ce484222325
597.ics 0bccc9cb9f4a808a ok 0 cbf29ce484222325
598.ics ccf9f2c19ff5ea57 error:KeyError 0 cbf29ce484222325
599.ics 345b5054bdc457f7 error:KeyError 0 cbf29ce484222325
600.ics b3fd9b0926d944fc ok 0 cbf29ce484222325
601.ics 803cdf54ac2980c1 ok 2 27dd95158039e3bd
602.ics de5881fee211736f error:KeyError 0 cbf29ce484222325
603.ics 3169581748df4d4a error:KeyError 0 cbf29ce484222325
604.ics 675c997681f5696d ok 1 51775e34bf91afe9
605.ics de82136d7d3a5a0b ok 6 e4a3bc9424782ad3
606.ics 632404a61241b6e1 ok 32 c8e99812c3703113
607.ics 4c7cf910c0bd9692 error:ValueError 0 cbf29ce484222325
608.ics 182fcc336669a6ba error:ValueError 0 cbf29ce484222325
609.ics 9a41cfd355fe166d ok 1 51775e34bf91afe9
610.ics 036fd56cf46a257e ok 1 52080489dbd2a64d
611.ics 02dca623993e5fe8 ok 6235 d1e620ba09c1174b
612.ics e2f0de5bef8d4043 ok 1 51775e34bf91afe9
613.ics 2cf9d43c5e2d25d7 ok 1 51775e34bf91afe9
614.ics 0daf9bda7b5d2b1d ok 1 51775e34bf91afe9
615.ics 3c3ba976122e7e74 ok 1 99351a3ddcc831a8
616.ics cbf7e94326a004ec ok 1 99351a3ddcc831a8
617.ics 021086128b68b155 ok 1 51775e34bf91afe9
618.ics 6964e8283541be0f ok 1 51775e34bf91afe9
619.ics 69298e327ae86e76 ok 1 51775e34bf91afe9
620.ics e36906bd65bfb915 ok 1 51775e34bf91afe9
621.ics 577c884e69d0e925 ok 1 51775e34bf91afe9
622.ics 82e5490f780464d8 ok 1 51775e34bf91afe9
623.ics 1af72c72525e76f8 ok 1 51775e34bf91afe9
624.ics a4cc83cac70ff656 ok 14 3c10e8fcdebe3142
625.ics 162346056497c566 ok 191 6a4a41ad352c9d34
626.ics bd46f8354d088303 ok 1 51775e34bf91afe9
627.ics 422e4ab12efa9775 ok 1 51775e34bf91afe9
628.ics 5262c6e39ee03a6d ok 25 1e53e2bdbbc92982
629.ics 1b47a1d74a273fb5 ok 7 796bd1cbb5307446
630.ics 72ee142ad240f804 error:BrokenCalendarProperty 0 cbf29ce484222325
631.ics 8f67ebe7dbadd808 error:KeyError 0 cbf29ce484222325
632.ics 581b932dcc8ed614 ok 11 4a66e487c0331475
633.ics 0eb6b1f4707e4489 ok 1 ce9ec8198f60a6fe
634.ics 22e79d2c41f234de ok 70 169c6099a4f0f803
635.ics 3bbc486046876702 ok 1 5ca267a0363cae67
636.ics 7ee3efd08ef54c47 ok 123 f5fa0c7f7129ee43
637.ics e367d038575643fe ok 1 1c36c940d09c9962
638.ics fe8e021e822d8789 ok 103 2a86ff39dc7e6621
639.ics cff1e406233be77a ok 1 1c36c940d09c9962
640.ics bda4e08b7cd64368 ok 1 1c36c940d09c9962
641.ics 1c4c43b047d6215c ok 2 a55e9f50eed2d8e2
642.ics 75bb819fee326494 ok 1 8b1699d8c90f9807
643.ics d57522348f65d97a error:BrokenCalendarProperty 0 cbf29ce484222325
644.ics c493d4e92fee0c6d error:BrokenCalendarProperty 0 cbf29ce484222325
645.ics d0288d3b21b3be46 error:BrokenCalendarProperty 0 cbf29ce484222325
646.ics 04866be85b84c126 ok 569 ce0f05d1b71adea8
647.ics 01ccf6eb46b6e6e5 ok 565 f8bb14b3dad40612
648.ics 22c6e122e74d5bae ok 1419 bb18286af2b3a0fc
649.ics 45dbe3e1c5b08f0a ok 1 22c789764bc939aa
650.ics a708fc2ef9df093a ok 1 a70b1181e5c3f33d
651.ics 2c776121ae211361 ok 12 c22db001aafbcb05
652.ics 9bf777f032899eae ok 1 593f0b0b5b5335e9
653.ics 054316568c0d7ad2 ok 4 58af0e12793c6356
654.ics 2b605181734e388d ok 1 2de92f32be81d0cd
655.ics 59630d2b1cc0293f ok 1 8956135fb36a9641
656.ics 363f8f0ae2136030 ok 1 a70b1181e5c3f33d
657.ics 19d49dcee4b3a237 ok 2 a3e7ad35fb70abd7
658.ics 6ed3cbf7ecc6b47a ok 2 eba2a1a0be399146
659.ics 21ab0b780d4a467b ok 2 b25db49f085f4e4c
660.ics 69016c944eb604bd ok 2 47576239010359b1
661.ics 2da2575f1a848bf1 ok 1 ec0b446e3f7e85e1
662.ics 9e95415937e3cae7 ok 1 a70b1181e5c3f33d
663.ics b21cd6758081e2ea ok 1 b9180af72bd64c84
664.ics 082d3a2430b4beb2 ok 3 8cffc50c6e7fb90b
665.ics 46377e325c3fe828 ok 35 386e4720d12269c1
666.ics 724e5cebbc3921d9 ok 1 a70b1181e5c3f33d
667.ics 064bbf97cf17edba ok 1 a70b1181e5c3f33d
668.ics 4985fdb58bbd4305 ok 1 a70b1181e5c3f33d
669.ics 9f45c0e9099c808c ok 1 2de92f32be81d0cd
670.ics 96dd0c6ddaefa197 ok 1 a70b1181e5c3f33d
671.ics 479469f8503ea4e7 ok 1 a70b1181e5c3f33d
672.ics 584620a802c4f7ed ok 3 a9b262d9d305244e
673.ics e974c5dbdbc8ef90 ok 6 2c6127fdeef84390
674.ics 40a16086dc2e0f23 ok 7 ed354208ac45c02d
675.ics b9414c8abc153a83 ok 3 0cfbd7c62b19ff77
676.ics d12e3bd701fc25a3 ok 70 f9c958c87fe1db0b
677.ics de365534b6db2dd4 ok 27 4e41553911d83fef
678.ics 82ff5d3b01390233 ok 6 178a803d3e9283ee
679.ics 110f43ff83ea761a ok 6 178a803d3e9283ee
680.ics 9f2331459e50cbb4 ok 6 7754971c244339ef
681.ics 18648d7545c0b033 ok 5 2674c6879f673916
682.ics cc04ec0d2746c289 error:ValueError 0 cbf29ce484222325
683.ics 5a0434dfb2982b9e ok 12 bb8bd7628743102f
684.ics 91e861e68f38f01a error:InvalidCalendar 0 cbf29ce484222325
685.ics 8a791bb34c8e6790 ok 1 2e742b085fb7b09d
686.ics 106a56d42b1a043c ok 11 04b284172383c045
687.ics 3152869c4313a58f ok 1 a10d3085f3de87ce
688.ics a89abce2c10af28f ok 1 22c789764bc939aa
689.ics b23e3d71f5cf7541 error:KeyError 2 d6e250f9d8f22375
690.ics 1fcec2d5d2a4cd1e error:KeyError 0 cbf29ce484222325
691.ics 3d3e244887d91d1f ok 1 7e4670bdf07af4c2
692.ics 043cef8c6923e78d error:KeyError 0 cbf29ce484222325
693.ics 8c909f48b9106cf1 error:KeyError 0 cbf29ce484222325
694.ics 19d67b94c2c808d3 ok 0 cbf29ce484222325
695.ics b3710683a6768fd2 error:ValueError 0 cbf29ce484222325
696.ics dccd81c0e8f8f7be error:KeyError 2 d6e250f9d8f22375
697.ics 41181859dd84a2be ok 0 cbf29ce484222325
698.ics 7e20f9269406ad15 error:BrokenCalendarProperty 2 6c3fb4e0da17e9fa
699.ics 7a50ce0545a28e16 error:KeyError 4 a7bb8d31ab432784
700.ics 6b175bcfc2f36071 error:ValueError 0 cbf29ce484222325
701.ics 952c88df25bc5029 error:ValueError 0 cbf29ce484222325
702.ics 8991794421131d3e error:BadRuleStringFormat 0 cbf29ce484222325
703.ics 394deb39b27a11ee ok 4 76ecfcf9aa054f76
704.ics 804c7e810286f3aa ok 3 716dbefc24e051c2
705.ics 2d14816fac03018a ok 5 498e662216dcd30f
706.ics 2cf9efcb31ba1c6a ok 51 02f4aef3891b19e0
707.ics d74399321e0f1b95 ok 51 02f4aef3891b19e0
708.ics cc63b830f9396f8f ok 1 5004ac734b17c89d
709.ics 83cd137b154844f3 ok 3 09f0fe4f00bd870c
710.ics dee39d03c75b5460 error:BrokenCalendarProperty 0 cbf29ce484222325
711.ics cc6bdfd2d0fb7606 ok 2 9eceb1360fda4864
712.ics 029e24b4800e6bf0 ok 14 526c765cde204065
713.ics ddb47614d1dcefd2 ok 53 22a942e560a2b63e
714.ics 4cbd12fc94693fc7 ok 7 2d857923a40fdbe0
715.ics e77d5b7c0e7bc012 ok 30 90bb66933a104ce7
716.ics a112512e2d792494 ok 0 cbf29ce484222325
717.ics a47fd6f9628d4e3f ok 1 56055cf12826a285
718.ics 01ec2b3686fc30ed ok 3 3b32cab87fc9c752
719.ics 31b4b96810364a33 ok 159 9842cba7063244c4
720.ics 7d93a54060242e6b ok 7 286eadd3f3120010
721.ics 70e2cb38da97df6b ok 1 90f3f2d6e1ec2ee9
722.ics 30a2c76a32eabb1b ok 1 27f77b5c300ecf96
723.ics 82f050e2a0a8c0c1 ok 1 69647cf98ac256f8
724.ics ce37675544f51e01 ok 1 69647cf98ac256f8
725.ics 3d1006f7c20ada2c ok 1 69647cf98ac256f8
726.ics 28762e9650f0fd78 ok 1 69647cf98ac256f8
727.ics 2afd434ebd425e0a ok 1 69647cf98ac256f8
728.ics 6133c08cb6544c97 ok 6 9c1f96706bfab1eb
729.ics bc59e1991ee181e4 ok 5 2e202f011e9d29f8
730.ics 0c2f78bd4d267a0d ok 6 3046acdb4f96075e
731.ics a6d0dc5be562eddc ok 1 5c193227d0552a50
732.ics e1feebd3cfcacdd5 ok 3 6921754655cfc20a
733.ics efc9f48b29118d25 ok 3 cd87d54edb39957a
734.ics ac3f7cf40f383167 error:BadRuleStringFormat 0 cbf29ce484222325
735.ics d8d96501a8149a07 ok 2 99ee1ab68ec1db71
736.ics 4fceea29e7932cd2 ok 6836 e108295017895a64
737.ics 3e165a289017d66e ok 4 55d89e683691c677
738.ics 4cac6dcecc2e798e ok 20 b888d9e63909dfcf
739.ics e6f42628a3e6080e ok 22 112519d546b34e51
740.ics 574e29e08bc42ec1 ok 3 0622512888663362
741.ics 8cc46adff8a9d8f4 ok 3 82d19d8312137506
742.ics fc3e6d4ef12fe6a5 ok 6 ba121c04c3a12734
743.ics ec17c8b958ae15f4 ok 7 21a938f9f5a0e58f
744.ics ac94aedaf1829032 ok 10 412a3e692d060563
745.ics d16183fb5e8a835e ok 1 5afdccfdc14fe21d
746.ics 5ccb09f316b9c6a2 ok 10 e36f0429ba453111
747.ics a5cfc9d62c010a1f ok 291 9f3242d0c42bb412
748.ics 0ef2f526ccca1021 ok 23 7040c24931cddc86
749.ics 4b392595d0edbe16 ok 2 fd90eddd4de9024a
750.ics 87bcab1920a70ead ok 0 cbf29ce484222325
751.ics 2a488cc1dd8731f4 ok 7 d10a2082f1a85e81
752.ics da0d6edc6d96ab94 ok 1 edcbd06f30ba46b9
753.ics 4caeb2fb77166dca ok 2 6aa50fe358ea6205
754.ics cedceeeeea3f3c2f ok 3 b7d5730e252e33ea
755.ics 28fc5a3df68e30ba ok 3 a7bf601460e47773
756.ics b239dcbf6af12ccb ok 3 d562a97fb6e51f44
757.ics 96168bfa64dcff4f ok 2 6aa50fe358ea6205
758.ics 20f6c8663c573d05 ok 25 62d51c3044cc7792
759.ics f759eb2df5f261db ok 3 786d040474d942ad
760.ics 8411850b8288e31a ok 268 616b67fa26387518
761.ics 35ebafd9d1736f52 ok 274 64428cf24ad284ca
762.ics 0eccecdb80751a73 ok 5689 ff869cbd63a8e0b7
763.ics bf63e3e0e8496949 ok 43 23b9e2c7a18b017c
764.ics d1153061f6b8c5cf ok 1 69647cf98ac256f8
765.ics beacf3a5602ed594 ok 3 b70ccb14098d7bee
766.ics 10980d3d2130ae4b ok 1 9ee45d8c7821c003
767.ics 583647509c97cc9c ok 15 838c4b2ae5988f9c
768.ics 6a92f0ddaf0325bf ok 7 06be95e452fc0134
769.ics 9cb744c2de061368 ok 2 2adeb454577fc5f9
770.ics 0772b2b7a80b8776 ok 5 db240aeada668522
771.ics 0f3eebe36de8b281 ok 8888 0dd82c04b2292634
772.ics 17a480a49fc29dd5 ok 2 10c7b7b6bc8adac9
773.ics 297ecca6676dabf4 ok 2 6aa50fe358ea6205
774.ics eab6c562e2a3ec9f ok 2 6aa50fe358ea6205
775.ics 28471e69cc8092a4 ok 2 e373a215dc75e38b
776.ics f1538900621f1a16 ok 2 e373a215dc75e38b
777.ics 9e1f7cadab0c81a2 ok 24 16d9a9ecb8721473
778.ics d4af0c001049891f ok 2016 c11f0c179e88517e
779.ics fdd80f4de12b3711 ok 1088 290d5223648c92fd
780.ics 3e0768e76181010d ok 1 efd84803450d17fb
781.ics 2cca870d5555c77b ok 1006 4c790e922461a85c
782.ics 023186caf4b67d5e ok 14011 6ffc25b2aa343246
783.ics e9368a0e9e8a2176 ok 12 e01b364c98f9efed
784.ics c85ae89645ffec9b ok 2 297344391ea26be8
785.ics c0e32216039c47df ok 1 f4b123e2460a8487
786.ics aab52d71a8cedbf8 ok 218 8f01e3acfcabaa1d
787.ics 349ece23c0dc08d2 ok 6 b3a25dfdb240bb92
788.ics 89cf7a6c80bd796f ok 193 74596cf4d80e689f
789.ics 553110fa8ed4cdbf ok 1 1c36c940d09c9962
790.ics b9e08d574ded7732 ok 0 cbf29ce484222325
791.ics 50c08b70b116db7a ok 0 cbf29ce484222325
792.ics 7dc3676b95eb80e2 ok 0 cbf29ce484222325
793.ics 034bee0ba9e5db0e ok 0 cbf29ce484222325
794.ics c7508fd1ca20e44d ok 1 b32c732c89f57f31
795.ics 3dd4c88fcab1e35f ok 7608 96f5b735b989472f
796.ics 0a68c18bac0cfd42 ok 2537 67330fd380f423c3
797.ics 450e5c0cc22cf2f4 ok 1189 99a7eac9b556d10f
798.ics fb2136cdbb84577f ok 594 aebc890c9fdf4740
799.ics 5afe0d79a88aba44 ok 8 0d48144f0ce8137c
800.ics 4981bf0838196f60 ok 12 988d6f9cb9dedbcc
801.ics 2d3e9f9727a3bf1b ok 1008 ea47b74990aa84c6
802.ics 2f5e636d5a90da89 ok 7 32b760155622bd78
803.ics e13c867b61df1567 ok 7 e8535002c799644b
804.ics 1922a8146d206c32 ok 6 fc88fadebbbe17ff
805.ics f499cbaca38bd4d4 ok 1 82147abca4dcd2cc
806.ics e54a8f93e39c4f46 ok 3 a7faf578c55eb002
807.ics 8c7b555be40ae482 ok 3 82d19d8312137506
808.ics 7602ea65ba178c05 ok 2 e0cbf4685a8720b9
809.ics dea2bcf376126cd0 ok 1 5afdccfdc14fe21d
810.ics cc1ba9b89baf45bc ok 1 868a764113cb6c0a
811.ics aed07410585ba217 ok 1 868a764113cb6c0a
812.ics 63c548bd666bc406 ok 1 868a764113cb6c0a
813.ics f612672a7e69b8e7 ok 11 4077473a046fd702
814.ics aad6f5b8bcc19a61 ok 1036 dede2afa839b0042
815.ics d330836fa8591c6f ok 1 868a764113cb6c0a
816.ics b49c4bb18f173cb8 ok 901 38ff22c41375cbcf
817.ics 031ae281ff07689d ok 5 37d8e1b1fc624bdd
818.ics c5997fffc1c15e6d ok 1 868a764113cb6c0a
819.ics 640d9a58dbe34b1b ok 1 2d2b3e83024f585a
820.ics 3c39bd144339e462 ok 0 cbf29ce484222325
821.ics b95cede9d9783685 ok 0 cbf29ce484222325
822.ics da712a8da645cad8 ok 16 70b6dd6fdf71fa32
823.ics 991cf3803efa7d41 ok 16 a1c946ab7f767d68
824.ics 1684b97c93db5249 ok 1 c109468a62f92cb4
825.ics 026b3853a3af5245 ok 1 e6a8fcbd7848e6ad
826.ics 47331df8dfddb740 error:KeyError 0 cbf29ce484222325
827.ics f0ecda949bc14071 error:KeyError 0 cbf29ce484222325
828.ics 6a5cccecf538bbfa ok 1 95be385b598bc055
829.ics 4fb29234792859a8 ok 1 95be385b598bc055
830.ics de90da240aaad311 ok 1 05e2bd0526b289ef
831.ics 69365700ed714632 ok 1 9b171831c1e0bf17
832.ics 4bdbbcbd8ad36cf4 ok 1 3e24c5997d480b23
833.ics eb70e87a613ce782 ok 1 3e24c5997d480b23
834.ics 0d0c1aa48118d948 ok 0 cbf29ce484222325
835.ics 53b37ea28f10aebc ok 1 dc5d898a237f102e
836.ics 3a930e12a0f9872a ok 1 c3ee8e8c5199d4b9
837.ics e7a820ae6d552c38 ok 1 ca3bd30087b2258a
838.ics a8e98d97cabf23ba ok 1 97284607ab3bca2e
839.ics a7be2eb77d76b3b9 ok 1 97284607ab3bca2e
840.ics 56d1d01bb145ad71 ok 1 bb1121f55a82f20b
841.ics fe9335f3b898a608 ok 1 97284607ab3bca2e
842.ics 6da70a57461aa420 ok 1 3c07288a58da3517
843.ics 191f7494e287bad0 ok 1 3c07288a58da3517
844.ics a222b13cfe032e59 ok 1 5bd5a528e6ac7d8e
845.ics afd00ed3c69b9ce3 ok 1 3c07288a58da3517
846.ics 9291e9a07a0e57bd ok 1 04a0fc04ea5f5b8f
847.ics a45388d35a884a07 ok 1 04a0fc04ea5f5b8f
848.ics 98d783d10a8c5831 ok 1 a5ee44480a8625aa
849.ics c107253390ee07a0 ok 1 f870a1f167a36305
850.ics 72d0d29d02510403 ok 1 607f41acfabc0778
851.ics 6bf2925cf564dfb4 ok 529 4d76e8c020eecea4
852.ics e8a4bb7dbaa28724 ok 1270 61eb11ba500db1da
853.ics 2728f6bcde984b00 ok 5 32f22b4cf320cf12
854.ics 4c5bbc8ff973e2fc ok 5 d5e03bfc9c14948b
855.ics 6d4ad1674697e5d5 ok 93 cba57586ef8fa5c3
856.ics 586e2d185071bd85 ok 1 444eb43ce01de9a3
857.ics 1c7b17cd9a32b5ee ok 1 a38b0080deaf6909
858.ics fb1b021e67cb0ad6 ok 5 7b216264419c9f09
859.ics 15b2b388a18865ed ok 4 2b9ba0612a1f78f0
860.ics 08b6da197cc577fc ok 25 68c7b402aa07ea4f
861.ics cef0304e77518201 ok 24 15d8f852ec0dcd06
862.ics b65a02a3ec380e2a ok 1 f4dd1fb07ec017d2
863.ics 2b33db87e9debd6c ok 8112 e41813f63c343fcb
864.ics c47691a14f1f57b8 ok 8109 0c37881f2c4ab26b
865.ics 077813c0dee2d788 ok 3 b26299cd0a73874f
866.ics d85632d312e94c64 ok 1 d2e63dd53b7bf272
867.ics f04249f76c8ebf4c ok 1 d2e63dd53b7bf272
868.ics 45c59a8944a818dc ok 1 9e77db31d3fec7f5
869.ics adff18ef3eca755e ok 1 4cdca66c725fcfe0
870.ics e5a8860caed113d0 ok 1 9bb8db6c95308179
871.ics ba15f33f2fbfd874 ok 9477 0b3de491e43df6c2
872.ics 9bf0a3ab0d230ed8 ok 9477 0b3de491e43df6c2
873.ics b462d4df83bcfa13 ok 1 a0845d7f5b902184
874.ics ce6eb73108d1ed9f ok 1 87f0d4172d641369
875.ics 8eeaa9d183d8c971 ok 1 126a75505c6de5c3
876.ics 033a0f6d39362c0b ok 1 3f2fb08ae646ed8d
877.ics 7136ab5e6fcc9a8a ok 1 d359b7fb970e5801
878.ics 18be12d75e31b2b7 ok 1 d359b7fb970e5801
879.ics 0d701c35d6b266c9 ok 1 d359b7fb970e5801
880.ics f3c81a6354c1fe35 ok 1 64db1f205d050f44
881.ics ab65a3a7ebbece62 ok 1 a7a96e1cf4276a28
882.ics 3f85c28319163949 ok 11 695df59544dbb507
883.ics 37310ec6b722c5fa ok 1 9eec40cc6df5eab2
884.ics ba3f1f6ea6952f8a ok 103 f4299a5e4f17daf5
885.ics c62264671c2a31bf ok 1 a8c4974338bf734e
886.ics 5ea76732fec1420c ok 1 4e76743388eb2459
887.ics 275bec79cc6ede5d ok 1 bb2a4045f8dce025
888.ics fb7abb1650101b15 ok 1 bf6072b0d944cb60
889.ics 3eb02ec2b81d8a19 ok 1 bf6072b0d944cb60
890.ics 9e4ea3307c304591 ok 1 bd23172e789f790a
891.ics 4e480767930d32cb ok 1 cf6e1db1f503259f
892.ics a23223d4e45dfa3b ok 1 d31e94e4cef2a8c3
893.ics 9dc1234eb2e0965f ok 1 bd23172e789f790a
894.ics 05cdd630ad4fdc71 ok 23 d18c59cdea97988b
895.ics b1e36adde4788cc1 ok 9503 8411621df1d14551
896.ics bcb1928849221095 ok 6 72e84d1cd08b5f57
897.ics 0a8544f02c2d4225 ok 1394 98e27265e5ed6073
898.ics f60c70ef09b030cd ok 1394 98e27265e5ed6073
899.ics 8831bfbe4ec3dde8 ok 27 f8789ca60c94500a
900.ics e66d97bfdda50762 ok 22 812548b3edeeee6a
901.ics f37bce4529ced194 ok 30 d2097514f36bd66d
902.ics 995586aa158a81cf ok 1 a1c3e6949555a201
903.ics 52bb86032cc73ff8 ok 27 96593b1ef427a4e6
904.ics 976ab4ede41254f8 ok 1 bf7987d99f92de52
905.ics 9cdeef9c24967606 ok 1 250c2d921792efe3
906.ics 49109250444798a0 ok 1 efecbf26c154ca31
907.ics ff63cdb1e30be780 ok 6 735a4927227cd622
908.ics a22ca8584b1b7d61 ok 1 8ac74588b099f3c1
909.ics ef0bce2c5f144b99 ok 1 9e3a49a36a8ce98b
910.ics 378b0133ff7d290b ok 2 ae179b370459052d
911.ics 97b76d02f5859364 ok 1 6d4f3a31520c07cb
912.ics bc27e7de7c9dbf44 ok 0 cbf29ce484222325
913.ics 988ddb731b0619a2 ok 0 cbf29ce484222325
914.ics bc14770b8f29a7ce ok 0 cbf29ce484222325
915.ics 42535403d0cc6f09 ok 0 cbf29ce484222325
916.ics 35847f696a50739c ok 0 cbf29ce484222325
917.ics 37261d0cc7c108b6 ok 0 cbf29ce484222325
918.ics 05ac7385c4f473b7 ok 0 cbf29ce484222325
919.ics be426c0f65a21da8 ok 0 cbf29ce484222325
920.ics 6d6e671d916e865f ok 0 cbf29ce484222325
921.ics acae9cb728bf5e52 ok 1 4b41452927a012e6
922.ics ff2ba7b3e990d3aa ok 566 eeb4068d3c07ad37
923.ics c5f6dcd81dfd129b ok 8 f9604aa03c24024f
924.ics c4bc61395a44e256 ok 0 cbf29ce484222325
925.ics cd71b1dd2e52ed30 ok 0 cbf29ce484222325
926.ics 7debef77bdc5c3ef error:ValueError 0 cbf29ce484222325
927.ics aab91aaba2507550 error:ValueError 0 cbf29ce484222325
928.ics 02242908a6a129a2 ok 0 cbf29ce484222325
929.ics 1a9f1064c25fabeb error:ValueError 0 cbf29ce484222325
930.ics 591fb55c91ad0ca9 ok 0 cbf29ce484222325
931.ics 4f70f013533e19f5 ok 0 cbf29ce484222325
932.ics 05101f968ffda811 ok 0 cbf29ce484222325
933.ics 01ebd4b5daae19c9 error:ValueError 0 cbf29ce484222325
934.ics 8477ec0a045885da error:ValueError 0 cbf29ce484222325
935.ics 9adb12acfbb4b982 ok 0 cbf29ce484222325
936.ics 044b0dbd5185d441 ok 0 cbf29ce484222325
937.ics af953dee4356da7a ok 0 cbf29ce484222325
938.ics 548f63b1d08ad507 ok 0 cbf29ce484222325
939.ics a1b2d8c2033c81b9 error:ValueError 0 cbf29ce484222325
940.ics 2f52823098255dfa ok 0 cbf29ce484222325
941.ics a88385dae7706d8f error:ValueError 0 cbf29ce484222325
942.ics ab40df869b2debba ok 0 cbf29ce484222325
943.ics ecce113b82510df8 ok 0 cbf29ce484222325
944.ics 302fcee46697194a ok 0 cbf29ce484222325
945.ics 68be832085921efe ok 0 cbf29ce484222325
946.ics 09334fc61f80b879 ok 0 cbf29ce484222325
947.ics d4d63c82fa120835 ok 0 cbf29ce484222325
948.ics 392b99b52bbce406 ok 0 cbf29ce484222325
949.ics 8a7de28767d626a0 error:ValueError 0 cbf29ce484222325
950.ics d6d598b40e4fed0c ok 0 cbf29ce484222325
951.ics 485f5725dec8f823 ok 0 cbf29ce484222325
952.ics f0e9e7f5ec598e33 ok 0 cbf29ce484222325
953.ics aa53a141ad75a5b0 ok 0 cbf29ce484222325
954.ics 074624071edfb860 error:ValueError 0 cbf29ce484222325
955.ics b63332d9c721a3df ok 0 cbf29ce484222325
956.ics 8575a27d40970d21 ok 0 cbf29ce484222325
957.ics dca16883f08cd0cd ok 0 cbf29ce484222325
958.ics 15dd0c21fe48969b ok 0 cbf29ce484222325
959.ics 723501886a1f509f error:ValueError 0 cbf29ce484222325
960.ics 008456dd497b8661 error:ValueError 0 cbf29ce484222325
961.ics 85fbb8d7a8fcf6c1 ok 0 cbf29ce484222325
962.ics 613fa184e7701418 ok 0 cbf29ce484222325
963.ics 2761329ee0bf237c ok 0 cbf29ce484222325
964.ics 0dcb3f1a53e9ee39 ok 0 cbf29ce484222325
965.ics a3508bf071abe2bd ok 0 cbf29ce484222325
966.ics 3af03fcb54822e32 ok 0 cbf29ce484222325
967.ics c8987033406f31d3 error:ValueError 0 cbf29ce484222325
968.ics 42d6492c2246294a error:ValueError 0 cbf29ce484222325
969.ics 4da9ee3660464fa6 ok 0 cbf29ce484222325
970.ics e481ce5ec5e39ae4 ok 0 cbf29ce484222325
971.ics 8f7a7cef2c202f17 ok 0 cbf29ce484222325
972.ics 8b66333413ea6192 ok 0 cbf29ce484222325
973.ics 6b81e0703c7e1fa0 error:ValueError 0 cbf29ce484222325
974.ics 97a90c23ece697ac ok 0 cbf29ce484222325
975.ics bdf195d53867f66c ok 0 cbf29ce484222325
976.ics 0a57c4b2827b0159 ok 0 cbf29ce484222325
977.ics 770915221136265d ok 0 cbf29ce484222325
978.ics 2293ae73436476bf ok 0 cbf29ce484222325
979.ics 2c962830c3840818 ok 0 cbf29ce484222325
980.ics 09daf65542eeba3c ok 0 cbf29ce484222325
981.ics 7da0206decd3b1f8 ok 0 cbf29ce484222325
982.ics f5e281c5ed98409a ok 0 cbf29ce484222325
983.ics 5dbb53a96328db1b ok 0 cbf29ce484222325
984.ics 69a20cb92edc45f0 error:ValueError 0 cbf29ce484222325
985.ics 212e1d9b60f0a320 error:ValueError 0 cbf29ce484222325
986.ics e56adc32035a6b55 error:KeyError 0 cbf29ce484222325
987.ics 1a0c3c8062b58a4e ok 2730 8b5feaf5eae1399b
988.ics 4500fc021aae3b05 error:ValueError 0 cbf29ce484222325
989.ics dbf6132be095341d error:ValueError 0 cbf29ce484222325
990.ics 065b1a35889ebe55 ok 48 74d7ba63f07448a8
991.ics 203a9f86bb57a40f error:ValueError 0 cbf29ce484222325
992.ics b0891c8a28a226b3 error:BrokenCalendarProperty 0 cbf29ce484222325
993.ics 9505a1d23a18ec29 error:BrokenCalendarProperty 0 cbf29ce484222325
994.ics 263b08a961b93bcf error:AttributeError 1 fd208164eb8c65f8
995.ics 7d85f50f10d38fbc error:ValueError 0 cbf29ce484222325
996.ics e9493ac936a55f9f error:ValueError 0 cbf29ce484222325
997.ics 6ffd4e40a3c09830 ok 34 3f4b58a12e586034
▾Apimdav/tests/expand.rs
@@ -0,0 +1,259 @@
//! Cases the corpus oracle cannot check, and the bugs that made calcard's own
//! expansion unusable.
use chrono::{DateTime, Utc};
use pimdav::calcard::icalendar::ICalendar;
use pimdav::expand::expand;
use pimdav::zone::Zone;
fn utc(s: &str) -> DateTime<Utc> {
format!("{s}:00Z").parse().unwrap()
}
fn fmt(t: DateTime<Utc>) -> String {
t.format("%Y-%m-%dT%H:%M").to_string()
}
/// `start/end` pairs in UTC of the instances overlapping `lo..hi`.
fn instances(body: &str, lo: &str, hi: &str) -> Vec<String> {
let ics = format!("BEGIN:VCALENDAR\r\nVERSION:2.0\r\n{body}END:VCALENDAR\r\n");
let cal = ICalendar::parse(&ics).unwrap();
let out = expand(&cal, utc(lo)..utc(hi), Zone::Utc);
assert!(!out.truncated);
out.instances
.iter()
.map(|i| format!("{}/{}", fmt(i.start), fmt(i.end)))
.collect()
}
fn event(uid: &str, props: &str) -> String {
format!("BEGIN:VEVENT\r\nUID:{uid}\r\n{props}END:VEVENT\r\n")
}
#[test]
fn utc_until_east_of_utc_keeps_last_instance() {
let body = event(
"a",
"SEQUENCE:1\r\nDTSTART;TZID=Europe/Berlin:20190318T040000\r\nDTEND;TZID=Europe/Berlin:20190318T050000\r\nRRULE:FREQ=DAILY;UNTIL=20190320T030000Z\r\n",
) + &event(
"a",
"SEQUENCE:2\r\nRECURRENCE-ID;TZID=Europe/Berlin:20190319T040000\r\nDTSTART;TZID=Europe/Berlin:20190319T100000\r\nDTEND;TZID=Europe/Berlin:20190319T110000\r\n",
);
// The override has a higher SEQUENCE than the master and still replaces
// the instance.
assert_eq!(
instances(&body, "2019-03-01T00:00", "2019-04-01T00:00"),
[
"2019-03-18T03:00/2019-03-18T04:00",
"2019-03-19T09:00/2019-03-19T10:00",
"2019-03-20T03:00/2019-03-20T04:00",
]
);
}
#[test]
fn rdate_is_deduplicated_and_excluded() {
let body = event(
"a",
"DTSTART:20240101T100000Z\r\nDTEND:20240101T110000Z\r\nRRULE:FREQ=DAILY;COUNT=3\r\nRDATE:20240101T100000Z,20240110T100000Z\r\nEXDATE:20240110T100000Z\r\n",
);
assert_eq!(
instances(&body, "2024-01-01T00:00", "2024-02-01T00:00"),
[
"2024-01-01T10:00/2024-01-01T11:00",
"2024-01-02T10:00/2024-01-02T11:00",
"2024-01-03T10:00/2024-01-03T11:00",
]
);
}
#[test]
fn dst_gap_and_fold() {
let body = event(
"a",
"DTSTART;TZID=Europe/Berlin:20240330T023000\r\nDURATION:PT30M\r\nRRULE:FREQ=DAILY;COUNT=2\r\n",
) + &event(
"b",
"DTSTART;TZID=Europe/Berlin:20241027T023000\r\nDURATION:PT30M\r\n",
);
// 02:30 on 03-31 does not exist and takes the offset before the gap.
// 02:30 on 10-27 happens twice and takes the first.
assert_eq!(
instances(&body, "2024-03-01T00:00", "2024-11-01T00:00"),
[
"2024-03-30T01:30/2024-03-30T02:00",
"2024-03-31T01:30/2024-03-31T02:00",
"2024-10-27T00:30/2024-10-27T01:00",
]
);
}
#[test]
fn duration_days_are_nominal_and_dtend_is_exact() {
let nominal = event(
"a",
"DTSTART;TZID=Europe/Berlin:20240330T120000\r\nDURATION:P1D\r\n",
);
let exact = event(
"a",
"DTSTART;TZID=Europe/Berlin:20240330T120000\r\nDTEND;TZID=Europe/Berlin:20240331T120000\r\nRRULE:FREQ=DAILY;COUNT=2\r\n",
);
assert_eq!(
instances(&nominal, "2024-03-01T00:00", "2024-04-01T00:00"),
["2024-03-30T11:00/2024-03-31T10:00"]
);
// The second instance keeps the 23 hours of the first.
assert_eq!(
instances(&exact, "2024-03-01T00:00", "2024-04-02T00:00"),
[
"2024-03-30T11:00/2024-03-31T10:00",
"2024-03-31T10:00/2024-04-01T09:00",
]
);
}
#[test]
fn custom_vtimezone_rules() {
let body = "BEGIN:VTIMEZONE\r\nTZID:Customized Time Zone\r\nBEGIN:STANDARD\r\nDTSTART:16010101T020000\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0500\r\nRRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=11\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nDTSTART:16010101T020000\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0400\r\nRRULE:FREQ=YEARLY;BYDAY=2SU;BYMONTH=3\r\nEND:DAYLIGHT\r\nEND:VTIMEZONE\r\n".to_string()
+ &event("a",
"DTSTART;TZID=Customized Time Zone:20240301T090000\r\nDURATION:PT1H\r\nRRULE:FREQ=WEEKLY;COUNT=2\r\n",
);
assert_eq!(
instances(&body, "2024-03-01T00:00", "2024-04-01T00:00"),
[
"2024-03-01T14:00/2024-03-01T15:00",
"2024-03-08T14:00/2024-03-08T15:00",
]
);
let body = body.replace("20240301T090000", "20240315T090000");
assert_eq!(
instances(&body, "2024-03-01T00:00", "2024-04-01T00:00"),
[
"2024-03-15T13:00/2024-03-15T14:00",
"2024-03-22T13:00/2024-03-22T14:00",
]
);
}
#[test]
fn this_and_future_moves_later_instances() {
let body = event(
"a",
"DTSTART:20240101T100000Z\r\nDURATION:PT1H\r\nRRULE:FREQ=DAILY;COUNT=5\r\n",
) + &event(
"a",
"RECURRENCE-ID;RANGE=THISANDFUTURE:20240103T100000Z\r\nDTSTART:20240103T120000Z\r\nDURATION:PT2H\r\n",
) + &event(
"a",
"RECURRENCE-ID:20240105T100000Z\r\nDTSTART:20240105T150000Z\r\nDURATION:PT1H\r\n",
);
assert_eq!(
instances(&body, "2024-01-01T00:00", "2024-02-01T00:00"),
[
"2024-01-01T10:00/2024-01-01T11:00",
"2024-01-02T10:00/2024-01-02T11:00",
"2024-01-03T12:00/2024-01-03T14:00",
"2024-01-04T12:00/2024-01-04T14:00",
"2024-01-05T15:00/2024-01-05T16:00",
]
);
}
#[test]
fn exdate_forms() {
// A DATE EXDATE removes the day of a DATE-TIME series. A floating EXDATE
// is read in the series zone. An override of an excluded instance goes too.
let body = event(
"a",
"DTSTART;TZID=Europe/Berlin:20240101T100000\r\nDURATION:PT1H\r\nRRULE:FREQ=DAILY;COUNT=4\r\nEXDATE;VALUE=DATE:20240102\r\nEXDATE:20240103T100000\r\n",
) + &event(
"a",
"RECURRENCE-ID;TZID=Europe/Berlin:20240103T100000\r\nDTSTART;TZID=Europe/Berlin:20240103T110000\r\nDURATION:PT1H\r\n",
);
assert_eq!(
instances(&body, "2024-01-01T00:00", "2024-02-01T00:00"),
[
"2024-01-01T09:00/2024-01-01T10:00",
"2024-01-04T09:00/2024-01-04T10:00",
]
);
}
#[test]
fn window_overlap() {
let body = event(
"a",
"DTSTART;VALUE=DATE:20240101\r\nDTEND;VALUE=DATE:20240104\r\n",
) + &event("b", "DTSTART:20240105T000000Z\r\n");
// The three-day event starts before the window and overlaps it. The
// zero-length one sits exactly on the window end.
assert_eq!(
instances(&body, "2024-01-03T00:00", "2024-01-05T00:00"),
["2024-01-01T00:00/2024-01-04T00:00"]
);
}
#[test]
fn todo_uses_due() {
let body = "BEGIN:VTODO\r\nUID:t\r\nDTSTART:20240101T100000Z\r\nDUE:20240101T120000Z\r\nRRULE:FREQ=WEEKLY;COUNT=2\r\nEND:VTODO\r\n";
assert_eq!(
instances(body, "2024-01-01T00:00", "2024-02-01T00:00"),
[
"2024-01-01T10:00/2024-01-01T12:00",
"2024-01-08T10:00/2024-01-08T12:00",
]
);
}
#[test]
fn mixed_numbered_and_plain_weekdays() {
let body = event(
"a",
"DTSTART:20240102T100000Z\r\nRRULE:FREQ=MONTHLY;COUNT=4;BYDAY=1MO,TU\r\n",
);
assert_eq!(
instances(&body, "2024-01-01T00:00", "2024-02-01T00:00"),
[
"2024-01-02T10:00/2024-01-02T10:00",
"2024-01-09T10:00/2024-01-09T10:00",
"2024-01-16T10:00/2024-01-16T10:00",
"2024-01-23T10:00/2024-01-23T10:00",
]
);
}
#[test]
fn unsupported_rules_leave_dtstart_only() {
for rule in [
"FREQ=MONTHLY;COUNT=3;BYMONTH=13",
"FREQ=YEARLY;COUNT=3;BYMONTH=5L",
"RSCALE=CHINESE;FREQ=YEARLY;COUNT=3",
"RSCALE=GREGORIAN;FREQ=YEARLY;SKIP=FORWARD;COUNT=3",
"FREQ=YEARLY;UNTIL=20243456T000000Z",
] {
let body = event(
"a",
&format!("DTSTART:20240101T100000Z\r\nRRULE:{rule}\r\n"),
);
assert_eq!(
instances(&body, "2024-01-01T00:00", "2030-01-01T00:00"),
["2024-01-01T10:00/2024-01-01T10:00"],
"{rule}"
);
}
}
#[test]
fn runaway_rule_is_truncated() {
let ics = format!(
"BEGIN:VCALENDAR\r\n{}END:VCALENDAR\r\n",
event("a", "DTSTART:20000101T000000Z\r\nRRULE:FREQ=MINUTELY\r\n")
);
let cal = ICalendar::parse(&ics).unwrap();
let out = expand(
&cal,
utc("2020-01-01T00:00")..utc("2020-01-02T00:00"),
Zone::Utc,
);
assert!(out.truncated);
}