ADR 0017 - Source value model
- Status: accepted
- Date: 2026-05-08
- Spec target: XTL 0.1 draft
- Affects: evaluation.md, language.md, ADR-0009
- Extends: ADR-0009 — closes the deferred Date canonical string form and adds the Date timestamp branch to the comparison algorithm. Read together as the comparison/value-model contract.
Context
ADR-0004's reference-impl coupling audit catalogued a gap: the spec talks about source values having "types" (string, number, boolean, date) without ever defining the value model normatively. ADR-0009 explicitly deferred date canonical-string form and Excel error sentinels. ADR-0007 defined empty for missing/null/whitespace-string but did not address error cells.
Three concrete consequences of this gap:
- Dates round-trip through
&concatenation host-dependently.[Customer] & " (" & [signup_date] & ")"produces different strings on different hosts because canonical-string-form falls through toString(date)for date values. - Excel error cells (
#N/A,#VALUE!,#DIV/0!, etc.) currently fall throughparseCellValueas the underlying object. Templates that try to render them get[object Object]-like text; aggregates over them produceNaNand downstream weirdness. - Percentage cells are sometimes confusing: Excel stores
0.5for "50%". Authors expecting "50%" in output need to know that their template should useTEXT(value, …)or a template cell format.
ADR-0017 closes these by defining the value model normatively. The ambition is bounded: lock the round-trip-stable types, defer the impl-defined edge cases.
Considered Options
Date canonical string form
A. ISO 8601 with always-present time (YYYY-MM-DDTHH:mm:ss).
Cost: a date column without time renders as 2026-05-08T00:00:00
which most authors don't want.
B. Date-only when time is midnight, datetime otherwise (chosen).
2026-05-08 for a pure date; 2026-05-08T09:30:00 for a datetime.
Cost: a single conditional in the canonicalizer; matches author
intuition.
C. Implementation-defined. Status quo. Cost: explicitly the problem ADR-0017 exists to fix.
Excel error sentinels
D. Treat as empty (chosen). A cell carrying #N/A / #VALUE!
reads as empty per ADR-0007. IFEMPTY([Status], "—") catches it;
COUNT([field]) skips it; aggregates ignore it.
E. Surface as a distinct error value. A new value kind that
templates could detect via ISERROR(). Cost: a new function and
value type for an edge case; spec surface grows.
F. Throw at read time. Cost: error cells are common in real workbooks (formulas that haven't recalculated, missing lookups); hard-failing every conversion would be operator-hostile.
Percentage cells
G. Underlying number, no spec rule (chosen). Excel stores 0.5
for "50%" — that's the numeric value, and that's what flows through
XTL. Authors who need formatted output use TEXT(value, "0%") (an
implementation extension at this point) or rely on the template cell
format being preserved.
H. Detect percentage cells and multiply by 100. Cost: surprises
authors who want the underlying 0.5 for arithmetic
(SUM([rate])); breaks the principle that source values flow
through unchanged.
Decision
XTL value kinds
A source value is one of these kinds:
- Missing — the source column doesn't exist on this row, or the cell is blank. Treated as empty per ADR-0007.
- String — Unicode text. Empty per ADR-0007 only when entirely whitespace.
- Number — IEEE 754 double.
NaNand the infinities are not produced by spec-conformant operations; if they appear, they stringify to""(per ADR-0009) and flow as empty. - Boolean —
true/false. - Date — a calendar instant. May or may not carry a time component; canonical-string form distinguishes.
Source workbook cells map onto these kinds:
| Excel cell shape | XTL kind |
|---|---|
| Blank cell | Missing |
| String / inline string / shared string | String |
| Number (including dates stored as serials with non-date format) | Number |
| Date-formatted cell | Date |
| Boolean | Boolean |
| Formula with cached result | The result's kind |
Error cell (#N/A, #VALUE!, #DIV/0!, etc.) | Missing (per ADR-0007) |
Date canonical string form
The canonical string form of a Date value is:
YYYY-MM-DDwhen the time component is exactly midnight (00:00:00).YYYY-MM-DDTHH:mm:sswhen the time component is non-midnight.
Example: a Date for 2026-05-08 (no time) renders as "2026-05-08"
in & concatenation; a Date for 2026-05-08 09:30:00 renders as
"2026-05-08T09:30:00".
This form supersedes the "implementation-defined" wording in ADR-0009 §"Canonical string form".
Timezone (normative). Implementations MUST extract date
components in UTC (e.g., getUTCFullYear, getUTCMonth,
getUTCDate, …). Excel cells store timezone-naive serial dates;
ExcelJS and similar libraries expose them as Date objects anchored
at UTC midnight. Using local-timezone accessors introduces
off-by-one drift in any non-UTC host, breaking the conformance
contract.
The same UTC discipline applies to:
- The
TEXT()function when its target is a date. TODAY(), which returns "today in UTC". Hosts that need locale-specific dates compute them outside the engine and pass them via__inputs__/__config__.- String-to-Date coercion (ADR-0003 numFmt path): a string like
"2026-05-08"MUST coerce to UTC midnight, not local midnight. - Excel-serial-to-Date conversion: the resulting
DateMUST round- trip to the sameYYYY-MM-DDregardless of host TZ.
Error sentinels read as empty
When the reader encounters an Excel error cell — either a static
error cell or a formula whose cached result is an error object —
the value is treated as empty per ADR-0007. Aggregate functions
skip it (COUNT([field]), SUM, MIN, MAX); IFEMPTY returns
the fallback; @filter ... in __lists__[…] does not match it.
Implementations MAY emit a warning when an error sentinel is read. Warnings MUST NOT change output semantics.
Percentage cells
Percentage-formatted Excel cells flow as their underlying number
(50% → 0.5). Templates that need formatted percentage output use
TEXT(value, "0%") (extension) or rely on the template cell's
number format preserving "0%". The spec does not introduce a
"percentage" value kind.
Spec text additions
evaluation.mdgains a new section "Source Value Model" immediately after "Cell Text Extraction", stating the kinds and the Excel-cell mapping.language.md"Comparison and String Coercion" §"Canonical String Form" replaces "Date — implementation-defined in XTL 0.1" with the YYYY-MM-DD / YYYY-MM-DDTHH:mm:ss rule.evaluation.md"Empty Values" gains a bullet covering Excel error cells.
Consequences
&concatenation over a Date now produces a deterministic, cross-impl-stable string. Templates that previously showedString(date)host-dependent text will see2026-05-08form. This is a deliberate, signalled break for the 0.x window.- Error sentinels stop polluting downstream renders. Aggregates
become numerically meaningful even when source data has occasional
#N/A. Templates can detect viaIFEMPTY(per ADR-0007). - Percentage cells continue to behave as they always did — this ADR documents the rationale rather than changing the rule.
- The reference impl gains:
parseCellValuehandles{ error }objects (and formula results that are error objects) by returning''.canonicalStringhandles Date values per the new rule.
compareValues(ADR-0009) gains a Date branch: two Dates compare by their underlying timestamp; mixed Date / non-Date falls through to canonical-string-form ordering (the existing total order).- This ADR does not introduce
ISERROR(),ISDATE(), or other predicate functions. A future ADR can extend if real demand appears. - Locale-specific date formatting (e.g.
2026년 5월 8일) remains a template author concern viaTEXT(date, format)with the XTL 0.1 minimum format set; locale formatters are extensions.
References
- ADR-0004: reference-impl coupling audit (gap #9 — value representation).
- ADR-0007: empty value definition (now extended to error cells).
- ADR-0009: comparison and string coercion (date canonical form was deferred from there to here).
- ADR-0010: runtime user input date coercion (already aligned with the YYYY-MM-DD form for resolved input values).
spec/evaluation.md"Cell Text Extraction", "Empty Values".spec/language.md"Comparison and String Coercion".