Skip to content

Definition Format

Karthik P edited this page Nov 1, 2026 · 2 revisions

Definition-Format

A fiscalclock calendar is one JSON definition made of five independent blocks, and this page explains each field, the validation codes and four worked definitions.

Last reviewed: 1 Nov 2026

The normative text is spec/definition-v1.md. This page explains it and adds the history behind it. Where the two disagree, the spec wins; please report the difference through the documentation issue form linked in the footer.

Status of each part

Part Status
year types fixed_date and week_based; calendar_months and week_pattern subdivisions; the week block with contains_start; naming schemes with start, end and majority Built in 0.1.0.
table years, anchored subdivision, stub, special periods, other extra-week placements, first_full numbering, eras, exceptions Planned for 0.2. Rejected today with UNSUPPORTED_FEATURE, so a definition is never silently misread.

The decision to keep the blocks separate is ADR-0005. The decision to have no built-in calendars is ADR-0004.

Top level

Field Required Meaning
spec_version yes Must be 1. Any other value gives UNSUPPORTED_SPEC_VERSION, and that is then the only error reported.
$schema no Ignored by the engine and left out of the canonical form.
year yes How the year is bounded.
week yes How weeks run.
subdivision yes How the year splits into quarters and periods.
naming yes How years and periods are numbered and labelled.
exceptions no Reserved for 0.2. Any value gives UNSUPPORTED_FEATURE.

Unknown fields give UNKNOWN_FIELD, missing required fields MISSING_FIELD, and wrong JSON types INVALID_TYPE. A repeated key in any object gives INVALID_JSON, because RFC 8259 leaves the meaning of a repeated key to the parser. Weekdays are lower-case English names (monday to sunday); months are integers 1 to 12.

year

Fixed-date years

type: "fixed_date" starts every fiscal year on the same month and day.

Field Required Meaning
start.month yes Month the year starts in, 1 to 12.
start.day yes Day of that month, 1 to 31.
short_month when start.day is 29 or more clamp uses the month's last day when the day does not exist; next uses the first day of the following month.

A start day of 29 to 31 without short_month gives SHORT_MONTH_POLICY_MISSING. A short_month with a start day of 28 or less gives INVALID_VALUE, because it would never apply.

Week-based years

type: "week_based" builds the year from whole weeks, with its boundary fixed by an anchor rule. Every such year has 364 or 371 days, and a 371-day year has an extra week.

Field Required Meaning
anchor.edge yes end fixes the year's last day; start fixes its first day.
anchor.month yes Month of the target date.
anchor.day yes 1 to 28, or "last" for the month's last day. Days 29 to 31 give INVALID_VALUE with a hint to use "last".
anchor.rule yes last: the latest qualifying weekday on or before the target date. first: the earliest on or after it. nearest: the one closest to it.

The anchor has no weekday field. The qualifying weekday for edge: "end" is the day before week.starts_on, and for edge: "start" it is week.starts_on itself (ADR-0028). The last and nearest rules follow the two 52-53-week rules in IRS Publication 538; first and edge: "start" are fiscalclock's generalisation.

Table years (planned for 0.2)

type: "table" will list year boundaries explicitly, for calendars that follow no rule. It is designed but not built (ADR-0021). ADR-0022 (Accepted, 1 Nov 2026) decides the shape: boundaries is a list of dates, year numbers come from naming, and period boundaries that follow no rule go in a planned subdivision.type: "table".

week

Field Required Meaning
starts_on yes First day of every week at every level.
numbering yes contains_start in 0.1.0. first_full is reserved for 0.2.

numbering has no default (ADR-0023).

  • In a week-based year, week 1 starts on the year's first day, every week has 7 days, and weeks run 1 to 52 or 1 to 53.
  • In a fixed-date year with contains_start, week 1 runs from the year's first day to the day before the next starts_on, so it can have 1 to 7 days. The last week can also be partial, and a year has 53 or 54 weeks.

subdivision

Calendar months

type: "calendar_months" is valid only inside a fixed-date year; inside a week-based year it gives UNSUPPORTED_COMBINATION.

