Repository navigation
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.
| 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.
| 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.
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.
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.
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".
| 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 nextstarts_on, so it can have 1 to 7 days. The last week can also be partial, and a year has 53 or 54 weeks.
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. |
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.
| 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.
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).
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().
| 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 |
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.
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'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}" } } }
}
}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}" } } }
}
}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}" } }
}
}
}-
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
tableyears. 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
anchoredsubdivision, planned for 0.2. Its month and quarter lengths are reproduced by checks C10 to C14.
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.
fiscalclock is licensed under MIT OR Apache-2.0 (MIT, Apache-2.0). Last reviewed: 31 Oct 2026. Found an error on this page? Report it with the documentation form.