Fix recurring event RRULE INTERVAL and COUNT parameter loss
This commit fixes a critical bug where INTERVAL and COUNT parameters were being stripped from recurring events during backend processing. Frontend was correctly generating complete RRULE strings like: FREQ=WEEKLY;INTERVAL=2;BYDAY=TU,FR;COUNT=6 But backend was ignoring the complete RRULE and rebuilding from scratch, resulting in simplified RRULEs like: FREQ=WEEKLY;BYDAY=TU,FR (missing INTERVAL and COUNT) Changes: - Modified both events and series handlers to detect complete RRULE strings - Added logic to use frontend RRULE directly when it starts with "FREQ=" - Maintained backwards compatibility with simple recurrence types - Added comprehensive debug logging for RRULE generation - Fixed weekly BYDAY occurrence counting to respect COUNT parameter - Enhanced frontend RRULE generation with detailed logging This ensures all RFC 5545 RRULE parameters are preserved from frontend creation through CalDAV storage and retrieval. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -384,29 +384,44 @@ pub async fn create_event(
|
||||
}
|
||||
};
|
||||
|
||||
// Parse recurrence with BYDAY support for weekly recurrence
|
||||
let rrule = match request.recurrence.to_uppercase().as_str() {
|
||||
"DAILY" => Some("FREQ=DAILY".to_string()),
|
||||
"WEEKLY" => {
|
||||
// Handle weekly recurrence with optional BYDAY parameter
|
||||
let mut rrule = "FREQ=WEEKLY".to_string();
|
||||
|
||||
// Check if specific days are selected (recurrence_days has 7 elements: [Sun, Mon, Tue, Wed, Thu, Fri, Sat])
|
||||
if request.recurrence_days.len() == 7 {
|
||||
let selected_days: Vec<&str> = request.recurrence_days
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(i, &selected)| {
|
||||
if selected {
|
||||
Some(match i {
|
||||
0 => "SU", // Sunday
|
||||
1 => "MO", // Monday
|
||||
2 => "TU", // Tuesday
|
||||
3 => "WE", // Wednesday
|
||||
4 => "TH", // Thursday
|
||||
5 => "FR", // Friday
|
||||
6 => "SA", // Saturday
|
||||
_ => return None,
|
||||
// DEBUG: Log the recurrence field to see what we're receiving
|
||||
println!("🔍 DEBUG: Received recurrence field: '{}'", request.recurrence);
|
||||
println!("🔍 DEBUG: Starts with FREQ=? {}", request.recurrence.starts_with("FREQ="));
|
||||
|
||||
// Check if recurrence is already a full RRULE or just a simple type
|
||||
let rrule = if request.recurrence.starts_with("FREQ=") {
|
||||
// Frontend sent a complete RRULE string, use it directly
|
||||
println!("🎯 Using complete RRULE from frontend: {}", request.recurrence);
|
||||
if request.recurrence.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(request.recurrence.clone())
|
||||
}
|
||||
} else {
|
||||
// Legacy path: Parse recurrence with BYDAY support for weekly recurrence
|
||||
println!("🔄 Building RRULE from simple recurrence type: {}", request.recurrence);
|
||||
match request.recurrence.to_uppercase().as_str() {
|
||||
"DAILY" => Some("FREQ=DAILY".to_string()),
|
||||
"WEEKLY" => {
|
||||
// Handle weekly recurrence with optional BYDAY parameter
|
||||
let mut rrule = "FREQ=WEEKLY".to_string();
|
||||
|
||||
// Check if specific days are selected (recurrence_days has 7 elements: [Sun, Mon, Tue, Wed, Thu, Fri, Sat])
|
||||
if request.recurrence_days.len() == 7 {
|
||||
let selected_days: Vec<&str> = request.recurrence_days
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(i, &selected)| {
|
||||
if selected {
|
||||
Some(match i {
|
||||
0 => "SU", // Sunday
|
||||
1 => "MO", // Monday
|
||||
2 => "TU", // Tuesday
|
||||
3 => "WE", // Wednesday
|
||||
4 => "TH", // Thursday
|
||||
5 => "FR", // Friday
|
||||
6 => "SA", // Saturday
|
||||
_ => return None,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
@@ -419,11 +434,12 @@ pub async fn create_event(
|
||||
}
|
||||
}
|
||||
|
||||
Some(rrule)
|
||||
},
|
||||
"MONTHLY" => Some("FREQ=MONTHLY".to_string()),
|
||||
"YEARLY" => Some("FREQ=YEARLY".to_string()),
|
||||
_ => None,
|
||||
Some(rrule)
|
||||
},
|
||||
"MONTHLY" => Some("FREQ=MONTHLY".to_string()),
|
||||
"YEARLY" => Some("FREQ=YEARLY".to_string()),
|
||||
_ => None,
|
||||
}
|
||||
};
|
||||
|
||||
// Create the VEvent struct (RFC 5545 compliant)
|
||||
|
||||
@@ -161,8 +161,16 @@ pub async fn create_event_series(
|
||||
// Set priority
|
||||
event.priority = request.priority;
|
||||
|
||||
// Generate the RRULE for recurrence
|
||||
let rrule = build_series_rrule_with_freq(&request, recurrence_freq)?;
|
||||
// Check if recurrence is already a full RRULE or just a simple type
|
||||
let rrule = if request.recurrence.starts_with("FREQ=") {
|
||||
// Frontend sent a complete RRULE string, use it directly
|
||||
println!("🎯 SERIES: Using complete RRULE from frontend: {}", request.recurrence);
|
||||
request.recurrence.clone()
|
||||
} else {
|
||||
// Legacy path: Generate the RRULE for recurrence
|
||||
println!("🔄 SERIES: Building RRULE from simple recurrence type: {}", request.recurrence);
|
||||
build_series_rrule_with_freq(&request, recurrence_freq)?
|
||||
};
|
||||
event.rrule = Some(rrule);
|
||||
|
||||
println!("🔁 Generated RRULE: {:?}", event.rrule);
|
||||
|
||||
@@ -306,9 +306,11 @@ mod rrule_tests {
|
||||
data.yearly_by_month = vec![false, false, true, false, true, false, false, false, false, false, false, false]; // March, May
|
||||
|
||||
let rrule = data.build_rrule();
|
||||
println!("YEARLY RRULE: {}", rrule);
|
||||
assert!(rrule.contains("FREQ=YEARLY"));
|
||||
assert!(rrule.contains("BYMONTH=3,5"));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Debug)]
|
||||
@@ -385,6 +387,8 @@ impl EventCreationData {
|
||||
return String::new();
|
||||
}
|
||||
|
||||
web_sys::console::log_1(&format!("🔧 Building RRULE with interval: {}, count: {:?}", self.recurrence_interval, self.recurrence_count).into());
|
||||
|
||||
let mut parts = Vec::new();
|
||||
|
||||
// Add frequency (required)
|
||||
@@ -399,6 +403,9 @@ impl EventCreationData {
|
||||
// Add interval if not 1
|
||||
if self.recurrence_interval > 1 {
|
||||
parts.push(format!("INTERVAL={}", self.recurrence_interval));
|
||||
web_sys::console::log_1(&format!("➕ Added INTERVAL={}", self.recurrence_interval).into());
|
||||
} else {
|
||||
web_sys::console::log_1(&format!("⏭️ Skipped INTERVAL (value is {})", self.recurrence_interval).into());
|
||||
}
|
||||
|
||||
// Add frequency-specific rules
|
||||
@@ -458,11 +465,17 @@ impl EventCreationData {
|
||||
if let Some(until_date) = self.recurrence_until {
|
||||
// Format as UNTIL=YYYYMMDDTHHMMSSZ
|
||||
parts.push(format!("UNTIL={}T000000Z", until_date.format("%Y%m%d")));
|
||||
web_sys::console::log_1(&format!("➕ Added UNTIL={}", until_date).into());
|
||||
} else if let Some(count) = self.recurrence_count {
|
||||
parts.push(format!("COUNT={}", count));
|
||||
web_sys::console::log_1(&format!("➕ Added COUNT={}", count).into());
|
||||
} else {
|
||||
web_sys::console::log_1(&format!("⏭️ No COUNT or UNTIL specified").into());
|
||||
}
|
||||
|
||||
parts.join(";")
|
||||
let rrule = parts.join(";");
|
||||
web_sys::console::log_1(&format!("🎯 Final RRULE: {}", rrule).into());
|
||||
rrule
|
||||
}
|
||||
|
||||
pub fn to_create_event_params(&self) -> (String, String, String, String, String, String, String, bool, String, String, Option<u8>, String, String, String, String, String, Vec<bool>, Option<String>) {
|
||||
|
||||
@@ -328,6 +328,8 @@ impl CalendarService {
|
||||
.and_then(|s| s.parse().ok())
|
||||
.unwrap_or(100)
|
||||
.min(365); // Cap at 365 occurrences for performance
|
||||
|
||||
web_sys::console::log_1(&format!("📊 RRULE parsed - FREQ: {}, INTERVAL: {}, COUNT: {}", freq, interval, count).into());
|
||||
|
||||
// Get UNTIL date if specified
|
||||
let until_date = components.get("UNTIL")
|
||||
@@ -398,6 +400,7 @@ impl CalendarService {
|
||||
}
|
||||
|
||||
occurrences.push(occurrence_event);
|
||||
occurrence_count += 1; // Only count when we actually add an occurrence
|
||||
}
|
||||
}
|
||||
|
||||
@@ -424,24 +427,61 @@ impl CalendarService {
|
||||
}
|
||||
}
|
||||
"MONTHLY" => {
|
||||
// Simple monthly increment (same day of month)
|
||||
if let Some(next_month) = Self::add_months(current_date, interval) {
|
||||
current_date = next_month;
|
||||
// Handle MONTHLY with BYMONTHDAY or BYDAY
|
||||
if let Some(bymonthday) = components.get("BYMONTHDAY") {
|
||||
// Monthly by day of month (e.g., 15th of every month)
|
||||
return Self::generate_monthly_bymonthday_occurrences(
|
||||
base_event,
|
||||
bymonthday,
|
||||
interval,
|
||||
start_range,
|
||||
end_range,
|
||||
until_date,
|
||||
count
|
||||
);
|
||||
} else if let Some(byday) = components.get("BYDAY") {
|
||||
// Monthly by weekday position (e.g., first Monday)
|
||||
return Self::generate_monthly_byday_occurrences(
|
||||
base_event,
|
||||
byday,
|
||||
interval,
|
||||
start_range,
|
||||
end_range,
|
||||
until_date,
|
||||
count
|
||||
);
|
||||
} else {
|
||||
break; // Invalid date
|
||||
// Simple monthly increment (same day of month)
|
||||
if let Some(next_month) = Self::add_months(current_date, interval) {
|
||||
current_date = next_month;
|
||||
} else {
|
||||
break; // Invalid date
|
||||
}
|
||||
}
|
||||
}
|
||||
"YEARLY" => {
|
||||
if let Some(next_year) = Self::add_years(current_date, interval) {
|
||||
current_date = next_year;
|
||||
if let Some(bymonth) = components.get("BYMONTH") {
|
||||
// Yearly with specific months (e.g., January, March, June)
|
||||
return Self::generate_yearly_bymonth_occurrences(
|
||||
base_event,
|
||||
bymonth,
|
||||
interval,
|
||||
start_range,
|
||||
end_range,
|
||||
until_date,
|
||||
count
|
||||
);
|
||||
} else {
|
||||
break; // Invalid date
|
||||
// Simple yearly increment (same date each year)
|
||||
if let Some(next_year) = Self::add_years(current_date, interval) {
|
||||
current_date = next_year;
|
||||
} else {
|
||||
break; // Invalid date
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => break, // Unsupported frequency
|
||||
}
|
||||
|
||||
occurrence_count += 1;
|
||||
}
|
||||
|
||||
occurrences
|
||||
@@ -464,26 +504,46 @@ impl CalendarService {
|
||||
return occurrences;
|
||||
}
|
||||
|
||||
web_sys::console::log_1(&format!("🗓️ Generating WEEKLY BYDAY occurrences for days: {:?}", weekdays).into());
|
||||
web_sys::console::log_1(&format!("🗓️ Generating WEEKLY BYDAY occurrences for days: {:?}, interval: {}, count: {}", weekdays, interval, count).into());
|
||||
|
||||
let start_date = base_event.dtstart.date_naive();
|
||||
let mut current_week_start = start_date - Duration::days(start_date.weekday().num_days_from_monday() as i64);
|
||||
let mut total_occurrences = 0;
|
||||
|
||||
// Find the Monday of the week containing the start_date (reference week)
|
||||
let reference_week_start = start_date - Duration::days(start_date.weekday().num_days_from_monday() as i64);
|
||||
|
||||
let mut total_events_generated = 0; // Count of actual events generated (matches RFC 5545 COUNT)
|
||||
let mut week_interval_number = 0; // Which interval week we're processing (0, 1, 2, ...)
|
||||
let max_weeks = (count * 10).min(520); // Prevent infinite loops - max 10 years
|
||||
|
||||
web_sys::console::log_1(&format!("📅 Reference week start: {}, event start: {}, INTERVAL: {}", reference_week_start, start_date, interval).into());
|
||||
|
||||
// Generate occurrences week by week
|
||||
while current_week_start <= end_range && total_occurrences < count {
|
||||
// Continue generating until we reach count or date limits
|
||||
while total_events_generated < count && week_interval_number < max_weeks {
|
||||
// Calculate the actual week we're processing: original + (interval_number * interval) weeks
|
||||
let current_week_start = reference_week_start + Duration::weeks((week_interval_number as i32 * interval) as i64);
|
||||
|
||||
// Stop if we've gone past the end range
|
||||
if current_week_start > end_range {
|
||||
web_sys::console::log_1(&format!("🛑 Week start {} exceeds end range {}", current_week_start, end_range).into());
|
||||
break;
|
||||
}
|
||||
|
||||
web_sys::console::log_1(&format!("🔄 Processing interval week {} (week start: {}, total events so far: {})", week_interval_number, current_week_start, total_events_generated).into());
|
||||
|
||||
// Generate occurrences for all matching weekdays in this week
|
||||
for &weekday in &weekdays {
|
||||
let occurrence_date = current_week_start + Duration::days(weekday.num_days_from_monday() as i64);
|
||||
|
||||
// Skip if occurrence is before start_range or after end_range
|
||||
if occurrence_date < start_range || occurrence_date > end_range {
|
||||
web_sys::console::log_1(&format!("⏭️ Skipping {} (outside range {}-{})", occurrence_date, start_range, end_range).into());
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip if we've reached the count limit
|
||||
if total_occurrences >= count {
|
||||
break;
|
||||
|
||||
// Skip if this occurrence is before the original event date
|
||||
if occurrence_date < start_date {
|
||||
web_sys::console::log_1(&format!("⏭️ Skipping {} (before event start {})", occurrence_date, start_date).into());
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check UNTIL constraint
|
||||
@@ -496,11 +556,6 @@ impl CalendarService {
|
||||
}
|
||||
}
|
||||
|
||||
// Skip if this occurrence is before the original event date
|
||||
if occurrence_date < start_date {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Calculate the occurrence datetime
|
||||
let days_diff = occurrence_date.signed_duration_since(start_date).num_days();
|
||||
let occurrence_datetime = base_event.dtstart + Duration::days(days_diff);
|
||||
@@ -529,20 +584,308 @@ impl CalendarService {
|
||||
occurrence_event.dtend = Some(end + Duration::days(days_diff));
|
||||
}
|
||||
|
||||
web_sys::console::log_1(&format!("📅 Generated weekly occurrence on {}", occurrence_date).into());
|
||||
total_events_generated += 1;
|
||||
web_sys::console::log_1(&format!("✅ Generated weekly occurrence on {} (event {} of {})", occurrence_date, total_events_generated, count).into());
|
||||
occurrences.push(occurrence_event);
|
||||
total_occurrences += 1;
|
||||
|
||||
// Stop if we've reached the count limit
|
||||
if total_events_generated >= count {
|
||||
web_sys::console::log_1(&format!("🎯 Reached COUNT limit of {} events", count).into());
|
||||
return occurrences;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Move to next week interval
|
||||
current_week_start = current_week_start + Duration::weeks(interval as i64);
|
||||
// Move to the next interval week
|
||||
week_interval_number += 1;
|
||||
}
|
||||
|
||||
web_sys::console::log_1(&format!("✅ Generated {} total weekly BYDAY occurrences", occurrences.len()).into());
|
||||
occurrences
|
||||
}
|
||||
|
||||
/// Generate occurrences for MONTHLY frequency with BYMONTHDAY
|
||||
fn generate_monthly_bymonthday_occurrences(
|
||||
base_event: &VEvent,
|
||||
bymonthday: &str,
|
||||
interval: i32,
|
||||
start_range: NaiveDate,
|
||||
end_range: NaiveDate,
|
||||
until_date: Option<chrono::DateTime<chrono::Utc>>,
|
||||
count: usize,
|
||||
) -> Vec<VEvent> {
|
||||
let mut occurrences = Vec::new();
|
||||
|
||||
// Parse BYMONTHDAY (e.g., "15" or "1,15,31")
|
||||
let monthdays: Vec<u32> = bymonthday
|
||||
.split(',')
|
||||
.filter_map(|day| day.trim().parse().ok())
|
||||
.filter(|&day| day >= 1 && day <= 31)
|
||||
.collect();
|
||||
|
||||
if monthdays.is_empty() {
|
||||
return occurrences;
|
||||
}
|
||||
|
||||
web_sys::console::log_1(&format!("📅 Generating MONTHLY BYMONTHDAY occurrences for days: {:?}", monthdays).into());
|
||||
|
||||
let start_date = base_event.dtstart.date_naive();
|
||||
let mut current_month_start = NaiveDate::from_ymd_opt(start_date.year(), start_date.month(), 1).unwrap();
|
||||
let mut total_occurrences = 0;
|
||||
let mut months_processed = 0;
|
||||
let max_months = (count * 12).min(120); // Prevent infinite loops - max 10 years
|
||||
|
||||
web_sys::console::log_1(&format!("📅 Starting monthly BYMONTHDAY generation from {} with count {} and interval {}", current_month_start, count, interval).into());
|
||||
|
||||
// Generate occurrences month by month
|
||||
while current_month_start <= end_range && total_occurrences < count && months_processed < max_months {
|
||||
// Generate occurrences for all matching days in this month
|
||||
for &day in &monthdays {
|
||||
// Try to create the date, skip if invalid (e.g., Feb 31)
|
||||
if let Some(occurrence_date) = NaiveDate::from_ymd_opt(current_month_start.year(), current_month_start.month(), day) {
|
||||
// Skip if occurrence is before start_range or after end_range
|
||||
if occurrence_date < start_range || occurrence_date > end_range {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip if we've reached the count limit
|
||||
if total_occurrences >= count {
|
||||
break;
|
||||
}
|
||||
|
||||
// Skip if this occurrence is before the original event date
|
||||
if occurrence_date < start_date {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check UNTIL constraint
|
||||
if let Some(until) = until_date {
|
||||
let days_diff = occurrence_date.signed_duration_since(start_date).num_days();
|
||||
let occurrence_datetime = base_event.dtstart + Duration::days(days_diff);
|
||||
if occurrence_datetime > until {
|
||||
web_sys::console::log_1(&format!("🛑 Stopping at {} due to UNTIL {}", occurrence_datetime, until).into());
|
||||
return occurrences;
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate the occurrence datetime
|
||||
let days_diff = occurrence_date.signed_duration_since(start_date).num_days();
|
||||
let occurrence_datetime = base_event.dtstart + Duration::days(days_diff);
|
||||
|
||||
// Check if this occurrence is in the exception dates (EXDATE)
|
||||
let is_exception = base_event.exdate.iter().any(|exception_date| {
|
||||
let exception_naive = exception_date.naive_utc();
|
||||
let occurrence_naive = occurrence_datetime.naive_utc();
|
||||
let diff = occurrence_naive - exception_naive;
|
||||
diff.num_seconds().abs() < 60
|
||||
});
|
||||
|
||||
if !is_exception {
|
||||
// Create occurrence event
|
||||
let mut occurrence_event = base_event.clone();
|
||||
occurrence_event.dtstart = occurrence_datetime;
|
||||
occurrence_event.dtstamp = chrono::Utc::now();
|
||||
|
||||
if let Some(end) = base_event.dtend {
|
||||
occurrence_event.dtend = Some(end + Duration::days(days_diff));
|
||||
}
|
||||
|
||||
web_sys::console::log_1(&format!("📅 Generated monthly BYMONTHDAY occurrence on {}", occurrence_date).into());
|
||||
occurrences.push(occurrence_event);
|
||||
total_occurrences += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Move to next month interval
|
||||
months_processed += 1;
|
||||
if let Some(next_month_start) = Self::add_months(current_month_start, interval) {
|
||||
current_month_start = NaiveDate::from_ymd_opt(next_month_start.year(), next_month_start.month(), 1).unwrap();
|
||||
web_sys::console::log_1(&format!("📅 Advanced to month {} (processed {} months, {} occurrences so far)", current_month_start, months_processed, total_occurrences).into());
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
web_sys::console::log_1(&format!("✅ Generated {} total monthly BYMONTHDAY occurrences", occurrences.len()).into());
|
||||
occurrences
|
||||
}
|
||||
|
||||
/// Generate occurrences for MONTHLY frequency with BYDAY (e.g., "1MO" = first Monday)
|
||||
fn generate_monthly_byday_occurrences(
|
||||
base_event: &VEvent,
|
||||
byday: &str,
|
||||
interval: i32,
|
||||
start_range: NaiveDate,
|
||||
end_range: NaiveDate,
|
||||
until_date: Option<chrono::DateTime<chrono::Utc>>,
|
||||
count: usize,
|
||||
) -> Vec<VEvent> {
|
||||
let mut occurrences = Vec::new();
|
||||
|
||||
web_sys::console::log_1(&format!("📅 Generating MONTHLY BYDAY occurrences for: {}", byday).into());
|
||||
|
||||
// Parse BYDAY for monthly (e.g., "1MO" = first Monday, "-1FR" = last Friday)
|
||||
if let Some((position, weekday)) = Self::parse_monthly_byday(byday) {
|
||||
let start_date = base_event.dtstart.date_naive();
|
||||
let mut current_month_start = NaiveDate::from_ymd_opt(start_date.year(), start_date.month(), 1).unwrap();
|
||||
let mut total_occurrences = 0;
|
||||
|
||||
// Generate occurrences month by month
|
||||
while current_month_start <= end_range && total_occurrences < count {
|
||||
if let Some(occurrence_date) = Self::find_nth_weekday_in_month(current_month_start, position, weekday) {
|
||||
// Skip if occurrence is before start_range or after end_range
|
||||
if occurrence_date < start_range || occurrence_date > end_range {
|
||||
} else if occurrence_date >= start_date && total_occurrences < count {
|
||||
// Check UNTIL constraint
|
||||
if let Some(until) = until_date {
|
||||
let days_diff = occurrence_date.signed_duration_since(start_date).num_days();
|
||||
let occurrence_datetime = base_event.dtstart + Duration::days(days_diff);
|
||||
if occurrence_datetime > until {
|
||||
return occurrences;
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate the occurrence datetime
|
||||
let days_diff = occurrence_date.signed_duration_since(start_date).num_days();
|
||||
let occurrence_datetime = base_event.dtstart + Duration::days(days_diff);
|
||||
|
||||
// Check EXDATE
|
||||
let is_exception = base_event.exdate.iter().any(|exception_date| {
|
||||
let exception_naive = exception_date.naive_utc();
|
||||
let occurrence_naive = occurrence_datetime.naive_utc();
|
||||
let diff = occurrence_naive - exception_naive;
|
||||
diff.num_seconds().abs() < 60
|
||||
});
|
||||
|
||||
if !is_exception {
|
||||
let mut occurrence_event = base_event.clone();
|
||||
occurrence_event.dtstart = occurrence_datetime;
|
||||
occurrence_event.dtstamp = chrono::Utc::now();
|
||||
|
||||
if let Some(end) = base_event.dtend {
|
||||
occurrence_event.dtend = Some(end + Duration::days(days_diff));
|
||||
}
|
||||
|
||||
occurrences.push(occurrence_event);
|
||||
total_occurrences += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Move to next month interval
|
||||
if let Some(next_month) = Self::add_months(current_month_start, interval) {
|
||||
current_month_start = NaiveDate::from_ymd_opt(next_month.year(), next_month.month(), 1).unwrap();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
web_sys::console::log_1(&format!("✅ Generated {} total monthly BYDAY occurrences", occurrences.len()).into());
|
||||
occurrences
|
||||
}
|
||||
|
||||
/// Generate occurrences for YEARLY frequency with BYMONTH
|
||||
fn generate_yearly_bymonth_occurrences(
|
||||
base_event: &VEvent,
|
||||
bymonth: &str,
|
||||
interval: i32,
|
||||
start_range: NaiveDate,
|
||||
end_range: NaiveDate,
|
||||
until_date: Option<chrono::DateTime<chrono::Utc>>,
|
||||
count: usize,
|
||||
) -> Vec<VEvent> {
|
||||
let mut occurrences = Vec::new();
|
||||
|
||||
// Parse BYMONTH (e.g., "1,3,6" -> [January, March, June])
|
||||
let months: Vec<u32> = bymonth
|
||||
.split(',')
|
||||
.filter_map(|month| month.trim().parse().ok())
|
||||
.filter(|&month| month >= 1 && month <= 12)
|
||||
.collect();
|
||||
|
||||
if months.is_empty() {
|
||||
return occurrences;
|
||||
}
|
||||
|
||||
web_sys::console::log_1(&format!("📅 Generating YEARLY BYMONTH occurrences for months: {:?}", months).into());
|
||||
|
||||
let start_date = base_event.dtstart.date_naive();
|
||||
let mut current_year = start_date.year();
|
||||
let mut total_occurrences = 0;
|
||||
|
||||
// Generate occurrences year by year
|
||||
while total_occurrences < count {
|
||||
// Generate occurrences for all matching months in this year
|
||||
for &month in &months {
|
||||
// Create the date for this year/month/day
|
||||
if let Some(occurrence_date) = NaiveDate::from_ymd_opt(current_year, month, start_date.day()) {
|
||||
// Skip if occurrence is before start_range or after end_range
|
||||
if occurrence_date < start_range || occurrence_date > end_range {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip if we've reached the count limit
|
||||
if total_occurrences >= count {
|
||||
break;
|
||||
}
|
||||
|
||||
// Skip if this occurrence is before the original event date
|
||||
if occurrence_date < start_date {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check UNTIL constraint
|
||||
if let Some(until) = until_date {
|
||||
let days_diff = occurrence_date.signed_duration_since(start_date).num_days();
|
||||
let occurrence_datetime = base_event.dtstart + Duration::days(days_diff);
|
||||
if occurrence_datetime > until {
|
||||
return occurrences;
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate the occurrence datetime
|
||||
let days_diff = occurrence_date.signed_duration_since(start_date).num_days();
|
||||
let occurrence_datetime = base_event.dtstart + Duration::days(days_diff);
|
||||
|
||||
// Check EXDATE
|
||||
let is_exception = base_event.exdate.iter().any(|exception_date| {
|
||||
let exception_naive = exception_date.naive_utc();
|
||||
let occurrence_naive = occurrence_datetime.naive_utc();
|
||||
let diff = occurrence_naive - exception_naive;
|
||||
diff.num_seconds().abs() < 60
|
||||
});
|
||||
|
||||
if !is_exception {
|
||||
let mut occurrence_event = base_event.clone();
|
||||
occurrence_event.dtstart = occurrence_datetime;
|
||||
occurrence_event.dtstamp = chrono::Utc::now();
|
||||
|
||||
if let Some(end) = base_event.dtend {
|
||||
occurrence_event.dtend = Some(end + Duration::days(days_diff));
|
||||
}
|
||||
|
||||
web_sys::console::log_1(&format!("📅 Generated yearly BYMONTH occurrence on {}", occurrence_date).into());
|
||||
occurrences.push(occurrence_event);
|
||||
total_occurrences += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Move to next year interval
|
||||
current_year += interval;
|
||||
|
||||
// Stop if we've gone beyond reasonable range
|
||||
if current_year > end_range.year() + 10 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
web_sys::console::log_1(&format!("✅ Generated {} total yearly BYMONTH occurrences", occurrences.len()).into());
|
||||
occurrences
|
||||
}
|
||||
|
||||
|
||||
/// Calculate next weekday occurrence for WEEKLY frequency with BYDAY
|
||||
fn next_weekday_occurrence(current_date: NaiveDate, byday: &str, interval: i32) -> NaiveDate {
|
||||
@@ -1275,5 +1618,76 @@ impl CalendarService {
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse monthly BYDAY (e.g., "1MO" -> (1, Monday), "-1FR" -> (-1, Friday))
|
||||
fn parse_monthly_byday(byday: &str) -> Option<(i8, chrono::Weekday)> {
|
||||
if byday.len() < 3 {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Extract position and weekday
|
||||
let (position_str, weekday_str) = if byday.starts_with('-') {
|
||||
(&byday[..2], &byday[2..])
|
||||
} else {
|
||||
(&byday[..1], &byday[1..])
|
||||
};
|
||||
|
||||
let position = position_str.parse::<i8>().ok()?;
|
||||
let weekday = match weekday_str {
|
||||
"MO" => chrono::Weekday::Mon,
|
||||
"TU" => chrono::Weekday::Tue,
|
||||
"WE" => chrono::Weekday::Wed,
|
||||
"TH" => chrono::Weekday::Thu,
|
||||
"FR" => chrono::Weekday::Fri,
|
||||
"SA" => chrono::Weekday::Sat,
|
||||
"SU" => chrono::Weekday::Sun,
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
Some((position, weekday))
|
||||
}
|
||||
|
||||
/// Find nth weekday in a month (e.g., 1st Monday, 2nd Tuesday, -1 = last)
|
||||
fn find_nth_weekday_in_month(month_start: NaiveDate, position: i8, weekday: chrono::Weekday) -> Option<NaiveDate> {
|
||||
let year = month_start.year();
|
||||
let month = month_start.month();
|
||||
|
||||
if position > 0 {
|
||||
// Find nth occurrence from beginning of month
|
||||
let mut current = NaiveDate::from_ymd_opt(year, month, 1)?;
|
||||
let mut count = 0;
|
||||
|
||||
while current.month() == month {
|
||||
if current.weekday() == weekday {
|
||||
count += 1;
|
||||
if count == position as u8 {
|
||||
return Some(current);
|
||||
}
|
||||
}
|
||||
current = current.succ_opt()?;
|
||||
}
|
||||
} else if position < 0 {
|
||||
// Find nth occurrence from end of month
|
||||
let next_month = if month == 12 {
|
||||
NaiveDate::from_ymd_opt(year + 1, 1, 1)?
|
||||
} else {
|
||||
NaiveDate::from_ymd_opt(year, month + 1, 1)?
|
||||
};
|
||||
let mut current = next_month.pred_opt()?; // Last day of current month
|
||||
let mut count = 0;
|
||||
|
||||
while current.month() == month {
|
||||
if current.weekday() == weekday {
|
||||
count += 1;
|
||||
if count == (-position) as u8 {
|
||||
return Some(current);
|
||||
}
|
||||
}
|
||||
current = current.pred_opt()?;
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user