Operations and outputs, API version 1
This page is a copy of spec/api-v1.md in the repository, which is the source of truth.
Status: draft for fiscalclock 0.1.0. Last reviewed: 31 Oct 2026.
Every operation is defined once, in the Rust core, as JSON in and JSON out. The Rust crate, the Python package and the npm package expose these operations with native types at the edge, and each also returns the core’s JSON unchanged, so the same definition and inputs give byte-identical JSON in every language.
Conventions
Section titled “Conventions”- Dates are civil dates in ISO 8601 extended format,
YYYY-MM-DD, proleptic Gregorian, years 0001–9999. Anything else, including a time or zone suffix, isINVALID_DATE. - Every period is the half-open interval
[start, end_exclusive).last_dayisend_exclusiveminus one day. - Levels are
"year","quarter","period"and"week". - No operation reads the clock, the environment or any global state. “Current” questions take the date as an argument.
- JSON output has keys in the order shown in this document, no insignificant whitespace, and integers in plain decimal. Output contains no floating-point numbers in API version 1.
Values
Section titled “Values”Period
Section titled “Period”{"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}| Key | Meaning |
|---|---|
level |
The period’s level. |
year |
Fiscal year number under the default naming scheme, including its offset. |
index |
1-based position within the fiscal year; null for a year. |
start, end_exclusive, last_day |
Bounds. |
days |
Number of days. |
weeks |
Number of weeks when the year is week_based; null otherwise. |
has_extra_week |
For a year, quarter or period: true when it contains the extra week. For a week: true when it is the extra week. Always false in fixed_date years. |
Location
Section titled “Location”Returned by locate.
{"date":"2024-02-03","year":{…},"quarter":{…},"period":{…},"week":{…},"day_of_year":371,"day_of_quarter":98,"day_of_period":35,"day_of_week":7}day_of_* values are 1-based. day_of_week counts from week.starts_on (1) to the day before it (7).
{"code":"WEEKS_SUM_MISMATCH","path":"subdivision.periods","message":"periods sum to 51 weeks, expected 52","hint":"add one week to a period","details":null}codeis stable and listed in the error catalogue. Code and path must match across languages;messageandhintare produced by the core and are therefore also identical, but tests must not depend on their wording.pathis the JSON path of the offending field in dotted form with[i]for array indexes, such asnaming.schemes.fy.templates.yearorsubdivision.periods[3]. An empty string means the whole input. For operation arguments,pathnames the argument, such asdate.detailsisnullor an object.AMBIGUOUS_LABELputs its candidates here.
Paths for common errors:
| Error | Path |
|---|---|
SHORT_MONTH_POLICY_MISSING |
year.short_month |
EXTRA_WEEK_MISSING |
subdivision.extra_week |
UNSUPPORTED_COMBINATION |
subdivision.type |
CENTURY_PIVOT_MISSING |
naming.schemes.<name>.century_pivot |
YEAR_NUMBER_COLLISION, MAJORITY_TIE |
naming.schemes.<name>.year_number |
INVALID_JSON |
"" (empty string) |
| An invalid scheme name | naming.schemes.<name> |
PERIOD_NOT_FOUND from period |
index (OUT_OF_RANGE from period is at year) |
Any error about a period argument, including PERIOD_NOT_FOUND from shift |
period |
OUT_OF_RANGE from shift |
n |
TEMPLATE_MISSING, and OUT_OF_RANGE from format |
period |
OUT_OF_RANGE from dimension |
start if its fiscal year is out of range, otherwise end_exclusive when the fiscal year of the range’s last day (end_exclusive minus one day) is out of range. An empty range returns [] without this check. |
INVALID_ARGUMENT from dimension (start after end_exclusive, or more than 146,097 days) |
end_exclusive |
LEVEL_NOT_NESTED and an invalid level from children |
level |
INVALID_ARGUMENT from years (from after to, or more than 10,000 years) |
to |
PERIOD_NOT_FOUND from period for a negative index or one too large to represent |
index |
INVALID_ARGUMENT from period when an index is given for a year |
index |
OUT_OF_RANGE for a year argument that is an integer too large for 32 bits |
year |
OUT_OF_RANGE from parse when every matching label names a fiscal year outside the supported range |
label |
INVALID_ARGUMENT for an unknown operation name |
op |
INVALID_ARGUMENT when the arguments are not a JSON object, or repeat a key |
"" (empty string) |
When validation finds a MAJORITY_TIE in a scheme, it does not also report YEAR_NUMBER_COLLISION for that scheme.
A failed operation returns {"errors":[Error, …]}. Validation reports every error it finds, sorted by path and then code. Other operations report one error.
Operations
Section titled “Operations”| Operation | Arguments | Result |
|---|---|---|
validate |
definition |
{"ok":true,"calendar_id":"…","canonical":"…"} or errors |
locate |
date |
Location |
period |
level, year, index (omit or null for a year) |
Period, or PERIOD_NOT_FOUND |
shift |
period, n (integer) |
The period n steps away at the same level, crossing years as needed |
parent |
period |
The enclosing period one level up: week → period in a week_based year, week → year in a fixed_date year, period → quarter, quarter → year. A year has no parent (INVALID_ARGUMENT). |
children |
period, level |
Periods at level inside period, in order. week children of a quarter or period in a fixed_date year are LEVEL_NOT_NESTED, because weeks cross month boundaries there. |
years |
from, to (fiscal year numbers, inclusive, from ≤ to, at most 10,000 years) |
Array of year Periods |
format |
period, scheme (optional) |
{"label":"…","scheme":"…"}, or TEMPLATE_MISSING when the scheme has no template for the level, or SCHEME_NOT_FOUND |
parse |
label |
Period, or AMBIGUOUS_LABEL, or LABEL_NOT_RECOGNISED |
dimension |
start, end_exclusive (start ≤ end_exclusive, at most 146,097 days, which is 400 years) |
Array of rows, see below; an empty range gives [] |
dimension_csv |
start, end_exclusive |
The same rows as CSV text |
explain |
date |
{"date":"…","calendar_id":"…","steps":[{"level":"…","rule":"…","result":"…"},…]}. The rule and result strings are English for people to read. Their wording is not part of the API and may change in any release. |
Argument keys that an operation does not use are ignored, so a binding may pass a whole Period where coordinates are expected.
A period argument is a Period value or its coordinates {"level","year","index"}. The core recomputes the period from its coordinates and ignores any other keys, so a stale or edited Period cannot produce wrong bounds.
- Each template of each scheme is compiled to an exact, case-sensitive pattern.
{yyyy}matches 4 digits,{yy}and{yy_next}2 digits,{pp}and{ww}2 digits, and{q}a digit 1–9 optionally followed by a second digit (no leading zero). - Every template that matches the whole label yields a candidate, provided its values are consistent: indexes are in range for that year, and when
{yyyy}and{yy_next}both appear,{yy_next}equals the last two digits of{yyyy}+ 1. Two-digit years without{yyyy}resolve through the scheme’scentury_pivot. The scheme’soffsetis removed before the fiscal year is looked up. - Candidates that denote the same period (same level and start) are merged.
- One candidate: return its Period. More than one:
AMBIGUOUS_LABELwithdetails: {"candidates":[{"period":Period,"scheme":"…","level":"…"},…]}, ordered by start date, then scheme name, then level (yearfirst). None:LABEL_NOT_RECOGNISED, unless every match names a fiscal year outside the supported range, which isOUT_OF_RANGEatlabel.
dimension
Section titled “dimension”One row per day in [start, end_exclusive). Columns, in order:
date, calendar_id, fiscal_year, quarter, period, week, day_of_year, day_of_quarter, day_of_period, day_of_week, year_start, year_last_day, quarter_start, quarter_last_day, period_start, period_last_day, week_start, week_last_day, year_has_extra_week, period_has_extra_week, week_is_extra_week
followed by one label_<scheme>_<level> column for every template, ordered by scheme name and then by level (year, quarter, period, week). A label that cannot be formatted, such as a {yyyy} year number outside 1–9999, is null in JSON and an empty field in CSV.
In JSON each row is an object with those keys in that order. CSV output follows RFC 4180 with a header row (present even when there are no rows), \n line endings, a final newline, booleans as true/false, and fields quoted only when they contain a comma, quote or line break.
Error catalogue
Section titled “Error catalogue”| Code | Raised by | When |
|---|---|---|
INVALID_JSON |
validate | The definition is not valid JSON, is not an object, or repeats a key in some object. |
UNKNOWN_FIELD |
validate | A field the format does not define. |
MISSING_FIELD |
validate | A required field is absent. |
INVALID_TYPE |
validate | A field has the wrong JSON type. |
INVALID_VALUE |
validate | A value is outside its allowed set or range. |
UNSUPPORTED_SPEC_VERSION |
validate | spec_version is not 1. |
UNSUPPORTED_FEATURE |
validate | A reserved part of the format is used; the hint names the planned release. |
UNSUPPORTED_COMBINATION |
validate | A subdivision type is not valid inside the year type. |
WEEKS_SUM_MISMATCH |
validate | periods does not sum to 52. |
QUARTERS_SUM_MISMATCH |
validate | quarters does not sum to the number of periods. |
EXTRA_WEEK_MISSING |
validate | A week-based year has no extra_week. |
EXTRA_WEEK_OUT_OF_RANGE |
validate | extra_week.period names a period the year does not have. |
SHORT_MONTH_POLICY_MISSING |
validate | start.day is 29–31 and short_month is absent. |
SUBDIVISION_YEAR_MISMATCH |
validate | month_start_day differs from year.start.day. |
DEFAULT_SCHEME_MISSING |
validate | naming.default is not a scheme name. |
INVALID_TEMPLATE |
validate | A template has no year placeholder, an unknown or misplaced placeholder, or unbalanced braces. |
CENTURY_PIVOT_MISSING |
validate | A two-digit year template has no century_pivot. |
YEAR_NUMBER_COLLISION |
validate | A scheme gives two fiscal years the same number, or skips a number. |
MAJORITY_TIE |
validate | A majority scheme meets a fiscal year split evenly between two calendar years. |
INVALID_DATE |
any date argument | Not a valid YYYY-MM-DD civil date. |
TIMESTAMP_REJECTED |
bindings | A timestamp type (datetime.datetime, pandas Timestamp, JS Date) was passed. The hint says to convert it to a date in the intended time zone first. |
INVALID_ARGUMENT |
any | An argument is missing, of the wrong type or out of its allowed range. |
OUT_OF_RANGE |
any | The fiscal year involved is outside the supported range. |
PERIOD_NOT_FOUND |
period, shift | No period has those coordinates. |
LEVEL_NOT_NESTED |
children | The requested level does not nest inside the period. |
SCHEME_NOT_FOUND |
format | No scheme has that name. |
TEMPLATE_MISSING |
format | The scheme has no template for the period’s level. |
LABEL_NOT_RECOGNISED |
parse | No template matches the label. |
AMBIGUOUS_LABEL |
parse | More than one period matches; candidates in details. |