Field Required Meaning
month_start_day yes Day each fiscal month starts. Must equal year.start.day, otherwise SUBDIVISION_YEAR_MISMATCH.
quarters yes Periods per quarter, in order. Must sum to 12, otherwise QUARTERS_SUM_MISMATCH.

Week patterns

type: "week_pattern" is valid only inside a week-based year in 0.1.0. Inside a fixed-date year it would need stub, which is reserved, so it gives UNSUPPORTED_COMBINATION.

Field Required Meaning
periods yes 12 or 13 positive integers: weeks per period. Must sum to 52 (WEEKS_SUM_MISMATCH); another length is INVALID_VALUE.
quarters yes Periods per quarter. Must sum to the number of periods (QUARTERS_SUM_MISMATCH).
extra_week.period yes The period that gains the extra week as its last week. Missing extra_week is EXTRA_WEEK_MISSING; a period outside the pattern is EXTRA_WEEK_OUT_OF_RANGE.

extra_week is required because every week-based rule produces 53-week years: 71 in every 400 years for all 168 last and nearest rules (checks C01 and C02 in research/verification/).

A quarter's length comes only from its periods. No rule assumes 13-week quarters, so Costco-style quarters of 3, 3, 3 and 4 periods are expressible.

naming

Field Required Meaning
default yes The scheme used for coordinates and for format when no scheme is named. Must be a key of schemes (DEFAULT_SCHEME_MISSING).
schemes yes One or more named schemes. Names match ^[a-z][a-z0-9_]{0,31}$.

Each scheme:

Field Required Meaning
year_number yes start, end or majority: the calendar year of the fiscal year's first day, of its last day, or that holds more than half its days. table is reserved for 0.2.
offset no, default 0 Added to the year number, between -1,000,000,000 and 1,000,000,000.
century_pivot when a template uses {yy} without {yyyy} Resolves two-digit years when parsing. Missing gives CENTURY_PIVOT_MISSING.
templates yes Label templates keyed by year, quarter, period and week. At least one.
eras no Reserved for 0.2.

year_number has no default (ADR-0029). Templates use the placeholders {yyyy}, {yy}, {yy_next}, {q}, {pp} and {ww}. Every template must contain a year placeholder, and a quarter, period or week template must contain its index placeholder; otherwise INVALID_TEMPLATE.

Validation numbers every fiscal year in the supported range under every scheme. It rejects a scheme that gives two years the same number or skips one (YEAR_NUMBER_COLLISION), and a majority scheme where some year has exactly half its days in each of two calendar years (MAJORITY_TIE). Semantics explains why.

Canonical form and calendar_id

After parsing, the definition is serialised with $schema removed, defaults written out, keys sorted by UTF-8 bytes and no insignificant whitespace. calendar_id is the first 16 lower-case hexadecimal characters of the SHA-256 of that text, so two definitions that differ only in key order, whitespace or written-out defaults share an id (ADR-0008).

Validation error codes

All codes below are built in 0.1.0 and listed in the error catalogue. Validation reports every error it finds, sorted by path and then code. Each code has at least one error vector in conformance/vectors/errors/.

Code Raised when
INVALID_JSON Not JSON, not an object, or a repeated key.
UNKNOWN_FIELD A field the format does not define.
MISSING_FIELD A required field is absent.
INVALID_TYPE A field has the wrong JSON type.
INVALID_VALUE A value is outside its allowed set or range.
UNSUPPORTED_SPEC_VERSION spec_version is not 1.
UNSUPPORTED_FEATURE A reserved part of the format is used; the hint names the planned release.
UNSUPPORTED_COMBINATION A subdivision type is not valid inside the year type.
WEEKS_SUM_MISMATCH periods does not sum to 52.
QUARTERS_SUM_MISMATCH quarters does not sum to the number of periods.
EXTRA_WEEK_MISSING A week-based year has no extra_week.
EXTRA_WEEK_OUT_OF_RANGE extra_week.period names a period the year does not have.
SHORT_MONTH_POLICY_MISSING start.day is 29 to 31 and short_month is absent.
SUBDIVISION_YEAR_MISMATCH month_start_day differs from year.start.day.
DEFAULT_SCHEME_MISSING naming.default is not a scheme name.
INVALID_TEMPLATE A template lacks a year or index placeholder, or has an unknown or misplaced placeholder or unbalanced braces.
CENTURY_PIVOT_MISSING A two-digit year template has no century_pivot.
YEAR_NUMBER_COLLISION A scheme gives two fiscal years the same number, or skips one.
MAJORITY_TIE A majority scheme meets a year split evenly between two calendar years.

