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:
Connor Johnstone
2025-08-31 00:28:41 -04:00
parent d0aa6fda08
commit c599598390
4 changed files with 509 additions and 58 deletions

View File

@@ -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)

View File

@@ -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);

View File

@@ -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>) {

View File

@@ -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
}
}