use super::funejson::Value; use super::dates_add_days::{days_in_month, format_iso_date, parse_iso_date, CivilDate}; /// Add whole months, keeping the day of the month and clamping it to the last /// day of the target month when that month is shorter (31 Jan + 1 = 28/29 Feb). /// /// The clamp is applied to the result only. Callers stepping through a schedule /// must add k months to the original anchor, not 1 month k times, or the 31st /// decays to the 28th after February. /// /// # Panics /// Panics on a malformed or impossible date, or a result outside 0001-9999. pub fn add_months(iso: &str, months: i64) -> String { let date = parse_iso_date(iso); // Count months from year 0 so the year and month fall out of one division. // div_euclid floors like Python's // and Math.floor for a negative total. let total = date.year * 12 + (date.month - 1) + months; let year = total.div_euclid(12); let month = total.rem_euclid(12) + 1; if !(1..=9999).contains(&year) { panic!( "\"{}\" plus {} months is outside the supported range 0001-01-01 to 9999-12-31", iso, months ); } let day = date.day.min(days_in_month(year, month)); format_iso_date(&CivilDate { year, month, day }) } pub fn fune_vector(args: &[Value]) -> Value { // Refuse what the typed signature cannot hold, with the wording TypeScript // and Python use, rather than let the conversion below quietly change it. if let Value::Float(f) = args[1] { if f.fract() != 0.0 { panic!("months must be an integer, received {}", f); } } Value::str(&add_months(args[0].as_str(), args[1].as_i64())) }