Compute fiscal calendars from a JSON definition
Describe your organisation’s fiscal calendar as a JSON definition, and fiscalclock tells you which fiscal year, quarter, period and week any date falls in, the bounds of every period, and the labels you use for them. One Rust core gives the same answers in Python, JavaScript and Rust.
pip install fiscalclocknpm install fiscalclockcargo add fiscalclockAn illustrative 4-5-4 definition, not any organisation’s published calendar. Its year rule gives the 53-week years NRF publishes. fiscalclock has no built-in calendars.
{ "spec_version": 1, "year": { "type": "week_based", "anchor": { "edge": "end", "month": 1, "day": "last", "rule": "nearest" } }, "week": { "starts_on": "sunday", "numbering": "contains_start" }, "subdivision": { "type": "week_pattern", "periods": [4, 5, 4, 4, 5, 4, 4, 5, 4, 4, 5, 4], "quarters": [3, 3, 3, 3], "extra_week": { "period": 12 } }, "naming": { "default": "nrf", "schemes": { "nrf": { "year_number": "start", "templates": { "year": "FY{yyyy}", "quarter": "FY{yyyy} Q{q}", "period": "FY{yyyy} P{pp}", "week": "FY{yyyy} W{ww}" } } } }}Fiscal 2023 under that definition, drawn to scale: 53 weeks from 29 Jan 2023 up to, but not including, 4 Feb 2024. Each period is a half-open interval, so the date under a boundary line is the first day of the period to its right. The extra 53rd week is the last week of period 12. The dates were computed by the fiscalclock 0.1.0 npm package when this page was built, and the build fails unless the year has the 53 weeks that NRF’s list of 53-week years gives it and the dates match the dates from an independent implementation of the spec.
Every period's dates
| Period | Start | End (excluded) | Last day | Weeks |
|---|---|---|---|---|
| P1 | 4 | |||
| P2 | 5 | |||
| P3 | 4 | |||
| P4 | 4 | |||
| P5 | 5 | |||
| P6 | 4 | |||
| P7 | 4 | |||
| P8 | 5 | |||
| P9 | 4 | |||
| P10 | 4 | |||
| P11 | 5 | |||
| P12, with the extra week | 5 |
Ask it a question
Section titled “Ask it a question”Which fiscal quarter is 3 Feb 2024 in, and what is it called? Save the definition above as retail-4-5-4.json and ask:
from pathlib import Path
from fiscalclock import FiscalCalendar
cal = FiscalCalendar(Path("retail-4-5-4.json").read_text(encoding="utf-8"))
here = cal.locate("2024-02-03")print(cal.format(here.quarter))print(cal.call_json("locate", '{"date": "2024-02-03"}'))import { readFileSync } from "node:fs";import { FiscalCalendar } from "fiscalclock";
const cal = new FiscalCalendar(readFileSync("retail-4-5-4.json", "utf8"));
const here = cal.locate("2024-02-03");console.log(cal.format(here.quarter));console.log(cal.callJson("locate", '{"date": "2024-02-03"}'));use std::error::Error;use std::fs;
use fiscalclock::FiscalCalendar;
fn main() -> Result<(), Box<dyn Error>> { let cal = FiscalCalendar::from_json(&fs::read_to_string("retail-4-5-4.json")?)?;
let here = cal.locate("2024-02-03".parse()?)?; println!("{}", cal.format(&here.quarter, None)?); println!("{}", cal.call_json("locate", r#"{"date": "2024-02-03"}"#));
Ok(())}All three print the same two lines: the label, then the core’s JSON for the date, byte for byte the same in every language. Each snippet is a file in website/examples/ that npm run test:examples runs against version 0.1.0 of its package, and it must print exactly the output below.
FY2023 Q4
{"date":"2024-02-03","year":{"level":"year","year":2023,"index":null,"start":"2023-01-29","end_exclusive":"2024-02-04","last_day":"2024-02-03","days":371,"weeks":53,"has_extra_week":true},"quarter":{"level":"quarter","year":2023,"index":4,"start":"2023-10-29","end_exclusive":"2024-02-04","last_day":"2024-02-03","days":98,"weeks":14,"has_extra_week":true},"period":{"level":"period","year":2023,"index":12,"start":"2023-12-31","end_exclusive":"2024-02-04","last_day":"2024-02-03","days":35,"weeks":5,"has_extra_week":true},"week":{"level":"week","year":2023,"index":53,"start":"2024-01-28","end_exclusive":"2024-02-04","last_day":"2024-02-03","days":7,"weeks":1,"has_extra_week":true},"day_of_year":371,"day_of_quarter":98,"day_of_period":35,"day_of_week":7} Why you can check the results
Section titled “Why you can check the results”- Shared conformance vectors. The vector files in
conformance/are run by the test suite of every package. Expected results come from cited sources or from generators written independently of the library, never from the library’s own output. - External oracles. Week-based years are checked against Python’s ISO 8601 week dates (
date.isocalendar()) for every year from 1600 to 2399, and the NRF 4-5-4 definition against the 53-week years NRF publishes (2006, 2012, 2017 and 2023). - A second implementation. A deliberately slow day-by-day reference implementation is compared with the core for every date from 1900 to 2200 across 960 generated definitions.
- Property tests. Periods tile their year without gaps or overlaps, nest correctly, and round-trip through
shiftand throughformatandparse. Week-based years have 364 or 371 days and 71 long years in every 400. - Identical output in every language. On every push, CI runs all vectors through Rust and through the Python and npm packages on Linux, macOS and Windows, and requires the raw outputs to match the Rust output byte for byte.
- Reproducible verification. Every date-arithmetic claim in the design documents is reproduced by a standard-library script in
research/verification/, which CI reruns on every pull request.
These statements are taken from the project README.
Packages
Section titled “Packages”All three packages are built from the same Rust core and return the same JSON for the same input. Version 0.1.0 is the first release; the definition format and API are version 1 drafts and may change before 1.0.
| Package | Install | Supports |
|---|---|---|
| Python package | pip install fiscalclock |
CPython 3.10 and later, and free-threaded 3.14; wheels for Linux, macOS and Windows |
| npm package | npm install fiscalclock |
Node.js 22 and later, and browsers (WebAssembly) |
| Rust crate | cargo add fiscalclock |
Rust 1.85 and later, no_std with alloc |