Skip to content

Operations and outputs, API version 1

View as Markdown

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.

  • 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, is INVALID_DATE.
  • Every period is the half-open interval [start, end_exclusive). last_day is end_exclusive minus 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.
{"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.

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}
  • code is stable and listed in the error catalogue. Code and path must match across languages; message and hint are produced by the core and are therefore also identical, but tests must not depend on their wording.
  • path is the JSON path of the offending field in dotted form with [i] for array indexes, such as naming.schemes.fy.templates.year or subdivision.periods[3]. An empty string means the whole input. For operation arguments, path names the argument, such as date.
  • details is null or an object. AMBIGUOUS_LABEL puts 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.

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.

  1. 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).
  2. 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’s century_pivot. The scheme’s offset is removed before the fiscal year is looked up.
  3. Candidates that denote the same period (same level and start) are merged.
  4. One candidate: return its Period. More than one: AMBIGUOUS_LABEL with details: {"candidates":[{"period":Period,"scheme":"…","level":"…"},…]}, ordered by start date, then scheme name, then level (year first). None: LABEL_NOT_RECOGNISED, unless every match names a fiscal year outside the supported range, which is OUT_OF_RANGE at label.

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.

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.