use super::*;
#[test]
fn test_readme_example() {
let ics_content = "BEGIN:VCALENDAR\r\n\
BEGIN:VEVENT\r\n\
DTSTART;TZID=Europe/Rome:20230101T090000\r\n\
LAST-MODIFIED:20230101T170000Z\r\n\
CREATED:20230101T170000Z\r\n\
DTSTAMP:20230101T170000Z\r\n\
SUMMARY:Rust Pair Programming\r\n\
SEQUENCE:0\r\n\
RRULE:FREQ=WEEKLY;BYDAY=MO,WE,FR;COUNT=10\r\n\
END:VEVENT\r\n\
END:VCALENDAR";
let cal = VCalendar::try_from(ics_content).unwrap();
assert_eq!(cal.events.len(), 1);
let event = &cal.events[0];
assert_eq!(event.summary, "Rust Pair Programming");
let occurrences: Vec<_> = event.into_iter().collect();
assert_eq!(occurrences.len(), 10);
}
#[test]
fn test_bug1_monthly_recurrence_december() {
let ics_content = "BEGIN:VCALENDAR\r\n\
BEGIN:VEVENT\r\n\
DTSTART;VALUE=DATE:20221105\r\n\
LAST-MODIFIED:20221105T170000Z\r\n\
CREATED:20221105T170000Z\r\n\
DTSTAMP:20221105T170000Z\r\n\
SUMMARY:Monthly Team Sync\r\n\
SEQUENCE:0\r\n\
RRULE:FREQ=MONTHLY;BYMONTHDAY=5;COUNT=3\r\n\
END:VEVENT\r\n\
END:VCALENDAR";
let cal = VCalendar::try_from(ics_content).unwrap();
let event = &cal.events[0];
let occurrences: Vec<_> = event.into_iter().collect();
assert_eq!(occurrences.len(), 3);
assert_eq!(occurrences[0].start.year(), 2022);
assert_eq!(occurrences[0].start.month(), 11);
assert_eq!(
occurrences[1].start.year(),
2022,
"Second occurrence should be in 2022 (December)"
);
assert_eq!(
occurrences[1].start.month(),
12,
"Second occurrence should be December (12)"
);
assert_eq!(occurrences[2].start.year(), 2023);
assert_eq!(occurrences[2].start.month(), 1);
}
#[test]
fn test_bug2_double_interval_monthly() {
let ics_content = "BEGIN:VCALENDAR\r\n\
BEGIN:VEVENT\r\n\
DTSTART;VALUE=DATE:20221105\r\n\
LAST-MODIFIED:20221105T170000Z\r\n\
CREATED:20221105T170000Z\r\n\
DTSTAMP:20221105T170000Z\r\n\
SUMMARY:Bi-Monthly Team Sync\r\n\
SEQUENCE:0\r\n\
RRULE:FREQ=MONTHLY;INTERVAL=2;BYMONTHDAY=5;COUNT=3\r\n\
END:VEVENT\r\n\
END:VCALENDAR";
let cal = VCalendar::try_from(ics_content).unwrap();
let event = &cal.events[0];
let occurrences: Vec<_> = event.into_iter().collect();
assert_eq!(occurrences.len(), 3);
assert_eq!(occurrences[0].start.year(), 2022);
assert_eq!(occurrences[0].start.month(), 11);
assert_eq!(
occurrences[1].start.year(),
2023,
"Second occurrence should be in 2023 (January)"
);
assert_eq!(
occurrences[1].start.month(),
1,
"Second occurrence should be January (1)"
);
assert_eq!(occurrences[2].start.year(), 2023);
assert_eq!(
occurrences[2].start.month(),
3,
"Third occurrence should be March (3)"
);
}
#[test]
fn test_bug3_invalid_timezone_fails_safely() {
let ics_content = "BEGIN:VCALENDAR\r\n\
BEGIN:VEVENT\r\n\
DTSTART;TZID=Invalid/Timezone:20230101T090000\r\n\
LAST-MODIFIED:20230101T170000Z\r\n\
CREATED:20230101T170000Z\r\n\
DTSTAMP:20230101T170000Z\r\n\
SUMMARY:Invalid TZID test\r\n\
SEQUENCE:0\r\n\
END:VEVENT\r\n\
END:VCALENDAR";
let result = VCalendar::try_from(ics_content);
assert!(
result.is_err(),
"Expected an error instead of panic or successful parse"
);
}
#[test]
fn test_bug3_missing_datetime_fails_safely() {
let ics_content = "BEGIN:VCALENDAR\r\n\
BEGIN:VEVENT\r\n\
DTSTART;TZID=Europe/Rome\r\n\
LAST-MODIFIED:20230101T170000Z\r\n\
CREATED:20230101T170000Z\r\n\
DTSTAMP:20230101T170000Z\r\n\
SUMMARY:Missing datetime test\r\n\
SEQUENCE:0\r\n\
END:VEVENT\r\n\
END:VCALENDAR";
let result = VCalendar::try_from(ics_content);
assert!(
result.is_err(),
"Expected an error instead of panic or successful parse"
);
}
#[test]
fn test_bug4_yearly_by_month_by_day() {
let ics_content = "BEGIN:VCALENDAR\r\n\
BEGIN:VEVENT\r\n\
DTSTART;TZID=Europe/Rome:20221124T090000\r\n\
LAST-MODIFIED:20221124T170000Z\r\n\
CREATED:20221124T170000Z\r\n\
DTSTAMP:20221124T170000Z\r\n\
SUMMARY:Thanksgiving Dinner\r\n\
SEQUENCE:0\r\n\
RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=4TH;COUNT=3\r\n\
END:VEVENT\r\n\
END:VCALENDAR";
let cal = VCalendar::try_from(ics_content).unwrap();
assert_eq!(cal.events.len(), 1);
let event = &cal.events[0];
let occurrences: Vec<_> = event.into_iter().collect();
assert_eq!(occurrences.len(), 3);
assert_eq!(occurrences[0].start.year(), 2022);
assert_eq!(occurrences[0].start.month(), 11);
assert_eq!(occurrences[0].start.day(), 24);
assert_eq!(occurrences[1].start.year(), 2023);
assert_eq!(occurrences[1].start.month(), 11);
assert_eq!(occurrences[1].start.day(), 23);
assert_eq!(occurrences[2].start.year(), 2024);
assert_eq!(occurrences[2].start.month(), 11);
assert_eq!(occurrences[2].start.day(), 28);
}
#[test]
fn test_bug5_exdate_multi_value() {
let ics_content = "BEGIN:VCALENDAR\r\n\
BEGIN:VEVENT\r\n\
DTSTART;TZID=Europe/Rome:20230101T090000\r\n\
LAST-MODIFIED:20230101T170000Z\r\n\
CREATED:20230101T170000Z\r\n\
DTSTAMP:20230101T170000Z\r\n\
SUMMARY:Daily Standup\r\n\
SEQUENCE:0\r\n\
RRULE:FREQ=DAILY;COUNT=5\r\n\
EXDATE;TZID=Europe/Rome:20230102T090000,20230104T090000\r\n\
END:VEVENT\r\n\
END:VCALENDAR";
let cal = VCalendar::try_from(ics_content).unwrap();
assert_eq!(cal.events.len(), 1);
let event = &cal.events[0];
let occurrences: Vec<_> = event.into_iter().collect();
assert_eq!(occurrences.len(), 5, "Expected exactly 5 occurrences total");
assert_eq!(occurrences[0].start.year(), 2023);
assert_eq!(occurrences[0].start.month(), 1);
assert_eq!(occurrences[0].start.day(), 1);
assert_eq!(occurrences[1].start.year(), 2023);
assert_eq!(occurrences[1].start.month(), 1);
assert_eq!(occurrences[1].start.day(), 3);
assert_eq!(occurrences[2].start.year(), 2023);
assert_eq!(occurrences[2].start.month(), 1);
assert_eq!(occurrences[2].start.day(), 5);
assert_eq!(occurrences[3].start.year(), 2023);
assert_eq!(occurrences[3].start.month(), 1);
assert_eq!(occurrences[3].start.day(), 6);
assert_eq!(occurrences[4].start.year(), 2023);
assert_eq!(occurrences[4].start.month(), 1);
assert_eq!(occurrences[4].start.day(), 7);
}
#[test]
fn test_bug6_missing_begin_tag_fails_safely() {
let ics_content = "INVALID CALENDAR CONTENT\r\nSUMMARY:Test";
let result = VCalendar::try_from(ics_content);
assert!(
result.is_err(),
"Expected an error instead of panic or successful parse"
);
}
#[test]
fn test_bug7_lf_line_endings_supported() {
let ics_content = "BEGIN:VCALENDAR\n\
BEGIN:VEVENT\n\
DTSTART;TZID=Europe/Rome:20230101T090000\n\
LAST-MODIFIED:20230101T170000Z\n\
CREATED:20230101T170000Z\n\
DTSTAMP:20230101T170000Z\n\
SUMMARY:LF Line Ending Test\n\
SEQUENCE:0\n\
END:VEVENT\n\
END:VCALENDAR";
let cal = VCalendar::try_from(ics_content).unwrap();
assert_eq!(cal.events.len(), 1);
assert_eq!(cal.events[0].summary, "LF Line Ending Test");
}
#[test]
fn test_bug8_tab_line_folding_supported() {
let ics_content = "BEGIN:VCALENDAR\r\n\
BEGIN:VEVENT\r\n\
DTSTART;TZID=Europe/Rome:20230101T090000\r\n\
LAST-MODIFIED:20230101T170000Z\r\n\
CREATED:20230101T170000Z\r\n\
DTSTAMP:20230101T170000Z\r\n\
SUMMARY:LF Line Ending \r\n\
\twith Tab Folding\r\n\
SEQUENCE:0\r\n\
END:VEVENT\r\n\
END:VCALENDAR";
let cal = VCalendar::try_from(ics_content).unwrap();
assert_eq!(cal.events.len(), 1);
assert_eq!(cal.events[0].summary, "LF Line Ending with Tab Folding");
}
#[test]
fn test_bug9_month_skipping_for_end_of_month_dates() {
let ics_content = "BEGIN:VCALENDAR\r\n\
BEGIN:VEVENT\r\n\
DTSTART;VALUE=DATE:20230131\r\n\
LAST-MODIFIED:20230131T170000Z\r\n\
CREATED:20230131T170000Z\r\n\
DTSTAMP:20230131T170000Z\r\n\
SUMMARY:End of Month Recurrence\r\n\
SEQUENCE:0\r\n\
RRULE:FREQ=MONTHLY;BYMONTHDAY=31;COUNT=5\r\n\
END:VEVENT\r\n\
END:VCALENDAR";
let cal = VCalendar::try_from(ics_content).unwrap();
assert_eq!(cal.events.len(), 1);
let event = &cal.events[0];
let occurrences: Vec<_> = event.into_iter().collect();
assert_eq!(occurrences.len(), 5);
assert_eq!(occurrences[0].start.year(), 2023);
assert_eq!(occurrences[0].start.month(), 1);
assert_eq!(occurrences[0].start.day(), 31);
assert_eq!(occurrences[1].start.year(), 2023);
assert_eq!(occurrences[1].start.month(), 2);
assert_eq!(occurrences[1].start.day(), 28);
assert_eq!(occurrences[2].start.year(), 2023);
assert_eq!(occurrences[2].start.month(), 3);
assert_eq!(occurrences[2].start.day(), 31);
assert_eq!(occurrences[3].start.year(), 2023);
assert_eq!(occurrences[3].start.month(), 4);
assert_eq!(occurrences[3].start.day(), 30);
assert_eq!(occurrences[4].start.year(), 2023);
assert_eq!(occurrences[4].start.month(), 5);
assert_eq!(occurrences[4].start.day(), 31);
}
#[test]
fn test_bug10_weekly_by_day_interval_skipping() {
let ics_content = "BEGIN:VCALENDAR\r\n\
BEGIN:VEVENT\r\n\
DTSTART;VALUE=DATE:20230103\r\n\
LAST-MODIFIED:20230103T170000Z\r\n\
CREATED:20230103T170000Z\r\n\
DTSTAMP:20230103T170000Z\r\n\
SUMMARY:Bi-Weekly Multi-Day Meeting\r\n\
SEQUENCE:0\r\n\
RRULE:FREQ=WEEKLY;INTERVAL=2;BYDAY=TU,TH;COUNT=4\r\n\
END:VEVENT\r\n\
END:VCALENDAR";
let cal = VCalendar::try_from(ics_content).unwrap();
assert_eq!(cal.events.len(), 1);
let event = &cal.events[0];
let occurrences: Vec<_> = event.into_iter().collect();
assert_eq!(occurrences.len(), 4);
assert_eq!(occurrences[0].start.year(), 2023);
assert_eq!(occurrences[0].start.month(), 1);
assert_eq!(occurrences[0].start.day(), 3);
assert_eq!(occurrences[1].start.year(), 2023);
assert_eq!(occurrences[1].start.month(), 1);
assert_eq!(occurrences[1].start.day(), 5);
assert_eq!(occurrences[2].start.year(), 2023);
assert_eq!(occurrences[2].start.month(), 1);
assert_eq!(occurrences[2].start.day(), 17);
assert_eq!(occurrences[3].start.year(), 2023);
assert_eq!(occurrences[3].start.month(), 1);
assert_eq!(occurrences[3].start.day(), 19);
}
#[test]
fn test_issue7_dst_transition_preserves_local_wall_clock_time() {
let ics_content = "BEGIN:VCALENDAR\r\n\
BEGIN:VEVENT\r\n\
DTSTART;TZID=Europe/Rome:20230325T090000\r\n\
LAST-MODIFIED:20230325T080000Z\r\n\
CREATED:20230325T080000Z\r\n\
DTSTAMP:20230325T080000Z\r\n\
SUMMARY:Morning Standup Across DST\r\n\
SEQUENCE:0\r\n\
RRULE:FREQ=DAILY;COUNT=2\r\n\
END:VEVENT\r\n\
END:VCALENDAR";
let cal = VCalendar::try_from(ics_content).unwrap();
let event = &cal.events[0];
let occurrences: Vec<_> = event.into_iter().collect();
assert_eq!(occurrences.len(), 2);
assert_eq!(occurrences[0].start.hour(), 8);
assert_eq!(occurrences[1].start.hour(), 7);
}
#[test]
fn test_issue8_monthly_by_day_same_month_and_day1() {
let ics_content = "BEGIN:VCALENDAR\r\n\
BEGIN:VEVENT\r\n\
DTSTART;VALUE=DATE:20230127\r\n\
LAST-MODIFIED:20230101T000000Z\r\n\
CREATED:20230101T000000Z\r\n\
DTSTAMP:20230101T000000Z\r\n\
SUMMARY:Monthly Mon/Wed/Fri\r\n\
SEQUENCE:0\r\n\
RRULE:FREQ=MONTHLY;BYDAY=MO,WE,FR;COUNT=3\r\n\
END:VEVENT\r\n\
END:VCALENDAR";
let cal = VCalendar::try_from(ics_content).unwrap();
let occurrences: Vec<_> = cal.events[0].into_iter().collect();
assert_eq!(occurrences.len(), 3);
assert_eq!(
(occurrences[0].start.month(), occurrences[0].start.day()),
(1, 27)
);
assert_eq!(
(occurrences[1].start.month(), occurrences[1].start.day()),
(1, 30)
);
assert_eq!(
(occurrences[2].start.month(), occurrences[2].start.day()),
(2, 1)
);
}
#[test]
fn test_issue9_yearly_by_month_month_day_and_leap_year() {
let ics_content = "BEGIN:VCALENDAR\r\n\
BEGIN:VEVENT\r\n\
DTSTART;VALUE=DATE:20240229\r\n\
LAST-MODIFIED:20240101T000000Z\r\n\
CREATED:20240101T000000Z\r\n\
DTSTAMP:20240101T000000Z\r\n\
SUMMARY:Leap Day Event\r\n\
SEQUENCE:0\r\n\
RRULE:FREQ=YEARLY;BYMONTH=2;BYMONTHDAY=29;COUNT=5\r\n\
END:VEVENT\r\n\
END:VCALENDAR";
let cal = VCalendar::try_from(ics_content).unwrap();
let occurrences: Vec<_> = cal.events[0].into_iter().collect();
assert_eq!(occurrences.len(), 5);
assert_eq!(
(
occurrences[0].start.year(),
occurrences[0].start.month(),
occurrences[0].start.day()
),
(2024, 2, 29)
);
assert_eq!(
(
occurrences[4].start.year(),
occurrences[4].start.month(),
occurrences[4].start.day()
),
(2028, 2, 29)
);
}
#[test]
fn test_issue10_exdate_date_comparison_ignores_time() {
let ics_content = "BEGIN:VCALENDAR\r\n\
BEGIN:VEVENT\r\n\
DTSTART:20230101T090000Z\r\n\
LAST-MODIFIED:20230101T000000Z\r\n\
CREATED:20230101T000000Z\r\n\
DTSTAMP:20230101T000000Z\r\n\
SUMMARY:Daily Meeting with Date Exdate\r\n\
SEQUENCE:0\r\n\
RRULE:FREQ=DAILY;COUNT=2\r\n\
EXDATE;VALUE=DATE:20230102\r\n\
END:VEVENT\r\n\
END:VCALENDAR";
let cal = VCalendar::try_from(ics_content).unwrap();
let occurrences: Vec<_> = cal.events[0].into_iter().collect();
assert_eq!(occurrences.len(), 2);
assert_eq!(occurrences[0].start.day(), 1);
assert_eq!(occurrences[1].start.day(), 3);
}
#[test]
fn test_issue11_vevent_property_parameters_and_plain_tags() {
let ics_content = "BEGIN:VCALENDAR\r\n\
BEGIN:VEVENT\r\n\
DTSTART:20230101\r\n\
LAST-MODIFIED:20230101T000000Z\r\n\
CREATED:20230101T000000Z\r\n\
DTSTAMP:20230101T000000Z\r\n\
SUMMARY;LANGUAGE=en-US:Team Sync; Planning\r\n\
DESCRIPTION;LANGUAGE=en-US:Notes for the meeting\r\n\
ORGANIZER:mailto:boss@example.com\r\n\
SEQUENCE:0\r\n\
RRULE:FREQ=DAILY;COUNT=2\r\n\
EXDATE:20230102\r\n\
END:VEVENT\r\n\
END:VCALENDAR";
let cal = VCalendar::try_from(ics_content).unwrap();
let event = &cal.events[0];
assert_eq!(event.summary, "Team Sync; Planning");
assert_eq!(event.description.as_deref(), Some("Notes for the meeting"));
assert_eq!(event.organizer.as_deref(), Some("mailto:boss@example.com"));
let occurrences: Vec<_> = event.into_iter().collect();
assert_eq!(occurrences.len(), 2);
assert_eq!(occurrences[0].start.day(), 1);
assert_eq!(occurrences[1].start.day(), 3);
}
#[test]
fn test_issue13_rrule_flexible_token_order_and_monthly_defaults() {
let rrule1: crate::rrule::RRule = "COUNT=3;FREQ=DAILY".parse().unwrap();
assert_eq!(rrule1.frequency, crate::Frequency::Daily);
let rrule2: crate::rrule::RRule = "FREQ=YEARLY;BYMONTH=6;COUNT=2".parse().unwrap();
assert_eq!(rrule2.frequency, crate::Frequency::Yearly);
assert_eq!(rrule2.by_month, vec![6]);
let ics_content = "BEGIN:VCALENDAR\r\n\
BEGIN:VEVENT\r\n\
DTSTART;VALUE=DATE:20230115\r\n\
LAST-MODIFIED:20230101T000000Z\r\n\
CREATED:20230101T000000Z\r\n\
DTSTAMP:20230101T000000Z\r\n\
SUMMARY:Plain Monthly\r\n\
SEQUENCE:0\r\n\
RRULE:FREQ=MONTHLY;COUNT=3\r\n\
END:VEVENT\r\n\
BEGIN:VEVENT\r\n\
DTSTART;VALUE=DATE:20230131\r\n\
LAST-MODIFIED:20230101T000000Z\r\n\
CREATED:20230101T000000Z\r\n\
DTSTAMP:20230101T000000Z\r\n\
SUMMARY:Last Day of Month\r\n\
SEQUENCE:0\r\n\
RRULE:FREQ=MONTHLY;BYMONTHDAY=-1;COUNT=3\r\n\
END:VEVENT\r\n\
END:VCALENDAR";
let cal = VCalendar::try_from(ics_content).unwrap();
let occ0: Vec<_> = cal.events[0].into_iter().collect();
assert_eq!(occ0.len(), 3);
assert_eq!((occ0[1].start.month(), occ0[1].start.day()), (2, 15));
assert_eq!((occ0[2].start.month(), occ0[2].start.day()), (3, 15));
let occ1: Vec<_> = cal.events[1].into_iter().collect();
assert_eq!(occ1.len(), 3);
assert_eq!((occ1[0].start.month(), occ1[0].start.day()), (1, 31));
assert_eq!((occ1[1].start.month(), occ1[1].start.day()), (2, 28));
assert_eq!((occ1[2].start.month(), occ1[2].start.day()), (3, 31));
}
#[test]
fn test_issue14_first_occurrence_respects_until_and_count() {
let ics_content = "BEGIN:VCALENDAR\r\n\
BEGIN:VEVENT\r\n\
DTSTART:20230110T090000Z\r\n\
LAST-MODIFIED:20230101T000000Z\r\n\
CREATED:20230101T000000Z\r\n\
DTSTAMP:20230101T000000Z\r\n\
SUMMARY:Zero Count Event\r\n\
SEQUENCE:0\r\n\
RRULE:FREQ=DAILY;COUNT=0\r\n\
END:VEVENT\r\n\
BEGIN:VEVENT\r\n\
DTSTART:20230110T090000Z\r\n\
LAST-MODIFIED:20230101T000000Z\r\n\
CREATED:20230101T000000Z\r\n\
DTSTAMP:20230101T000000Z\r\n\
SUMMARY:Expired Until Event\r\n\
SEQUENCE:0\r\n\
RRULE:FREQ=DAILY;UNTIL=20230101T000000Z\r\n\
END:VEVENT\r\n\
END:VCALENDAR";
let cal = VCalendar::try_from(ics_content).unwrap();
let occ0: Vec<_> = cal.events[0].into_iter().collect();
assert!(occ0.is_empty());
let occ1: Vec<_> = cal.events[1].into_iter().collect();
assert!(occ1.is_empty());
}
#[test]
fn test_issue16_vevent_optional_created_last_modified_sequence() {
let ics_content = "BEGIN:VCALENDAR\r\n\
BEGIN:VEVENT\r\n\
DTSTART:20230101T090000Z\r\n\
DTSTAMP:20230101T080000Z\r\n\
SUMMARY:Minimal Event\r\n\
END:VEVENT\r\n\
END:VCALENDAR";
let cal = VCalendar::try_from(ics_content).unwrap();
assert_eq!(cal.events.len(), 1);
let event = &cal.events[0];
assert_eq!(event.summary, "Minimal Event");
assert_eq!(event.sequence, 0);
assert_eq!(event.dt_created, event.dt_stamp);
assert_eq!(event.dt_last_modified, event.dt_stamp);
}
#[test]
fn test_issue17_vcalendar_validates_root_tag_and_ignores_other_components() {
let not_a_calendar = "BEGIN:VEVENT\r\nDTSTART:20230101T090000Z\r\nSUMMARY:Test\r\nEND:VEVENT";
assert!(VCalendar::try_from(not_a_calendar).is_err());
let with_vtodo = "BEGIN:VCALENDAR\r\n\
BEGIN:VTODO\r\n\
SUMMARY:Buy milk\r\n\
END:VTODO\r\n\
BEGIN:VEVENT\r\n\
DTSTART:20230101T090000Z\r\n\
SUMMARY:Actual Event\r\n\
END:VEVENT\r\n\
END:VCALENDAR";
let cal = VCalendar::try_from(with_vtodo).unwrap();
assert_eq!(cal.events.len(), 1);
assert_eq!(cal.events[0].summary, "Actual Event");
}
#[test]
fn test_improvement1_unified_date_or_date_time_preserves_tz_and_naive_date() {
let ics_content = "BEGIN:VCALENDAR\r\n\
BEGIN:VEVENT\r\n\
DTSTART;TZID=Europe/Rome:20230101T090000\r\n\
DTEND;TZID=America/New_York:20230101T050000\r\n\
SUMMARY:Cross-Timezone Event\r\n\
EXDATE;TZID=Europe/Rome:20230102T090000\r\n\
END:VEVENT\r\n\
BEGIN:VEVENT\r\n\
DTSTART;VALUE=DATE:20230510\r\n\
SUMMARY:All Day Event\r\n\
END:VEVENT\r\n\
END:VCALENDAR";
let cal = VCalendar::try_from(ics_content).unwrap();
assert_eq!(cal.events.len(), 2);
let ev0 = &cal.events[0];
assert_eq!(ev0.dt_start.timezone(), Some(chrono_tz::Europe::Rome));
assert_eq!(ev0.dt_end.timezone(), Some(chrono_tz::America::New_York));
assert_eq!(ev0.exdates[0].timezone(), Some(chrono_tz::Europe::Rome));
let ev1 = &cal.events[1];
assert_eq!(ev1.dt_start.timezone(), None);
assert_eq!(
ev1.dt_start,
crate::DateOrDateTime::WholeDay(chrono::NaiveDate::from_ymd_opt(2023, 5, 10).unwrap())
);
}
#[test]
fn test_improvement2_composable_rrule_multi_ordinal_byday_and_interval_validation() {
assert!(
"FREQ=WEEKLY;INTERVAL=0;BYDAY=MO"
.parse::<crate::RRule>()
.is_err()
);
let ics_content = "BEGIN:VCALENDAR\r\n\
BEGIN:VEVENT\r\n\
DTSTART;VALUE=DATE:20230102\r\n\
SUMMARY:1st and 3rd Monday\r\n\
RRULE:FREQ=MONTHLY;BYDAY=1MO,3MO;COUNT=4\r\n\
END:VEVENT\r\n\
END:VCALENDAR";
let cal = VCalendar::try_from(ics_content).unwrap();
let occ: Vec<_> = cal.events[0].into_iter().collect();
assert_eq!(occ.len(), 4);
assert_eq!((occ[0].start.month(), occ[0].start.day()), (1, 2));
assert_eq!((occ[1].start.month(), occ[1].start.day()), (1, 16));
assert_eq!((occ[2].start.month(), occ[2].start.day()), (2, 6));
assert_eq!((occ[3].start.month(), occ[3].start.day()), (2, 20));
}
#[test]
fn test_improvement4_content_line_multi_params_and_text_unescaping() {
let ics_content = "BEGIN:VCALENDAR\r\n\
BEGIN:VEVENT\r\n\
DTSTART;VALUE=DATE-TIME;TZID=\"Europe/Rome\":20230101T090000\r\n\
SUMMARY;LANGUAGE=en-US:Meeting\\, Planning\\; Phase 1\\nAgenda\r\n\
DESCRIPTION:Line 1\\NLine 2\\\\End\r\n\
END:VEVENT\r\n\
END:VCALENDAR";
let cal = VCalendar::try_from(ics_content).unwrap();
let ev = &cal.events[0];
assert_eq!(ev.dt_start.timezone(), Some(chrono_tz::Europe::Rome));
assert_eq!(ev.summary, "Meeting, Planning; Phase 1\nAgenda");
assert_eq!(ev.description.as_deref(), Some("Line 1\nLine 2\\End"));
}
#[test]
fn test_improvement5_calendar_query_api() {
let ics_content = "BEGIN:VCALENDAR\r\n\
BEGIN:VEVENT\r\n\
DTSTART;VALUE=DATE:20230101\r\n\
SUMMARY:Daily Standup\r\n\
RRULE:FREQ=DAILY\r\n\
END:VEVENT\r\n\
BEGIN:VEVENT\r\n\
DTSTART;VALUE=DATE:20230102\r\n\
SUMMARY:One-Off Review\r\n\
END:VEVENT\r\n\
END:VCALENDAR";
let cal = VCalendar::try_from(ics_content).unwrap();
let start = "20230102".parse::<DateOrDateTime>().unwrap();
let end = "20230104".parse::<DateOrDateTime>().unwrap();
let between = cal.occurrences_between(start, end).unwrap();
assert_eq!(between.len(), 4);
assert_eq!(between[0].1.start.day(), 2);
assert_eq!(between[1].1.start.day(), 2);
assert_eq!(between[2].1.start.day(), 3);
assert_eq!(between[3].1.start.day(), 4);
let next = cal.next_occurrences_since(start).unwrap();
assert_eq!(next.len(), 2);
assert_eq!(next[0].1.occurrence.start.day(), 2);
assert_eq!(next[1].1.occurrence.start.day(), 2);
}
#[test]
fn test_improvement6_from_str_eq_and_lightweight_errors() {
let ev_str = "BEGIN:VEVENT\r\n\
DTSTART;VALUE=DATE:20230101\r\n\
SUMMARY:Direct Event Parse\r\n\
END:VEVENT";
let ev1: VEvent = ev_str.parse().unwrap();
let ev2 = VEvent::try_from(ev_str).unwrap();
assert_eq!(ev1, ev2);
let cal_str = format!("BEGIN:VCALENDAR\r\n{ev_str}\r\nEND:VCALENDAR");
let cal1: VCalendar = cal_str.parse().unwrap();
let cal2 = VCalendar::try_from(cal_str.as_str()).unwrap();
assert_eq!(cal1, cal2);
let missing_dtstart = "BEGIN:VEVENT\r\nSUMMARY:No Start\r\nEND:VEVENT";
let err = missing_dtstart.parse::<VEvent>().unwrap_err();
assert_eq!(
err.clone(),
crate::errors::VEventParseError::MissingMandatoryField {
field: "DTSTART".to_string()
}
);
}
#[test]
fn test_rfc1_vevent_duration_default_date_end_optional_summary_and_uid() {
let ev_with_duration = "BEGIN:VEVENT\r\n\
UID:event-123@example.com\r\n\
DTSTART:20230101T090000Z\r\n\
DURATION:PT1H30M\r\n\
END:VEVENT";
let ev: VEvent = ev_with_duration.parse().unwrap();
assert_eq!(ev.uid.as_deref(), Some("event-123@example.com"));
assert_eq!(ev.summary, "");
assert_eq!(ev.dt_end - ev.dt_start, chrono::Duration::minutes(90));
let all_day_ev = "BEGIN:VEVENT\r\n\
DTSTART;VALUE=DATE:20230101\r\n\
SUMMARY:New Year\r\n\
END:VEVENT";
let ev_day: VEvent = all_day_ev.parse().unwrap();
assert_eq!(
ev_day.dt_end,
DateOrDateTime::WholeDay(chrono::NaiveDate::from_ymd_opt(2023, 1, 2).unwrap())
);
let invalid_both = "BEGIN:VEVENT\r\n\
DTSTART:20230101T090000Z\r\n\
DTEND:20230101T100000Z\r\n\
DURATION:PT1H\r\n\
SUMMARY:Conflict\r\n\
END:VEVENT";
assert!(invalid_both.parse::<VEvent>().is_err());
}
#[test]
fn test_rfc2_rdate_and_recurrence_id_override() {
let ev_rdate: VEvent = "BEGIN:VEVENT\r\n\
DTSTART;VALUE=DATE:20230101\r\n\
SUMMARY:With RDATE\r\n\
RRULE:FREQ=DAILY;COUNT=2\r\n\
RDATE;VALUE=DATE:20230110,20230105\r\n\
EXDATE;VALUE=DATE:20230105\r\n\
END:VEVENT"
.parse()
.unwrap();
let occs: Vec<_> = ev_rdate.into_iter().collect();
assert_eq!(occs.len(), 3);
assert_eq!(occs[0].start.day(), 1);
assert_eq!(occs[1].start.day(), 2);
assert_eq!(occs[2].start.day(), 10);
let cal_str = "BEGIN:VCALENDAR\r\n\
BEGIN:VEVENT\r\n\
UID:team-sync@example.com\r\n\
DTSTART;VALUE=DATE:20230101\r\n\
SUMMARY:Daily Sync\r\n\
RRULE:FREQ=DAILY;COUNT=3\r\n\
END:VEVENT\r\n\
BEGIN:VEVENT\r\n\
UID:team-sync@example.com\r\n\
RECURRENCE-ID;VALUE=DATE:20230102\r\n\
DTSTART;VALUE=DATE:20230105\r\n\
SUMMARY:Moved Daily Sync\r\n\
END:VEVENT\r\n\
END:VCALENDAR";
let cal: VCalendar = cal_str.parse().unwrap();
let start = "20230101".parse::<DateOrDateTime>().unwrap();
let end = "20230110".parse::<DateOrDateTime>().unwrap();
let all_occs = cal.occurrences_between(start, end).unwrap();
assert_eq!(all_occs.len(), 3);
assert_eq!(all_occs[0].1.start.day(), 1);
assert_eq!(all_occs[1].1.start.day(), 3);
assert_eq!(all_occs[2].1.start.day(), 5);
assert_eq!(all_occs[2].0.summary, "Moved Daily Sync");
}
#[test]
fn test_rfc3_rrule_count_until_exclusivity_until_display_and_wkst() {
use crate::RRule;
assert!(
"FREQ=DAILY;COUNT=5;UNTIL=20230110T000000Z"
.parse::<RRule>()
.is_err()
);
let rrule_until: RRule = "FREQ=DAILY;UNTIL=20230110;WKST=SU".parse().unwrap();
let formatted = rrule_until.to_string();
assert!(formatted.contains("UNTIL=20230110"), "got: {formatted}");
assert!(formatted.contains("WKST=SU"), "got: {formatted}");
let ev_wkst_su: VEvent = "BEGIN:VEVENT\r\n\
DTSTART;VALUE=DATE:20230107\r\n\
SUMMARY:Biweekly Weekend\r\n\
RRULE:FREQ=WEEKLY;INTERVAL=2;WKST=SU;BYDAY=SA,SU;COUNT=4\r\n\
END:VEVENT"
.parse()
.unwrap();
let days: Vec<u32> = ev_wkst_su.into_iter().map(|o| o.start.day()).collect();
assert_eq!(days, vec![7, 15, 21, 29]);
}
#[test]
fn test_rfc4_multi_bymonth_bymonthday_and_bysetpos() {
let ev_bmd: VEvent = "BEGIN:VEVENT\r\n\
DTSTART;VALUE=DATE:20230101\r\n\
SUMMARY:Paydays\r\n\
RRULE:FREQ=MONTHLY;BYMONTHDAY=1,15,-1;COUNT=4\r\n\
END:VEVENT"
.parse()
.unwrap();
let bmd_dates: Vec<(u32, u32)> = ev_bmd
.into_iter()
.map(|o| (o.start.month(), o.start.day()))
.collect();
assert_eq!(bmd_dates, vec![(1, 1), (1, 15), (1, 31), (2, 1)]);
let ev_bm: VEvent = "BEGIN:VEVENT\r\n\
DTSTART;VALUE=DATE:20230610\r\n\
SUMMARY:Semi-Annual Review\r\n\
RRULE:FREQ=YEARLY;BYMONTH=6,12;BYMONTHDAY=10;COUNT=3\r\n\
END:VEVENT"
.parse()
.unwrap();
let bm_dates: Vec<(i32, u32, u32)> = ev_bm
.into_iter()
.map(|o| (o.start.year(), o.start.month(), o.start.day()))
.collect();
assert_eq!(bm_dates, vec![(2023, 6, 10), (2023, 12, 10), (2024, 6, 10)]);
let ev_setpos: VEvent = "BEGIN:VEVENT\r\n\
DTSTART;VALUE=DATE:20230131\r\n\
SUMMARY:Last Weekday of Month\r\n\
RRULE:FREQ=MONTHLY;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=-1;COUNT=3\r\n\
END:VEVENT"
.parse()
.unwrap();
let setpos_dates: Vec<(u32, u32)> = ev_setpos
.into_iter()
.map(|o| (o.start.month(), o.start.day()))
.collect();
assert_eq!(setpos_dates, vec![(1, 31), (2, 28), (3, 31)]);
}
#[test]
fn test_rfc5_custom_vtimezone_non_iana_tzid_resolution() {
let cal_str = "BEGIN:VCALENDAR\r\n\
BEGIN:VTIMEZONE\r\n\
TZID:W. Europe Standard Time\r\n\
BEGIN:STANDARD\r\n\
DTSTART:19701025T030000\r\n\
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\n\
TZOFFSETFROM:+0200\r\n\
TZOFFSETTO:+0100\r\n\
END:STANDARD\r\n\
BEGIN:DAYLIGHT\r\n\
DTSTART:19700329T020000\r\n\
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\n\
TZOFFSETFROM:+0100\r\n\
TZOFFSETTO:+0200\r\n\
END:DAYLIGHT\r\n\
END:VTIMEZONE\r\n\
BEGIN:VEVENT\r\n\
DTSTART;TZID=\"W. Europe Standard Time\":20230115T100000\r\n\
DTEND;TZID=\"W. Europe Standard Time\":20230115T110000\r\n\
SUMMARY:Winter Event (+01:00)\r\n\
END:VEVENT\r\n\
BEGIN:VEVENT\r\n\
DTSTART;TZID=\"W. Europe Standard Time\":20230715T100000\r\n\
DTEND;TZID=\"W. Europe Standard Time\":20230715T110000\r\n\
SUMMARY:Summer Event (+02:00)\r\n\
END:VEVENT\r\n\
END:VCALENDAR";
let cal: VCalendar = cal_str.parse().unwrap();
assert_eq!(cal.events.len(), 2);
assert_eq!(cal.events[0].dt_start.hour(), 9);
assert_eq!(cal.events[1].dt_start.hour(), 8);
}
#[test]
fn test_rfc6_case_insensitivity_rfc6868_and_vcalendar_metadata() {
let cal_str = "begin:vcalendar\r\n\
version:2.0\r\n\
prodid:-//Example Corp//CalClient//EN\r\n\
calscale:GREGORIAN\r\n\
method:PUBLISH\r\n\
begin:vevent\r\n\
uid:case-test@example.com\r\n\
dtstart;tzid=\"Europe/Rome\":20230102T090000\r\n\
duration:pt1h\r\n\
summary;cn=\"John ^'Doc^' Doe^nJr.\":Case Insensitive Event\r\n\
rrule:freq=weekly;byday=mo,we;count=2\r\n\
end:vevent\r\n\
end:vcalendar";
let cal: VCalendar = cal_str.parse().unwrap();
assert_eq!(cal.version.as_deref(), Some("2.0"));
assert_eq!(
cal.prod_id.as_deref(),
Some("-//Example Corp//CalClient//EN")
);
assert_eq!(cal.calscale.as_deref(), Some("GREGORIAN"));
assert_eq!(cal.method.as_deref(), Some("PUBLISH"));
assert_eq!(cal.events.len(), 1);
assert_eq!(cal.events[0].summary, "Case Insensitive Event");
let occs: Vec<_> = cal.events[0].into_iter().collect();
assert_eq!(occs.len(), 2);
let line = crate::property::ContentLine::parse(
" ATTENDEE;CN=\"John ^'Doc^' Doe^nLine2^^End\":mailto:john@example.com",
)
.unwrap();
assert_eq!(line.param("cn"), Some("John \"Doc\" Doe\nLine2^End"));
}