quario 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +435 -0
- package/README.md +49 -25
- package/lib/band-height.js +123 -0
- package/lib/format.js +140 -32
- package/lib/index.d.ts +209 -21
- package/lib/index.js +13 -8
- package/lib/license.js +1 -1
- package/lib/locate.js +32 -0
- package/lib/math.js +71 -0
- package/lib/plan.js +485 -184
- package/lib/precision.js +75 -0
- package/lib/reducers.js +165 -0
- package/lib/scope.js +7 -86
- package/lib/stream.js +20 -15
- package/lib/style.js +485 -20
- package/package.json +2 -2
package/lib/precision.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How many fraction digits a `format` kind presents when the declaration
|
|
3
|
+
* names no count of its own. One table, read once — by `./style.js`, where a
|
|
4
|
+
* `format` declaration resolves — so an author's own `digits` beats a default
|
|
5
|
+
* here exactly as it beats any other (docs/adr/0056).
|
|
6
|
+
*
|
|
7
|
+
* The counts are fixed rather than maxima — `number` and `percent` are 2 —
|
|
8
|
+
* and they are stated here rather than read back out of Intl's own defaults,
|
|
9
|
+
* which would tie a workbook's bytes to the host's ICU version (docs/adr/0025)
|
|
10
|
+
* and would settle `percent` at whatever ICU happens to prefer.
|
|
11
|
+
*
|
|
12
|
+
* `currency` is the one hybrid: its minor units come from Intl, because the
|
|
13
|
+
* alternative is vendoring ISO 4217. Nothing else asks ICU anything.
|
|
14
|
+
*
|
|
15
|
+
* The public `fractionDigits` helper is **not** here, and the split is the
|
|
16
|
+
* point. A resolved `format` crosses the stream carrying its own `digits`, so
|
|
17
|
+
* every target reads the count off the declaration it already has and asks
|
|
18
|
+
* nothing (docs/adr/0054's authority moved from this table to the stream).
|
|
19
|
+
* What remains public is a reader of that resolved declaration, and it lives
|
|
20
|
+
* beside the resolution in `./style.js`.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/** Fixed counts for the kinds that carry one of their own. @type {Record<string, number>} */
|
|
24
|
+
let COUNTS = { number: 2, percent: 2 };
|
|
25
|
+
|
|
26
|
+
// Minor units are a property of the currency, not of the locale — Intl gives
|
|
27
|
+
// JPY 0 under every one — so the code alone is the whole key. Bounded by the
|
|
28
|
+
// distinct codes a process ever sees, and memoising a fact that cannot change
|
|
29
|
+
// within an ICU version: an uncached probe costs ~10 µs, so ~100 ms for every
|
|
30
|
+
// ten thousand currency cells a target presents, against ~19 ns memoised.
|
|
31
|
+
/** @type {Map<string, number | undefined>} */
|
|
32
|
+
let minorUnits = new Map();
|
|
33
|
+
|
|
34
|
+
// Every unreadable code lands in the catch and memoises as nothing — a
|
|
35
|
+
// missing one included, since Intl demands a currency under this style. So
|
|
36
|
+
// there is no separate guard: one answer, reached one way.
|
|
37
|
+
/** @type {(currency: unknown) => number | undefined} */
|
|
38
|
+
let unitsOf = (currency) => {
|
|
39
|
+
// Gate before the map, not inside the try: a non-string would otherwise be
|
|
40
|
+
// retained as a key for the life of the process for no answer.
|
|
41
|
+
if (typeof currency !== "string") return undefined;
|
|
42
|
+
if (minorUnits.has(currency)) return minorUnits.get(currency);
|
|
43
|
+
let units;
|
|
44
|
+
try {
|
|
45
|
+
// Intl takes any well-formed three-letter code and throws on the rest,
|
|
46
|
+
// which is the same line `format()` already falls back on.
|
|
47
|
+
units = new Intl.NumberFormat("en-US", {
|
|
48
|
+
style: "currency",
|
|
49
|
+
currency: /** @type {string} */ (currency),
|
|
50
|
+
}).resolvedOptions().maximumFractionDigits;
|
|
51
|
+
} catch {
|
|
52
|
+
units = undefined;
|
|
53
|
+
}
|
|
54
|
+
minorUnits.set(currency, units);
|
|
55
|
+
return units;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The count a kind presents when the declaration names none.
|
|
60
|
+
*
|
|
61
|
+
* @param {string} kind A `format` kind.
|
|
62
|
+
* @param {unknown} [currency] The code the cell wears, under `"currency"`.
|
|
63
|
+
* @returns {number | undefined} The digits, or nothing when the kind carries
|
|
64
|
+
* no count — `date`, an unknown kind, and a `currency` whose code Intl will
|
|
65
|
+
* not read. A kind that resolves to no count presents no digits anywhere:
|
|
66
|
+
* `format()` falls back to `display()` and the worksheet writes no number
|
|
67
|
+
* format at all.
|
|
68
|
+
*/
|
|
69
|
+
export function defaultDigits(kind, currency) {
|
|
70
|
+
if (kind === "currency") return unitsOf(currency);
|
|
71
|
+
// Own-key lookup, as everywhere a name from a document indexes a table: a
|
|
72
|
+
// kind named after an Object.prototype member is unknown, not an inherited
|
|
73
|
+
// function (`style.js`).
|
|
74
|
+
return Object.hasOwn(COUNTS, kind) ? COUNTS[kind] : undefined;
|
|
75
|
+
}
|
package/lib/reducers.js
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every fact the engine holds about a reducer, in one table.
|
|
3
|
+
*
|
|
4
|
+
* A reducer is three things at once: a fold over a whole array — the function
|
|
5
|
+
* form `sum(@.lines, l => l.total)` and a declared aggregate alike — a step
|
|
6
|
+
* folded across rows in render order for a declared running value, and a
|
|
7
|
+
* spelling rule, since `count` is the one declared form that takes no
|
|
8
|
+
* expression. Those three used to live in two hand-enumerated tables, a
|
|
9
|
+
* derived name set, and a reducer named in the validator, so adding a reducer
|
|
10
|
+
* was three edits and forgetting the runner passed validation and threw a raw
|
|
11
|
+
* TypeError at render. One entry per reducer; every role derives from it.
|
|
12
|
+
*
|
|
13
|
+
* `prev` is the entry with no fold. There is no whole-array "previous", so it
|
|
14
|
+
* is a running value only — and that missing half is exactly what keeps it out
|
|
15
|
+
* of an `aggregates` block, where `RUN = AGG ∪ {prev}` used to say so by hand.
|
|
16
|
+
*
|
|
17
|
+
* Coercion is quiet: only a finite number contributes, everything else folds
|
|
18
|
+
* to 0, as the spec promises.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/** @typedef {(rows: any[] | null | undefined, of?: ((row: any) => any) | null) => any} Reducer */
|
|
22
|
+
/** @typedef {{ fold: Reducer | null, run: () => (value: any) => any, expr: boolean }} Entry */
|
|
23
|
+
/** @typedef {"aggregate" | "run"} Role */
|
|
24
|
+
|
|
25
|
+
/** @type {(value: any) => number} */
|
|
26
|
+
let num = (value) => {
|
|
27
|
+
let n = +value;
|
|
28
|
+
return Number.isFinite(n) ? n : 0;
|
|
29
|
+
};
|
|
30
|
+
// The per-row read every reducer but `count` makes of its lambda: a projection
|
|
31
|
+
// rather than a predicate. `count` is the one that filters instead, which is
|
|
32
|
+
// why `countDistinct` reads its lambda this way despite the shared prefix.
|
|
33
|
+
/** @type {(rows: any[] | null | undefined, of?: ((row: any) => any) | null) => any[]} */
|
|
34
|
+
let project = (rows, of) => (rows || []).map((row) => (of ? of(row) : row));
|
|
35
|
+
// `min`/`max` share one shape: project through the lambda, then pick a winner.
|
|
36
|
+
/** @type {(pick: (a: any, b: any) => any) => Reducer} */
|
|
37
|
+
let extreme = (pick) => (rows, of) => {
|
|
38
|
+
let values = project(rows, of);
|
|
39
|
+
return values.length ? values.reduce(pick) : null;
|
|
40
|
+
};
|
|
41
|
+
/** @type {Reducer} */
|
|
42
|
+
let sum = (rows, of) => (rows || []).reduce((total, row) => total + num(of ? of(row) : row), 0);
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The table. Each entry is one reducer entire: how it folds an array, how it
|
|
46
|
+
* steps a row, and whether its declared form carries `:=<expression>`.
|
|
47
|
+
*
|
|
48
|
+
* The running halves fold in render order. `min`/`max` have no zero, so they
|
|
49
|
+
* seed to the first row's value — the one deliberate `undefined` here, since
|
|
50
|
+
* xprsn reads absent values as null.
|
|
51
|
+
* @type {Record<string, Entry>}
|
|
52
|
+
*/
|
|
53
|
+
let REGISTRY = {
|
|
54
|
+
sum: {
|
|
55
|
+
fold: sum,
|
|
56
|
+
run: () => {
|
|
57
|
+
let total = 0;
|
|
58
|
+
return (value) => (total += num(value));
|
|
59
|
+
},
|
|
60
|
+
expr: true,
|
|
61
|
+
},
|
|
62
|
+
count: {
|
|
63
|
+
fold: (rows, of) => (of ? (rows || []).filter(of) : rows || []).length,
|
|
64
|
+
run: () => {
|
|
65
|
+
let seen = 0;
|
|
66
|
+
return () => ++seen;
|
|
67
|
+
},
|
|
68
|
+
expr: false,
|
|
69
|
+
},
|
|
70
|
+
countDistinct: {
|
|
71
|
+
// Distinctness is a `Set`'s — SameValueZero, which is also what
|
|
72
|
+
// `Map.groupBy` gives a group's `by` key, so this counts exactly what that
|
|
73
|
+
// key would partition into. Objects compare by reference there and here
|
|
74
|
+
// alike.
|
|
75
|
+
fold: (rows, of) => new Set(project(rows, of)).size,
|
|
76
|
+
run: () => {
|
|
77
|
+
let seen = new Set();
|
|
78
|
+
return (value) => seen.add(value).size;
|
|
79
|
+
},
|
|
80
|
+
expr: true,
|
|
81
|
+
},
|
|
82
|
+
avg: {
|
|
83
|
+
fold: (rows, of) => (rows && rows.length ? sum(rows, of) / rows.length : null),
|
|
84
|
+
run: () => {
|
|
85
|
+
let total = 0,
|
|
86
|
+
seen = 0;
|
|
87
|
+
return (value) => (total += num(value)) / ++seen;
|
|
88
|
+
},
|
|
89
|
+
expr: true,
|
|
90
|
+
},
|
|
91
|
+
min: {
|
|
92
|
+
fold: extreme((a, b) => (a < b ? a : b)),
|
|
93
|
+
run: () => {
|
|
94
|
+
let lowest = /** @type {any} */ (undefined);
|
|
95
|
+
return (value) => (lowest = lowest === undefined || value < lowest ? value : lowest);
|
|
96
|
+
},
|
|
97
|
+
expr: true,
|
|
98
|
+
},
|
|
99
|
+
max: {
|
|
100
|
+
fold: extreme((a, b) => (a > b ? a : b)),
|
|
101
|
+
run: () => {
|
|
102
|
+
let highest = /** @type {any} */ (undefined);
|
|
103
|
+
return (value) => (highest = highest === undefined || value > highest ? value : highest);
|
|
104
|
+
},
|
|
105
|
+
expr: true,
|
|
106
|
+
},
|
|
107
|
+
prev: {
|
|
108
|
+
fold: null,
|
|
109
|
+
run: () => {
|
|
110
|
+
let previous = /** @type {any} */ (null);
|
|
111
|
+
return (value) => {
|
|
112
|
+
let out = previous;
|
|
113
|
+
previous = value;
|
|
114
|
+
return out;
|
|
115
|
+
};
|
|
116
|
+
},
|
|
117
|
+
expr: true,
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* The reducer a declaration names, in the role it is declared under, or
|
|
123
|
+
* `undefined` if that role does not have it: an entry with no fold is a
|
|
124
|
+
* running value only, which is what refuses `prev` in an `aggregates` block.
|
|
125
|
+
*
|
|
126
|
+
* The name comes from splitting an author's spec string, so the lookup asks
|
|
127
|
+
* for an own property — `"constructor:=@.v"` must be an unknown reducer, not
|
|
128
|
+
* a function off `Object.prototype`.
|
|
129
|
+
* @type {(role: Role, fn: string) => Entry | undefined}
|
|
130
|
+
*/
|
|
131
|
+
export let reducerFor = (role, fn) => {
|
|
132
|
+
if (!Object.hasOwn(REGISTRY, fn)) return undefined;
|
|
133
|
+
let entry = REGISTRY[fn];
|
|
134
|
+
return role === "run" || entry.fold ? entry : undefined;
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Every reducer name a role accepts, in table order. The vocabulary as a whole,
|
|
139
|
+
* where `reducerFor` answers about one name — a sweep over the registry needs
|
|
140
|
+
* it, so nothing re-enumerates the reducers to iterate them.
|
|
141
|
+
* @type {(role: Role) => string[]}
|
|
142
|
+
*/
|
|
143
|
+
export let namesFor = (role) =>
|
|
144
|
+
Object.keys(REGISTRY).filter((fn) => reducerFor(role, fn) !== undefined);
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* The running step a declared run value folds with. Only ever asked for a name
|
|
148
|
+
* `reducerFor("run", …)` has already accepted, so every entry answers.
|
|
149
|
+
* @type {(fn: string) => (value: any) => any}
|
|
150
|
+
*/
|
|
151
|
+
export let runnerFor = (fn) => REGISTRY[fn].run();
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* The reducers as a plain name→function record: what an expression's function
|
|
155
|
+
* scope spreads in, and what a declared aggregate evaluates through. It is the
|
|
156
|
+
* aggregate role's own entries, so it asks for them rather than restating the
|
|
157
|
+
* has-a-fold rule `reducerFor` already owns.
|
|
158
|
+
* @type {Record<string, Reducer>}
|
|
159
|
+
*/
|
|
160
|
+
export let REDUCERS = Object.fromEntries(
|
|
161
|
+
// The cast is the derivation's own guarantee: the aggregate role is exactly
|
|
162
|
+
// the entries carrying a fold, which is why this asks for that role rather
|
|
163
|
+
// than testing for the fold a second time.
|
|
164
|
+
namesFor("aggregate").map((fn) => [fn, /** @type {Reducer} */ (REGISTRY[fn].fold)]),
|
|
165
|
+
);
|
package/lib/scope.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The scope model at render time: the anchors a band binds, the
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* The scope model at render time: the anchors a band binds, and the runner set
|
|
3
|
+
* a band renders under. What a reducer *is* — its fold, its step, its spelling
|
|
4
|
+
* — lives in `reducers.js`; this module only asks it for the step and folds
|
|
5
|
+
* the result in render order.
|
|
5
6
|
*/
|
|
7
|
+
import { REDUCERS, runnerFor } from "./reducers.js";
|
|
6
8
|
/** @typedef {Record<string, any>} Scope */
|
|
7
9
|
/** @typedef {(scope: Scope) => any} Eval */
|
|
8
10
|
/** @typedef {{ fn: string, per: Eval | null }} Fold */
|
|
@@ -15,42 +17,6 @@
|
|
|
15
17
|
* bind: (scope: Scope, row: any) => Scope }} RunnerSet
|
|
16
18
|
*/
|
|
17
19
|
|
|
18
|
-
// Built-in reducers. Each takes an array and an optional xprsn lambda, so an
|
|
19
|
-
// author can aggregate a row's own sub-array inline (`sum(@.lines, l => l.total)`);
|
|
20
|
-
// the same reducers back the declared group aggregates. Coercion is quiet: only
|
|
21
|
-
// a finite number contributes, everything else folds to 0, as the spec promises.
|
|
22
|
-
/** @type {(value: any) => number} */
|
|
23
|
-
let num = (value) => {
|
|
24
|
-
let n = +value;
|
|
25
|
-
return Number.isFinite(n) ? n : 0;
|
|
26
|
-
};
|
|
27
|
-
/** @typedef {(rows: any[] | null | undefined, of?: ((row: any) => any) | null) => any} Reducer */
|
|
28
|
-
// The per-row read every reducer but `count` makes of its lambda: a projection
|
|
29
|
-
// rather than a predicate. `count` is the one that filters instead, which is
|
|
30
|
-
// why `countDistinct` reads its lambda this way despite the shared prefix.
|
|
31
|
-
/** @type {(rows: any[] | null | undefined, of?: ((row: any) => any) | null) => any[]} */
|
|
32
|
-
let project = (rows, of) => (rows || []).map((row) => (of ? of(row) : row));
|
|
33
|
-
// `min`/`max` share one shape: project through the lambda, then pick a winner.
|
|
34
|
-
/** @type {(pick: (a: any, b: any) => any) => Reducer} */
|
|
35
|
-
let extreme = (pick) => (rows, of) => {
|
|
36
|
-
let values = project(rows, of);
|
|
37
|
-
return values.length ? values.reduce(pick) : null;
|
|
38
|
-
};
|
|
39
|
-
/** @type {Record<string, Reducer>} */
|
|
40
|
-
export let REDUCERS = {
|
|
41
|
-
sum: (rows, of) => (rows || []).reduce((total, row) => total + num(of ? of(row) : row), 0),
|
|
42
|
-
count: (rows, of) => (of ? (rows || []).filter(of) : rows || []).length,
|
|
43
|
-
// Distinctness is a `Set`'s — SameValueZero, which is also what `Map.groupBy`
|
|
44
|
-
// gives a group's `by` key, so this counts exactly what that key would
|
|
45
|
-
// partition into. Objects compare by reference there and here alike.
|
|
46
|
-
countDistinct: (rows, of) => new Set(project(rows, of)).size,
|
|
47
|
-
avg: (rows, of) => (rows && rows.length ? REDUCERS.sum(rows, of) / rows.length : null),
|
|
48
|
-
min: extreme((a, b) => (a < b ? a : b)),
|
|
49
|
-
max: extreme((a, b) => (a > b ? a : b)),
|
|
50
|
-
};
|
|
51
|
-
export let AGG = new Set(Object.keys(REDUCERS));
|
|
52
|
-
export let RUN = new Set([...AGG, "prev"]);
|
|
53
|
-
|
|
54
20
|
// Bind a row as `@` on a child of `scope`, ready for an xprsn per-row read.
|
|
55
21
|
/** @type {(scope: Scope, row: any) => Scope} */
|
|
56
22
|
export let withRow = (scope, row) => {
|
|
@@ -68,57 +34,12 @@ export let withPage = (scope, page) => {
|
|
|
68
34
|
return child;
|
|
69
35
|
};
|
|
70
36
|
|
|
71
|
-
// Evaluate one declared aggregate over `rows` by delegating to the same
|
|
37
|
+
// Evaluate one declared aggregate over `rows` by delegating to the same folds
|
|
72
38
|
// authors call inline: the per-row xprsn expression becomes the lambda.
|
|
73
39
|
/** @type {(fold: Fold, rows: any[], scope: Scope) => any} */
|
|
74
40
|
export let aggregateValue = ({ fn, per }, rows, scope) =>
|
|
75
41
|
REDUCERS[fn](rows, per ? (row) => per(withRow(scope, row)) : null);
|
|
76
42
|
|
|
77
|
-
// Folded across rows in render order, one factory per name. `min`/`max` have
|
|
78
|
-
// no zero, so they seed to the first row's value — the one deliberate
|
|
79
|
-
// `undefined` here, since xprsn reads absent values as null.
|
|
80
|
-
//
|
|
81
|
-
// Every name in `REDUCERS` needs a factory here. `RUN` derives from `AGG`
|
|
82
|
-
// rather than enumerating a second list, so a reducer added there without a
|
|
83
|
-
// runner added here passes validation and then throws at render. The invariant
|
|
84
|
-
// is a test rather than a comment alone — semantics.test.js, "every reducer
|
|
85
|
-
// the run block accepts has a runner behind it".
|
|
86
|
-
/** @type {Record<string, () => (value: any) => any>} */
|
|
87
|
-
let RUNNERS = {
|
|
88
|
-
sum: () => {
|
|
89
|
-
let total = 0;
|
|
90
|
-
return (value) => (total += num(value));
|
|
91
|
-
},
|
|
92
|
-
count: () => {
|
|
93
|
-
let seen = 0;
|
|
94
|
-
return () => ++seen;
|
|
95
|
-
},
|
|
96
|
-
countDistinct: () => {
|
|
97
|
-
let seen = new Set();
|
|
98
|
-
return (value) => seen.add(value).size;
|
|
99
|
-
},
|
|
100
|
-
avg: () => {
|
|
101
|
-
let total = 0,
|
|
102
|
-
seen = 0;
|
|
103
|
-
return (value) => (total += num(value)) / ++seen;
|
|
104
|
-
},
|
|
105
|
-
min: () => {
|
|
106
|
-
let lowest = /** @type {any} */ (undefined);
|
|
107
|
-
return (value) => (lowest = lowest === undefined || value < lowest ? value : lowest);
|
|
108
|
-
},
|
|
109
|
-
max: () => {
|
|
110
|
-
let highest = /** @type {any} */ (undefined);
|
|
111
|
-
return (value) => (highest = highest === undefined || value > highest ? value : highest);
|
|
112
|
-
},
|
|
113
|
-
prev: () => {
|
|
114
|
-
let previous = /** @type {any} */ (null);
|
|
115
|
-
return (value) => {
|
|
116
|
-
let out = previous;
|
|
117
|
-
previous = value;
|
|
118
|
-
return out;
|
|
119
|
-
};
|
|
120
|
-
},
|
|
121
|
-
};
|
|
122
43
|
// The runner set as a value, and the two things a band does with it. `extend`
|
|
123
44
|
// is the whole of persist-vs-reset: a group's own runners are fresh per
|
|
124
45
|
// instance, the outer ones fold on unchanged.
|
|
@@ -131,7 +52,7 @@ let runnerSet = (runners) => {
|
|
|
131
52
|
// block is absent renders under this very value rather than a copy.
|
|
132
53
|
if (!folds.length) return value;
|
|
133
54
|
let started = folds.map(([name, { fn, per }]) => {
|
|
134
|
-
let step =
|
|
55
|
+
let step = runnerFor(fn);
|
|
135
56
|
return /** @type {[string, Eval]} */ ([name, (scope) => step(per ? per(scope) : 1)]);
|
|
136
57
|
});
|
|
137
58
|
return runnerSet([...runners, ...started]);
|
package/lib/stream.js
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
// the scalar `display()` rule — re-exported together so event consumers
|
|
13
13
|
// derive display text from a cell's tokens without hand-rolling the join.
|
|
14
14
|
export { display, text } from "sjabloon";
|
|
15
|
+
import { kindOf } from "./style.js";
|
|
15
16
|
|
|
16
17
|
/** @type {(value: any) => boolean} */
|
|
17
18
|
export let finiteNum = (value) => typeof value === "number" && Number.isFinite(value);
|
|
@@ -24,9 +25,14 @@ let asTyped = (value) => {
|
|
|
24
25
|
if (finiteDate(value)) return value;
|
|
25
26
|
return undefined;
|
|
26
27
|
};
|
|
27
|
-
// The seam's one coercion, kept behind the declared kind (ADR 0041).
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
// The seam's one coercion, kept behind the declared kind (ADR 0041). It asks
|
|
29
|
+
// `kindOf` rather than reading `decl.kind`, because the stream carries the
|
|
30
|
+
// resolved object while a host may still hold the shorthand and both must
|
|
31
|
+
// answer alike -- and because reading one field is all this needs, where
|
|
32
|
+
// resolving the whole declaration would allocate per cell for nothing
|
|
33
|
+
// (docs/adr/0056).
|
|
34
|
+
/** @type {(value: any, decl: unknown) => Date | undefined} */
|
|
35
|
+
let asDeclared = (value, decl) => (kindOf(decl) === "date" ? reviveDate(value) : undefined);
|
|
30
36
|
|
|
31
37
|
// The two RFC 3339 forms a JSON document conventionally carries a date in:
|
|
32
38
|
// a calendar date, and a timestamp that names its offset. A zoneless
|
|
@@ -38,8 +44,6 @@ let asDeclared = (value, kind) => (kind === "date" ? reviveDate(value) : undefin
|
|
|
38
44
|
let RFC3339 =
|
|
39
45
|
/^(\d{4})-(\d{2})-(\d{2})(?:T(\d{2}):(\d{2}):(\d{2})(?:\.\d+)?(?:Z|[+-](\d{2}):(\d{2})))?$/;
|
|
40
46
|
|
|
41
|
-
/** @type {(year: string, month: string) => number} */
|
|
42
|
-
let monthLength = (year, month) => new Date(Date.UTC(+year, +month, 0)).getUTCDate();
|
|
43
47
|
// An absent group is the timestamp half of an optional match, not a breach.
|
|
44
48
|
/** @type {(bound: [string | undefined, number, number]) => boolean} */
|
|
45
49
|
let within = ([n, lo, hi]) => n === undefined || (+n >= lo && +n <= hi);
|
|
@@ -69,7 +73,7 @@ export let reviveDate = (value) => {
|
|
|
69
73
|
/** @type {[string | undefined, number, number][]} */
|
|
70
74
|
let bounds = [
|
|
71
75
|
[month, 1, 12],
|
|
72
|
-
[day, 1,
|
|
76
|
+
[day, 1, new Date(Date.UTC(+year, +month, 0)).getUTCDate()],
|
|
73
77
|
[hour, 0, 23],
|
|
74
78
|
[minute, 0, 59],
|
|
75
79
|
[second, 0, 59],
|
|
@@ -86,23 +90,24 @@ export let reviveDate = (value) => {
|
|
|
86
90
|
* including a lone null, matching the display-text treatment everywhere
|
|
87
91
|
* else — reports `undefined` and joins to text.
|
|
88
92
|
*
|
|
89
|
-
* Passing the cell's
|
|
90
|
-
* seam performs: under `
|
|
91
|
-
* revives to the `Date` it names. The gate is the declaration, and
|
|
92
|
-
* is `date` alone, because a date is the one type a JSON document
|
|
93
|
-
* carry — a number arrives as a number, so no other kind has anything
|
|
94
|
-
* revive (ADR 0041). Callers that read no style omit the argument and see
|
|
93
|
+
* Passing the cell's resolved `format` declaration opts into the one coercion
|
|
94
|
+
* the seam performs: under the `date` kind, a string in either accepted RFC
|
|
95
|
+
* 3339 form revives to the `Date` it names. The gate is the declaration, and
|
|
96
|
+
* the kind is `date` alone, because a date is the one type a JSON document
|
|
97
|
+
* cannot carry — a number arrives as a number, so no other kind has anything
|
|
98
|
+
* to revive (ADR 0041). Callers that read no style omit the argument and see
|
|
95
99
|
* the seam exactly as it was.
|
|
96
100
|
*
|
|
97
101
|
* @param {any[]} tokens A cell's tokens.
|
|
98
|
-
* @param {unknown} [
|
|
102
|
+
* @param {unknown} [decl] The cell's resolved `format` declaration — the
|
|
103
|
+
* `{ kind, digits? | form? }` object the stream carries (docs/adr/0056).
|
|
99
104
|
* @returns {number | boolean | Date | undefined} The native value, if any.
|
|
100
105
|
*/
|
|
101
|
-
export function typed(tokens,
|
|
106
|
+
export function typed(tokens, decl) {
|
|
102
107
|
let [token] = tokens;
|
|
103
108
|
if (tokens.length !== 1 || !("value" in token)) return undefined;
|
|
104
109
|
// `??` and not `||`: a boolean cell's own `false` is a typed value.
|
|
105
|
-
return asTyped(token.value) ?? asDeclared(token.value,
|
|
110
|
+
return asTyped(token.value) ?? asDeclared(token.value, decl);
|
|
106
111
|
}
|
|
107
112
|
|
|
108
113
|
// The report's own bands, by the role their items wear (CONTEXT.md, "Band").
|