ADR 0019 - Deferred: date arithmetic functions
- Status: accepted
- Date: 2026-05-08
- Spec target: XTL 0.1 (deferral); functions land no earlier than XTL 1.x
- Affects: STABILITY.md "What 1.0 does NOT include"; future ADR for the actual functions
Context
Real reporting templates routinely need date arithmetic — "renewal
month + 30 days", "end of fiscal quarter", "days between two dates".
Excel ships these as EOMONTH, EDATE, DATEDIF, WORKDAY,
NETWORKDAYS, etc. XTL 0.1 has no date arithmetic surface; templates
that need it currently compute the values upstream (in the source
workbook, in __inputs__, or in a wrapper script) and pass the
resolved value into the template.
This ADR records the decision to defer, not a design for the functions themselves. Adding new functions is backwards-compatible (per ADR-0014's pattern) and does not require a major version bump, so deferring to XTL 1.x is a clean cut.
Considered Options
A. Ship a date arithmetic subset in 1.0.
Pro: matches Excel mental model; reduces the upstream-computation
burden for template authors. Con: each function carries its own
edge-case surface (month-end clamping, leap-year semantics, holiday
calendars for WORKDAY). Specifying them properly is a multi-ADR
effort that delays 1.0.
B. Defer all date arithmetic to a future ADR after 1.0. Pro: 1.0 cut is unblocked; templates that need arithmetic compute it upstream, which works today. Con: authors of templates that expect Excel-like ergonomics will hit a wall.
C. Ship just TODAY() (already in 0.1) and a single DATE_ADD
primitive.
Pro: minimum viable arithmetic. Con: still requires specifying
month-end semantics; one function is rarely enough — once shipped,
authors will ask for EOMONTH, EDATE, etc.
Decision
Adopt option B. XTL 0.1 ships with TODAY() only (per ADR-0001).
Date arithmetic functions are explicitly deferred to a future ADR
that may land any time during the XTL 1.x line.
A separate ADR — when proposed — MUST cover:
- The function set to add (likely
EOMONTH,EDATE,DATEDIFas the highest-value subset;WORKDAY/NETWORKDAYSare out of scope until a holiday-calendar model is specified). - Month-end clamping rules (Excel: clamp to last day of target month; spec MUST be explicit).
- Argument coercion: do these accept strings like
"2026-05-08", or only Date values? - Time component handling: do they preserve / drop / zero out the time portion?
- Conformance fixtures for each function, including edge cases (leap year Feb 29, month-end clamping, negative offsets).
Until that ADR ships, templates needing arithmetic compute it
outside the engine and pass the result through __inputs__ or
__config__.
Consequences
- 1.0 templates that need arithmetic look slightly clunkier than Excel formulas. This is a real ergonomics cost.
- The cost is paid in template-author time, not in implementation-time or porter-time. Removing this cost from the 1.0 critical path is the right tradeoff.
- A 1.x release adding date arithmetic does not bump the spec major version. Implementations that don't yet support the new functions remain conformant for the surface they target.
References
- ADR-0001 —
TODAY()UTC semantics (the only date function in 0.1) - ADR-0017 — Source value model (Date type definition)
STABILITY.md"What 1.0 does NOT include"
Amendment (2026-05-18) — promote 6 functions
- Status: accepted (this amendment)
- Date: 2026-05-18
- Spec target: XTL 0.1
- Affects:
spec/language.md§ "Functions" (function table + a new "Date Component and Arithmetic Functions" subsection),src/functions.ts,src/error-codes.ts(newxl3/eval/type-mismatchcode), conformance corpus (one fixture per function).
The original deferral above is preserved as the prior decision. This amendment promotes the highest-value subset to accepted for the next minor release. The deferred-status header at the top of this ADR is superseded by the accepted status of this amendment; the prose is kept for historical context per ADR-0034 Corollary 1 (absorb experience, name the decision).
Per ADR-0034 Corollary 1, the function set, semantics, and edge cases below are absorbed from JXLS / Excel experience without importing JXLS's expression-language form. The functions are Excel-native names and are evaluated by XTL's runtime so that template output does not depend on Excel recalculating formulas in the rendered workbook.
Functions added
| Function | Arity | Returns | Behavior |
|---|---|---|---|
YEAR(date) | 1 | Number (integer) | 4-digit calendar year of date, in UTC. |
MONTH(date) | 1 | Number (integer) | Month 1–12 of date, in UTC. |
DAY(date) | 1 | Number (integer) | Day-of-month 1–31 of date, in UTC. |
EOMONTH(date, months) | 2 | Date (UTC midnight) | The last day of the month that is months calendar months away from date. months MAY be negative. |
EDATE(date, months) | 2 | Date (UTC midnight) | The date that is months calendar months away from date, preserving day-of-month. If the source day-of-month does not exist in the target month, the result is clamped to that month's last day (Excel semantics). |
DATEDIF(start, end, unit) | 3 | Number (integer) | Count of complete units between start and end. unit is one of "Y", "M", "D" (case-sensitive per Excel). End-exclusive: a one-second gap before the boundary does not count. |
All six entries are also added to the FUNCTION_ARITY table per
ADR-0024.
Semantics (normative)
-
Timezone. All extraction and arithmetic operate in UTC, per ADR-0017 § "Timezone (normative)". Implementations MUST NOT use host-locale accessors (
getFullYear,getMonth,getDate); they MUST use the UTC variants. This rule covers component extraction, month rollover inEOMONTH/EDATE, and the day count inDATEDIF. -
Argument types. The first argument of all six functions MUST be a Date value per ADR-0017. A non-Date argument (string, number, Boolean, empty) raises
xl3/eval/type-mismatch. String-to-Date coercion is intentionally not applied here: authors who hold a string like"2026-05-08"pass it through user-input coercion (ADR-0010) or compute the Date upstream. This keeps the per-call surface narrow and matches the ADR-0023 "fail loudly on operand shape" principle. -
monthsargument (EOMONTH,EDATE). MUST be an integer (Number with zero fractional part). Fractional or non-numeric values raisexl3/eval/type-mismatch. Negative values are valid. Out-of-range values (very large positive or negative) are valid as long as the resulting calendar date is representable by the host's Date type; implementations MAY reject results outside the Excel serial-date range (1900–9999) asxl3/eval/type-mismatch. -
unitargument (DATEDIF). MUST be one of the string literals"Y","M","D". Other values — including lowercase"y", Excel's extended"YM"/"YD"/"MD"units, or any other string — raisexl3/eval/type-mismatch. The lowercase / extended units MAY be added by a future ADR. -
DATEDIFsign. Whenstart > end, the result is the negative of the count that would be returned for the swapped arguments. This is a deliberate departure from Excel's#NUM!error for reversed arguments: reports rendered through XTL routinely compute "days remaining until renewal" where the order can be either sign, and the negative result is more useful than an error. -
DATEDIFend-exclusivity. Matches Excel:DATEDIF(d, d, "D")is0;DATEDIF(d, d+1day, "D")is1;DATEDIF(d, d+1day-1s, "D")is0. -
Output types.
YEAR,MONTH,DAY,DATEDIFreturn Number (integer).EOMONTHandEDATEreturn Date values at UTC midnight — not strings — so that template cell number formats for dates (e.g.,yyyy-mm-dd) apply at render time as they would for a Date column. Authors who want a string render compose withTEXT()per ADR-0017's canonical date idiom:{{ TEXT(EOMONTH(TODAY(), 0), "YYYY-MM-DD") }}This is the canonical month-end-string idiom and is pinned by a conformance fixture.
Spec text additions
spec/language.md§ "Functions" table gains six rows in the orderYEAR,MONTH,DAY,EOMONTH,EDATE,DATEDIF, placed afterTODAYso the date-related entries stay grouped.- A new subsection "Date Component and Arithmetic Functions" follows
"Row and Date Functions", with one short example per function and
the
TEXT(EOMONTH(...), ...)idiom called out. src/error-codes.tsgainsxl3/eval/type-mismatchin the union and in the ADR-0015 snapshot test. Per ADR-0015 the catalog is append-only.src/functions.tsimplements the six functions using UTC accessors and the arity validation already wired through ADR-0024.
Use cases
These functions are the floor of Korean B2B operations reporting:
- "이번 달 말일까지의 정산 기간" — settlement period ending at this
month-end:
EOMONTH(TODAY(), 0). - "결제일 + 30 일" — payment due date:
EDATE(...)is not exactly this (EDATEis month-stride), but the function is part of the same idiom set; for day-stride the canonical form is upstream arithmetic on the input Date, withDATEDIFfor the inverse query. - "근속 일수" — tenure in days:
DATEDIF([HireDate], TODAY(), "D"). - "분기말 보고" — quarter-end reporting:
EOMONTH(TODAY(), 0)after upstream selection of the right month.
Considered options (amendment)
A. Ship all six as specified (chosen). Highest-value subset; one ADR; one fixture per function. Matches the original deferral's "likely subset" wording.
B. Ship only YEAR / MONTH / DAY first. Pro: smaller surface
per release. Con: the component-extractors are the lowest-value
half — most templates that need date arithmetic need EOMONTH and
DATEDIF for the month-end and tenure idioms.
C. Coerce strings to dates inside these functions. Pro: forgives authors who hold a string. Con: ADR-0017 already routes string-Date coercion through the input pipeline; per-function string coercion duplicates that pipeline and risks divergent rules per function. Rejected.
D. Match Excel's #NUM! for DATEDIF with reversed args. Pro:
strict Excel compatibility. Con: the negative-result form is
strictly more useful and Excel-default behavior on #NUM! is to
surface the error cell into the rendered workbook, which ADR-0017
then routes through the error-sentinel-as-empty rule — a worse UX
than a signed integer. Rejected.
E. Hold WORKDAY / NETWORKDAYS for a later ADR. Unchanged
from the original deferral above. These require a holiday-calendar
model and remain out of scope.
Consequences
- Templates that previously computed month-end, tenure, or component extraction upstream can now express them inline. Source-side workarounds remain valid; this is additive.
- Three of the six functions return Date values, so template numFmt
formatting for date cells continues to work end-to-end without an
intermediate
TEXT()call. - A new error code
xl3/eval/type-mismatchjoins the ADR-0015 catalog. The snapshot test asserts the additional code. - Conformance corpus grows by six fixtures (one per function),
including:
YEARover a date and a non-date (error),MONTHbasic,DAYbasic,EOMONTHwithmonths=0,months=-1, andmonths=+12,EDATEwith day clamping (e.g., Jan 31 + 1 month → Feb 28/29),DATEDIFfor all three units including the reversed- argument negative case. WORKDAY/NETWORKDAYSand locale-aware variants remain out of scope and unchanged by this amendment.