Skip to content

Semantics

Karthik P edited this page Oct 31, 2026 · 1 revision

Semantics

This page states the rules that decide whether two systems built on fiscalclock agree: what is built in 0.1.0, and what is planned for 0.2 and 0.3.

Last reviewed: 31 Oct 2026

The normative texts are spec/definition-v1.md and spec/api-v1.md. The field-by-field format is on Definition-Format.

Built in 0.1.0

Half-open intervals

Every period is the half-open interval [start, end_exclusive). last_day is end_exclusive minus one day and is derived, never stored (ADR-0007). A period value carries level, year, index, start, end_exclusive, last_day, days, weeks and has_extra_week. For example, NRF fiscal 2023 is start: "2023-01-29", end_exclusive: "2024-02-04", last_day: "2024-02-03", days: 371 (check C04 in research/verification/).

Half-open intervals tile without overlap: one period's end_exclusive is the next period's start. The property tests check this at every level (see Testing-Strategy).

Coordinates and levels

The levels are year, quarter, period and week. A period is identified by its coordinates (level, year, index), where year is the year number under the default naming scheme and index is 1-based within the year (null for the year itself). When an operation receives a period, the core recomputes it from its coordinates and ignores any other keys, so an edited or stale period object cannot produce wrong bounds.

In a fixed-date year, weeks cross month boundaries, so children of a quarter or period at the week level returns LEVEL_NOT_NESTED, and the parent of a week is the year.

Year numbering

A fiscal year's number comes only from its naming scheme, never from a guess (ADR-0029).

year_number The year is numbered by
start The calendar year of its first day.
end The calendar year of its last day.
majority The calendar year that holds more than half of its days.

offset is then added, for example 1 to label each year by the following calendar year.

Validation numbers every fiscal year in the supported range under every scheme and rejects a scheme in two cases.

  • YEAR_NUMBER_COLLISION: two fiscal years get the same number, or a number is skipped. This happens with start or end when the boundary can fall on either side of 1 January. ISO week years numbered by start give both the year starting Monday 1 January 2018 and the year starting Monday 31 December 2018 the number 2018.
  • MAJORITY_TIE: under majority, some fiscal year has exactly half of its days in each of two calendar years.

There is no default year_number, because organisations differ: NRF names fiscal 2026 by its start year (NRF), while Apple's Form 10-Q refers to the year that ended on 30 September 2023 as fiscal 2023, naming it by its end year (10-Q). The research proposal also says that "many Japanese companies" name April to March years by start year and that "FY 2025-26" naming is common in India, the UK and Australia. No primary source has been found for those two statements, so they are unverified (contradiction D10).

Labels and parsing

A naming scheme turns a period into a label with its templates, and parse turns a label back into a period.

  1. Each template is compiled to an exact, case-sensitive pattern.
  2. Every template of every scheme that matches the whole label gives a candidate, if its values are consistent: indexes are in range, and {yy_next} equals the year plus one.
  3. Candidates that denote the same period are merged.
  4. One candidate returns the period. More than one returns AMBIGUOUS_LABEL with every candidate in details, ordered by start date, then scheme name, then level. None returns LABEL_NOT_RECOGNISED.

Ambiguity is an error. fiscalclock never picks one reading (ADR-0010). The vector labels-ambiguous-india.json has two schemes that both produce "2025-26" for different years, and expects both candidates.

Two-digit years ({yy} without {yyyy}) resolve only through the scheme's century_pivot: a value yy means the unique year in [century_pivot - 99, century_pivot] ending in yy. There is no implicit reference year.

Planned, not in 0.1.0:

  • TEMPLATES_AMBIGUOUS, which would reject at validation time a scheme whose templates can produce the same string for different periods, is planned for 0.2 (Roadmap). Today the ambiguity is reported when such a label is parsed.
  • The proposal's built-in template aliases ("Q3 FY26", "3Q26") and aggregate labels that parse to ranges (H1, 9M, YTD, LTM) are not scheduled for any release yet.

Date inputs: civil dates only

Every date argument is a civil date: a year, month and day with no time and no time zone, in the proleptic Gregorian calendar (ADR-0006). The reason is that a timestamp's calendar day depends on the time zone. In JavaScript, new Date("2026-03-31") is midnight UTC while new Date(2026, 2, 31) is local midnight. In Python, datetime.datetime and pandas Timestamp are subclasses of datetime.date, so a timestamp would pass a plain date type check and be silently truncated.

