use super::funejson::Value; use super::dates_add_days::{add_days, epoch_day_from_iso, parse_iso_date}; use super::dates_add_months::add_months; /// The first billing date strictly after `after_date`: anchor + k intervals /// for the smallest k >= 0 that lands after it. Each candidate is computed /// from the anchor, never from the previous billing date, so the 31st stays on /// month ends instead of decaying to the 28th after February. /// /// # Panics /// Panics if `interval_count` is below 1, the interval is unknown, or either /// date is not a real ISO date. pub fn next_billing_date(anchor_date: &str, interval: &str, interval_count: i64, after_date: &str) -> String { if interval_count < 1 { panic!("interval count must be 1 or more, received {}", interval_count); } let (days, months): (i64, i64) = match interval { "day" => (1, 0), "week" => (7, 0), "month" => (0, 1), "quarter" => (0, 3), "year" => (0, 12), other => panic!( "unknown billing interval \"{}\": expected day, week, month, quarter or year", other ), }; let anchor = epoch_day_from_iso(anchor_date); let after = epoch_day_from_iso(after_date); if after < anchor { return anchor_date.to_string(); } if days > 0 { let step = days * interval_count; let k = (after - anchor).div_euclid(step) + 1; return add_days(anchor_date, k * step); } // Jump straight to the period containing after_date's month; the clamp can // only put that candidate on or before after_date, so at most one or two // steps forward are needed from there. let step = months * interval_count; let a = parse_iso_date(anchor_date); let b = parse_iso_date(after_date); let months_apart = (b.year - a.year) * 12 + (b.month - a.month); let mut k = months_apart.div_euclid(step); let mut candidate = add_months(anchor_date, k * step); while epoch_day_from_iso(&candidate) <= after { k += 1; candidate = add_months(anchor_date, k * step); } candidate } pub fn fune_vector(args: &[Value]) -> Value { Value::str(&next_billing_date( args[0].as_str(), args[1].as_str(), args[2].as_i64(), args[3].as_str(), )) }