The research proposal also lists codes that 0.1.0 does not have: STUB_WITHOUT_FIXED_YEAR, TABLE_NOT_INCREASING, EXCEPTIONS_OVERLAP, RULE_CONFLICT and TEMPLATES_AMBIGUOUS. They belong to features planned for 0.2 (see Roadmap). The proposal's TEMPLATE_LEVEL_MISSING is covered in 0.1.0 by UNKNOWN_FIELD for an unknown template level. The proposal says older spec versions are upgraded by an upgrade() function; 0.1.0 has only version 1 and no upgrade().

Reserved features

Field or value Planned release
year.type: "table" 0.2
subdivision.type: "anchored" (the broadcast calendar shape) 0.2
subdivision.stub 0.2, as decided in ADR-0024
subdivision.special_periods 0.2
extra_week.standalone, extra_week.by_year 0.2
week.numbering: "first_full" 0.2
year_number: "table", eras 0.2
exceptions (overrides, history, transitions) 0.2

Worked definitions

Each definition below is an example, not a built-in. The first three are checked by conformance vectors whose expected values come from independent sources; the fourth is checked by an independent generator.

NRF 4-5-4

The year ends on the Saturday nearest 31 January, weeks start on Sunday, and years are named by the calendar year in which they start (NRF). Fiscal 2023 runs from 29 Jan 2023 to 3 Feb 2024 with 53 weeks (check C04), and the 53-week years from 2016 to 2030 are 2017, 2023 and 2028 (check C03). The vector is nrf-4-5-4.json.

{
  "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}" } } }
  }
}

Apple-style: last Saturday of September

Apple's Form 10-Q states that its fiscal year "ends on the last Saturday of September" (10-Q; quote recorded in the cited vector apple-10q-2023-12-30.json). The 53-week years from 2016 to 2030 are 2017, 2023 and 2028 (check C09). The period pattern and the extra week in period 1 are illustrative: the filing says the first quarter of 2023 had 14 weeks, but it does not state Apple's period pattern.

{
  "spec_version": 1,
  "year": { "type": "week_based", "anchor": { "edge": "end", "month": 9, "day": "last", "rule": "last" } },
  "week": { "starts_on": "sunday", "numbering": "contains_start" },
  "subdivision": {
    "type": "week_pattern",
    "periods": [4, 4, 5, 4, 4, 5, 4, 4, 5, 4, 4, 5],
    "quarters": [3, 3, 3, 3],
    "extra_week": { "period": 1 }
  },
  "naming": {
    "default": "fy",
    "schemes": { "fy": { "year_number": "end", "century_pivot": 2069, "templates": { "year": "FY{yy}", "quarter": "Q{q} FY{yy}" } } }
  }
}

ISO 8601 week-numbering year

The ISO week year starts on the Monday nearest 1 January. Check C15 confirms that this rule matches Python's date.isocalendar() for every year from 1600 to 2399. It must use year_number: "majority": under start or end, two consecutive years get the same number, which validation rejects with YEAR_NUMBER_COLLISION (error vectors definition-year-number-collision-iso-start.json and definition-year-number-collision-iso-end.json). ISO 8601 has no periods or quarters, but subdivision is required, so the thirteen 4-week periods below are an arbitrary choice. The vector is iso-week-year.json.

