use super::funejson::Value; use super::dates_add_days::{days_in_month, format_iso_date, parse_iso_date, CivilDate}; /// A month counted from January of year 0, back to its year and month. fn month_from_index(index: i64) -> (i64, i64) { (index.div_euclid(12), index.rem_euclid(12) + 1) } fn first_day(index: i64) -> String { let (year, month) = month_from_index(index); format_iso_date(&CivilDate { year, month, day: 1 }) } fn last_day(index: i64) -> String { let (year, month) = month_from_index(index); format_iso_date(&CivilDate { year, month, day: days_in_month(year, month), }) } /// The calendar and fiscal quarter of a date. The fiscal year is given as /// dates, not a number, because "FY2027" means the year ending in 2027 to one /// organisation and the year starting in it to another. /// /// # Panics /// Panics on a malformed date, a start month outside 1-12, or a fiscal year /// outside 0001-9999. pub fn quarter(iso: &str, fiscal_year_start_month: i64) -> Quarter { if !(1..=12).contains(&fiscal_year_start_month) { panic!( "fiscalYearStartMonth must be 1-12, received {}", fiscal_year_start_month ); } let date = parse_iso_date(iso); let months_into_year = (date.month - fiscal_year_start_month).rem_euclid(12); let fiscal_quarter = months_into_year / 3 + 1; // Work in months counted from year 0 so year boundaries need no special case. let year_start = date.year * 12 + (date.month - 1) - months_into_year; let quarter_start = year_start + 3 * (fiscal_quarter - 1); if month_from_index(year_start).0 < 1 || month_from_index(year_start + 11).0 > 9999 { panic!( "the fiscal year containing \"{}\" is outside the supported range 0001-01-01 to 9999-12-31", iso ); } Quarter { calendar_year: date.year, calendar_quarter: (date.month - 1) / 3 + 1, fiscal_quarter, fiscal_year_start: first_day(year_start), fiscal_year_end: last_day(year_start + 11), fiscal_quarter_start: first_day(quarter_start), fiscal_quarter_end: last_day(quarter_start + 2), } } pub fn quarter_to_value(q: &Quarter) -> Value { Value::obj(vec![ ("calendarYear", Value::Int(q.calendar_year)), ("calendarQuarter", Value::Int(q.calendar_quarter)), ("fiscalQuarter", Value::Int(q.fiscal_quarter)), ("fiscalYearStart", Value::str(&q.fiscal_year_start)), ("fiscalYearEnd", Value::str(&q.fiscal_year_end)), ("fiscalQuarterStart", Value::str(&q.fiscal_quarter_start)), ("fiscalQuarterEnd", Value::str(&q.fiscal_quarter_end)), ]) } 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!("fiscalYearStartMonth must be an integer, received {}", f); } } quarter_to_value(&quarter(args[0].as_str(), args[1].as_i64())) }