Time and IDs

Fiscal calendars

Fiscal calendars for reports: a date's fiscal year, quarter and month for years starting in any month, retail 4-4-5, 4-5-4 and 5-4-4 calendars of 52 or 53 weeks (the NRF's, Apple's), and ISO weeks.

Version 1.0.0 · 4 functions · 2 crates

#dates#accounting#reports#finance

Functions

What use fiscal_calendar.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use fiscal_year, fiscal_retail from fiscal_calendar (Quillo 0.6).

fiscal_year(date: str, start_month: int) → anyor an error

The fiscal year holding date for a year that starts on the 1st of start_month (10 for the US government, 4 for the UK and Japan, 7 for Australia, 1 for calendar years): { fiscal_year, label, quarter, quarter_label, period, start, end, quarter_start, quarter_end, day, days }. A fiscal year is named by the calendar year it ends in ("FY2027" from October 2026); period is the month of the fiscal year, 1 to 12; day the day of the fiscal year.

fiscal_retail(date: str, pattern: str, year_end_month: int, end_weekday: str, method: str) → anyor an error

Where date falls in a retail calendar: weeks grouped by pattern ("4-4-5", "4-5-4" or "5-4-4"), the year ending on end_weekday ("Sat") that is the last in year_end_month, or the nearest to that month's end with method "nearest" (the NRF calendar: "4-5-4", 1, "Sat", "nearest"). { fiscal_year, quarter, period, week, week_of_period, weeks_in_year, year_start, year_end, period_start, period_end }. A year has 52 weeks, or 53 when its days run over; the 53rd week goes to the last period. The year is named by the calendar year holding most of its days.

fiscal_retail_periods(date: str, pattern: str, year_end_month: int, end_weekday: str, method: str) → listor an error

The twelve periods of the retail year that holds date: a list of { period, quarter, weeks, start, end }, for report headings and budgets.

fiscal_iso_week(date: str) → anyor an error

The ISO 8601 week of date: { year, week, weekday, text, week_start, week_end, weeks_in_year }; text as "2026-W44-4", weeks from Monday, week 1 the one with the year's first Thursday.

Get it

$ quillo install fiscal_calendar               # fiscal_calendar.qwer into this folder$ quillo install fiscal_calendar --example     # and its example, fiscal_calendar.qwe

Or with curl:

$ curl -O https://quillolang.org/plugins/files/fiscal_calendar.qwer$ curl -O https://quillolang.org/plugins/files/fiscal_calendar.qwe

Then, in your program, all of it or only what you call:

main.qwe
use fiscal_calendar.qweruse fiscal_year, fiscal_retail from fiscal_calendar      // Quillo 0.6: only these

It uses these crates, which cargo fetches the first time the plugin is built:

[dependencies]
chrono = { version = "0.4", default-features = false, features = ["std"] }serde_json = { version = "1", features = ["preserve_order"] }

Example

fiscal_calendar.qwe
// A finance dashboard: each sale filed under its fiscal year and quarter, a store's 4-5-4 retail periods, and ISO weeks for the warehouse.////   quillo run fiscal_calendar.qwe      then open http://localhost:8080/fiscaluse "fiscal_calendar.qwer"route GET /fiscal {    {        us_government: fiscal_year("2026-10-29", 10)?,        uk_government: fiscal_year("2026-04-05", 4)?.label,        calendar_year: fiscal_year("2026-10-29", 1)?.quarter_label,        // The NRF retail calendar: 4-5-4, the year ending on the Saturday nearest the end of January.        nrf: fiscal_retail("2026-10-29", "4-5-4", 1, "Sat", "nearest")?,        nrf_53_weeks: fiscal_retail("2023-12-31", "4-5-4", 1, "Sat", "nearest")?.weeks_in_year,        // Apple's: 4-4-5, ending on the last Saturday of September.        apple: fiscal_retail("2026-10-29", "4-4-5", 9, "Sat", "last")?.fiscal_year,        iso: fiscal_iso_week("2026-10-29")?,        iso_new_year: fiscal_iso_week("2027-01-01")?.text,        bad: fiscal_retail("2026-10-29", "4-4-4", 1, "Sat", "last").error,    }}route GET /periods = fiscal_retail_periods("2026-10-29", "4-5-4", 1, "Sat", "nearest")?
$ quillo run fiscal_calendar.qwe$ curl 'localhost:8080/fiscal'$ curl 'localhost:8080/periods'

Source

