@gui-chat-plugin/spreadsheet 2.0.0 → 3.0.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.
Files changed (64) hide show
  1. package/README.md +33 -4
  2. package/dist/core/dispatch.d.ts +8 -0
  3. package/dist/core/index.d.ts +4 -2
  4. package/dist/core/paths.d.ts +13 -0
  5. package/dist/core/plugin.d.ts +7 -3
  6. package/dist/core/sheetGuards.d.ts +5 -0
  7. package/dist/core/types.d.ts +25 -3
  8. package/dist/core.cjs +1 -1
  9. package/dist/core.js +2 -2
  10. package/dist/engine/calculator.d.ts +15 -3
  11. package/dist/engine/cellBuilder.d.ts +44 -0
  12. package/dist/engine/cellEmpty.d.ts +13 -0
  13. package/dist/engine/cellFormatting.d.ts +25 -0
  14. package/dist/engine/coerce-boolean.d.ts +7 -0
  15. package/dist/engine/condition.d.ts +49 -0
  16. package/dist/engine/date-locale.d.ts +12 -0
  17. package/dist/engine/date-parser.d.ts +7 -1
  18. package/dist/engine/date-utils.d.ts +7 -4
  19. package/dist/engine/datedif.d.ts +13 -0
  20. package/dist/engine/evaluator.d.ts +44 -1
  21. package/dist/engine/financial-math.d.ts +28 -0
  22. package/dist/engine/formatter.d.ts +9 -0
  23. package/dist/engine/formulaError.d.ts +37 -0
  24. package/dist/engine/formulaRefs.d.ts +37 -0
  25. package/dist/engine/functions/lookup-math.d.ts +19 -0
  26. package/dist/engine/functions/statistical-math.d.ts +41 -0
  27. package/dist/engine/functions/text.d.ts +17 -1
  28. package/dist/engine/guards.d.ts +10 -0
  29. package/dist/engine/index.d.ts +11 -0
  30. package/dist/engine/jsonCellLocator.d.ts +27 -0
  31. package/dist/engine/math-ops.d.ts +41 -0
  32. package/dist/engine/numericCoercion.d.ts +38 -0
  33. package/dist/engine/parser.d.ts +0 -24
  34. package/dist/engine/registry.d.ts +30 -2
  35. package/dist/engine/responseDecoder.d.ts +40 -0
  36. package/dist/engine/spreadsheet-errors.d.ts +42 -0
  37. package/dist/engine/textFormat.d.ts +32 -0
  38. package/dist/engine/translateFormula.d.ts +37 -0
  39. package/dist/engine/types.d.ts +17 -3
  40. package/dist/index.cjs +1 -1
  41. package/dist/index.js +2 -2
  42. package/dist/lang/de.d.ts +3 -0
  43. package/dist/lang/en.d.ts +3 -0
  44. package/dist/lang/es.d.ts +3 -0
  45. package/dist/lang/fr.d.ts +3 -0
  46. package/dist/lang/index.d.ts +1 -0
  47. package/dist/lang/ja.d.ts +3 -0
  48. package/dist/lang/ko.d.ts +3 -0
  49. package/dist/lang/messages.d.ts +20 -0
  50. package/dist/lang/ptBR.d.ts +3 -0
  51. package/dist/lang/zh.d.ts +3 -0
  52. package/dist/samples-BzI0GdUj.cjs +1 -0
  53. package/dist/{samples-sKAnErVb.js → samples-Cylf7cMw.js} +110 -17
  54. package/dist/style.css +1 -1
  55. package/dist/vue/View.vue.d.ts +1 -1
  56. package/dist/vue/cellHighlights.d.ts +42 -0
  57. package/dist/vue/htmlText.d.ts +4 -0
  58. package/dist/vue/index.d.ts +6 -4
  59. package/dist/vue/keyboardNav.d.ts +9 -0
  60. package/dist/vue/sheetSession.d.ts +24 -0
  61. package/dist/vue.cjs +72 -72
  62. package/dist/vue.js +7023 -6575
  63. package/package.json +16 -15
  64. package/dist/samples-ClV76j10.cjs +0 -1
package/README.md CHANGED
@@ -16,12 +16,14 @@ This plugin provides a full-featured spreadsheet component with:
16
16
  - Lookup functions (VLOOKUP, HLOOKUP, INDEX, MATCH)
17
17
  - Financial functions (PMT, PV, FV, NPV, IRR)
18
18
  - Multi-sheet support
19
- - Excel file import/export (.xlsx)
19
+ - Excel file export (.xlsx)
20
+ - Saving to the host's artifacts area, with cell edits written back
21
+ - UI in 8 languages, following the host's locale
20
22
  - Cell formatting and styling
21
23
 
22
24
  ## Important: SheetJS Dependency
23
25
 
