use super::funejson::Value; use super::dates_add_days::{days_in_month, format_iso_date, parse_iso_date, CivilDate}; /// The first and last day of the month containing a date, and its length. /// /// All three are returned together because a caller needs them together - a /// billing period, a statement range, a pro-rata fraction - and because /// deriving `end` from `start` in caller code is where "31 January plus one /// month" bugs come from. /// /// `end` is the last day of the month, inclusive, not the 1st of the next /// month. Inclusive is what a statement prints and what a human checks against. /// /// # Panics /// Panics if the date is malformed or impossible. pub fn month_boundaries(iso: &str) -> MonthBoundaries { let date = parse_iso_date(iso); // February is the only month whose length is not a constant, and it follows // the full Gregorian rule: 2024 had 29 days, 1900 had 28 (a century year), // 2000 had 29 (divisible by 400). let days = days_in_month(date.year, date.month); MonthBoundaries { start: format_iso_date(&CivilDate { year: date.year, month: date.month, day: 1, }), end: format_iso_date(&CivilDate { year: date.year, month: date.month, day: days, }), days, } } /// True when the date is the last day of its month - a common billing trigger. pub fn is_month_end(iso: &str) -> bool { let date = parse_iso_date(iso); date.day == days_in_month(date.year, date.month) } pub fn month_boundaries_to_value(bounds: &MonthBoundaries) -> Value { Value::obj(vec![ ("start", Value::str(&bounds.start)), ("end", Value::str(&bounds.end)), ("days", Value::Int(bounds.days)), ]) } pub fn fune_vector(args: &[Value]) -> Value { month_boundaries_to_value(&month_boundaries(args[0].as_str())) }