fiscal_calendar.qwer
//! Fiscal calendars for reports: a date's fiscal year, quarter and month for a year starting in any month; retail 4-4-5, 4-5-4 and 5-4-4 calendars of 52 and 53 weeks ending on a weekday (the NRF calendar, Apple's, Costco's); and ISO weeks.//! version: 1.0.0//!//! [dependencies]//! chrono = { version = "0.4", default-features = false, features = ["std"] }//! serde_json = { version = "1", features = ["preserve_order"] }use chrono::{Datelike, Duration, NaiveDate, Weekday};use serde_json::{json, Value};fn parse(d: &str) -> Result<NaiveDate, String> {    let t = d.trim();    NaiveDate::parse_from_str(t.get(..10).unwrap_or(t), "%Y-%m-%d").map_err(|_| format!("`{}` is not a date (2026-10-29)", d))}fn month_end(y: i32, m: u32) -> NaiveDate {    (if m == 12 { NaiveDate::from_ymd_opt(y + 1, 1, 1) } else { NaiveDate::from_ymd_opt(y, m + 1, 1) }).unwrap() - Duration::days(1)}fn add_months(y: i32, m: u32, n: i32) -> (i32, u32) {    let t = y * 12 + (m as i32 - 1) + n;    (t.div_euclid(12), (t.rem_euclid(12) + 1) as u32)}/// The fiscal year holding `date` for a year that starts on the 1st of `start_month` (10 for the US government, 4 for the UK and Japan, 7 for Australia, 1 for calendar years):/// `{ fiscal_year, label, quarter, quarter_label, period, start, end, quarter_start, quarter_end, day, days }`. A fiscal year is named by the calendar year it ends in ("FY2027" from October 2026); `period` is the month of the fiscal year, 1 to 12; `day` the day of the fiscal year.pub fn fiscal_year(date: &str, start_month: i64) -> Result<Value, String> {    if !(1..=12).contains(&start_month) { return Err(format!("start_month is 1 to 12, not {}", start_month)) }    let d = parse(date)?;    let sm = start_month as u32;    let start_year = if d.month() >= sm { d.year() } else { d.year() - 1 };    let start = NaiveDate::from_ymd_opt(start_year, sm, 1).unwrap();    let (ey, em) = add_months(start_year, sm, 11);    let end = month_end(ey, em);    let fy = if sm == 1 { start_year } else { start_year + 1 };    let period = ((d.year() - start_year) * 12 + d.month() as i32 - sm as i32) as u32 + 1;    let q = (period - 1) / 3 + 1;    let (qy, qm) = add_months(start_year, sm, ((q - 1) * 3) as i32);    let (qey, qem) = add_months(qy, qm, 2);    Ok(json!({        "fiscal_year": fy, "label": format!("FY{}", fy),        "quarter": q, "quarter_label": format!("Q{} FY{}", q, fy),        "period": period,        "start": start.to_string(), "end": end.to_string(),        "quarter_start": NaiveDate::from_ymd_opt(qy, qm, 1).unwrap().to_string(), "quarter_end": month_end(qey, qem).to_string(),        "day": (d - start).num_days() + 1, "days": (end - start).num_days() + 1,    }))}struct Retail { pattern: [i64; 3], end_month: u32, weekday: Weekday, nearest: bool }fn retail_opts(pattern: &str, year_end_month: i64, end_weekday: &str, method: &str) -> Result<Retail, String> {    let p: Vec<i64> = pattern.split('-').filter_map(|x| x.trim().parse().ok()).collect();    if p.len() != 3 || p.iter().sum::<i64>() != 13 || p.iter().any(|&w| !(4..=5).contains(&w)) {        return Err(format!("`{}` is not a retail pattern: 4-4-5, 4-5-4 or 5-4-4", pattern));    }    if !(1..=12).contains(&year_end_month) { return Err("year_end_month is 1 to 12".into()) }    let weekday: Weekday = end_weekday.trim().parse().map_err(|_| format!("`{}` is not a weekday", end_weekday))?;    let nearest = match method.trim().to_lowercase().as_str() { "nearest" => true, "last" | "" => false, m => return Err(format!("method is \"last\" or \"nearest\", not `{}`", m)) };    Ok(Retail { pattern: [p[0], p[1], p[2]], end_month: year_end_month as u32, weekday, nearest })}impl Retail {    // The last day of the fiscal year that ends in calendar year y.    fn year_end(&self, y: i32) -> NaiveDate {        let last = month_end(y, self.end_month);        let back = (last.weekday().num_days_from_monday() as i64 - self.weekday.num_days_from_monday() as i64).rem_euclid(7);        let on_or_before = last - Duration::days(back);        if self.nearest && back > 3 { on_or_before + Duration::days(7) } else { on_or_before }    }    fn year_of(&self, d: NaiveDate) -> (NaiveDate, NaiveDate) {        let mut y = d.year() - 1;        loop {            let e = self.year_end(y);            if e >= d { return (self.year_end(y - 1) + Duration::days(1), e) }            y += 1;        }    }    // Each period's weeks: the pattern four times, a 53rd week going to the last period.    fn periods(&self, weeks: i64) -> Vec<i64> {        let mut v: Vec<i64> = (0..12).map(|i| self.pattern[i % 3]).collect();        if weeks == 53 { v[11] += 1 }        v    }}fn name_of(end: NaiveDate) -> i32 {    // Named by the calendar year that holds most of its days.    (end - Duration::days(182)).year()}/// Where `date` falls in a retail calendar: weeks grouped by `pattern` (`"4-4-5"`, `"4-5-4"` or `"5-4-4"`), the year ending on `end_weekday` (`"Sat"`) that is the last in `year_end_month`, or the nearest to that month's end with `method` `"nearest"` (the NRF calendar: `"4-5-4"`, 1, `"Sat"`, `"nearest"`)./// `{ fiscal_year, quarter, period, week, week_of_period, weeks_in_year, year_start, year_end, period_start, period_end }`. A year has 52 weeks, or 53 when its days run over; the 53rd week goes to the last period. The year is named by the calendar year holding most of its days.pub fn fiscal_retail(date: &str, pattern: &str, year_end_month: i64, end_weekday: &str, method: &str) -> Result<Value, String> {    let r = retail_opts(pattern, year_end_month, end_weekday, method)?;    let d = parse(date)?;    let (start, end) = r.year_of(d);    let weeks = ((end - start).num_days() + 1) / 7;    let week = (d - start).num_days() / 7 + 1;    let mut acc = 0;    let mut out = Value::Null;    for (i, w) in r.periods(weeks).iter().enumerate() {        if week <= acc + w {            let ps = start + Duration::days(acc * 7);            out = json!({                "fiscal_year": name_of(end), "quarter": i / 3 + 1, "period": i + 1, "week": week, "week_of_period": week - acc,                "weeks_in_year": weeks, "year_start": start.to_string(), "year_end": end.to_string(),                "period_start": ps.to_string(), "period_end": (ps + Duration::days(w * 7 - 1)).to_string(),            });            break;        }        acc += w;    }    Ok(out)}/// The twelve periods of the retail year that holds `date`: a list of `{ period, quarter, weeks, start, end }`, for report headings and budgets.pub fn fiscal_retail_periods(date: &str, pattern: &str, year_end_month: i64, end_weekday: &str, method: &str) -> Result<Vec<Value>, String> {    let r = retail_opts(pattern, year_end_month, end_weekday, method)?;    let (start, end) = r.year_of(parse(date)?);    let weeks = ((end - start).num_days() + 1) / 7;    let mut s = start;    Ok(r.periods(weeks).iter().enumerate().map(|(i, w)| {        let e = s + Duration::days(w * 7 - 1);        let v = json!({ "period": i + 1, "quarter": i / 3 + 1, "weeks": w, "start": s.to_string(), "end": e.to_string() });        s = e + Duration::days(1);        v    }).collect())}/// The ISO 8601 week of `date`: `{ year, week, weekday, text, week_start, week_end, weeks_in_year }`; `text` as "2026-W44-4", weeks from Monday, week 1 the one with the year's first Thursday.pub fn fiscal_iso_week(date: &str) -> Result<Value, String> {    let d = parse(date)?;    let w = d.iso_week();    let wd = d.weekday().number_from_monday();    let start = d - Duration::days(wd as i64 - 1);    let dec28 = NaiveDate::from_ymd_opt(w.year(), 12, 28).unwrap();    Ok(json!({        "year": w.year(), "week": w.week(), "weekday": wd,        "text": format!("{}-W{:02}-{}", w.year(), w.week(), wd),        "week_start": start.to_string(), "week_end": (start + Duration::days(6)).to_string(),        "weeks_in_year": dec28.iso_week().week(),    }))}
OFX statements ofx.qwer · 1.0.0Bank and credit card statements in OFX and QFX, old SGML or XML: accounts, balances and each transaction with its bank id, ready to import without doubles.bankingimportaccountingfinance6 views · 9 downloadsBusiness days business_days.qwer · 1.0.0Working days for due dates, delivery estimates and SLAs: add them, count them between dates, skip weekends and holidays, with US, UK, French and German public holidays built in.datestimeinvoicesshipping6 views · 5 downloadsChinese calendar chinese_calendar.qwer · 1.0.0The Chinese lunisolar calendar: a date's lunar year, month and day in Chinese, leap months included, the year's stem and branch, zodiac animal and element, Chinese New Year, lunar dates back to Gregorian, and the 24 solar terms.datescalendarchineselocale8 views · 7 downloadsEnglish dates dates_english.qwer · 1.0.0Dates typed in English read as times, in the user's time zone and US or UK style: next friday at 3pm, tomorrow 9am, in 2 weeks, 3 days ago.datestimeformstext8 views · 11 downloadsHijri calendar hijri.qwer · 1.0.0Islamic (Hijri) dates: a Gregorian date in Umm al-Qura, civil or observational reckoning, written in English and Arabic; a Hijri date back to the Gregorian; a month's first and last days; and the year's holy days.datescalendarlocalei18n7 views · 8 downloadsISO durations iso_durations.qwer · 1.0.0ISO 8601 durations such as P1DT2H and PT30M read, written, turned into seconds and added to dates and times, months moving the calendar.timedatesapicalendars7 views · 10 downloads
Thread
711Useful? Like it, or add to its thread below.

Thread 0 posts

On Fiscal calendars: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.