Labels
A label is the name your organisation writes for a period, such as FY2023 Q4. The naming block of a definition holds one or more schemes, and each scheme has a template for some of the levels year, quarter, period and week (spec).
Templates
Section titled “Templates”A template is literal text with placeholders. {yyyy} is the year number, {yy} its last two digits, {yy_next} the last two digits of the next year number, {q} the quarter, {pp} the period and {ww} the week. Every template must contain {yyyy} or {yy}, and a quarter, period or week template must contain its own index, so that every label names exactly one period of one year. The templates table in the spec gives the padding of each placeholder and the levels it is allowed in.
A template that uses {yy} without {yyyy} needs a century_pivot, which says which century a two-digit year belongs to when a label is parsed.
Format and parse
Section titled “Format and parse”format gives the label of a period under the default scheme, or under the scheme you name. parse reads a label back and returns its period. With the example definition from the quick start:
from pathlib import Path
from fiscalclock import FiscalCalendar
cal = FiscalCalendar(Path("retail-4-5-4.json").read_text(encoding="utf-8"))
label = cal.format(cal.period("period", 2023, 12))print(label)
parsed = cal.parse(label)print(parsed.level, parsed.year, parsed.index, parsed.start, parsed.last_day)FY2023 P12period 2023 12 2023-12-31 2024-02-03import { readFileSync } from "node:fs";import { FiscalCalendar } from "fiscalclock";
const cal = new FiscalCalendar(readFileSync("retail-4-5-4.json", "utf8"));
const label = cal.format(cal.period("period", 2023, 12));console.log(label);
const parsed = cal.parse(label);console.log(parsed.level, parsed.year, parsed.index, parsed.start, parsed.last_day);FY2023 P12period 2023 12 2023-12-31 2024-02-03use std::error::Error;use std::fs;
use fiscalclock::{FiscalCalendar, Level};
fn main() -> Result<(), Box<dyn Error>> { let cal = FiscalCalendar::from_json(&fs::read_to_string("retail-4-5-4.json")?)?;
let label = cal.format(&cal.period(Level::Period, 2023, Some(12))?, None)?; println!("{label}");
let parsed = cal.parse(&label)?; println!( "{} {} {:?} {} {}", parsed.level.name(), parsed.year, parsed.index, parsed.start, parsed.last_day() );
Ok(())}FY2023 P12period 2023 Some(12) 2023-12-31 2024-02-03parse matches the whole label, case-sensitively, against every template of every scheme. A label that matches nothing returns LABEL_NOT_RECOGNISED.
Ambiguous labels
Section titled “Ambiguous labels”When a label matches more than one period, parse does not pick one. It returns AMBIGUOUS_LABEL with every candidate in the error’s details, ordered by start date, then scheme name, then level (spec).
This happens, for example, when two schemes use the same template but number years differently. This illustrative definition has the 4-5-4 years of the quick start and two schemes that both write FY{yyyy}, one numbering each year by the calendar year it starts in and one by the calendar year it ends in:
{ "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": "by_start", "schemes": { "by_start": { "year_number": "start", "templates": { "year": "FY{yyyy}" } }, "by_end": { "year_number": "end", "templates": { "year": "FY{yyyy}" } } } }}from pathlib import Path
from fiscalclock import FiscalCalendar, FiscalclockError
cal = FiscalCalendar(Path("two-schemes.json").read_text(encoding="utf-8"))
try: cal.parse("FY2024")except FiscalclockError as error: print(error.code) for candidate in error.details["candidates"]: period = candidate["period"] print(candidate["scheme"], period["year"], period["start"], period["last_day"])AMBIGUOUS_LABELby_end 2023 2023-01-29 2024-02-03by_start 2024 2024-02-04 2025-02-01import { readFileSync } from "node:fs";import { FiscalCalendar } from "fiscalclock";
const cal = new FiscalCalendar(readFileSync("two-schemes.json", "utf8"));
try { cal.parse("FY2024");} catch (error) { console.log(error.code); for (const { scheme, period } of error.details.candidates) { console.log(scheme, period.year, period.start, period.last_day); }}AMBIGUOUS_LABELby_end 2023 2023-01-29 2024-02-03by_start 2024 2024-02-04 2025-02-01use std::error::Error;use std::fs;
use fiscalclock::{Details, FiscalCalendar};
fn main() -> Result<(), Box<dyn Error>> { let cal = FiscalCalendar::from_json(&fs::read_to_string("two-schemes.json")?)?;
if let Err(error) = cal.parse("FY2024") { println!("{}", error.code); if let Some(Details::Candidates(candidates)) = &error.details { for c in candidates { println!("{} {} {} {}", c.scheme, c.period.year, c.period.start, c.period.last_day()); } } } Ok(())}AMBIGUOUS_LABELby_end 2023 2023-01-29 2024-02-03by_start 2024 2024-02-04 2025-02-01FY2024 is the year that starts on 4 Feb 2024 under by_start, and the year that ends on 3 Feb 2024 under by_end. Each candidate’s year is its number under the default scheme, by_start, so the by_end reading appears as year 2023. To make every label unambiguous, give each scheme a template that the others cannot match, such as FY{yyyy} and FYE{yyyy}.