@sheetxl/primitives 0.8.0-beta.1 → 0.8.0-beta.10

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.
@@ -21,6 +21,8 @@ import { LocaleProfile } from './LocaleProfile';
21
21
  export declare class StubbedFormulaContext implements IFormulaContext {
22
22
  /** @inheritdoc IFormulaContext.createRange */
23
23
  createRange<TS extends Scalar = Scalar>(rows?: number, columns?: number, values?: Scalar | Scalar[][] | ((value: TS, coords: IRange.CellCoords, range: IRange<TS>) => TS)): IRange;
24
+ /** @inheritdoc IFormulaContext.flattenAreas */
25
+ flattenAreas(value: IRange | IRange[]): IRange;
24
26
  /** @inheritdoc IFormulaContext.getReference */
25
27
  getReference(coords?: Readonly<IRange.Coords>, r1c1?: boolean): IReferenceRange;
26
28
  /** @inheritdoc IFormulaContext.getResizedReference */
@@ -37,6 +39,8 @@ export declare class StubbedFormulaContext implements IFormulaContext {
37
39
  getSerialDate(year: number, month: number, day: number, hour?: number, minute?: number, second?: number, millis?: number): number;
38
40
  /** @inheritdoc IFormulaContext.getNow */
39
41
  getNow(): number;
42
+ /** @inheritdoc IFormulaContext.getRandom */
43
+ getRandom(): number;
40
44
  /** @inheritdoc IFormulaContext.getValueAt */
41
45
  getValueAt(row: number, column: number, sheetName?: string, options?: IRange.ValueOptions): Scalar | null;
42
46
  /** @inheritdoc IFormulaContext.getEntireCoords */
@@ -47,6 +51,8 @@ export declare class StubbedFormulaContext implements IFormulaContext {
47
51
  getRow(): number;
48
52
  /** @inheritdoc IFormulaContext.getColumn */
49
53
  getColumn(): number;
54
+ /** @inheritdoc IFormulaContext.isImplicitIntersect */
55
+ isImplicitIntersect(): boolean;
50
56
  /** @inheritdoc IFormulaContext.getFormulaAt */
51
57
  getFormulaAt(row: number, column: number, sheetName?: string): string | null;
52
58
  /** @inheritdoc IFormulaContext.getSpillAt */
@@ -60,7 +66,7 @@ export declare class StubbedFormulaContext implements IFormulaContext {
60
66
  /** @inheritdoc IFormulaContext.formatResults */
61
67
  formatResults(numFmt: string): void;
62
68
  /** @inheritdoc IFormulaContext.filterHidden */
63
- filterHidden(range: IRange, axes?: IRange.AxisSelection): IRange;
69
+ filterHidden(range: IRange, options?: IFormulaContext.FilterHiddenOptions): IRange;
64
70
  /** @inheritdoc IFormulaContext.filterTotals */
65
71
  filterTotals(range: IRange): IRange;
66
72
  /** @inheritdoc IFormulaContext.getFunction */
@@ -83,7 +89,14 @@ export declare class StubbedFormulaContext implements IFormulaContext {
83
89
  * plausibly and wrongly, where throwing names the actual mistake (evaluating criteria outside an
84
90
  * installed scope).
85
91
  */
86
- createCriteria(criteria: Scalar): IFormulaContext.CriteriaEvaluator;
92
+ createCriteria(criteria: Scalar, options?: IFormulaContext.CriteriaOptions): IFormulaContext.CriteriaEvaluator;
93
+ /**
94
+ * Unimplemented. See {@link IFormulaContext.createGrouping}.
95
+ *
96
+ * @remarks
97
+ * Grouping needs Excel's key equality, which this stub does not have.
98
+ */
99
+ createGrouping(keys: IRange, options?: IFormulaContext.GroupingOptions): IFormulaContext.Grouping;
87
100
  /**
88
101
  * Parses the canonical English boolean and error literals.
89
102
  * See {@link IFormulaContext.parseAsLiteral}.
@@ -171,7 +184,9 @@ export declare class ScopedFormulaContext implements IFormulaContext {
171
184
  parseAsNumber(textOrNumber: Scalar, options?: IFormulaContext.ParseNumberOptions): number;
172
185
  parseAsNumber(textOrNumber: Scalar | undefined, options?: IFormulaContext.ParseNumberOptions): number | undefined;
173
186
  /** @inheritdoc IFormulaContext.createCriteria */
174
- createCriteria(criteria: Scalar): IFormulaContext.CriteriaEvaluator;
187
+ createCriteria(criteria: Scalar, options?: IFormulaContext.CriteriaOptions): IFormulaContext.CriteriaEvaluator;
188
+ /** @inheritdoc IFormulaContext.createGrouping */
189
+ createGrouping(keys: IRange, options?: IFormulaContext.GroupingOptions): IFormulaContext.Grouping;
175
190
  /** @inheritdoc IFormulaContext.parseAsLiteral */
176
191
  parseAsLiteral(text: string): boolean | FormulaError.Known | undefined;
177
192
  /** @inheritdoc IFormulaContext.toText */
@@ -188,12 +203,18 @@ export declare class ScopedFormulaContext implements IFormulaContext {
188
203
  getSerialDate(year: number, month: number, day: number, hour?: number, minute?: number, second?: number, millis?: number): number;
189
204
  /** @inheritdoc IFormulaContext.getNow */
190
205
  getNow(): number;
206
+ /** @inheritdoc IFormulaContext.getRandom */
207
+ getRandom(): number;
191
208
  /** @inheritdoc IFormulaContext.createRange */
192
209
  createRange<TS extends Scalar = Scalar>(rows?: number, columns?: number, values?: Scalar | Scalar[][] | ((value: TS, coords: IRange.CellCoords, range: IRange<TS>) => TS)): IRange;
210
+ /** @inheritdoc IFormulaContext.flattenAreas */
211
+ flattenAreas(value: IRange | IRange[]): IRange;
193
212
  /** @inheritdoc IFormulaContext.getRow */
194
213
  getRow(): number;
195
214
  /** @inheritdoc IFormulaContext.getColumn */
196
215
  getColumn(): number;
216
+ /** @inheritdoc IFormulaContext.isImplicitIntersect */
217
+ isImplicitIntersect(): boolean;
197
218
  /** @inheritdoc IFormulaContext.getReference */
198
219
  getReference(coords?: Readonly<IRange.Coords>, r1c1?: boolean): IReferenceRange;
199
220
  /** @inheritdoc IFormulaContext.getResizedReference */
@@ -213,7 +234,7 @@ export declare class ScopedFormulaContext implements IFormulaContext {
213
234
  /** @inheritdoc IFormulaContext.formatResults */
214
235
  formatResults(numFmt: string): void;
215
236
  /** @inheritdoc IFormulaContext.filterHidden */
216
- filterHidden(range: IRange, axes?: IRange.AxisSelection): IRange;
237
+ filterHidden(range: IRange, options?: IFormulaContext.FilterHiddenOptions): IRange;
217
238
  /** @inheritdoc IFormulaContext.filterTotals */
218
239
  filterTotals(range: IRange): IRange;
219
240
  /** @inheritdoc IFormulaContext.getFunction */
@@ -232,13 +253,15 @@ export declare class ScopedFormulaContext implements IFormulaContext {
232
253
  /**
233
254
  * A plain en-US profile, for stubs and for environments without `Intl`.
234
255
  *
235
- * @remarks
236
- * Deliberately static data -- no probing. Real profiles come from
237
- * `makeLocaleProfile` in `@sheetxl/sdk`.
256
+ * @internal
257
+ * @hidden
238
258
  */
239
259
  export declare const DEFAULT_LOCALE_PROFILE: LocaleProfile;
240
260
  /**
241
261
  * The shared {@link IFormulaContext} for formula implementations. See {@link ScopedFormulaContext}.
262
+ *
263
+ * @internal
264
+ * @hidden
242
265
  */
243
266
  export declare const FormulaContext: IFormulaContext;
244
267
  //# sourceMappingURL=FormulaContext.d.ts.map
@@ -28,15 +28,16 @@ import { IRuntime } from './IRuntime';
28
28
  *
29
29
  * | Category | Key methods |
30
30
  * |---|---|
31
- * | Evaluation lifecycle | `markVolatile`, `getRow`, `getColumn`, `getEntireCoords` |
31
+ * | Evaluation lifecycle | `markVolatile`, `getRow`, `getColumn`, `getEntireCoords`, `getRandom` |
32
32
  * | Argument reflection | `getArgs` |
33
33
  * | Output hints | `formatResults` |
34
- * | Range construction | `createRange`, `filterHidden` |
34
+ * | Range construction | `createRange`, `flattenAreas`, `filterHidden` |
35
35
  * | Dynamic references | `getReference`, `getValueAt`, `getFormulaAt`, `getSpillAt` |
36
36
  * | Function lookup | `getFunction` |
37
37
  * | Dates | `getNow`, `toSerialDate`, `fromSerialDate`, `getDateParts`, `getSerialDate`, `parseAsDate`, `parseAsSerialDate`, `isValidDate` |
38
38
  * | Coercion | `parseAsNumber`, `parseAsLiteral`, `toText`, `compare` |
39
39
  * | Criteria | `createCriteria` |
40
+ * | Grouping | `createGrouping` |
40
41
  * | Sheet introspection | `getSheetCount`, `getSheetIndex`, `getAddress`, `getNumberFormat` |
41
42
  *
42
43
  * ### Implementors
@@ -54,6 +55,25 @@ export interface IFormulaContext {
54
55
  * invoked per cell. Default is `null` (an empty range).
55
56
  */
56
57
  createRange<TS extends Scalar = Scalar>(rows?: number, columns?: number, values?: Scalar | Scalar[][] | ((value: TS, coords: IRange.CellCoords, range: IRange<TS>) => TS)): IRange;
58
+ /**
59
+ * One range over every cell of a `ReportType` slot's argument — the AREAS of a reference union
60
+ * folded into a single column, or the range returned unchanged.
61
+ *
62
+ * @param value The argument as it arrived: a range, or one range per area.
63
+ *
64
+ * @remarks
65
+ * A union `(A1:A5,B1:B2)` reaches a {@link IFunction.ParameterTypeJSON.reportType} slot as one
66
+ * `IRange` per area. Functions that answer over it do so because they walk MEMBERS and a union
67
+ * simply has more of them — `LARGE((DA1:DA5,DK1:DK2),2)` is the second largest of all seven
68
+ * cells. This is that walk, and it is a helper rather than a marshalling step on purpose: Excel
69
+ * flattens in exactly the same place, a shared reducer its bodies opt into, which is why the
70
+ * same union is flattened by `LARGE`, refused by `SUMPRODUCT` and answered `#REF!` by `ROWS`.
71
+ *
72
+ * The result is a single column in area order, row-major within each area. SHAPE IS NOT
73
+ * PRESERVED — a union has none. A body that needs an extent must ask
74
+ * {@link IFunctionArguments.getAreaCountAt} and answer for itself.
75
+ */
76
+ flattenAreas(value: IRange | IRange[]): IRange;
57
77
  /**
58
78
  * Returns coordinates as an address string.
59
79
  *
@@ -94,6 +114,20 @@ export interface IFormulaContext {
94
114
  * Returns the 0-based column of the cell being evaluated.
95
115
  */
96
116
  getColumn(): number;
117
+ /**
118
+ * Whether the call being evaluated is in Excel's VALUE mode, where a multi-cell reference at a
119
+ * slot that reads ONE value is implicitly intersected with the formula's cell (the `@` rule);
120
+ * `false` is ARRAY mode, where such a slot reads the reference's first cell.
121
+ *
122
+ * @remarks
123
+ * Only a body that accepts a whole reference at a single-value slot and reads it itself asks
124
+ * (`LINEST`'s `const`). The engine decides the mode from the formula around the call — a
125
+ * scalar-valued root, an aggregator or a lookup above it are value mode; a spilling root, an
126
+ * operator or lifting slot over an array-valued operand, a `LAMBDA` body and an array-entered
127
+ * block are array mode — and the mode is inherited through every call that does not set it.
128
+ * Outside a calculation this is `false`.
129
+ */
130
+ isImplicitIntersect(): boolean;
97
131
  /**
98
132
  * Resolves a range reference at runtime, returning an {@link IReferenceRange}.
99
133
  *
@@ -186,16 +220,30 @@ export interface IFormulaContext {
186
220
  */
187
221
  getSheetIndex(sheet?: string | IReferenceRange): number;
188
222
  /**
189
- * Returns a copy of a range with hidden rows or columns removed.
223
+ * Returns a range as it reads without its hidden rows or columns.
190
224
  *
191
225
  * @param range The range to filter.
192
- * @param axes Which axis to filter on. Default is `IRange.AxisSelection.Row`.
193
- * @returns A new range without the hidden rows or columns.
226
+ * @param options See {@link IFormulaContext.FilterHiddenOptions}.
227
+ * @returns `range` itself when nothing in it is hidden; otherwise the same range reading with
228
+ * the hidden rows or columns skipped ({@link IRange.IteratorOptions.processRowSkip}).
194
229
  *
195
230
  * @remarks
196
231
  * Use this for functions that ignore hidden data, such as `SUBTOTAL`.
232
+ *
233
+ * Nothing is copied. The shape of `range` is kept, and a reference stays a reference: a hidden
234
+ * cell is not visited and reads as empty, so every position still means the cell it meant, and
235
+ * an aggregate that ignores blanks ignores it. A computed array has no rows or columns to hide
236
+ * and is returned as it is.
237
+ *
238
+ * A hidden row is a hidden row, whoever hid it: a filter and the user leave the same mark, and
239
+ * nothing here tells them apart. What a filter adds is a state of the range's SHEET, which
240
+ * {@link IFormulaContext.FilterHiddenOptions.onlyWhileFiltered} reads.
241
+ *
242
+ * A formula that calls `SUBTOTAL` or `AGGREGATE` is recalculated when a row or column is hidden
243
+ * or shown, and when a filter is set or cleared. One that reaches this only through a custom
244
+ * function is not: it reads the state when it is next calculated.
197
245
  */
198
- filterHidden(range: IRange, axes?: IRange.AxisSelection): IRange;
246
+ filterHidden(range: IRange, options?: IFormulaContext.FilterHiddenOptions): IRange;
199
247
  /**
200
248
  * Returns a copy of a range with any cell whose formula is a nested total removed.
201
249
  *
@@ -277,6 +325,7 @@ export interface IFormulaContext {
277
325
  *
278
326
  * @param criteria The criteria, exactly as the user wrote it — a comparison (`">10"`, `"<>x"`),
279
327
  * a wildcard pattern (`"a*"`), or a literal of any type.
328
+ * @param options How the criteria is matched. See {@link IFormulaContext.CriteriaOptions}.
280
329
  * @returns A predicate to test cell values with. Call it once per cell.
281
330
  *
282
331
  * @remarks
@@ -298,7 +347,34 @@ export interface IFormulaContext {
298
347
  * const count = range.reduce((total, value) => matches(value) ? total + 1 : total, 0);
299
348
  * ```
300
349
  */
301
- createCriteria(criteria: Scalar): IFormulaContext.CriteriaEvaluator;
350
+ createCriteria(criteria: Scalar, options?: IFormulaContext.CriteriaOptions): IFormulaContext.CriteriaEvaluator;
351
+ /**
352
+ * Groups the records of a range — its rows — by their values.
353
+ *
354
+ * @param keys The key columns. Each row is one record, and records with equal values in every
355
+ * column form one group.
356
+ * @param options Which records take part. See {@link IFormulaContext.GroupingOptions}.
357
+ * @returns The groups, numbered in the order they first appear.
358
+ *
359
+ * @remarks
360
+ * Two keys are equal by the rule `UNIQUE` uses: text ignores case, and types never mix, so `1`,
361
+ * `"1"` and `TRUE` are three groups. A blank key is a key of its own, distinct from `""`, and an
362
+ * error is a key like any other value. Every row is a record, blank rows included.
363
+ *
364
+ * Group ONCE and ask the grouping for each group's members. The grouping does the work a
365
+ * per-group scan would repeat, so it stays proportional to the data rather than to the data
366
+ * times the number of groups.
367
+ *
368
+ * @example
369
+ * ```typescript
370
+ * const grouping = FormulaContext.createGrouping(regions);
371
+ * for (let g = 0; g < grouping.getGroupCount(); g++) {
372
+ * const [region] = grouping.getKeys(g);
373
+ * const sales = grouping.getMembers(amounts, [g]);
374
+ * }
375
+ * ```
376
+ */
377
+ createGrouping(keys: IRange, options?: IFormulaContext.GroupingOptions): IFormulaContext.Grouping;
302
378
  /**
303
379
  * Reads a typed literal in the workbook's language — the parse side of {@link toText}.
304
380
  *
@@ -386,6 +462,30 @@ export interface IFormulaContext {
386
462
  * ```
387
463
  */
388
464
  getNow(): number;
465
+ /**
466
+ * Returns a random number in `[0, 1)`, like `Math.random()`.
467
+ *
468
+ * @remarks
469
+ * Always draw through this, never `Math.random()`. The source is the runtime's, so a test or a
470
+ * replay can seed it, and the engine sees the draw: a draw is a PER-CELL read — two cells calling
471
+ * the same function in one pass differ — where `getNow` is one value for the whole pass, and the
472
+ * engine only shares an evaluation across the cells of a block when nothing per-cell was read.
473
+ * Pair it with `Volatile<T>` so the function draws again on the next pass.
474
+ *
475
+ * Under a seeded runtime ({@link IRuntime.getRandomSeed}) each draw is keyed by the cell, the
476
+ * pass and which draw it is within that cell, so a function that draws more than once gets
477
+ * distinct values and a cell gets the same ones however the pass was ordered. Take the draws
478
+ * where they are used rather than hoisting them, and the values follow the cells rather than
479
+ * the code path.
480
+ *
481
+ * @example
482
+ * ```typescript
483
+ * export function RAND(): Volatile<number> {
484
+ * return FormulaContext.getRandom();
485
+ * }
486
+ * ```
487
+ */
488
+ getRandom(): number;
389
489
  /**
390
490
  * Returns `true` if the value is a usable date — either a `Date` or a number in the
391
491
  * workbook's serial date range.
@@ -447,14 +547,12 @@ export interface IFormulaContext {
447
547
  * Fields may fall outside their normal range and carry, which is what makes `DATE` and
448
548
  * `TIME` arithmetic work: month 13 is January of the next year, hour 50 is two days plus
449
549
  * two hours, and `getSerialDate(1900, 1, 0)` is serial 0. The `day` carries in SERIAL
450
- * space, so it runs through the phantom `1900-02-29` — measured 25/25 by
451
- * `probe-date-1900-day-overflow.ps1`.
550
+ * space, so it runs through the phantom `1900-02-29`.
452
551
  *
453
552
  * **This validates nothing, and that is deliberate — a caller MUST choose its own
454
553
  * policy.** No range check, no clamping, no bounds on any field, and no `DATE`-style
455
554
  * `0..1899 -> +1900` year offset. Those belong to `DATE`'s ARGUMENTS, not to the
456
- * calendar, and the three things that prove it were measured together
457
- * (`probe-date-arg-layer.ps1`, 2026-08-20):
555
+ * calendar, and three things prove it:
458
556
  *
459
557
  * * **The day policy is the caller's.** `DATE` and `DATEDIF`'s `md`/`yd` re-anchoring
460
558
  * want the raw carry above — `DATEDIF("2020-01-31","2020-03-01","md")` is `-1`
@@ -510,6 +608,28 @@ export interface IFormulaContext {
510
608
  * {@link IFormulaContext}
511
609
  */
512
610
  export declare namespace IFormulaContext {
611
+ /**
612
+ * Options for {@link IFormulaContext.filterHidden}.
613
+ */
614
+ interface FilterHiddenOptions {
615
+ /**
616
+ * Which axis to filter on.
617
+ * @defaultValue `IRange.AxisSelection.Row`
618
+ */
619
+ axes?: IRange.AxisSelection;
620
+ /**
621
+ * Skip the hidden rows or columns only while the range's sheet is filtered: while some
622
+ * AutoFilter or table on that sheet holds a condition, whether or not it hides a row. Until
623
+ * then `range` is returned as it is, hidden rows included.
624
+ *
625
+ * @remarks
626
+ * This is `SUBTOTAL` 1-11 (Excel: they "include manually hidden rows", which is what an
627
+ * unfiltered sheet reads as). `SUBTOTAL` 101-111 and `AGGREGATE` leave it off and skip
628
+ * always.
629
+ * @defaultValue false
630
+ */
631
+ onlyWhileFiltered?: boolean;
632
+ }
513
633
  /**
514
634
  * Options for {@link IFormulaContext.parseAsNumber}.
515
635
  */
@@ -523,6 +643,80 @@ export declare namespace IFormulaContext {
523
643
  * there.
524
644
  */
525
645
  type CriteriaEvaluator = (value: Scalar) => boolean;
646
+ /**
647
+ * How a criteria is matched — see {@link IFormulaContext.createCriteria}.
648
+ */
649
+ interface CriteriaOptions {
650
+ /**
651
+ * Where a TEXT criteria has to line up with the cell: `'whole'` (the default) for the whole
652
+ * cell, `'start'` for a prefix.
653
+ *
654
+ * @remarks
655
+ * The `D` functions' criteria table is the prefix form and the `*IF` family is not: the
656
+ * criterion `appl` takes the row whose value is `Apple` at `DSUM` and no row at all at
657
+ * `COUNTIF`, and the empty criterion — a prefix of everything — takes every TEXT cell at
658
+ * `DSUM` where `COUNTIF` takes only a cell that IS an empty string.
659
+ *
660
+ * It applies to a criteria that stays TEXT and to nothing else. An OPERATOR turns it off
661
+ * (`=appl` selects no row), and so does a criteria that reads as a number, a boolean or an
662
+ * error — `1` does not reach a cell holding `12`.
663
+ */
664
+ anchor?: 'whole' | 'start';
665
+ }
666
+ /**
667
+ * Options for {@link IFormulaContext.createGrouping}.
668
+ */
669
+ interface GroupingOptions {
670
+ /**
671
+ * One column, as tall as the keys, selecting the records that take part. A record is kept
672
+ * when its member is `TRUE` or a nonzero number, and left out otherwise.
673
+ */
674
+ filter?: IRange;
675
+ }
676
+ /**
677
+ * The records of a range divided into groups — see {@link IFormulaContext.createGrouping}.
678
+ */
679
+ interface Grouping {
680
+ /**
681
+ * The number of records kept by the filter.
682
+ */
683
+ getRecordCount(): number;
684
+ /**
685
+ * The number of groups.
686
+ */
687
+ getGroupCount(): number;
688
+ /**
689
+ * The number of records in a group.
690
+ */
691
+ getSize(group: number): number;
692
+ /**
693
+ * A group's key, one value per key column, as spelled by the group's first record. A blank
694
+ * key is `null`.
695
+ */
696
+ getKeys(group: number): readonly Scalar[];
697
+ /**
698
+ * The group a record belongs to, or `-1` when the filter left it out.
699
+ *
700
+ * @param record The record's row within the keys.
701
+ */
702
+ getGroup(record: number): number;
703
+ /**
704
+ * The row of a group's first record within the keys.
705
+ *
706
+ * @remarks
707
+ * With {@link getGroup} on another grouping of the same records, this maps a group of one
708
+ * grouping onto the group of another — a finer grouping's group onto its parent's.
709
+ */
710
+ getFirstRecord(group: number): number;
711
+ /**
712
+ * The members of `values` that belong to some groups, as one column in record order.
713
+ *
714
+ * @param values One column, as tall as the keys.
715
+ * @param groups The groups to take members from. Omit it for every kept record.
716
+ * @returns An array, never a reference. A blank member stays blank.
717
+ */
718
+ getMembers(values: IRange, groups?: readonly number[]): IRange;
719
+ }
526
720
  /**
527
721
  * A serial date split into fields — see {@link IFormulaContext.getDateParts}.
528
722
  *