Package Accepts Rejected with TIMESTAMP_REJECTED Test that proves the rejection
Rust crate The crate's own Date, or an ISO YYYY-MM-DD string in the JSON API Not applicable: the crate has no timestamp input json_api.rs covers INVALID_DATE for strings with a time or zone suffix
Python An object whose type() is exactly datetime.date, or an ISO string datetime.datetime and pandas Timestamp test_api.py, class TimestampsAreRejected
npm An ISO string or a Temporal.PlainDate Date, including one from another realm, and Temporal PlainDateTime, ZonedDateTime and Instant api.test.js

The TIMESTAMP_REJECTED error is produced by the core, so its code, path, message and hint are the same in Python and JavaScript. In 0.1.0 the hint tells the caller to convert the timestamp to a date in the time zone they mean. The proposal's helper that converts an instant to a civil date given an IANA zone is not built and has no release yet.

Any string that is not a valid YYYY-MM-DD date, including one with a time or zone suffix, returns INVALID_DATE.

Supported range

The public API covers fiscal years that lie wholly between 0001-01-02 and 9999-12-30 (ADR-0018). A date in, or a request for, any other fiscal year returns OUT_OF_RANGE, never a guess. The reason is Python: date(9999, 12, 31) + timedelta(days=1) raises OverflowError (check C18), so a period ending on 9999-12-31 would have an end_exclusive that Python cannot represent. The vectors range-edges-nrf.json, range-edges-apple.json and range-edges-jan-1.json pin the edges.

Determinism

No operation reads the clock, the environment or any global state (ADR-0017). A "current period" question takes the date as an argument, so repeated runs give the same answer.

Other edge cases

Case Rule in 0.1.0
Fixed-date start on day 29, 30 or 31 short_month is required: clamp uses the month's last day, next the first of the following month.
Leap day in a fixed-date year Years have 365 or 366 days, and day-of-year counts include 29 February.
The extra week Goes exactly where extra_week.period says, as that period's last week; has_extra_week marks the week, period, quarter and year.
Unequal quarters A quarter's length comes only from its periods.

Planned semantics

None of the rules in this section is built in 0.1.0. They are designed in the research proposal and scheduled on the Roadmap. Each needs a mode or basis that the caller must name: there is no default (ADR-0016, the required-mode rule). Leaving the mode out will be an error, because different organisations give different answers and a silent default would hide that.

Comparable periods (planned for 0.3)

A 53-week year is compared differently depending on which year it is compared with. comparable(period, target_year, mode) will return the comparable period with its own bounds, and the dates it excluded, so totals reconcile with the fiscal view.

Mode Used when comparing Rule
fiscal Any period with itself The period as reported, including the extra week.
truncated A 53-week year with the 52-week year before it The extra week is left out and returned as excluded dates.
restated The year after a 53-week year with the 53-week year NRF's method of "pushing each week of the 53-week year back one week, thereby ignoring the first week of the fiscal year" (NRF). The research proposal quoted this sentence cut short. For 2017 the first comparable week moves from 29 Jan to 4 Feb, to 5 to 11 Feb (check C08).

Gate D requires restated-view vectors that reproduce NRF's published restatement.

Equivalent day (planned for 0.3)

Mode Rule
same_position The same fiscal week and weekday: 364 days back, except in the year after a 53-week year, where it is 371 days back (check C07). A date in the extra week has no counterpart and returns NO_COUNTERPART.
same_weekday Always 364 days back. In the year after a 53-week year this lands one fiscal week later: NRF fiscal 2024 starts on 4 Feb 2024, and 364 days earlier is 5 Feb 2023, in week 2 of fiscal 2023 (check C06).
same_date Fixed-date years only: the same month and day.

Contradiction D6: the gap analysis puts equivalent day in v0.1, while the research proposal's feature table puts it in v0.3. The proposal is the source of truth, so this wiki follows it.

Proration (planned for 0.2)

split(range, level, basis) will divide a date range across periods with a required basis.

Basis Each period's share
actual_days Its share of the range's days.
equal_per_period The same for every full period, whatever its length.
weeks By whole weeks.
days_30_360 By the 30/360 day-count convention.

Shares will be exact rationals. allocate will split an amount in minor units by the largest-remainder method, with ties going to the earliest period, so no minor unit of currency is lost or created. JavaScript will take amounts as BigInt or strings, because numbers above 2^53 lose precision. Business-day bases are out of scope (ADR-0015). Gate C requires proration properties to pass in every binding: shares sum to exactly 1 and allocated amounts sum to the input.

API version 1 output contains no floating-point numbers. Whether the planned float convenience field changes that is not yet decided.

Clone this wiki locally