use crate::helpers::fix_timezone;
use chrono::{Datelike, TimeZone};
use chrono_tz::Tz;
pub fn patch_rrule(
recurrence: &str,
prev_datetime: &str,
new_datetime: &str,
tzid: &str,
) -> String {
let rrule_part = if recurrence.contains("\n") {
recurrence
.lines()
.find(|line| line.starts_with("RRULE:"))
.unwrap_or(recurrence)
} else {
recurrence
};
fn parse_local(dt_str: &str) -> Result<(chrono::NaiveDateTime, bool), String> {
if let Ok(ndt) = chrono::NaiveDateTime::parse_from_str(dt_str, "%Y-%m-%dT%H:%M:%S") {
return Ok((ndt, false));
}
if let Ok(nd) = chrono::NaiveDate::parse_from_str(dt_str, "%Y-%m-%d") {
let ndt = nd.and_hms_opt(0, 0, 0).ok_or("Invalid date")?;
return Ok((ndt, true));
}
Err(
"Invalid datetime format. Expected YYYY-MM-DD or YYYY-MM-DDTHH:MM:SS (no timezone)"
.to_string(),
)
}
let (prev_naive, _prev_is_date) =
parse_local(prev_datetime).expect("prev_datetime has invalid format");
let (new_naive, new_is_date) =
parse_local(new_datetime).expect("new_datetime has invalid format");
let fixed_tz: String = fix_timezone(tzid, Some("UTC"));
let tz: Tz = fixed_tz.parse().unwrap_or(chrono_tz::UTC);
let prev_dt = tz
.from_local_datetime(&prev_naive)
.single()
.expect("Ambiguous/non-existent local time for prev_datetime");
let new_dt = tz
.from_local_datetime(&new_naive)
.single()
.expect("Ambiguous/non-existent local time for new_datetime");
let prev_day_of_week = prev_dt.format("%u").to_string();
let new_day_of_week = new_dt.format("%u").to_string();
let prev_day_of_month = prev_dt.day();
let new_day_of_month = new_dt.day();
print!(
"Prev day of week: {}, new day of week: {}\n",
prev_dt, new_dt
);
if prev_day_of_week == new_day_of_week && prev_day_of_month == new_day_of_month {
return rrule_part.to_string();
}
let prev_byday = match prev_day_of_week.as_str() {
"1" => "MO",
"2" => "TU",
"3" => "WE",
"4" => "TH",
"5" => "FR",
"6" => "SA",
"7" => "SU",
_ => return rrule_part.to_string(), };
let new_byday = match new_day_of_week.as_str() {
"1" => "MO",
"2" => "TU",
"3" => "WE",
"4" => "TH",
"5" => "FR",
"6" => "SA",
"7" => "SU",
_ => return rrule_part.to_string(), };
let is_weekly = rrule_part.contains("FREQ=WEEKLY");
let is_monthly = rrule_part.contains("FREQ=MONTHLY");
let is_yearly = rrule_part.contains("FREQ=YEARLY");
if !is_weekly && !is_monthly && !is_yearly {
return rrule_part.to_string();
}
let mut modified_recurrence = rrule_part.to_string();
if is_yearly {
let bymonthday_key = "BYMONTHDAY=";
if let Some(bymonthday_pos) = rrule_part.find(bymonthday_key) {
let bymonthday_start = bymonthday_pos + bymonthday_key.len();
let bymonthday_rest = &rrule_part[bymonthday_start..];
let bymonthday_len = bymonthday_rest.find(';').unwrap_or(bymonthday_rest.len());
let bymonthday_val = &bymonthday_rest[..bymonthday_len];
if !bymonthday_val.contains(',') {
if bymonthday_val == prev_day_of_month.to_string() {
let new_bymonthday = new_day_of_month.to_string();
modified_recurrence = splice_value(
&modified_recurrence,
bymonthday_start,
bymonthday_len,
&new_bymonthday,
);
}
let bymonth_key = "BYMONTH=";
if let Some(bymonth_pos) = modified_recurrence.find(bymonth_key) {
let bymonth_start = bymonth_pos + bymonth_key.len();
let bymonth_rest = &modified_recurrence[bymonth_start..];
let bymonth_len = bymonth_rest.find(';').unwrap_or(bymonth_rest.len());
let bymonth_val = &bymonth_rest[..bymonth_len];
if !bymonth_val.contains(',') {
let prev_month = prev_dt.month();
let new_month = new_dt.month();
if bymonth_val == prev_month.to_string() && prev_month != new_month {
let new_bymonth = new_month.to_string();
modified_recurrence = splice_value(
&modified_recurrence,
bymonth_start,
bymonth_len,
&new_bymonth,
);
}
}
} else {
let prev_month = prev_dt.month();
let new_month = new_dt.month();
if prev_month != new_month {
if modified_recurrence.ends_with('\n')
|| modified_recurrence.ends_with('\r')
{
modified_recurrence.push_str(&format!("BYMONTH={}", new_month));
} else if modified_recurrence.contains(';')
|| modified_recurrence.contains(':')
{
modified_recurrence.push_str(&format!(";BYMONTH={}", new_month));
} else {
modified_recurrence.push_str(&format!("BYMONTH={}", new_month));
}
}
}
return update_until_if_needed_tz(&modified_recurrence, new_dt, &tz, new_is_date);
}
}
return rrule_part.to_string();
}
if is_monthly && prev_day_of_month != new_day_of_month {
let bymonthday_key = "BYMONTHDAY=";
if let Some(bymonthday_pos) = rrule_part.find(bymonthday_key) {
let bymonthday_start = bymonthday_pos + bymonthday_key.len();
let bymonthday_rest = &rrule_part[bymonthday_start..];
let bymonthday_len = bymonthday_rest.find(';').unwrap_or(bymonthday_rest.len());
let bymonthday_val = &bymonthday_rest[..bymonthday_len];
if bymonthday_val.contains(',') {
let days: Vec<&str> = bymonthday_val.split(',').collect();
let prev_day_str = prev_day_of_month.to_string();
if days.contains(&prev_day_str.as_str()) {
let new_days: Vec<String> = days
.iter()
.map(|&day| {
if day == prev_day_str {
new_day_of_month.to_string()
} else {
day.to_string()
}
})
.collect();
let new_bymonthday = new_days.join(",");
modified_recurrence = splice_value(
rrule_part,
bymonthday_start,
bymonthday_len,
&new_bymonthday,
);
return update_until_if_needed_tz(
&modified_recurrence,
new_dt,
&tz,
new_is_date,
);
}
} else {
if bymonthday_val == prev_day_of_month.to_string() {
let new_bymonthday = new_day_of_month.to_string();
modified_recurrence = splice_value(
rrule_part,
bymonthday_start,
bymonthday_len,
&new_bymonthday,
);
return update_until_if_needed_tz(
&modified_recurrence,
new_dt,
&tz,
new_is_date,
);
}
}
}
}
let byday_key = "BYDAY=";
let Some(byday_pos) = rrule_part.find(byday_key) else {
return rrule_part.to_string();
};
let byday_start = byday_pos + byday_key.len();
let byday_rest = &rrule_part[byday_start..];
let byday_len = byday_rest.find(';').unwrap_or(byday_rest.len());
let byday_val = &byday_rest[..byday_len];
let days: Vec<&str> = byday_val.split(',').collect();
if days.len() == 1 {
if is_monthly {
let has_position_prefix = days[0].len() > 2
&& (days[0].starts_with('-') || days[0].chars().next().unwrap().is_ascii_digit());
if has_position_prefix {
let prev_week_of_month = (prev_day_of_month as f32 / 7.0).ceil() as i32;
let new_week_of_month = (new_day_of_month as f32 / 7.0).ceil() as i32;
let days_in_prev_month =
days_in_month(prev_dt.date_naive().year(), prev_dt.date_naive().month());
let is_prev_last_week = prev_day_of_month + 7 > days_in_prev_month;
let days_in_new_month =
days_in_month(new_dt.date_naive().year(), new_dt.date_naive().month());
let is_new_last_week = new_day_of_month + 7 > days_in_new_month;
let prev_position_byday = if is_prev_last_week {
format!("-1{}", prev_byday)
} else {
format!("{}{}", prev_week_of_month, prev_byday)
};
let new_position_byday = if is_new_last_week {
format!("-1{}", new_byday)
} else {
format!("{}{}", new_week_of_month, new_byday)
};
if days[0] == prev_position_byday {
modified_recurrence = splice_value(
&modified_recurrence,
byday_start,
byday_len,
&new_position_byday,
);
}
} else {
if days[0] == prev_byday {
modified_recurrence =
splice_value(&modified_recurrence, byday_start, byday_len, new_byday);
}
}
} else {
if days[0] == prev_byday {
modified_recurrence =
splice_value(&modified_recurrence, byday_start, byday_len, new_byday);
}
}
} else {
let (old_day, new_day) = if is_monthly {
let has_position_prefix = days.iter().any(|&day| {
day.len() > 2
&& (day.starts_with('-') || day.chars().next().unwrap().is_ascii_digit())
});
if has_position_prefix {
let prev_week_of_month = (prev_day_of_month as f32 / 7.0).ceil() as i32;
let new_week_of_month = (new_day_of_month as f32 / 7.0).ceil() as i32;
let days_in_prev_month =
days_in_month(prev_dt.date_naive().year(), prev_dt.date_naive().month());
let is_prev_last_week = prev_day_of_month + 7 > days_in_prev_month;
let days_in_new_month =
days_in_month(new_dt.date_naive().year(), new_dt.date_naive().month());
let is_new_last_week = new_day_of_month + 7 > days_in_new_month;
let prev_position_byday = if is_prev_last_week {
format!("-1{}", prev_byday)
} else {
format!("{}{}", prev_week_of_month, prev_byday)
};
let new_position_byday = if is_new_last_week {
format!("-1{}", new_byday)
} else {
format!("{}{}", new_week_of_month, new_byday)
};
(prev_position_byday, new_position_byday)
} else {
(prev_byday.to_string(), new_byday.to_string())
}
} else {
(prev_byday.to_string(), new_byday.to_string())
};
if days.contains(&old_day.as_str()) && !days.contains(&new_day.as_str()) {
let mut new_days: Vec<String> = days
.iter()
.filter(|&&day| day != old_day.as_str())
.map(|&day| day.to_string())
.collect();
new_days.push(new_day);
new_days.sort(); let new_byday_val = new_days.join(",");
modified_recurrence =
splice_value(&modified_recurrence, byday_start, byday_len, &new_byday_val);
}
}
update_until_if_needed_tz(&modified_recurrence, new_dt, &tz, new_is_date)
}
fn update_until_if_needed_tz(
recurrence: &str,
new_local: chrono::DateTime<Tz>,
_tz: &Tz,
new_is_date: bool,
) -> String {
let until_key = "UNTIL=";
let mut current = recurrence.to_string();
let new_utc = new_local.with_timezone(&chrono::Utc);
let new_until_str = if new_is_date {
new_local.format("%Y%m%d").to_string()
} else {
new_utc.format("%Y%m%dT%H%M%SZ").to_string()
};
if let Some(until_pos) = current.find(until_key) {
let until_start = until_pos + until_key.len();
let until_rest = ¤t[until_start..];
let until_len = until_rest.find(';').unwrap_or(until_rest.len());
let until_val = &until_rest[..until_len];
let should_update = if until_val.ends_with('Z') && until_val.len() == 16 {
let year: i32 = until_val[0..4].parse().unwrap_or(0);
let month: u32 = until_val[4..6].parse().unwrap_or(0);
let day: u32 = until_val[6..8].parse().unwrap_or(0);
let hour: u32 = until_val[9..11].parse().unwrap_or(0);
let min: u32 = until_val[11..13].parse().unwrap_or(0);
let sec: u32 = until_val[13..15].parse().unwrap_or(0);
if let Some(existing) = chrono::Utc
.with_ymd_and_hms(year, month, day, hour, min, sec)
.single()
{
existing < new_utc
} else {
false
}
} else if until_val.len() == 8 {
let y1: i32 = until_val[0..4].parse().unwrap_or(0);
let m1: u32 = until_val[4..6].parse().unwrap_or(0);
let d1: u32 = until_val[6..8].parse().unwrap_or(0);
let existing_num = y1 as i64 * 10000 + m1 as i64 * 100 + d1 as i64;
let y2 = new_local.year();
let m2 = new_local.month() as i64;
let d2 = new_local.day() as i64;
let new_num = y2 as i64 * 10000 + m2 * 100 + d2;
existing_num < new_num
} else {
false
};
if should_update {
current = splice_value(¤t, until_start, until_len, &new_until_str);
}
return current;
}
current
}
fn splice_value(orig: &str, start: usize, len: usize, replacement: &str) -> String {
let mut out = String::with_capacity(orig.len() + replacement.len().saturating_sub(len));
out.push_str(&orig[..start]);
out.push_str(replacement);
out.push_str(&orig[start + len..]);
out
}
fn days_in_month(year: i32, month: u32) -> u32 {
match month {
1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
4 | 6 | 9 | 11 => 30,
2 => {
if (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0) {
29
} else {
28
}
}
_ => 30, }
}
#[cfg(test)]
mod tests4 {
#[test]
fn no_change_when_same_day_of_week() {
let prev_s = "2025-09-01T10:00:00"; let new_s = "2025-09-08T10:00:00";
let input = "RRULE:FREQ=WEEKLY;BYDAY=MO";
let result = super::patch_rrule(input, prev_s, new_s, "UTC");
assert_eq!(result, input);
}
#[test]
fn changes_single_day_weekly_recurrence() {
let prev_s = "2025-09-02T10:00:00"; let new_s = "2025-09-04T10:00:00";
let input = "RRULE:FREQ=WEEKLY;BYDAY=TU";
let expected = "RRULE:FREQ=WEEKLY;BYDAY=TH";
let result = super::patch_rrule(input, prev_s, new_s, "UTC");
assert_eq!(result, expected);
}
#[test]
fn updates_multiple_days_weekly_recurrence() {
let prev_s = "2025-09-01T10:00:00"; let new_s = "2025-09-03T10:00:00";
let input = "RRULE:FREQ=WEEKLY;BYDAY=MO,FR";
let expected = "RRULE:FREQ=WEEKLY;BYDAY=FR,WE";
let result = super::patch_rrule(input, prev_s, new_s, "UTC");
assert_eq!(result, expected);
}
#[test]
fn no_change_when_new_day_already_in_multiple_days() {
let prev_s = "2025-09-01T10:00:00"; let new_s = "2025-09-03T10:00:00";
let input = "RRULE:FREQ=WEEKLY;BYDAY=MO,WE,FR";
let result = super::patch_rrule(input, prev_s, new_s, "UTC");
assert_eq!(result, input);
}
#[test]
fn updates_until_when_lower_than_new_date() {
let prev_s = "2025-09-02"; let new_s = "2025-09-25";
let input = "RRULE:FREQ=WEEKLY;UNTIL=20250920;BYDAY=TU";
let expected = "RRULE:FREQ=WEEKLY;UNTIL=20250925;BYDAY=TH";
let result = super::patch_rrule(input, prev_s, new_s, "UTC");
assert_eq!(result, expected);
}
#[test]
fn updates_until_with_time_when_lower_than_new_date() {
let prev_s = "2025-09-02T10:00:00"; let new_s = "2025-09-25T10:00:00";
let input = "RRULE:FREQ=WEEKLY;UNTIL=20250920T100000Z;BYDAY=TU";
let expected = "RRULE:FREQ=WEEKLY;UNTIL=20250925T100000Z;BYDAY=TH";
let result = super::patch_rrule(input, prev_s, new_s, "UTC");
assert_eq!(result, expected);
}
#[test]
fn updates_monthly_recurrence_with_simple_byday() {
let prev_s = "2025-09-02T10:00:00"; let new_s = "2025-09-04T10:00:00";
let input = "RRULE:FREQ=MONTHLY;BYDAY=TU";
let expected = "RRULE:FREQ=MONTHLY;BYDAY=TH";
let result = super::patch_rrule(input, prev_s, new_s, "UTC");
assert_eq!(result, expected);
}
#[test]
fn updates_monthly_recurrence_with_position_byday() {
let prev_s = "2025-09-02T10:00:00"; let new_s = "2025-09-04T10:00:00";
let input = "RRULE:FREQ=MONTHLY;BYDAY=1TU";
let expected = "RRULE:FREQ=MONTHLY;BYDAY=1TH";
let result = super::patch_rrule(input, prev_s, new_s, "UTC");
assert_eq!(result, expected);
}
#[test]
fn updates_monthly_recurrence_with_last_day_position() {
let prev_s = "2025-09-30T10:00:00"; let new_s = "2025-09-25T10:00:00";
let input = "RRULE:FREQ=MONTHLY;BYDAY=-1TU";
let expected = "RRULE:FREQ=MONTHLY;BYDAY=-1TH";
let result = super::patch_rrule(input, prev_s, new_s, "UTC");
assert_eq!(result, expected);
}
#[test]
fn handles_complex_monthly_rrule() {
let prev_s = "2025-09-09T10:00:00"; let new_s = "2025-09-12T10:00:00";
let input = "RRULE:FREQ=MONTHLY;WKST=SU;UNTIL=20250930T235959Z;INTERVAL=2;BYDAY=2TU";
let expected = "RRULE:FREQ=MONTHLY;WKST=SU;UNTIL=20250930T235959Z;INTERVAL=2;BYDAY=2FR";
let result = super::patch_rrule(input, prev_s, new_s, "UTC");
assert_eq!(result, expected);
}
#[test]
fn handles_multiline_recurrence_with_exdate() {
let prev_s = "2025-09-02T10:00:00"; let new_s = "2025-09-04T10:00:00";
let input = "EXDATE;TZID=America/Denver:20250212T140000\nRRULE:FREQ=MONTHLY;BYDAY=TU";
let expected = "RRULE:FREQ=MONTHLY;BYDAY=TH";
let result = super::patch_rrule(input, prev_s, new_s, "UTC");
assert_eq!(result, expected);
}
#[test]
fn handles_complex_rrule() {
let prev_s = "2025-09-02T10:00:00"; let new_s = "2025-09-05T10:00:00";
let input = "RRULE:FREQ=WEEKLY;WKST=SU;UNTIL=20250930T235959Z;INTERVAL=2;BYDAY=TU";
let expected = "RRULE:FREQ=WEEKLY;WKST=SU;UNTIL=20250930T235959Z;INTERVAL=2;BYDAY=FR";
let result = super::patch_rrule(input, prev_s, new_s, "UTC");
assert_eq!(result, expected);
}
#[test]
fn updates_monthly_bymonthday() {
let prev_s = "2025-09-01T10:00:00"; let new_s = "2025-09-02T10:00:00";
let input = "RRULE:FREQ=MONTHLY;BYMONTHDAY=1";
let expected = "RRULE:FREQ=MONTHLY;BYMONTHDAY=2";
let result = super::patch_rrule(input, prev_s, new_s, "UTC");
assert_eq!(result, expected);
}
#[test]
fn updates_monthly_bymonthday_with_other_params() {
let prev_s = "2025-09-15T10:00:00"; let new_s = "2025-09-20T10:00:00";
let input = "RRULE:FREQ=MONTHLY;INTERVAL=2;BYMONTHDAY=15;COUNT=10";
let expected = "RRULE:FREQ=MONTHLY;INTERVAL=2;BYMONTHDAY=20;COUNT=10";
let result = super::patch_rrule(input, prev_s, new_s, "UTC");
assert_eq!(result, expected);
}
#[test]
fn updates_monthly_bymonthday_with_until() {
let prev_s = "2025-09-05T10:00:00"; let new_s = "2025-10-10T10:00:00";
let input = "RRULE:FREQ=MONTHLY;BYMONTHDAY=5;UNTIL=20250930T235959Z";
let expected = "RRULE:FREQ=MONTHLY;BYMONTHDAY=10;UNTIL=20251010T100000Z";
let result = super::patch_rrule(input, prev_s, new_s, "UTC");
assert_eq!(result, expected);
}
#[test]
fn no_change_when_bymonthday_doesnt_match() {
let prev_s = "2025-09-05T10:00:00"; let new_s = "2025-09-10T10:00:00";
let input = "RRULE:FREQ=MONTHLY;BYMONTHDAY=15";
let result = super::patch_rrule(input, prev_s, new_s, "UTC");
assert_eq!(result, input);
}
#[test]
fn handles_multiline_recurrence_with_bymonthday() {
let prev_s = "2025-09-01T10:00:00"; let new_s = "2025-09-02T10:00:00";
let input = "EXDATE;TZID=America/Denver:20250212T140000\nRRULE:FREQ=MONTHLY;BYMONTHDAY=1";
let expected = "RRULE:FREQ=MONTHLY;BYMONTHDAY=2";
let result = super::patch_rrule(input, prev_s, new_s, "UTC");
assert_eq!(result, expected);
}
#[test]
fn handles_multiple_bymonthday_values() {
let prev_s = "2025-09-05T10:00:00"; let new_s = "2025-09-06T10:00:00";
let input = "RRULE:FREQ=MONTHLY;BYMONTHDAY=1,5,15";
let expected = "RRULE:FREQ=MONTHLY;BYMONTHDAY=1,6,15";
let result = super::patch_rrule(input, prev_s, new_s, "UTC");
assert_eq!(result, expected);
}
#[test]
fn handles_combined_byday_and_bymonthday() {
let prev_s = "2025-09-02T10:00:00"; let new_s = "2025-09-04T10:00:00";
let input = "RRULE:FREQ=MONTHLY;BYDAY=TU;BYMONTHDAY=2";
let expected = "RRULE:FREQ=MONTHLY;BYDAY=TU;BYMONTHDAY=4";
let result = super::patch_rrule(input, prev_s, new_s, "UTC");
assert_eq!(result, expected);
}
#[test]
fn handles_invalid_recurrence_rule() {
let prev_s = "2025-09-02T10:00:00"; let new_s = "2025-09-04T10:00:00";
let input = "INVALID:FREQ=WEEKLY;BYDAY=TU";
let expected = "INVALID:FREQ=WEEKLY;BYDAY=TH";
let result = super::patch_rrule(input, prev_s, new_s, "UTC");
assert_eq!(result, expected); }
#[test]
fn handles_leap_year_edge_case() {
let prev_s = "2024-02-29T10:00:00"; let new_s = "2024-03-01T10:00:00";
let input = "RRULE:FREQ=MONTHLY;BYMONTHDAY=29";
let expected = "RRULE:FREQ=MONTHLY;BYMONTHDAY=1";
let result = super::patch_rrule(input, prev_s, new_s, "UTC");
assert_eq!(result, expected);
}
#[test]
fn handles_yearly_recurrence() {
let prev_s = "2025-09-02T10:00:00"; let new_s = "2025-09-04T10:00:00";
let input = "RRULE:FREQ=YEARLY;BYDAY=TU;BYMONTH=9";
let result = super::patch_rrule(input, prev_s, new_s, "UTC");
assert_eq!(result, input); }
#[test]
fn handles_month_transition_with_different_days() {
let prev_s = "2025-01-31T10:00:00";
let new_s = "2025-02-28T10:00:00";
let input = "RRULE:FREQ=MONTHLY;BYMONTHDAY=31";
let expected = "RRULE:FREQ=MONTHLY;BYMONTHDAY=28";
let result = super::patch_rrule(input, prev_s, new_s, "UTC");
assert_eq!(result, expected);
}
#[test]
fn handles_multiple_position_based_byday() {
let prev_s = "2025-09-02T10:00:00"; let new_s = "2025-09-04T10:00:00";
let input = "RRULE:FREQ=MONTHLY;BYDAY=1TU,3TU";
let expected = "RRULE:FREQ=MONTHLY;BYDAY=1TH,3TU";
let result = super::patch_rrule(input, prev_s, new_s, "UTC");
assert_eq!(result, expected);
}
#[test]
fn handles_simultaneous_day_of_week_and_month_change() {
let prev_s = "2025-09-01T10:00:00"; let new_s = "2025-09-10T10:00:00";
let input = "RRULE:FREQ=MONTHLY;BYDAY=MO;BYMONTHDAY=1";
let expected = "RRULE:FREQ=MONTHLY;BYDAY=MO;BYMONTHDAY=10";
let result = super::patch_rrule(input, prev_s, new_s, "UTC");
assert_eq!(result, expected);
}
#[test]
fn handles_empty_recurrence_string() {
let prev_s = "2025-09-02T10:00:00";
let new_s = "2025-09-04T10:00:00";
let input = "";
let result = super::patch_rrule(input, prev_s, new_s, "UTC");
assert_eq!(result, ""); }
#[test]
fn handles_recurrence_without_freq() {
let prev_s = "2025-09-02T10:00:00"; let new_s = "2025-09-04T10:00:00";
let input = "RRULE:BYDAY=TU;COUNT=10";
let result = super::patch_rrule(input, prev_s, new_s, "UTC");
assert_eq!(result, input); }
#[test]
fn yearly_bymonthday_updates_day_same_month() {
let prev_s = "2025-05-10T10:00:00";
let new_s = "2025-05-20T10:00:00";
let input = "RRULE:FREQ=YEARLY;BYMONTHDAY=10;BYMONTH=5";
let expected = "RRULE:FREQ=YEARLY;BYMONTHDAY=20;BYMONTH=5";
let result = super::patch_rrule(input, prev_s, new_s, "UTC");
assert_eq!(result, expected);
}
#[test]
fn yearly_bymonthday_updates_day_and_month_when_month_changes() {
let prev_s = "2025-01-31T10:00:00";
let new_s = "2025-02-01T10:00:00";
let input = "RRULE:FREQ=YEARLY;BYMONTHDAY=31;BYMONTH=1";
let expected = "RRULE:FREQ=YEARLY;BYMONTHDAY=1;BYMONTH=2";
let result = super::patch_rrule(input, prev_s, new_s, "UTC");
assert_eq!(result, expected);
}
#[test]
fn yearly_bymonthday_appends_bymonth_if_missing_on_month_change() {
let prev_s = "2025-01-31T10:00:00";
let new_s = "2025-02-01T10:00:00";
let input = "RRULE:FREQ=YEARLY;BYMONTHDAY=31";
let expected = "RRULE:FREQ=YEARLY;BYMONTHDAY=1;BYMONTH=2";
let result = super::patch_rrule(input, prev_s, new_s, "UTC");
assert_eq!(result, expected);
}
#[test]
fn timezone_changes_are_applied_in_passed_tz_weekly() {
let prev_s = "2025-09-02T21:00:00";
let new_s = "2025-09-03T21:00:00";
let input = "RRULE:FREQ=WEEKLY;BYDAY=TU";
let expected = "RRULE:FREQ=WEEKLY;BYDAY=WE";
let result = super::patch_rrule(input, prev_s, new_s, "America/Los_Angeles");
assert_eq!(result, expected);
}
#[test]
fn until_date_only_is_interpreted_in_tz_and_updated() {
let prev_s = "2025-09-02"; let new_s = "2025-09-09"; let input = "RRULE:FREQ=WEEKLY;UNTIL=20250902;BYDAY=TU";
let expected = "RRULE:FREQ=WEEKLY;UNTIL=20250909;BYDAY=TU";
let result = super::patch_rrule(input, prev_s, new_s, "America/Los_Angeles");
assert_eq!(result, expected);
}
#[test]
fn same_local_day_different_utc_no_change() {
let prev_s = "2025-09-02T00:30:00"; let new_s = "2025-09-02T23:30:00"; let input = "RRULE:FREQ=WEEKLY;BYDAY=TU";
let expected = "RRULE:FREQ=WEEKLY;BYDAY=TU";
let result = super::patch_rrule(input, prev_s, new_s, "America/Los_Angeles");
assert_eq!(result, expected);
}
#[test]
fn same_utc_day_different_local_changes_byday() {
let prev_s = "2025-09-02T20:30:00"; let new_s = "2025-09-03T00:30:00"; let input = "RRULE:FREQ=WEEKLY;BYDAY=TU";
let expected = "RRULE:FREQ=WEEKLY;BYDAY=WE";
let result = super::patch_rrule(input, prev_s, new_s, "America/Los_Angeles");
assert_eq!(result, expected);
}
}