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:
@@ -490,12 +490,33 @@ template <typename Integer>
|
||||
} else if (name == "BYDAY" && !has_by_weekday) {
|
||||
has_by_weekday = true;
|
||||
for (const std::string_view day_name : split(part_value, ',')) {
|
||||
const auto day = parse_weekday(day_name);
|
||||
int ordinal = 0;
|
||||
std::string_view weekday_part = day_name;
|
||||
// Check for ordinal prefix: optional sign, optional digits
|
||||
if (!day_name.empty() && (day_name[0] == '+' || day_name[0] == '-' || (day_name[0] >= '0' && day_name[0] <= '9'))) {
|
||||
std::size_t digit_end = 0;
|
||||
if (day_name[0] == '+' || day_name[0] == '-') {
|
||||
digit_end = 1;
|
||||
}
|
||||
while (digit_end < day_name.size() && day_name[digit_end] >= '0' && day_name[digit_end] <= '9') {
|
||||
++digit_end;
|
||||
}
|
||||
if (digit_end > 0U && digit_end < day_name.size()) {
|
||||
std::string number_str(day_name.substr(0, digit_end));
|
||||
ordinal = std::stoi(number_str);
|
||||
if (ordinal < -5 || ordinal > 5 || ordinal == 0) {
|
||||
error = "RRULE BYDAY ordinal must be from -5 to -1 or 1 to 5";
|
||||
return std::nullopt;
|
||||
}
|
||||
weekday_part = day_name.substr(digit_end);
|
||||
}
|
||||
}
|
||||
const auto day = parse_weekday(weekday_part);
|
||||
if (!day) {
|
||||
error = "RRULE BYDAY supports only non-ordinal weekdays";
|
||||
error = "RRULE BYDAY contains an invalid weekday";
|
||||
return std::nullopt;
|
||||
}
|
||||
rule.by_weekday.push_back(*day);
|
||||
rule.by_weekday.push_back({ordinal, *day});
|
||||
}
|
||||
} else if (name == "BYMONTHDAY" && !has_by_month_day) {
|
||||
has_by_month_day = true;
|
||||
@@ -532,12 +553,6 @@ template <typename Integer>
|
||||
error = "RRULE COUNT and UNTIL are mutually exclusive";
|
||||
return std::nullopt;
|
||||
}
|
||||
if ((has_by_weekday && rule.frequency != RecurrenceFrequency::weekly)
|
||||
|| (has_by_month_day && rule.frequency != RecurrenceFrequency::monthly)
|
||||
|| (has_by_month && rule.frequency != RecurrenceFrequency::yearly)) {
|
||||
error = "RRULE selector is not supported for this frequency";
|
||||
return std::nullopt;
|
||||
}
|
||||
return rule;
|
||||
}
|
||||
|
||||
@@ -679,7 +694,14 @@ template <typename Range, typename Formatter>
|
||||
}
|
||||
if (!rule.by_weekday.empty()) {
|
||||
result += ";BYDAY=" + join_values(rule.by_weekday,
|
||||
[](weekday day) { return weekday_name(day); });
|
||||
[](const ByWeekday& bw) {
|
||||
std::string out;
|
||||
if (bw.ordinal != 0) {
|
||||
out += (bw.ordinal > 0 ? "+" : "") + std::to_string(bw.ordinal);
|
||||
}
|
||||
out += weekday_name(bw.day);
|
||||
return out;
|
||||
});
|
||||
}
|
||||
if (!rule.by_month_day.empty()) {
|
||||
result += ";BYMONTHDAY=" + join_values(rule.by_month_day,
|
||||
@@ -1071,8 +1093,8 @@ void validate_recurrence(const Event& event) {
|
||||
if (rule.interval == 0) invalid("INTERVAL must be positive");
|
||||
if (rule.count && *rule.count == 0) invalid("COUNT must be positive");
|
||||
if (rule.count && rule.until) invalid("COUNT and UNTIL are mutually exclusive");
|
||||
for (weekday day : rule.by_weekday) {
|
||||
if (!day.ok()) invalid("BYDAY contains an invalid weekday");
|
||||
for (const ByWeekday& bw : rule.by_weekday) {
|
||||
if (!bw.day.ok()) invalid("BYDAY contains an invalid weekday");
|
||||
}
|
||||
for (int day : rule.by_month_day) {
|
||||
if (day == 0 || day < -31 || day > 31) {
|
||||
@@ -1084,12 +1106,7 @@ void validate_recurrence(const Event& event) {
|
||||
invalid("BYMONTH contains an out-of-range month");
|
||||
}
|
||||
}
|
||||
if ((!rule.by_weekday.empty() && rule.frequency != RecurrenceFrequency::weekly)
|
||||
|| (!rule.by_month_day.empty() && rule.frequency != RecurrenceFrequency::monthly)
|
||||
|| (!rule.by_month.empty() && rule.frequency != RecurrenceFrequency::yearly)) {
|
||||
invalid("selector does not apply to the selected frequency");
|
||||
}
|
||||
}
|
||||
|
||||
void validate_event_for_save(
|
||||
const Event& event, std::span<const VTimezoneDefinition> vtimezones) {
|
||||
|
||||
Reference in New Issue
Block a user