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/lib/index.d.ts CHANGED
@@ -87,8 +87,26 @@ export interface StyleDeclarations extends BoxDeclarations {
87
87
  background?: ExpressionValue<string>;
88
88
  /** Horizontal alignment within the cell. */
89
89
  align?: ExpressionValue<Align>;
90
- /** How a number or date is presented. Locale stays on the instance. */
91
- format?: ExpressionValue<FormatKind>;
90
+ /**
91
+ * Vertical alignment within a box taller than its content: a table cell or
92
+ * row, a split slot (text or image). A definition error on a stacked item
93
+ * and on a band image, whose boxes have no such slack.
94
+ */
95
+ valign?: ExpressionValue<VAlign>;
96
+ /**
97
+ * How a number or date is presented: a kind, or an object naming one and
98
+ * carrying the single modifier that kind understands — `digits` for the
99
+ * three number kinds, `form` for `date`. Locale stays on the instance.
100
+ */
101
+ format?: ExpressionValue<FormatDeclaration>;
102
+ /**
103
+ * What this cell's money is denominated in, under `format: "currency"`:
104
+ * a three-letter ISO 4217 code, beating the instance's default. A literal
105
+ * that is not one is a definition error; an `=` result that is not one is
106
+ * the cell's own answer still, so the cell presents as display text rather
107
+ * than falling back to the instance's currency. Unread without the kind.
108
+ */
109
+ currency?: ExpressionValue<string>;
92
110
  /** Blank space before this item, in points. */
93
111
  spaceBefore?: ExpressionValue<number>;
94
112
  /** Blank space after this item, in points. */
@@ -104,8 +122,35 @@ export interface SortKey {
104
122
  export type CellValue = string;
105
123
 
106
124
  export type Align = "left" | "center" | "right";
125
+ export type VAlign = "top" | "middle" | "bottom";
107
126
  export type FormatKind = "number" | "currency" | "percent" | "date";
108
127
 
128
+ /** The four named date forms, Intl's own `dateStyle`. A `date` that names
129
+ * none is `"medium"`. */
130
+ export type DateForm = "short" | "medium" | "long" | "full";
131
+
132
+ /**
133
+ * How an author writes `format`: the kind alone, or the kind carrying its one
134
+ * modifier. `digits` is a whole number from 0 to 20 — the range both a
135
+ * presenting target and a worksheet number format can honour — and it beats
136
+ * the count the kind would otherwise present, a currency's minor units
137
+ * included. A modifier on a kind that does not take it is a definition error.
138
+ */
139
+ export type FormatDeclaration =
140
+ | FormatKind
141
+ | { kind: "number" | "currency" | "percent"; digits?: number }
142
+ | { kind: "date"; form?: DateForm };
143
+
144
+ /**
145
+ * What `format` resolves to on the event stream: never the shorthand, always
146
+ * the kind with the answer it presents. A number kind carries `digits` unless
147
+ * it has no count at all — a `currency` whose code cannot be read — and a
148
+ * `date` always carries its `form`.
149
+ */
150
+ export type ResolvedFormat =
151
+ | { kind: "number" | "currency" | "percent"; digits?: number }
152
+ | { kind: "date"; form: DateForm };
153
+
109
154
  export interface Cell {
110
155
  value: CellValue;
111
156
  visible?: ExpressionValue<boolean>;
@@ -155,12 +200,32 @@ export interface ImageItem {
155
200
  style?: ImageStyleDeclarations;
156
201
  }
157
202
 
203
+ /**
204
+ * What a text slot declares: the vocabulary less flow spacing, which is a
205
+ * band-item pad and does not apply inside a split.
206
+ */
207
+ export type SlotStyleDeclarations = Omit<StyleDeclarations, "spaceBefore" | "spaceAfter">;
208
+
209
+ /**
210
+ * What an image slot declares: an image item's names less flow spacing, plus
211
+ * `valign` — the one name an image accepts in a slot and nowhere else, because
212
+ * a slot's box is the split's height rather than the picture's own.
213
+ */
214
+ export type SlotImageStyleDeclarations = Omit<
215
+ ImageStyleDeclarations,
216
+ "spaceBefore" | "spaceAfter"
217
+ > &
218
+ Pick<StyleDeclarations, "valign">;
219
+
158
220
  /**
159
221
  * One slot of a split: an ordinary item, plus the width share that is a slot
160
222
  * property rather than an item one. A split may not stand in a slot —
161
223
  * placement inside placement is the coordinate system quario does not have.
162
224
  */
163
- export type SplitSlot = (TextItem | ImageItem) & {
225
+ export type SplitSlot = (
226
+ | (Omit<TextItem, "style"> & { style?: SlotStyleDeclarations })
227
+ | (Omit<ImageItem, "style"> & { style?: SlotImageStyleDeclarations })
228
+ ) & {
164
229
  /** Slot width as a percentage of the content width (0 < width <= 100). */
165
230
  width?: number;
166
231
  };
@@ -185,10 +250,19 @@ export interface TableHeader {
185
250
  value: CellValue;
186
251
  /** Literal only — never an expression. */
187
252
  style?: StyleDeclarations;
253
+ /**
254
+ * How many columns this header covers — a literal integer >= 1, never an
255
+ * expression; absent is 1. The columns it covers omit their own `header`.
256
+ */
257
+ span?: number;
188
258
  }
189
259
 
190
260
  export interface TableColumn extends Cell {
191
- header: string | TableHeader;
261
+ /**
262
+ * Absent exactly when a neighbour's header `span` covers this column: every
263
+ * column's header slot is filled once, by its own or by a span.
264
+ */
265
+ header?: string | TableHeader;
192
266
  /** Column width as a percentage of the table width (0 < width <= 100). */
193
267
  width?: number;
194
268
  }
@@ -198,12 +272,34 @@ export interface TableRow {
198
272
  style?: StyleDeclarations;
199
273
  }
200
274
 
275
+ /**
276
+ * One cell of a total row: an ordinary cell, plus the columns it covers. The
277
+ * spans in a row must sum to the column count exactly.
278
+ */
279
+ export type TotalCell = Cell & {
280
+ /**
281
+ * How many columns this cell covers — a literal integer >= 1, never an
282
+ * expression; absent is 1.
283
+ */
284
+ span?: number;
285
+ };
286
+
201
287
  export interface TotalRow {
202
- cells: [Cell, ...Cell[]];
288
+ cells: [TotalCell, ...TotalCell[]];
203
289
  visible?: ExpressionValue<boolean>;
204
290
  style?: StyleDeclarations;
205
291
  }
206
292
 
293
+ /**
294
+ * The table's summary block: the rows, plus what belongs to the block rather
295
+ * than to any one of them. Its `style` is the layer below each row's — box
296
+ * under box, everything else under everything else — and exact `false` on its
297
+ * `visible` omits the block whole, its rows never evaluated.
298
+ */
299
+ export interface TotalBlock extends TableRow {
300
+ rows: [TotalRow, ...TotalRow[]];
301
+ }
302
+
207
303
  /**
208
304
  * The header row's own box — distinct from `TableHeader`, which is one
209
305
  * column's header cell. This is the box the row wears, replayed with the
@@ -217,7 +313,7 @@ export interface TableDetail {
217
313
  header?: TableHeaderBox;
218
314
  row?: TableRow;
219
315
  columns: [TableColumn, ...TableColumn[]];
220
- total?: [TotalRow, ...TotalRow[]];
316
+ total?: TotalBlock;
221
317
  }
222
318
 
223
319
  export interface Group {
@@ -323,7 +419,10 @@ export interface QuarioOptions {
323
419
  license?: string;
324
420
  /** Locale for `format`. Defaults to `"en-US"` when a kind is presented. */
325
421
  locale?: string;
326
- /** Currency code for `format: "currency"`. Absent, that kind contributes nothing. */
422
+ /**
423
+ * Default currency code for `format: "currency"`, overridden by a cell's own
424
+ * `style.currency`. Absent from both, that kind contributes nothing.
425
+ */
327
426
  currency?: string;
328
427
  /** Timezone for `format: "date"`. Defaults to `"UTC"`. */
329
428
  timeZone?: string;
@@ -357,10 +456,15 @@ export type Token = LiteralToken | ValueToken;
357
456
  export interface EventCell {
358
457
  tokens: Token[];
359
458
  style?: Record<string, unknown>;
459
+ /**
460
+ * How many columns this cell covers, present only when more than one: a
461
+ * span of 1 is the absence of one and never reaches the stream.
462
+ */
463
+ span?: number;
360
464
  /**
361
465
  * The definition's schema path, on cells that belong to one: a `row` cell
362
466
  * carries its column's (`detail.columns[i]`), a `total-row` cell its
363
- * `detail.total[r].cells[i]` entry. Absent on page-band cells reached by closure.
467
+ * `detail.total.rows[r].cells[i]` entry. Absent on page-band cells reached by closure.
364
468
  */
365
469
  path?: string;
366
470
  }
@@ -373,14 +477,21 @@ export interface PageInfo {
373
477
  total: number;
374
478
  }
375
479
 
480
+ /**
481
+ * What a page band closure hands back: the band's items as the walk would
482
+ * emit them, wearing the `page-header`/`page-footer` role — a text item, an
483
+ * image, or a split's bracket with one item per slot between it.
484
+ */
485
+ export type PageBandEvent = ItemEvent | ImageEvent | SplitStartEvent | SplitEndEvent;
486
+
376
487
  /**
377
488
  * Per-render page band closures on `report-start` (present only when the
378
489
  * schema declares page bands). A paginated target calls them once per page;
379
- * each call returns resolved `page-header`/`page-footer` item events.
490
+ * each call returns that page's resolved band events.
380
491
  */
381
492
  export interface PageBandRenderers {
382
- header?: (page: PageInfo) => (ItemEvent | ImageEvent)[];
383
- footer?: (page: PageInfo) => (ItemEvent | ImageEvent)[];
493
+ header?: (page: PageInfo) => PageBandEvent[];
494
+ footer?: (page: PageInfo) => PageBandEvent[];
384
495
  }
385
496
 
386
497
  export interface ReportStartEvent {
@@ -490,9 +601,15 @@ export interface TableStartEvent {
490
601
  type: "table-start";
491
602
  /** Always `detail` — the table definition's schema path. */
492
603
  path: string;
493
- columns: { header: EventCell; path: string; width?: number }[];
494
- /** The header-row box from `detail.header`, when declared. */
495
- style?: Record<string, unknown>;
604
+ /**
605
+ * The header row: one cell per column a neighbour's span does not cover,
606
+ * so fewer cells than columns whenever one spans. Shaped like a `row`
607
+ * payload, `style` being what is left of `detail.header`'s block once its
608
+ * box has landed on these cells.
609
+ */
610
+ header: { cells: EventCell[]; style?: Record<string, unknown> };
611
+ /** The column geometry, one entry per column. */
612
+ columns: { width?: number }[];
496
613
  }
497
614
 
498
615
  export interface RowEvent {
@@ -672,6 +789,22 @@ export function validate(schema: unknown, functions?: FunctionRegistry): string[
672
789
  */
673
790
  export function isDiagnostic(e: unknown): e is QuarioDiagnostic;
674
791
 
792
+ /**
793
+ * Mint an image failure named on the item that asked for the bytes — for a
794
+ * **render target**, which is where every image failure past the magic
795
+ * numbers happens: the engine vouches for those and no further.
796
+ *
797
+ * `path` is the image item's, from a stream event or a display-list op;
798
+ * `.source` is appended, that being the key an author would change. `said` is
799
+ * what failed in your own words, and a library's own message belongs after it
800
+ * rather than instead of it. The result is `cause`'s class, so an embedder's
801
+ * `RangeError` stays a `RangeError`, with `cause` set.
802
+ *
803
+ * Not a diagnostic: `isDiagnostic` does not answer for it, a decoder that
804
+ * refused a file being nobody's delegated engine.
805
+ */
806
+ export function imageError(path: string | undefined, said: string, cause?: unknown): Error;
807
+
675
808
  /**
676
809
  * Join a token stream to display text: literals verbatim, values through
677
810
  * `display()`. Re-exported from sjabloon.
@@ -688,29 +821,62 @@ export function text(tokens: readonly Token[]): string;
688
821
  export function display(value: unknown): string;
689
822
 
690
823
  /**
691
- * Present a token for a resolved `format` kind. Returns nothing when the kind
692
- * does not apply, so the caller falls back to `display()`. Locale defaults to
693
- * `"en-US"`, dates to UTC.
824
+ * Which currency code a cell wears under `format: "currency"`: its own
825
+ * `style.currency` when it declares one — the gate's `null` included, which is
826
+ * why an unusable code presents nothing rather than the instance's currency —
827
+ * else the instance default. Exported for a target that needs the code itself
828
+ * rather than the presented text, as the XLSX one does.
829
+ */
830
+ export function currencyOf(
831
+ style?: { currency?: unknown } | null,
832
+ options?: { currency?: string } | null,
833
+ ): string | null;
834
+
835
+ /**
836
+ * Present a token for a cell's resolved `style`. Reads the `format` kind and,
837
+ * under `"currency"`, the cell's own `currency` code; returns nothing when the
838
+ * kind does not apply, so the caller falls back to `display()`. Locale
839
+ * defaults to `"en-US"`, dates to UTC.
694
840
  */
695
841
  export function format(
696
842
  value: unknown,
697
- kind: unknown,
843
+ style?: { format?: unknown; currency?: unknown } | null,
698
844
  options?: { locale?: string; currency?: string; timeZone?: string } | null,
699
845
  ): string | undefined;
700
846
 
847
+ /**
848
+ * How many fraction digits a cell presents. Takes the whole style, as
849
+ * `format()` and `currencyOf()` do. `number` and `percent` present a fixed
850
+ * count, `currency` its currency's minor units, and a declared `digits` beats
851
+ * either — so the digits converge wherever the cell is rendered.
852
+ *
853
+ * A resolved style carries the answer on `style.format.digits` and a target
854
+ * reads it there; this is for a host holding a style the stream has not
855
+ * resolved, and the shorthand is widened here first.
856
+ *
857
+ * Returns nothing when the cell presents no count: `date`, no `format`, an
858
+ * unknown kind, and a `currency` with no code or one Intl will not read. A
859
+ * caller that gets nothing presents some other way — `format()` falls back to
860
+ * `display()`, and a worksheet writes no number format.
861
+ */
862
+ export function fractionDigits(
863
+ style?: { format?: unknown; currency?: unknown } | null,
864
+ options?: { currency?: string } | null,
865
+ ): number | undefined;
866
+
701
867
  /**
702
868
  * The typed-cell seam: exactly one value token holding a finite number, a
703
869
  * boolean, or a valid Date keeps its pre-stringify value; anything else —
704
870
  * including a lone null — reports `undefined` and joins to display text.
705
871
  *
706
- * Passing the cell's resolved `format` kind opts into the seam's one
707
- * coercion: under `"date"`, a string in either accepted RFC 3339 form —
872
+ * Passing the cell's resolved `format` declaration opts into the seam's one
873
+ * coercion: under the `date` kind, a string in either accepted RFC 3339 form —
708
874
  * `YYYY-MM-DD`, or a timestamp naming its offset — revives to the Date it
709
875
  * names. Omitting the argument leaves the seam as it was.
710
876
  */
711
877
  export function typed(
712
878
  tokens: readonly Token[],
713
- kind?: unknown,
879
+ decl?: ResolvedFormat | FormatKind,
714
880
  ): number | boolean | Date | undefined;
715
881
 
716
882
  /**
@@ -721,3 +887,25 @@ export function typed(
721
887
  * one: the question is whose band a role names, not what a walk can hand over.
722
888
  */
723
889
  export function isReportBand(role: string | undefined): boolean;
890
+
891
+ /**
892
+ * Every name in the closed style vocabulary, in the order SCHEMA.md's table
893
+ * lists them. A tool that offers the vocabulary imports this rather than
894
+ * keeping a copy.
895
+ */
896
+ export const STYLE_NAMES: readonly string[];
897
+
898
+ /**
899
+ * The `format` vocabulary as data, for a tool that offers it rather than
900
+ * checks it — a style rail's kind picker and its modifier control — so no
901
+ * surface keeps a copy that has to be told about a change.
902
+ */
903
+ export const FORMAT_VOCABULARY: {
904
+ readonly kinds: readonly FormatKind[];
905
+ readonly forms: readonly DateForm[];
906
+ /** Which modifier each kind takes, so a surface offering the pair need not
907
+ * know that three kinds share `digits` and `date` does not. */
908
+ readonly modifiers: Readonly<Record<FormatKind, "digits" | "form">>;
909
+ readonly defaultForm: DateForm;
910
+ readonly maxDigits: number;
911
+ };
package/lib/index.js CHANGED
@@ -13,6 +13,7 @@
13
13
  * siblings beside it, one seam each. See SCHEMA.md for the document shape.
14
14
  */
15
15
  import { relocate as relocateQuery } from "padvinder";
16
+ import { LEAD_REFUSED, isLead, leadPath } from "./band-height.js";
16
17
  import { MARKING, verify } from "./license.js";
17
18
  import { err, fault, locate } from "./locate.js";
18
19
  import { BLOCKED, NAME, record } from "./names.js";
@@ -25,7 +26,8 @@ import { opt } from "./stream.js";
25
26
  // imports them from the package, not from a file inside it.
26
27
  export { breathe, display, isReportBand, text, typed, walk } from "./stream.js";
27
28
  export { format } from "./format.js";
28
- export { isDiagnostic } from "./locate.js";
29
+ export { currencyOf, FORMAT_VOCABULARY, fractionDigits, STYLE_NAMES } from "./style.js";
30
+ export { imageError, isDiagnostic } from "./locate.js";
29
31
 
30
32
  /** @typedef {import("./scope.js").Scope} Scope */
31
33
 
@@ -133,16 +135,19 @@ function events(schema, funcs, options, marking) {
133
135
  /** @type {(event: any) => boolean} */
134
136
  let occupying = (event) =>
135
137
  event.type === "item" || event.type === "image" || event.type === "split-start";
136
- /** @type {(n: any) => boolean} */
137
- let nonzeroLead = (n) => Number.isFinite(n) && n !== 0;
138
- /** @type {(event: any) => string} */
139
- let eventPath = (event) => event.path || "header";
140
138
  /** @type {(event: any) => void} */
141
139
  let refuseLead = (event) => {
142
- if (nonzeroLead(event.style?.spaceBefore))
143
- fault(eventPath(event) + ".style.spaceBefore", "must be 0 after a height-declared header");
140
+ if (isLead(event.style?.spaceBefore)) fault(leadPath(event.path), LEAD_REFUSED);
144
141
  };
145
- /** @type {(events: Iterable<any>) => Generator<any>} */
142
+ /**
143
+ * Refuse the lead the document could not settle. `plan()` has already refused
144
+ * every candidate the document itself decides (./band-height.js); what reaches
145
+ * here is the rest — a lead that is an expression, or an item whose occupancy
146
+ * only the data knows — so the first occupying event of the band under the
147
+ * pinned box is the one this sees, whichever band that turned out to be.
148
+ *
149
+ * @type {(events: Iterable<any>) => Generator<any>}
150
+ */
146
151
  function* guardPin(events) {
147
152
  let pending = true;
148
153
  for (let event of events) {
package/lib/license.js CHANGED
@@ -11,7 +11,7 @@
11
11
  // reassigns them. A key is valid for every version released inside its window
12
12
  // (LICENSE section 7), so validity compares ISO date strings and never reads
13
13
  // a clock — accepted output stays accepted.
14
- let RELEASE = "2026-09-03"; // x-release-please-date
14
+ let RELEASE = "2026-09-07"; // x-release-please-date
15
15
  // The verifying half of the signing pair: the 65-byte uncompressed P-256
16
16
  // point, base64url. The private half never enters the repo;
17
17
  // scripts/license/sign.mjs mints keys against it.
package/lib/locate.js CHANGED
@@ -37,6 +37,38 @@ export let fault = (path, msg) => {
37
37
  throw Object.assign(Error(path + ": " + msg), { [LOCATION]: { path } });
38
38
  };
39
39
 
40
+ /**
41
+ * An image failure a **target** raises, named on the item that asked for the
42
+ * bytes. The engine vouches for an image's magic numbers and no further
43
+ * (SCHEMA.md, "Image item"), so every failure past that point belongs to
44
+ * whichever target was sizing or embedding the file — and each of them was
45
+ * inventing its own wording, down to a corrupt PNG reaching a host as
46
+ * pdf-lib's bare "Invalid typed array length: 0" (quario-e0bz).
47
+ *
48
+ * Here rather than in a target because three of them raise it and no two of
49
+ * them may depend on each other; the engine is what they all already have.
50
+ * Minted rather than thrown, unlike `fault` beside it, so a caller can raise
51
+ * it from a `catch` or a rejection in whichever style its own code is.
52
+ *
53
+ * The shape is the one every render error has: the path prefixes the message
54
+ * and the failing class survives behind it. It carries no `LOCATION` and is no
55
+ * `isDiagnostic` diagnostic — the traversal never sees it, and a decoder that
56
+ * refused a file is not a delegated engine.
57
+ *
58
+ * @type {(path: string | undefined, said: string, cause?: unknown) => Error}
59
+ */
60
+ export let imageError = (path, said, cause) => {
61
+ let error = TypeError((path ? path + ".source: " : "") + said, { cause });
62
+ // Wear the failing class rather than construct it. Keeping
63
+ // `e instanceof RangeError` true matters — a host may already be catching
64
+ // one — but the class is a library's and its constructor need not take
65
+ // `(message, options)` at all: one taking other arguments would mangle the
66
+ // message and swallow `cause`, turning a located failure back into the
67
+ // stray throw this exists to replace. Reparenting asks the class for
68
+ // nothing, so there is no call to check and nothing it can refuse.
69
+ if (cause instanceof Error) Object.setPrototypeOf(error, Object.getPrototypeOf(cause));
70
+ return error;
71
+ };
40
72
  /**
41
73
  * Re-throw `error` located at `path`, with `src` — the offending author
42
74
  * source — in the message.
package/lib/math.js ADDED
@@ -0,0 +1,71 @@
1
+ /**
2
+ * The scalar built-ins: quario's half of an expression's function scope that
3
+ * folds nothing. They live beside the reducers in SCHEMA.md ("Built-in
4
+ * functions") and apart from them here, because the reducers are also the
5
+ * declared-aggregate vocabulary and these can never be one -- `reducers.js`
6
+ * names no function in this file, so `"total": "round:=..."` stays a definition
7
+ * error.
8
+ *
9
+ * `round`, `floor` and `ceil` round the DECIMAL the author wrote, not the
10
+ * binary double it is stored as, and `Intl.NumberFormat` is how they reach it:
11
+ * its rounding starts from the shortest decimal that round-trips, so
12
+ * `round(2.675, 2)` is 2.68 where `Math.round(2.675 * 100) / 100` is 2.67 and
13
+ * `Number((2.675).toFixed(2))` is also 2.67. Those two disagree with each other
14
+ * as well as with us, on different inputs -- the whole argument, with the
15
+ * table, is in docs/adr/0050. The locale is hardcoded `en-US` rather than the
16
+ * instance's: this is arithmetic, not presentation, and a locale with
17
+ * non-ASCII digits or its own grouping would not parse back through `Number`.
18
+ * Formatters are built per call, deliberately -- see the ADR.
19
+ */
20
+ /** @type {(x: number, n: number, mode: string) => number} */
21
+ let decimal = (x, n, mode) =>
22
+ Number(
23
+ new Intl.NumberFormat("en-US", {
24
+ minimumFractionDigits: n,
25
+ maximumFractionDigits: n,
26
+ useGrouping: false,
27
+ roundingMode: /** @type {any} */ (mode),
28
+ }).format(x),
29
+ );
30
+ // The one test both halves make of `x`. Named because the rule it stands for
31
+ // is the interesting part: rounding a value that is not a finite number would
32
+ // have to invent one, so it passes through instead. The reducers coerce
33
+ // quietly because a fold must produce a total; a scalar has no such
34
+ // obligation, and coercing here would turn a data error into a confident 0
35
+ // where passing through leaves the cell as display text -- a failure the
36
+ // author can see.
37
+ /** @type {(x: any) => boolean} */
38
+ let finite = (x) => typeof x === "number" && Number.isFinite(x);
39
+
40
+ // Split from `places` rather than inlined: the three tests plus the coercion
41
+ // and the throw put one function at cyclomatic 5, and the health gate's CRAP
42
+ // ceiling lands at 4 (docs/agents/commands.md).
43
+ /** @type {(digits: number) => boolean} */
44
+ let honourable = (digits) => Number.isFinite(digits) && digits >= 0 && digits <= 100;
45
+ // `n` is authored, so a bad one is a mistake worth failing on -- unlike `x`,
46
+ // which is data. Intl would throw its own RangeError naming
47
+ // `maximumFractionDigits`, an option the author never wrote, so the message is
48
+ // ours. Fractional `n` truncates rather than throwing, which is what Intl does
49
+ // with it and is not worth a second rule for an author to hold.
50
+ /** @type {(n: any) => number} */
51
+ let places = (n) => {
52
+ let digits = Math.trunc(+(n ?? 0));
53
+ if (!honourable(digits)) throw new RangeError("n must be a number from 0 to 100");
54
+ return digits;
55
+ };
56
+
57
+ /** @type {(mode: string) => (x: any, n: any) => any} */
58
+ let rounder = (mode) => (x, n) => {
59
+ let digits = places(n);
60
+ return finite(x) ? decimal(x, digits, mode) : x;
61
+ };
62
+ /** @type {Record<string, (...args: any[]) => any>} */
63
+ export let MATH = {
64
+ // Written `(x, n)` rather than `(x, n = 0)` on purpose: xprsn's signatures()
65
+ // reports arity from `fn.length`, and a default parameter would describe
66
+ // these to an editor as taking one argument.
67
+ round: rounder("halfExpand"),
68
+ floor: rounder("floor"),
69
+ ceil: rounder("ceil"),
70
+ abs: (x) => (finite(x) ? Math.abs(x) : x),
71
+ };