Functional Weave
Code in Python

dates.quarter@1.0.0

impl/rust.rs

3,146 bytes · the Rust implementation · view raw

Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.

use super::funejson::Value;  ← the fune runtime: the JSON value the test vectors use; fune build keeps it only where a signature takes one
use super::dates_add_days::{days_in_month, format_iso_date, parse_iso_date, CivilDate};  ← from dates.add-days ^1.0.0 · built alongside by fune

/// 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()))
}