{
  "spec_version": 1,
  "year": { "type": "week_based", "anchor": { "edge": "start", "month": 1, "day": 1, "rule": "nearest" } },
  "week": { "starts_on": "monday", "numbering": "contains_start" },
  "subdivision": {
    "type": "week_pattern",
    "periods": [4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4],
    "quarters": [3, 3, 3, 4],
    "extra_week": { "period": 13 }
  },
  "naming": {
    "default": "iso",
    "schemes": { "iso": { "year_number": "majority", "templates": { "year": "{yyyy}", "week": "{yyyy}-W{ww}" } } }
  }
}

Fixed 1 April year

A year that starts on 1 April and is named by its start year, as in "FY 2025-26". The vector fixed-apr-1-india.json checks this shape against the independent generator. The second scheme follows the research proposal's model of the Indian assessment year as a second label for the same dates. That model is under review: the Income-tax Act, 2025 replaced "assessment year" with "tax year" from 1 April 2026, as recorded on the Evidence page. Treat the ay scheme as illustrative of two schemes, not as a statement of Indian tax law.

{
  "spec_version": 1,
  "year": { "type": "fixed_date", "start": { "month": 4, "day": 1 } },
  "week": { "starts_on": "monday", "numbering": "contains_start" },
  "subdivision": { "type": "calendar_months", "month_start_day": 1, "quarters": [3, 3, 3, 3] },
  "naming": {
    "default": "fy",
    "schemes": {
      "fy": { "year_number": "start", "templates": { "year": "FY {yyyy}-{yy_next}", "quarter": "Q{q} FY {yyyy}-{yy_next}" } },
      "ay": { "year_number": "start", "offset": 1, "templates": { "year": "AY {yyyy}-{yy_next}" } }
    }
  }
}

Examples from the proposal that 0.1.0 cannot express

  • Vishay (a 31 December year with 13-week quarters) needs stub, reserved for 0.2. The proposal's Q1 anchor, the Saturday nearest 31 March, is illustrative: the filing does not name it. Vishay's reported Q1 ends of 1 Apr 2023 and 30 Mar 2024 are consistent with it (recorded on the Evidence page), but the definition has not been checked against every reported quarter end.
  • Nepal needs table years. The boundary dates in the proposal are illustrative, and the proposal itself says to fill them from an official Bikram Sambat converter. The source for "16, 17 or 18 July" is a user forum comment and is unverified.
  • The broadcast calendar needs the anchored subdivision, planned for 0.2. Its month and quarter lengths are reproduced by checks C10 to C14.

Contradictions in the source documents and how spec version 1 resolved them

The research proposal and the gap analysis contradict each other, or themselves, in several places. This wiki numbers them D1 to D12, and the PDR records the status of each. Those that touch the format are below.

# Contradiction Resolution in spec version 1
D1 stub is first_and_last in the block table but { "anchor": {...} } in the Vishay example. Resolved by ADR-0024 (Accepted, 1 Nov 2026): stub has a required anchor and absorb: "first_and_last". Still reserved and rejected in 0.1.0; planned for 0.2.
D2 The Nepal example uses year_number: "table" and boundaries[].number, which the block table does not define. Resolved by ADR-0022 (Accepted, 1 Nov 2026): boundaries is a list of dates and year_number: "table" is dropped; numbers come from naming and offset. Planned for 0.2.
D3 The anchor has no weekday field, and the proposal never says which weekday the year ends on. Resolved: the weekday derives from week.starts_on (ADR-0028).
D4 What a week_pattern sums to inside a fixed-date year is unspecified. Resolved for 0.1.0: that combination needs stub, so it is rejected with UNSUPPORTED_COMBINATION.
D9 The gap analysis says is_long; the proposal says has_extra_week. has_extra_week is the output name (see Glossary).
D12 Whether anchored and special_periods are in v0.1 is unclear. Both are reserved for 0.2 and rejected in 0.1.0.

Spec version 1 also narrows the proposal in two places. The proposal allows anchor.day 1 to 31; the spec allows 1 to 28 or "last", because days 29 to 31 do not exist in every month. The proposal says any subdivision works inside any year type; in 0.1.0 calendar_months needs a fixed-date year and week_pattern needs a week-based year.

Clone this wiki locally