feat: recurrence rule editing in EventEditor and ordinal BYDAY support
Add recurrence_frequency, recurrence_interval, recurrence_count, and
recurrence_by_weekday editor fields. Frequency cycles with Space.
BYDAY input accepts comma-separated names and ordinal prefixes (1MO).
Extend RecurrenceRule::by_weekday from std::chrono::weekday to ByWeekday
{ordinal, day} struct. Parser accepts ordinal BYDAY (+1MO, -1FR).
Remove strict BY*/FREQ pairing constraint to match RFC 5545.
This commit is contained in:
@@ -512,7 +512,7 @@ void test_supported_recurrence_and_exdates(const std::filesystem::path& root) {
|
||||
"multiple/comma-separated EXDATE values were not collected");
|
||||
const auto& weekly = *loaded.events[1].recurrence;
|
||||
require(weekly.frequency == nocal::RecurrenceFrequency::weekly
|
||||
&& weekly.by_weekday == std::vector<weekday>{Monday, Wednesday, Friday}
|
||||
&& weekly.by_weekday == std::vector<nocal::ByWeekday>{{0, Monday}, {0, Wednesday}, {0, Friday}}
|
||||
&& weekly.until.has_value(),
|
||||
"weekly RRULE fields differ");
|
||||
const auto& monthly = *loaded.events[2].recurrence;
|
||||
@@ -652,10 +652,61 @@ void test_supported_rdates(const std::filesystem::path& root) {
|
||||
}
|
||||
}
|
||||
|
||||
void test_ordinal_byday_round_trip(const std::filesystem::path& root) {
|
||||
const auto path = root / "ordinal-byday.ics";
|
||||
write_fixture(path,
|
||||
"BEGIN:VCALENDAR\r\nVERSION:2.0\r\n"
|
||||
"BEGIN:VEVENT\r\nUID:ordinal-weekly\r\nDTSTART:20260701T090000Z\r\n"
|
||||
"DTEND:20260701T100000Z\r\n"
|
||||
"RRULE:FREQ=MONTHLY;BYDAY=+1MO,-1FR\r\nEND:VEVENT\r\n"
|
||||
"END:VCALENDAR\r\n");
|
||||
|
||||
const auto loaded = nocal::storage::IcsStore::load(path);
|
||||
require(loaded.safe_to_rewrite && loaded.warnings.empty(),
|
||||
"ordinal BYDAY should be rewrite-safe");
|
||||
require(loaded.events.size() == 1, "ordinal BYDAY fixture event count differs");
|
||||
const auto& rule = *loaded.events[0].recurrence;
|
||||
require(rule.frequency == nocal::RecurrenceFrequency::monthly,
|
||||
"monthly frequency should be preserved");
|
||||
require(rule.by_weekday.size() == 2, "two BYDAY entries expected");
|
||||
require(rule.by_weekday[0].ordinal == 1 && rule.by_weekday[0].day == std::chrono::Monday,
|
||||
"first BYDAY should be +1MO");
|
||||
require(rule.by_weekday[1].ordinal == -1 && rule.by_weekday[1].day == std::chrono::Friday,
|
||||
"second BYDAY should be -1FR");
|
||||
|
||||
const auto round_trip = root / "ordinal-byday-round-trip.ics";
|
||||
nocal::storage::IcsStore::save(round_trip, loaded.events);
|
||||
const std::string serialized = read_fixture(round_trip);
|
||||
require(serialized.find("BYDAY=+1MO,-1FR") != std::string::npos,
|
||||
"ordinal BYDAY should serialize as +1MO,-1FR");
|
||||
const auto reloaded = nocal::storage::IcsStore::load(round_trip);
|
||||
require(reloaded.safe_to_rewrite && reloaded.warnings.empty(),
|
||||
"ordinal BYDAY round-trip should be rewrite-safe");
|
||||
require(reloaded.events[0].recurrence == loaded.events[0].recurrence,
|
||||
"ordinal BYDAY semantic round trip differs");
|
||||
}
|
||||
|
||||
void test_daily_with_byday_is_supported(const std::filesystem::path& root) {
|
||||
const auto path = root / "daily-byday.ics";
|
||||
write_fixture(path,
|
||||
"BEGIN:VCALENDAR\r\nVERSION:2.0\r\n"
|
||||
"BEGIN:VEVENT\r\nUID:daily-byday\r\nDTSTART:20260701T090000Z\r\n"
|
||||
"DTEND:20260701T100000Z\r\n"
|
||||
"RRULE:FREQ=DAILY;BYDAY=MO,WE,FR\r\nEND:VEVENT\r\n"
|
||||
"END:VCALENDAR\r\n");
|
||||
|
||||
const auto loaded = nocal::storage::IcsStore::load(path);
|
||||
require(loaded.safe_to_rewrite && loaded.warnings.empty(),
|
||||
"DAILY with BYDAY should now be rewrite-safe");
|
||||
require(loaded.events.size() == 1, "daily-byday fixture event count differs");
|
||||
const auto& rule = *loaded.events[0].recurrence;
|
||||
require(rule.frequency == nocal::RecurrenceFrequency::daily,
|
||||
"daily frequency should be preserved");
|
||||
require(rule.by_weekday.size() == 3, "three BYDAY entries expected");
|
||||
}
|
||||
|
||||
void test_unsupported_recurrence_and_zones_are_unsafe(const std::filesystem::path& root) {
|
||||
const std::vector<std::string> unsupported_properties{
|
||||
"RRULE:FREQ=WEEKLY;BYDAY=1MO",
|
||||
"RRULE:FREQ=DAILY;BYDAY=MO",
|
||||
"RRULE:FREQ=DAILY;COUNT=2;UNTIL=20260720T090000Z",
|
||||
"RRULE:FREQ=HOURLY",
|
||||
"RRULE:FREQ=DAILY;WKST=MO",
|
||||
@@ -779,7 +830,7 @@ void test_save_validation_preserves_existing_bytes(const std::filesystem::path&
|
||||
nocal::Event invalid_rule = event_named("rule@example.test", "Invalid rule");
|
||||
nocal::RecurrenceRule rule;
|
||||
rule.frequency = nocal::RecurrenceFrequency::daily;
|
||||
rule.by_weekday.push_back(Monday);
|
||||
rule.interval = 0;
|
||||
invalid_rule.recurrence = rule;
|
||||
bool rule_failed = false;
|
||||
try {
|
||||
@@ -973,6 +1024,8 @@ int main() {
|
||||
test_supported_recurrence_and_exdates(root);
|
||||
test_supported_rdates(root);
|
||||
test_unsupported_recurrence_and_zones_are_unsafe(root);
|
||||
test_ordinal_byday_round_trip(root);
|
||||
test_daily_with_byday_is_supported(root);
|
||||
test_save_validation_preserves_existing_bytes(root);
|
||||
test_vtimezone_custom_tzid_safe_and_round_trips(root);
|
||||
test_vtimezone_multiple_tzids(root);
|
||||
|
||||
Reference in New Issue
Block a user