use super::funejson::Value; use super::dates_add_days::{days_from_civil, days_in_month, epoch_day_from_iso, iso_from_epoch_day, parse_iso_date}; use super::dates_add_months::add_months; use super::dates_day_of_week::day_of_week; fn check_range(name: &str, value: i64, low: i64, high: i64, kind: &str) { if value < low || value > high { panic!("{} must be {}-{} for {}, received {}", name, low, high, kind, value); } } /// The date of the `day`-th (clamped) or last `weekday` in a month index counted from year 0. fn date_in_month(index: i64, rule: &RecurrenceRule) -> i64 { let year = index.div_euclid(12); let month = index.rem_euclid(12) + 1; if year > 9999 { panic!("the schedule runs past 9999-12-31, outside the supported range 0001-01-01 to 9999-12-31"); } let length = days_in_month(year, month); if rule.kind == "monthly-on-day" { return days_from_civil(year, month, rule.day.unwrap().min(length)); } let last = days_from_civil(year, month, length); let last_weekday = day_of_week(&iso_from_epoch_day(last)); last - (last_weekday - rule.weekday.unwrap()).rem_euclid(7) } /// The first `count` dates of a rule on or after `start_iso`. Monthly dates are /// clamped from the rule's day every time, never from the previous date, so the /// 31st does not decay to the 28th after February. /// /// # Panics /// Panics on an invalid rule, a malformed start date, a negative count, or a /// schedule running past 9999-12-31. pub fn recurrence(rule: &RecurrenceRule, start_iso: &str, count: i64) -> Vec { if count < 0 { panic!("count must be a non-negative integer, received {}", count); } if rule.interval < 1 { panic!("interval must be a positive integer, received {}", rule.interval); } let kind = rule.kind.as_str(); match kind { "monthly-on-day" => { match rule.day { None => panic!("day must be 1-31 for monthly-on-day, received null"), Some(day) => check_range("day", day, 1, 31, kind), } if rule.weekday.is_some() { panic!("weekday does not apply to monthly-on-day"); } } "last-weekday-of-month" => { match rule.weekday { None => panic!("weekday must be 1-7 for last-weekday-of-month, received null"), Some(weekday) => check_range("weekday", weekday, 1, 7, kind), } if rule.day.is_some() { panic!("day does not apply to last-weekday-of-month"); } } "every-days" | "every-weeks" | "yearly" => { if rule.day.is_some() { panic!("day does not apply to {}", kind); } if rule.weekday.is_some() { panic!("weekday does not apply to {}", kind); } } _ => panic!( "unknown recurrence kind \"{}\": expected monthly-on-day, last-weekday-of-month, every-days, every-weeks or yearly", kind ), } let start = epoch_day_from_iso(start_iso); let mut out: Vec = Vec::new(); match kind { "every-days" | "every-weeks" => { let step = if kind == "every-weeks" { 7 * rule.interval } else { rule.interval }; for k in 0..count { out.push(iso_from_epoch_day(start + k * step)); } } "yearly" => { // Always from the anchor, so 29 February comes back in leap years. for k in 0..count { out.push(add_months(start_iso, 12 * rule.interval * k)); } } _ => { let date = parse_iso_date(start_iso); let mut first = date.year * 12 + (date.month - 1); if count > 0 && date_in_month(first, rule) < start { first += 1; } for k in 0..count { out.push(iso_from_epoch_day(date_in_month(first + k * rule.interval, rule))); } } } out } fn optional_int(value: &Value) -> Option { if value.is_null() { None } else { Some(value.as_i64()) } } pub fn recurrence_rule_from_value(value: &Value) -> RecurrenceRule { RecurrenceRule { kind: value.get("kind").as_str().to_string(), interval: value.get("interval").as_i64(), day: optional_int(value.get("day")), weekday: optional_int(value.get("weekday")), } } pub fn fune_vector(args: &[Value]) -> Value { let rule = recurrence_rule_from_value(&args[0]); Value::Arr( recurrence(&rule, args[1].as_str(), args[2].as_i64()) .iter() .map(|d| Value::str(d)) .collect(), ) }