use chrono::NaiveDate;
use crate::holidays::HolidayCalendar;
#[derive(Debug, Clone, PartialEq)]
pub struct Repeater {
pub repeater_type: RepeaterType,
pub value: u32,
pub unit: RepeaterUnit,
}
#[derive(Debug, Clone, PartialEq)]
pub enum RepeaterType {
Cumulative, CatchUp, Restart, }
#[derive(Debug, Clone, PartialEq)]
pub enum RepeaterUnit {
Day,
Week,
Month,
Year,
Hour,
Workday,
}
pub fn parse_repeater(s: &str) -> Option<Repeater> {
let s = s.trim();
let (repeater_type, rest) = if let Some(r) = s.strip_prefix(".+") {
(RepeaterType::Restart, r)
} else if let Some(r) = s.strip_prefix("++") {
(RepeaterType::CatchUp, r)
} else if let Some(r) = s.strip_prefix('+') {
(RepeaterType::Cumulative, r)
} else {
return None;
};
if rest.is_empty() {
return None;
}
if let Some(value_str) = rest.strip_suffix("wd") {
let value: u32 = value_str.parse().ok()?;
return Some(Repeater {
repeater_type,
value,
unit: RepeaterUnit::Workday,
});
}
let unit_char = rest.chars().last()?;
let value_str = &rest[..rest.len() - 1];
let value: u32 = value_str.parse().ok()?;
let unit = match unit_char {
'd' => RepeaterUnit::Day,
'w' => RepeaterUnit::Week,
'm' => RepeaterUnit::Month,
'y' => RepeaterUnit::Year,
'h' => RepeaterUnit::Hour,
_ => return None,
};
Some(Repeater {
repeater_type,
value,
unit,
})
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum DatePreference {
Past, Future, }
pub fn closest_date(base_date: NaiveDate, current: NaiveDate, prefer: DatePreference, repeater: &Repeater) -> Option<NaiveDate> {
use chrono::Datelike;
if current <= base_date {
return Some(base_date);
}
match repeater.unit {
RepeaterUnit::Year => {
let value = repeater.value as i32;
let base_month = base_date.month();
let base_day = base_date.day();
let base_year = base_date.year();
let current_year = current.year();
let years_diff = current_year - base_year;
let complete_years = (years_diff / value) * value;
let year1 = base_year + complete_years;
let n1 = NaiveDate::from_ymd_opt(year1, base_month, base_day.min(days_in_month(year1, base_month)))?;
let year2 = year1 + value;
let n2 = NaiveDate::from_ymd_opt(year2, base_month, base_day.min(days_in_month(year2, base_month)))?;
match prefer {
DatePreference::Past => if current >= n2 { Some(n2) } else { Some(n1) },
DatePreference::Future => if current <= n1 { Some(n1) } else { Some(n2) },
}
}
RepeaterUnit::Month => {
let months_to_add = repeater.value as i32;
let base_day = base_date.day();
let months_diff = (current.year() - base_date.year()) * 12 + (current.month() as i32 - base_date.month() as i32);
let complete_months = (months_diff / months_to_add) * months_to_add;
let n1 = add_months(base_date, complete_months)?;
let n1 = NaiveDate::from_ymd_opt(n1.year(), n1.month(), base_day.min(days_in_month(n1.year(), n1.month())))?;
let n2 = add_months(n1, months_to_add)?;
match prefer {
DatePreference::Past => if current >= n2 { Some(n2) } else { Some(n1) },
DatePreference::Future => if current <= n1 { Some(n1) } else { Some(n2) },
}
}
RepeaterUnit::Day | RepeaterUnit::Week => {
let days = match repeater.unit {
RepeaterUnit::Day => repeater.value as i64,
RepeaterUnit::Week => (repeater.value * 7) as i64,
_ => unreachable!(),
};
let days_diff = (current - base_date).num_days();
let complete_periods = days_diff / days;
let n1 = base_date + chrono::Duration::days(complete_periods * days);
let n2 = n1 + chrono::Duration::days(days);
match prefer {
DatePreference::Past => if current >= n2 { Some(n2) } else { Some(n1) },
DatePreference::Future => if current <= n1 { Some(n1) } else { Some(n2) },
}
}
RepeaterUnit::Workday => {
let calendar = HolidayCalendar::load().ok()?;
let mut n1 = base_date;
let mut count = 0u32;
while n1 < current {
let next = calendar.next_workday(n1);
if next > current {
break;
}
n1 = next;
count += 1;
if count >= repeater.value {
count = 0;
}
}
let mut n2 = if n1 == current { n1 } else { current };
for _ in 0..repeater.value {
n2 = calendar.next_workday(n2);
}
match prefer {
DatePreference::Past => if current >= n2 { Some(n2) } else { Some(n1) },
DatePreference::Future => if current <= n1 { Some(n1) } else { Some(n2) },
}
}
RepeaterUnit::Hour => {
Some(current)
}
}
}
#[allow(dead_code)]
pub fn next_occurrence(base_date: NaiveDate, repeater: &Repeater, from_date: NaiveDate) -> Option<NaiveDate> {
use chrono::Datelike;
if repeater.unit == RepeaterUnit::Workday {
let calendar = HolidayCalendar::load().ok()?;
let mut current = base_date;
let mut count = 0u32;
match repeater.repeater_type {
RepeaterType::Cumulative => {
while current < from_date {
current = calendar.next_workday(current);
count += 1;
if count >= repeater.value {
count = 0;
}
}
for _ in 0..repeater.value.saturating_sub(count) {
current = calendar.next_workday(current);
}
Some(current)
}
RepeaterType::CatchUp | RepeaterType::Restart => {
current = from_date;
for _ in 0..repeater.value {
current = calendar.next_workday(current);
}
Some(current)
}
}
} else {
match repeater.repeater_type {
RepeaterType::Cumulative => {
match repeater.unit {
RepeaterUnit::Month | RepeaterUnit::Year => {
let months_to_add = if repeater.unit == RepeaterUnit::Year {
(repeater.value * 12) as i32
} else {
repeater.value as i32
};
if from_date < base_date {
return Some(base_date);
}
let mut current = base_date;
while current <= from_date {
current = add_months(current, months_to_add)?;
}
Some(current)
}
_ => {
let days = match repeater.unit {
RepeaterUnit::Day => repeater.value as i64,
RepeaterUnit::Week => (repeater.value * 7) as i64,
RepeaterUnit::Hour => 1,
RepeaterUnit::Workday => unreachable!(),
_ => unreachable!(),
};
let mut current = base_date;
while current <= from_date {
current += chrono::Duration::days(days);
}
Some(current)
}
}
}
RepeaterType::CatchUp => {
let days = match repeater.unit {
RepeaterUnit::Day => repeater.value as i64,
RepeaterUnit::Week => (repeater.value * 7) as i64,
RepeaterUnit::Month => return add_months(from_date, repeater.value as i32),
RepeaterUnit::Year => return add_months(from_date, (repeater.value * 12) as i32),
RepeaterUnit::Hour => 1,
RepeaterUnit::Workday => unreachable!(),
};
if repeater.unit == RepeaterUnit::Week {
let target_weekday = base_date.weekday();
let mut current = from_date;
while current.weekday() != target_weekday || current <= base_date {
current += chrono::Duration::days(1);
}
Some(current)
} else {
Some(from_date + chrono::Duration::days(days))
}
}
RepeaterType::Restart => {
let days = match repeater.unit {
RepeaterUnit::Day => repeater.value as i64,
RepeaterUnit::Week => (repeater.value * 7) as i64,
RepeaterUnit::Month => return add_months(from_date, repeater.value as i32),
RepeaterUnit::Year => return add_months(from_date, (repeater.value * 12) as i32),
RepeaterUnit::Hour => 1,
RepeaterUnit::Workday => unreachable!(),
};
Some(from_date + chrono::Duration::days(days))
}
}
}
}
pub fn add_months(date: NaiveDate, months: i32) -> Option<NaiveDate> {
use chrono::Datelike;
let mut year = date.year();
let mut month = date.month() as i32 + months;
while month > 12 {
month -= 12;
year += 1;
}
while month < 1 {
month += 12;
year -= 1;
}
let day = date.day().min(days_in_month(year, month as u32));
NaiveDate::from_ymd_opt(year, month as u32, day)
}
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 tests {
use super::*;
#[test]
fn test_parse_workday_repeater() {
let r = parse_repeater("+1wd").unwrap();
assert_eq!(r.repeater_type, RepeaterType::Cumulative);
assert_eq!(r.value, 1);
assert_eq!(r.unit, RepeaterUnit::Workday);
}
#[test]
fn test_parse_workday_repeater_multiple() {
let r = parse_repeater("+2wd").unwrap();
assert_eq!(r.value, 2);
assert_eq!(r.unit, RepeaterUnit::Workday);
}
#[test]
fn test_parse_workday_catchup() {
let r = parse_repeater("++1wd").unwrap();
assert_eq!(r.repeater_type, RepeaterType::CatchUp);
assert_eq!(r.unit, RepeaterUnit::Workday);
}
#[test]
fn test_parse_workday_restart() {
let r = parse_repeater(".+1wd").unwrap();
assert_eq!(r.repeater_type, RepeaterType::Restart);
assert_eq!(r.unit, RepeaterUnit::Workday);
}
#[test]
fn test_parse_regular_day() {
let r = parse_repeater("+1d").unwrap();
assert_eq!(r.unit, RepeaterUnit::Day);
}
#[test]
fn test_next_occurrence_workday() {
let base = NaiveDate::from_ymd_opt(2025, 12, 5).unwrap(); let repeater = Repeater {
repeater_type: RepeaterType::Cumulative,
value: 1,
unit: RepeaterUnit::Workday,
};
let from = NaiveDate::from_ymd_opt(2025, 12, 5).unwrap();
let next = next_occurrence(base, &repeater, from).unwrap();
let expected = NaiveDate::from_ymd_opt(2025, 12, 8).unwrap(); assert_eq!(next, expected);
}
#[test]
fn test_next_occurrence_workday_skip_holidays() {
let base = NaiveDate::from_ymd_opt(2026, 1, 5).unwrap(); let repeater = Repeater {
repeater_type: RepeaterType::Cumulative,
value: 1,
unit: RepeaterUnit::Workday,
};
let from = NaiveDate::from_ymd_opt(2026, 1, 5).unwrap();
let next = next_occurrence(base, &repeater, from).unwrap();
let expected = NaiveDate::from_ymd_opt(2026, 1, 12).unwrap(); assert_eq!(next, expected);
}
#[test]
fn test_parse_year_repeater() {
let r = parse_repeater("+1y").unwrap();
assert_eq!(r.repeater_type, RepeaterType::Cumulative);
assert_eq!(r.value, 1);
assert_eq!(r.unit, RepeaterUnit::Year);
}
#[test]
fn test_parse_hour_repeater() {
let r = parse_repeater("+1h").unwrap();
assert_eq!(r.repeater_type, RepeaterType::Cumulative);
assert_eq!(r.value, 1);
assert_eq!(r.unit, RepeaterUnit::Hour);
}
#[test]
fn test_next_occurrence_year() {
let base = NaiveDate::from_ymd_opt(2024, 12, 5).unwrap();
let repeater = Repeater {
repeater_type: RepeaterType::Cumulative,
value: 1,
unit: RepeaterUnit::Year,
};
let from = NaiveDate::from_ymd_opt(2024, 12, 5).unwrap();
let next = next_occurrence(base, &repeater, from).unwrap();
let expected = NaiveDate::from_ymd_opt(2025, 12, 5).unwrap();
assert_eq!(next, expected);
}
#[test]
fn test_next_occurrence_year_before_base() {
let base = NaiveDate::from_ymd_opt(2025, 12, 11).unwrap();
let repeater = Repeater {
repeater_type: RepeaterType::Cumulative,
value: 1,
unit: RepeaterUnit::Year,
};
let from = NaiveDate::from_ymd_opt(2025, 12, 6).unwrap();
let next = next_occurrence(base, &repeater, from).unwrap();
eprintln!("base: {}, from: {}, next: {}", base, from, next);
let expected = NaiveDate::from_ymd_opt(2025, 12, 11).unwrap();
assert_eq!(next, expected, "Next occurrence should be base date when from < base");
}
#[test]
fn test_next_occurrence_month() {
let base = NaiveDate::from_ymd_opt(2024, 12, 5).unwrap();
let repeater = Repeater {
repeater_type: RepeaterType::Cumulative,
value: 1,
unit: RepeaterUnit::Month,
};
let from = NaiveDate::from_ymd_opt(2024, 12, 5).unwrap();
let next = next_occurrence(base, &repeater, from).unwrap();
let expected = NaiveDate::from_ymd_opt(2025, 1, 5).unwrap();
assert_eq!(next, expected);
}
}