24
- > **Note:** This package uses [SheetJS](https://docs.sheetjs.com/docs/getting-started/installation/nodejs/) for Excel file import/export.
26
+ > **Note:** This package uses [SheetJS](https://docs.sheetjs.com/docs/getting-started/installation/nodejs/) for Excel file export.
25
27
  > SheetJS is **not installed from npm**, but directly from the official SheetJS CDN:
26
28
  > ```
27
29
  > https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz
@@ -58,10 +60,37 @@ import "@gui-chat-plugin/spreadsheet/style.css";
58
60
 
59
61
  ```typescript
60
62
  import { pluginCore, TOOL_NAME } from "@gui-chat-plugin/spreadsheet";
61
- // or
62
- import pluginCore from "@gui-chat-plugin/spreadsheet";
63
63
  ```
64
64
 
65
+ ## Saving workbooks (host integration)
66
+
67
+ The plugin works in two modes, chosen by what the host passes to `execute`:
68
+
69
+ - **With `files.artifacts`** (the gui-chat-protocol runtime capability): the workbook is written to `artifacts/spreadsheets/YYYY/MM/<id>.json` and `data.sheets` holds that path. The View reads and saves it through `useRuntime().dispatch`, so cell edits land in the same file and the model can read them back from it.
70
+ - **Without it**: `data.sheets` holds the workbook itself, and edits are reported through `updateResult` only.
71
+
72
+ The View sends two dispatch calls:
73
+
74
+ | `kind` | other args | result |
75
+ |--------|-----------|--------|
76
+ | `loadSpreadsheet` | `path` | `{ data: { sheets } }` |
77
+ | `saveSpreadsheet` | `path`, `sheets` | `{ data: { sheets: path } }` |
78
+
79
+ `path` must be a `.json` file under `artifacts/spreadsheets/`.
80
+
81
+ A host that routes the View's `dispatch` to the plugin's own `execute` (as MulmoTerminal does) needs nothing more: `pluginCore.execute` tells the two calls apart by `kind`. A host with its own dispatch route can call `executeSpreadsheetDispatch(files.artifacts, args)` after checking `isSpreadsheetDispatchArgs(args)`.
82
+
83
+ The View must be rendered inside the host's `PLUGIN_RUNTIME_KEY` provider to load or save a saved workbook; it also takes the UI language from that runtime (`en`, `ja`, `de`, `es`, `fr`, `ko`, `pt-BR`, `zh`, falling back to English).
84
+
85
+ ## Upgrading from 2.x
86
+
87
+ 3.0.0 changes what a host receives, so it is a major release:
88
+
89
+ - `SpreadsheetToolData.sheets` is `SpreadsheetSheet[] | string`. With `files.artifacts` the tool returns the saved file's path instead of the workbook, so code that reads `data.sheets` as an array has to handle the path (or load it with the `loadSpreadsheet` dispatch).
90
+ - `pluginCore.execute` also accepts `SpreadsheetDispatchArgs`.
91
+ - The `gui-chat-protocol` peer is `^2.3.0`, and the package no longer bundles it: the host's copy is used.
92
+ - Cells may hold a boolean `v`, and stored workbooks may contain bare values, `null` gaps and `{}` cells (`StorableCell`).
93
+
65
94
  ## Package Exports
66
95
 
67
96
  | Export | Description |
@@ -0,0 +1,8 @@
1
+ import type { FileOps, ToolResult } from "gui-chat-protocol";
2
+ import type { SpreadsheetDispatchArgs, SpreadsheetToolData } from "./types";
3
+ export declare function isSpreadsheetDispatchArgs(value: unknown): value is SpreadsheetDispatchArgs;
4
+ /** `files.artifacts` off a host context, when the host provides it. The
5
+ * protocol's `ToolContext` does not declare `files`, so it is checked rather
6
+ * than assumed. */
7
+ export declare function artifactsFileOps(context: unknown): FileOps | null;
8
+ export declare function executeSpreadsheetDispatch(artifacts: FileOps, args: SpreadsheetDispatchArgs): Promise<ToolResult<SpreadsheetToolData, never>>;
@@ -2,8 +2,10 @@
2
2
  * Spreadsheet Plugin - Core (Framework-agnostic)
3
3
  *
4
4
  * This module exports the core plugin logic without UI components.
5
- * Import from "@anthropic/guichat-plugin-spreadsheet" or "@anthropic/guichat-plugin-spreadsheet/core"
5
+ * Import from "@gui-chat-plugin/spreadsheet" or "@gui-chat-plugin/spreadsheet/core"
6
6
  */
7
- export type { SpreadsheetCell, SpreadsheetSheet, SpreadsheetToolData, SpreadsheetArgs, } from "./types";
7
+ export type { SpreadsheetCell, SpreadsheetCellValue, StorableCell, SpreadsheetSheet, SpreadsheetToolData, SpreadsheetArgs, SpreadsheetDispatchArgs, } from "./types";
8
8
  export { TOOL_NAME, TOOL_DEFINITION, SYSTEM_PROMPT, executeSpreadsheet, pluginCore, } from "./plugin";
9
+ export { executeSpreadsheetDispatch, isSpreadsheetDispatchArgs } from "./dispatch";
10
+ export { isSpreadsheetArtifactPath } from "./paths";
9
11
  export { samples } from "./samples";
@@ -0,0 +1,13 @@
1
+ export interface SpreadsheetPath {
2
+ /** Relative to the artifacts root — what `files.artifacts` takes. */
3
+ relPath: string;
4
+ /** Workspace-relative — what the tool reports and the View dispatches. */
5
+ filePath: string;
6
+ }
7
+ export declare function spreadsheetArtifactPath(now?: Date, fileId?: string): SpreadsheetPath;
8
+ /** True for a workspace-relative path inside `artifacts/spreadsheets/` ending
9
+ * in `.json`, with no traversal, empty, or backslash segments. */
10
+ export declare function isSpreadsheetArtifactPath(value: unknown): value is string;
11
+ /** `artifacts/spreadsheets/…` → `spreadsheets/…`. Assumes the input passed
12
+ * `isSpreadsheetArtifactPath`. */
13
+ export declare const toArtifactsRelative: (filePath: string) => string;
@@ -5,7 +5,11 @@
5
5
  * Can be used by any framework (Vue, React, etc.)
6
6
  */
7
7
  import type { ToolPluginCore, ToolContext, ToolResult } from "gui-chat-protocol";
8
- import type { SpreadsheetToolData, SpreadsheetArgs } from "./types";
8
+ import type { SpreadsheetToolData, SpreadsheetArgs, SpreadsheetDispatchArgs } from "./types";
9
9
  export { TOOL_NAME, TOOL_DEFINITION, SYSTEM_PROMPT } from "./definition";
10
- export declare const executeSpreadsheet: (_context: ToolContext, args: SpreadsheetArgs) => Promise<ToolResult<SpreadsheetToolData, never>>;
11
- export declare const pluginCore: ToolPluginCore<SpreadsheetToolData, never, SpreadsheetArgs>;
10
+ /**
11
+ * Create a spreadsheet. With the host's `files.artifacts`, the workbook is
12
+ * saved and `data.sheets` carries its path; without it, the workbook itself.
13
+ */
14
+ export declare const executeSpreadsheet: (context: ToolContext, args: SpreadsheetArgs) => Promise<ToolResult<SpreadsheetToolData, never>>;
15
+ export declare const pluginCore: ToolPluginCore<SpreadsheetToolData, never, SpreadsheetArgs | SpreadsheetDispatchArgs>;
@@ -0,0 +1,5 @@
1
+ import type { SpreadsheetSheet } from "./types";
2
+ export declare const isSheet: (value: unknown) => value is SpreadsheetSheet;
3
+ /** Taken whole or rejected whole — dropping the unreadable half of a workbook
4
+ * would hide part of a user's file behind cells that merely look empty. */
5
+ export declare function readSheets(value: unknown): SpreadsheetSheet[] | null;
@@ -4,18 +4,40 @@
4
4
  * Plugin-specific types only.
5
5
  * Common types are imported from gui-chat-protocol.
6
6
  */
7
+ /** Mirrors the engine's `StoredCellValue`. */
8
+ export type SpreadsheetCellValue = string | number | boolean;
7
9
  export interface SpreadsheetCell {
8
- v: string | number;
10
+ v: SpreadsheetCellValue;
9
11
  f?: string;
10
12
  }
13
+ /** A cell as a stored workbook actually holds it. The engine's `normalizeData`
14
+ * folds all four forms, and models and hand edits write all four, so a
15
+ * workbook read back off disk cannot promise `{ v }` for every cell. The tool
16
+ * SCHEMA is deliberately stricter — it says what a caller should emit, not
17
+ * what an existing file may contain. */
18
+ export type StorableCell = SpreadsheetCell | {
19
+ v?: null;
20
+ f?: string;
21
+ } | SpreadsheetCellValue | null;
11
22
  export interface SpreadsheetSheet {
12
23
  name: string;
13
- data: Array<Array<SpreadsheetCell>>;
24
+ data: StorableCell[][];
14
25
  }
26
+ /** `sheets` is the workbook itself, or — when the host gave the plugin
27
+ * `files.artifacts` — the workspace-relative path it was saved to. */
15
28
  export interface SpreadsheetToolData {
16
- sheets: SpreadsheetSheet[];
29
+ sheets: SpreadsheetSheet[] | string;
17
30
  }
18
31
  export interface SpreadsheetArgs {
19
32
  title: string;
20
33
  sheets: SpreadsheetSheet[];
21
34
  }
35
+ /** What the View sends through `useRuntime().dispatch`. */
36
+ export type SpreadsheetDispatchArgs = {
37
+ kind: "loadSpreadsheet";
38
+ path: string;
39
+ } | {
40
+ kind: "saveSpreadsheet";
41
+ path: string;
42
+ sheets: SpreadsheetSheet[];
43
+ };
package/dist/core.cjs CHANGED
@@ -1 +1 @@
1
- Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./samples-ClV76j10.cjs");exports.SYSTEM_PROMPT=e.i,exports.TOOL_DEFINITION=e.a,exports.TOOL_NAME=e.o,exports.executeSpreadsheet=e.n,exports.pluginCore=e.r,exports.samples=e.t;
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./samples-BzI0GdUj.cjs");exports.SYSTEM_PROMPT=e.d,exports.TOOL_DEFINITION=e.f,exports.TOOL_NAME=e.p,exports.executeSpreadsheet=e.n,exports.executeSpreadsheetDispatch=e.i,exports.isSpreadsheetArtifactPath=e.s,exports.isSpreadsheetDispatchArgs=e.a,exports.pluginCore=e.r,exports.samples=e.t;
package/dist/core.js CHANGED
@@ -1,2 +1,2 @@
1
- import { a as e, i as t, n, o as r, r as i, t as a } from "./samples-sKAnErVb.js";
2
- export { t as SYSTEM_PROMPT, e as TOOL_DEFINITION, r as TOOL_NAME, n as executeSpreadsheet, i as pluginCore, a as samples };
1
+ import { a as e, d as t, f as n, i as r, n as i, p as a, r as o, s, t as c } from "./samples-Cylf7cMw.js";
2
+ export { t as SYSTEM_PROMPT, n as TOOL_DEFINITION, a as TOOL_NAME, i as executeSpreadsheet, r as executeSpreadsheetDispatch, s as isSpreadsheetArtifactPath, e as isSpreadsheetDispatchArgs, o as pluginCore, c as samples };
@@ -3,7 +3,18 @@
3
3
  *
4
4
  * Core calculation engine with circular reference detection and cross-sheet support
5
5
  */
6
- import type { SheetData, CalculatedSheet } from "./types";
6
+ import type { SheetData, CalculatedSheet, SpreadsheetCell, CalculateOptions } from "./types";
7
+ /**
8
+ * Normalize malformed data structures
9
+ * Some models generate flat arrays instead of 2D arrays - fix them
10
+ *
11
+ * @param data - Potentially malformed sheet data
12
+ * @returns Normalized 2D array
13
+ */
14
+ export declare function normalizeData(data: any): SpreadsheetCell[][];
15
+ type SheetCalculateOptions = CalculateOptions & {
16
+ skipFormatting?: boolean;
17
+ };
7
18
  /**
8
19
  * Calculate formulas in a single sheet
9
20
  *
@@ -11,11 +22,12 @@ import type { SheetData, CalculatedSheet } from "./types";
11
22
  * @param allSheets - All sheets for cross-sheet references
12
23
  * @returns Calculated sheet with formulas evaluated
13
24
  */
14
- export declare function calculateSheet(sheet: SheetData, allSheets?: SheetData[]): CalculatedSheet;
25
+ export declare function calculateSheet(sheet: SheetData, allSheets?: SheetData[], options?: SheetCalculateOptions): CalculatedSheet;
15
26
  /**
16
27
  * Calculate all sheets in a workbook
17
28
  *
18
29
  * @param sheets - Array of sheets to calculate
19
30
  * @returns Array of calculated sheets
20
31
  */
21
- export declare function calculateWorkbook(sheets: SheetData[]): CalculatedSheet[];
32
+ export declare function calculateWorkbook(sheets: SheetData[], options?: CalculateOptions): CalculatedSheet[];
33
+ export {};
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Build a SpreadsheetCell from the raw input captured by the mini
3
+ * editor (type + value / formula / format). Extracted from
4
+ * `saveMiniEditor` in `src/plugins/spreadsheet/View.vue` where it
5
+ * was inlined as ~30 lines of nested if/else that pushed the
6
+ * surrounding function over the cognitive-complexity threshold.
7
+ *
8
+ * Pure — no refs, no DOM, no side effects. Given the same inputs
9
+ * it always returns the same SpreadsheetCell. Tested in
10
+ * `test/plugins/spreadsheet/engine/test_cellBuilder.ts`.
11
+ */
12
+ import type { SpreadsheetCell } from "./types.js";
13
+ /** Inputs to the cell builder. Mirrors the mini editor refs in the
14
+ * View but as plain values so unit tests don't need a Vue runtime. */
15
+ export interface MiniEditorInput {
16
+ /** "string" → value is stored as-is as a string.
17
+ * Anything else → the `formula` field is parsed (formula / number / raw string). */
18
+ type: string;
19
+ /** Used when type === "string". Coerced to string. */
20
+ value: unknown;
21
+ /** Used when type !== "string". Trimmed before classification. */
22
+ formula?: string;
23
+ /** Optional format code (e.g. "$#,##0.00"). */
24
+ format?: string;
25
+ }
26
+ /**
27
+ * Best-effort formula detection. The rules are conservative enough
28
+ * that plain text like "hello world" stays as text, but any input
29
+ * with arithmetic operators or function calls is treated as a
30
+ * formula and gets the "=" prefix the engine expects.
31
+ */
32
+ export declare function looksLikeFormula(input: string): boolean;
33
+ /**
34
+ * Parse the raw (non-string-type) editor input into a cell value.
35
+ * Priority: formula > number > raw string > empty string.
36
+ */
37
+ export declare function parseNonStringInput(raw: string): number | string;
38
+ /**
39
+ * Build the full SpreadsheetCell from a mini editor input record.
40
+ * String type short-circuits to `{ v: String(value) }`.
41
+ * Everything else goes through parseNonStringInput for formula /
42
+ * number / text classification, then optionally attaches `f`.
43
+ */
44
+ export declare function buildCellFromInput(input: MiniEditorInput): SpreadsheetCell;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Telling a genuinely empty cell apart from one that holds the number 0.
3
+ *
4
+ * The calculator reads a blank cell as 0 for arithmetic (`=A1+1` on a blank A1
5
+ * is 1, as in Excel). But an aggregate must not: `AVERAGE` divides by the count
6
+ * of real values, and `COUNT` counts numbers — a blank that reads as 0 inflates
7
+ * the denominator and the count. So range collection needs to skip the blanks,
8
+ * which means distinguishing them from a stored 0, which this does.
9
+ */
10
+ /** True when a cell holds no value at all — absent, null, or an empty/whitespace
11
+ * string, in either the bare or the `{ v }` form. A cell containing the number
12
+ * 0, `false`, or any non-empty text is NOT empty. */
13
+ export declare function isEmptyCell(cell: unknown): boolean;
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Cell display formatting
3
+ *
4
+ * Turns a cell's raw calculated value into its display value (currency,
5
+ * percentage, date, ...). Pure — no engine state — so cross-sheet reference
6
+ * resolution can deliberately SKIP it and keep raw serial numbers, while the
7
+ * final output pass applies it for presentation.
8
+ */
9
+ import type { CellValue, StoredCellValue } from "./types";
10
+ /** An integer within the date-serial window — a calculated number the engine
11
+ * should display as a date when the cell carries no explicit format. */
12
+ export declare const isLikelyDateSerial: (value: CellValue) => boolean;
13
+ /**
14
+ * Resolve the display value of one cell from its original definition and its
15
+ * calculated value.
16
+ *
17
+ * - A formula error renders as its code, so the cell still reads `#NUM!`.
18
+ * - Explicit format code wins (currency, percentage, date, ...).
19
+ * - A formula that produced a date serial auto-formats as a date.
20
+ * - Everything else (text, plain numbers, empty) passes through unchanged.
21
+ *
22
+ * The result is always a STORED value: this is the boundary where a computed
23
+ * error becomes the text a cell shows and a workbook serializes.
24
+ */
25
+ export declare const formatCellForDisplay: (originalCell: unknown, calculatedValue: CellValue, preferDDMMYYYY: boolean) => StoredCellValue;
@@ -0,0 +1,7 @@
1
+ import type { CellValue } from "./types";
2
+ /** Excel-style truthiness, shared by IF and AND/OR/NOT so the same value cannot
3
+ * read as true in one function and false in another. A number is false only
4
+ * when 0; blank and empty text are false; the words `true`/`false` are their
5
+ * logical values (case-insensitively); a numeric string follows its number
6
+ * (`"0"` → false); any other non-empty text is true. */
7
+ export declare function coerceToBoolean(value: CellValue | null | undefined): boolean;
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Evaluating a spreadsheet condition without running it as code.
3
+ *
4
+ * A condition is one comparison, or a bare value tested for truthiness. That is
5
+ * the whole grammar — small enough to read directly, which is the point: the
6
+ * previous implementation handed the substituted text to `eval`, so a cell
7
+ * containing `globalThis.x = 1` executed when any IFS referenced it.
8
+ */
9
+ import type { CellValue } from "./types";
10
+ export type ComparisonOperator = ">=" | "<=" | "<>" | "!=" | "==" | "=" | ">" | "<";
11
+ export interface Comparison {
12
+ left: string;
13
+ operator: ComparisonOperator;
14
+ right: string;
15
+ }
16
+ /** Remove parentheses that wrap the WHOLE expression, repeatedly. `(1>0)` is
17
+ * the same condition as `1>0`, but `(A)=(B)` is not `A)=(B` — the leading `(`
18
+ * closes before the end, so it wraps only its own operand and must stay.
19
+ * Unbalanced input is left untouched rather than guessed at. */
20
+ export declare function stripOuterParens(condition: string): string;
21
+ /** Split a condition into its two sides, or null when it holds no comparison.
22
+ * Only the FIRST top-level operator counts — `a>b>c` is not a chain here, and
23
+ * treating it as one is what let `1=1=1` reach a JS parser before.
24
+ *
25
+ * "Top-level" means outside quotes: a cell holding `a>b` substitutes into the
26
+ * condition as `"a>b"`, and splitting on that `>` would compare two fragments
27
+ * of one string literal. */
28
+ export declare function splitComparison(condition: string): Comparison | null;
29
+ /** Read an operand as the value it denotes: a quoted string stays text, a
30
+ * numeric literal becomes a number, `TRUE`/`FALSE` become booleans, and
31
+ * anything else stays the text it already is. */
32
+ export declare function readOperand(raw: string): CellValue;
33
+ /** True when a bare (non-comparison) condition counts as satisfied. */
34
+ export declare function isTruthyCondition(raw: string): boolean;
35
+ /** Evaluate a condition — one comparison, or a value tested for truthiness.
36
+ * Never executes its input. */
37
+ export declare function evaluateCondition(condition: string): boolean;
38
+ /** Like `evaluateCondition`, but each operand is resolved by `evaluate` — so a
39
+ * caller holding the engine can compute arithmetic and sub-expressions
40
+ * (`5+1>10`) instead of reading each side as a bare string. It still never runs
41
+ * the condition as code: it only splits on the top-level comparison and
42
+ * compares the two resolved values. */
43
+ export declare function evaluateConditionValues(condition: string, evaluate: (operand: string) => CellValue): boolean;
44
+ /** Render a cell's value as an operand for a condition string. A string is
45
+ * quoted, with its own quotes and backslashes escaped, so its contents cannot
46
+ * be re-parsed as operators — `evaluateCondition` then unquotes it back to the
47
+ * original text. A missing value becomes an empty string; numbers and booleans
48
+ * render as themselves. */
49
+ export declare function renderConditionOperand(value: CellValue | null | undefined): string;
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Which order an ambiguous slash date is written in, per locale.
3
+ *
4
+ * Only dates whose two leading numbers are BOTH 12 or under are ambiguous —
5
+ * `13/04/2025` can only be day-first, and `04/13/2025` can only be month-first,
6
+ * so those decide themselves. This is purely about `03/04/2025`.
7
+ */
8
+ /** True when an ambiguous `A/B/YYYY` should read as day-first. Accepts a full
9
+ * BCP 47 tag or a bare language subtag; the region is used when present so a
10
+ * caller that can supply `en-GB` gets the right answer even though the app's
11
+ * own locale resolution discards it. */
12
+ export declare function prefersDayFirst(locale: string | undefined | null): boolean;
@@ -10,6 +10,12 @@
10
10
  * @returns true if string matches common date patterns
11
11
  */
12
12
  export declare function isDateLike(str: string): boolean;
13
+ /**
14
+ * Build the Excel serial number for a year/month/day triple, or null when the
15
+ * triple is not a valid calendar date. Every dated branch of `parseDate` ends in
16
+ * this same validate → Date.UTC → dateToSerial step.
17
+ */
18
+ export declare function serialFromParts(year: number, month: number, day: number): number | null;
13
19
  /**
14
20
  * Parse a date string into Excel serial number
15
21
  *
@@ -31,4 +37,4 @@ export declare function parseDate(dateStr: string, preferDDMMYYYY?: boolean): nu
31
37
  * @param originalStr - Original date string
32
38
  * @returns Appropriate format code
33
39
  */
34
- export declare function getDefaultDateFormat(originalStr: string): string;
40
+ export declare function getDefaultDateFormat(originalStr: string, preferDDMMYYYY?: boolean): string;
@@ -19,13 +19,16 @@ export declare const dateToSerial: (date: Date) => number;
19
19
  * @returns JavaScript Date object
20
20
  */
21
21
  export declare const serialToDate: (serial: number) => Date;
22
+ /** A name list that always has a first entry, so a formatter can fall back to it
23
+ * when a date is unreadable and its month/weekday index comes out NaN. */
24
+ export type NameList = readonly [string, ...string[]];
22
25
  /**
23
26
  * Month names for formatting
24
27
  */
25
- export declare const MONTH_NAMES_SHORT: string[];
26
- export declare const MONTH_NAMES_FULL: string[];
28
+ export declare const MONTH_NAMES_SHORT: NameList;
29
+ export declare const MONTH_NAMES_FULL: NameList;
27
30
  /**
28
31
  * Day names for formatting
29
32
  */
30
- export declare const DAY_NAMES_SHORT: string[];
31
- export declare const DAY_NAMES_FULL: string[];
33
+ export declare const DAY_NAMES_SHORT: NameList;
34
+ export declare const DAY_NAMES_FULL: NameList;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * DATEDIF — complete elapsed time between two dates, in a chosen unit.
3
+ *
4
+ * Pure: two Excel serials and a unit in, a number (or a formula error value)
5
+ * out. The unit branches each have their own boundary handling (month-end
6
+ * borrowing, year wraparound), which is exactly what makes them worth testing
7
+ * apart from the handler that reads the arguments.
8
+ */
9
+ import { type SpreadsheetError } from "./spreadsheet-errors";
10
+ /** Complete `unit`s between two dates, or `#NUM!` when start is after end or
11
+ * the unit is not one of Y / M / D / MD / YM / YD. `unit` is matched
12
+ * case-insensitively. */
13
+ export declare function computeDatedif(startSerial: number, endSerial: number, unit: string): number | SpreadsheetError;
@@ -10,9 +10,48 @@ import type { CellValue } from "./types";
10
10
  export interface EvaluatorContext {
11
11
  getCellValue: (ref: string) => CellValue;
12
12
  getRangeValues: (range: string) => CellValue[];
13
- getRangeValuesRaw?: (range: string) => CellValue[];
13
+ getRangeValuesRaw?: ((range: string) => CellValue[]) | undefined;
14
14
  evaluateFormula: (formula: string) => CellValue;
15
+ /** Reading order for an ambiguous slash date; see engine/date-locale.ts. */
16
+ preferDDMMYYYY?: boolean | undefined;
15
17
  }
18
+ /** Render a cell's value as the text that stands in for it inside an
19
+ * expression. Strings are quoted so they cannot be read as identifiers or
20
+ * operators, and their own quotes and backslashes are escaped so the literal
21
+ * cannot be closed early. A missing value becomes 0, matching how blanks are
22
+ * treated everywhere else in the engine. */
23
+ export declare function renderOperand(value: CellValue | null | undefined): string;
24
+ /** Replace every quoted string literal with an empty pair of quotes, honouring
25
+ * `\` escapes so an escaped quote does not end the literal early. Used to
26
+ * validate the STRUCTURE of a concat/arithmetic expression without letting the
27
+ * arbitrary CONTENT of a string decide whether the whole thing looks safe. */
28
+ export declare function maskStringLiterals(expr: string): string;
29
+ /** Whether a `&`-to-`+` concatenation expression is safe to evaluate. Checks the
30
+ * structure with string CONTENT masked out — a string literal may hold any
31
+ * character (a `!`, a `\`), and validating those against a character allowlist
32
+ * rejected valid formulas like `=A1&"!"` and every escaped operand. Once the
33
+ * literals are masked, only the joining structure remains to validate. */
34
+ export declare function isSafeConcatExpression(expr: string): boolean;
35
+ /** Index just past the string literal that opens at `start` (a quote char),
36
+ * honouring `\` escapes so an escaped quote does not close it early. Returns
37
+ * `expr.length` when the literal is never closed. */
38
+ export declare function endOfStringLiteral(expr: string, start: number): number;
39
+ /** Every cell reference in an expression, as `{ref, start}` spans in source
40
+ * order. Quoted string literals are skipped whole: a `"A1"` in the text is a
41
+ * constant, not a reference, and substituting it would turn `="A1"&"!"` into
42
+ * A1's value. A `'` opens either a `'Sheet'!A1` reference or a string literal —
43
+ * only the former is a reference; the latter is skipped like a `"` literal. */
44
+ export interface CellRefSpan {
45
+ ref: string;
46
+ start: number;
47
+ }
48
+ export declare function findCellRefs(expr: string): CellRefSpan[];
49
+ /** Replace every reference span in `expr` with the text `render` gives for it,
50
+ * walking BACK TO FRONT so the spans ahead of each edit stay valid. A global
51
+ * string replace rewrote every occurrence of the shorter reference first, so
52
+ * `=A1+A10` had its `A10` broken into `<A1's value>0` and produced a plausible
53
+ * wrong number (#2357). */
54
+ export declare function substituteCellRefs(expr: string, cellRefs: CellRefSpan[], render: (ref: string) => string): string;
16
55
  /**
17
56
  * Parse function arguments, handling nested functions and quoted strings
18
57
  *
@@ -29,6 +68,10 @@ export declare function parseFunctionArgs(argsStr: string): string[];
29
68
  * - Arithmetic: 2+3, A1*B1, (A1+B1)/2
30
69
  * - Nested expressions: ROUND(SUM(A1:A10)/COUNT(A1:A10), 2)
31
70
  *
71
+ * A genuine failure THROWS (a typed FormulaError or any handler error) rather
72
+ * than returning the raw formula text; the calculator classifies it into
73
+ * errors[] and shows the Excel error value in the cell (#2359).
74
+ *
32
75
  * @param formula - Formula string (without leading =)
33
76
  * @param context - Evaluation context with cell/range accessors
34
77
  * @returns Evaluated result (number or string)
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Annuity math for the financial functions, as pure numeric helpers.
3
+ *
4
+ * Excel's cash-flow sign convention: money received is positive, money paid out
5
+ * is negative — so a loan's `pv` is positive and its payment and interest come
6
+ * back negative. Keeping the per-period split as plain number-in / number-out
7
+ * functions lets it be tested against known Excel values without routing string
8
+ * arguments back through the formula evaluator.
9
+ */
10
+ import { type SpreadsheetError } from "./spreadsheet-errors";
11
+ /** Future value after `nper` periods. `type` is 0 (end of period) or 1 (begin). */
12
+ export declare function computeFv(rate: number, nper: number, pmt: number, pv: number, type: number): number;
13
+ /** Constant per-period payment amortising a present value `pv` to `fv`. */
14
+ export declare function computePmt(rate: number, nper: number, pv: number, fv: number, type: number): number;
15
+ /** Interest portion of the `per`-th payment. */
16
+ export declare function computeIpmt(rate: number, per: number, nper: number, pv: number, fv: number, type: number): number;
17
+ /** Principal portion of the `per`-th payment (total payment minus interest). */
18
+ export declare function computePpmt(rate: number, per: number, nper: number, pv: number, fv: number, type: number): number;
19
+ /** Present value of an annuity that grows to `fv` after `nper` payments of `pmt`. */
20
+ export declare function computePv(rate: number, nper: number, pmt: number, fv: number, type: number): number;
21
+ /** Number of periods needed to amortise `pv` to `fv` at constant `pmt`. */
22
+ export declare function computeNper(rate: number, pmt: number, pv: number, fv: number, type: number): number;
23
+ /** Interest rate per period solving the annuity equation, via Newton-Raphson. */
24
+ export declare function computeRate(nper: number, pmt: number, pv: number, fv: number, type: number, guess: number): number | SpreadsheetError;
25
+ /** Net present value of `cashflows`, each discounted one period further into the future. */
26
+ export declare function computeNpv(rate: number, cashflows: number[]): number;
27
+ /** Internal rate of return of `values` (element 0 at period 0), via Newton-Raphson. */
28
+ export declare function computeIrr(values: number[], guess: number): number | SpreadsheetError;
@@ -3,6 +3,15 @@
3
3
  *
4
4
  * Handles Excel-style format codes for currency, percentages, decimals, dates, etc.
5
5
  */
6
+ /**
7
+ * Insert thousands separators into a run of digits ("1234567" → "1,234,567").
8
+ * Regex free on purpose: the classic `\B(?=(\d{3})+(?!\d))` lookahead
9
+ * backtracks badly on a long run of digits.
10
+ */
11
+ export declare const groupThousands: (digits: string) => string;
12
+ /** Group the integer part of a formatted number ("1234567.89" → "1,234,567.89"),
13
+ * leaving any fractional part after the decimal point untouched. */
14
+ export declare const addThousandSeparators: (formatted: string) => string;
6
15
  /**
7
16
  * Format a number according to Excel format code
8
17
  *
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Typed formula-evaluation errors
3
+ *
4
+ * A failed formula must surface as a typed error with an Excel-style error value
5
+ * in the cell, never as a swallowed bare string (issue #2359). This module holds
6
+ * the error taxonomy, the throwable carrier, and the pure helpers that classify a
7
+ * thrown error and propagate an error value across cell references. No engine
8
+ * state is captured — every export is input → output only.
9
+ */
10
+ import type { CalculationError } from "./types";
11
+ import type { SpreadsheetErrorCode } from "./spreadsheet-errors";
12
+ /** The evaluator-facing error kinds: every `CalculationError["type"]` except
13
+ * `circular`, which the calculator raises directly (not via a thrown error). */
14
+ export type FormulaErrorType = Exclude<CalculationError["type"], "circular">;
15
+ /** The error code shown for each kind, matching Excel's error literals. */
16
+ export declare const FORMULA_ERROR_VALUES: Record<FormulaErrorType, SpreadsheetErrorCode>;
17
+ /** A recoverable formula failure carrying the kind and the code to show. */
18
+ export declare class FormulaError extends Error {
19
+ readonly errorType: FormulaErrorType;
20
+ readonly display: SpreadsheetErrorCode;
21
+ constructor(errorType: FormulaErrorType, display: SpreadsheetErrorCode, message?: string);
22
+ }
23
+ export declare const isFormulaError: (error: unknown) => error is FormulaError;
24
+ export declare const divZeroError: () => FormulaError;
25
+ export declare const invalidRefError: (ref: string) => FormulaError;
26
+ export declare const nameError: (funcName: string) => FormulaError;
27
+ export declare const unknownError: (message?: string) => FormulaError;
28
+ /** The FormulaError that re-raises an error read from a referenced cell or a
29
+ * nested call, so `=A1+1` inherits A1's error instead of corrupting into
30
+ * `"#DIV/0!"+1`. */
31
+ export declare const propagatedError: (value: SpreadsheetErrorCode) => FormulaError;
32
+ /** Map any thrown value onto the typed entry the calculator records. A
33
+ * `FormulaError` keeps its own kind and display; anything else is `unknown`. */
34
+ export declare const classifyThrownError: (error: unknown) => {
35
+ type: FormulaErrorType;
36
+ display: SpreadsheetErrorCode;
37
+ };
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Extract the set of cells that a formula references.
3
+ *
4
+ * Extracted from `src/plugins/spreadsheet/View.vue` (was the body of
5
+ * `extractCellReferences`, cognitive complexity 32). The original
6
+ * function combined regex scanning, range expansion, single-cell
7
+ * parsing, and deduplication all in one body; splitting each concern
8
+ * into a named helper brings the top-level function well under the
9
+ * sonarjs/cognitive-complexity threshold of 15 and makes the pure
10
+ * logic unit-testable in isolation (see
11
+ * `test/plugins/spreadsheet/engine/test_formulaRefs.ts`).
12
+ *
13
+ * Tracks #175. No behavioural change — the wrapper in View.vue
14
+ * still returns exactly the same `{ row, col }` list as before.
15
+ */
16
+ export interface CellCoord {
17
+ row: number;
18
+ col: number;
19
+ }
20
+ export declare function stripFormulaPrefix(formula: string): string;
21
+ export declare function expandRange(rangeStr: string): CellCoord[];
22
+ export declare function expandRangeOrCell(ref: string): CellCoord[] | null;
23
+ export declare function parseSingleCellRef(refStr: string): CellCoord | null;
24
+ export interface RangeBounds {
25
+ sheetPrefix: string;
26
+ startCol: number;
27
+ startRow: number;
28
+ endCol: number;
29
+ endRow: number;
30
+ }
31
+ export declare function parseRangeBounds(range: string): RangeBounds | null;
32
+ export declare function resolveIndexTarget(bounds: RangeBounds, rowNum: number, colNum: number): {
33
+ colIndex: number;
34
+ row: number;
35
+ } | null;
36
+ export declare function resolveTableOffset(position: number, size: number): number | null;
37
+ export declare function extractCellReferences(formula: string): CellCoord[];
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Pure lookup rules, separated from the range-reading handlers so they can be
3
+ * unit-tested directly.
4
+ */
5
+ import type { CellValue } from "../types";
6
+ /**
7
+ * Interpret VLOOKUP/HLOOKUP's 4th argument (range_lookup): TRUE (or omitted) =
8
+ * approximate match, FALSE = exact.
9
+ *
10
+ * The literal `TRUE` reaches the handler as the STRING `"TRUE"` — the evaluator
11
+ * leaves bare words unquoted — so an accept-only-`true | 1 | "1"` check silently
12
+ * fell back to exact match and returned `#N/A` for a valid approximate lookup
13
+ * (#2360). Read it the way Excel coerces a logical instead: a boolean as-is, a
14
+ * non-zero number as TRUE, and the words `TRUE` / `FALSE` (case-insensitive)
15
+ * and `"1"` / `"0"` as their logical value. Anything else (blank, stray text)
16
+ * is treated as FALSE, matching Excel's coercion of a blank range_lookup — and
17
+ * so is a formula error, which is neither of the two logicals.
18
+ */
19
+ export declare function isApproximateMatch(rangeLookup: CellValue): boolean;