@gui-chat-plugin/spreadsheet 2.0.0 → 2.1.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/dist/engine/calculator.d.ts +15 -3
- package/dist/engine/cellBuilder.d.ts +44 -0
- package/dist/engine/cellEmpty.d.ts +13 -0
- package/dist/engine/cellFormatting.d.ts +25 -0
- package/dist/engine/coerce-boolean.d.ts +7 -0
- package/dist/engine/condition.d.ts +49 -0
- package/dist/engine/date-locale.d.ts +12 -0
- package/dist/engine/date-parser.d.ts +7 -1
- package/dist/engine/date-utils.d.ts +7 -4
- package/dist/engine/datedif.d.ts +13 -0
- package/dist/engine/evaluator.d.ts +44 -1
- package/dist/engine/financial-math.d.ts +28 -0
- package/dist/engine/formatter.d.ts +9 -0
- package/dist/engine/formulaError.d.ts +37 -0
- package/dist/engine/formulaRefs.d.ts +37 -0
- package/dist/engine/functions/lookup-math.d.ts +19 -0
- package/dist/engine/functions/statistical-math.d.ts +41 -0
- package/dist/engine/functions/text.d.ts +17 -1
- package/dist/engine/guards.d.ts +10 -0
- package/dist/engine/index.d.ts +11 -0
- package/dist/engine/jsonCellLocator.d.ts +27 -0
- package/dist/engine/math-ops.d.ts +41 -0
- package/dist/engine/numericCoercion.d.ts +38 -0
- package/dist/engine/parser.d.ts +0 -24
- package/dist/engine/registry.d.ts +30 -2
- package/dist/engine/responseDecoder.d.ts +40 -0
- package/dist/engine/spreadsheet-errors.d.ts +42 -0
- package/dist/engine/textFormat.d.ts +32 -0
- package/dist/engine/translateFormula.d.ts +37 -0
- package/dist/engine/types.d.ts +17 -3
- package/dist/style.css +1 -1
- package/dist/vue/cellHighlights.d.ts +42 -0
- package/dist/vue/keyboardNav.d.ts +9 -0
- package/dist/vue.cjs +71 -71
- package/dist/vue.js +6634 -6498
- package/package.json +14 -13
|
@@ -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:
|
|
26
|
-
export declare const MONTH_NAMES_FULL:
|
|
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:
|
|
31
|
-
export declare const DAY_NAMES_FULL:
|
|
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;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure statistical rules, separated from the range-reading handlers so they can
|
|
3
|
+
* be unit-tested directly.
|
|
4
|
+
*/
|
|
5
|
+
import { type SpreadsheetError } from "../spreadsheet-errors";
|
|
6
|
+
/**
|
|
7
|
+
* The arithmetic mean, or `#DIV/0!` when there is nothing to average.
|
|
8
|
+
*
|
|
9
|
+
* Excel divides by the count of NUMBERS, so a range of blanks or text leaves a
|
|
10
|
+
* zero denominator and reports the division rather than a 0 that reads like a
|
|
11
|
+
* genuine average of zeros.
|
|
12
|
+
*/
|
|
13
|
+
export declare function computeAverage(values: number[]): number | SpreadsheetError;
|
|
14
|
+
/**
|
|
15
|
+
* The middle value (the mean of the middle two when the count is even), or
|
|
16
|
+
* `#NUM!` when there is no value to sit in the middle.
|
|
17
|
+
*
|
|
18
|
+
* Excel's code here is `#NUM!`, not AVERAGE's `#DIV/0!` — nothing is divided by
|
|
19
|
+
* zero, the median of an empty set simply does not exist. Sorts a copy so the
|
|
20
|
+
* caller's array keeps its order.
|
|
21
|
+
*/
|
|
22
|
+
export declare function computeMedian(values: number[]): number | SpreadsheetError;
|
|
23
|
+
/**
|
|
24
|
+
* The most frequently occurring value, or `#N/A` when no value repeats.
|
|
25
|
+
*
|
|
26
|
+
* Excel's MODE is undefined for an all-distinct set, so returning the first
|
|
27
|
+
* element (as a naive "highest frequency wins" loop does when every count is 1)
|
|
28
|
+
* is a silent wrong answer. Ties resolve to the value that appears first, which
|
|
29
|
+
* Map insertion order preserves.
|
|
30
|
+
*/
|
|
31
|
+
export declare function computeMode(values: number[]): number | SpreadsheetError;
|
|
32
|
+
/**
|
|
33
|
+
* Sample variance (Excel VAR): the mean squared deviation divided by `n - 1`,
|
|
34
|
+
* not `n`. Dividing by `n` is the POPULATION variance (Excel's VARP); using it
|
|
35
|
+
* for VAR understates the spread. Fewer than two values leave no `n - 1` to
|
|
36
|
+
* divide by, so Excel reports `#DIV/0!` rather than a silent 0.
|
|
37
|
+
*/
|
|
38
|
+
export declare function sampleVariance(values: number[]): number | SpreadsheetError;
|
|
39
|
+
/** Sample standard deviation (Excel STDEV): the square root of the sample
|
|
40
|
+
* variance, and `#DIV/0!` on the same fewer-than-two-values boundary. */
|
|
41
|
+
export declare function sampleStdev(values: number[]): number | SpreadsheetError;
|
|
@@ -1,4 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Text Functions
|
|
3
3
|
*/
|
|
4
|
-
|
|
4
|
+
import { type SpreadsheetError } from "../spreadsheet-errors";
|
|
5
|
+
export declare const toProperCase: (text: string) => string;
|
|
6
|
+
export declare const takeLeft: (text: string, count: number) => string | SpreadsheetError;
|
|
7
|
+
export declare const takeRight: (text: string, count: number) => string | SpreadsheetError;
|
|
8
|
+
export declare const substituteText: (text: string, oldText: string, newText: string, instance?: number) => string | SpreadsheetError;
|
|
9
|
+
/** Excel MID: 1-based start, count of characters. `substring` SWAPS its bounds
|
|
10
|
+
* when they are reversed, so a negative count read backwards from `start` and
|
|
11
|
+
* returned earlier characters — `MID("Hello",3,-1)` gave "e" instead of an
|
|
12
|
+
* error. Both arguments are validated here instead. */
|
|
13
|
+
export declare const takeMid: (text: string, start: number, count: number) => string | SpreadsheetError;
|
|
14
|
+
export declare const locateSubstring: (find: string, within: string, start: number, options: {
|
|
15
|
+
caseInsensitive: boolean;
|
|
16
|
+
}) => number | SpreadsheetError;
|
|
17
|
+
/** Excel VALUE: the WHOLE string must be a number once its currency symbols and
|
|
18
|
+
* thousands separators are stripped; trailing text is an error, not a prefix to
|
|
19
|
+
* salvage. */
|
|
20
|
+
export declare const parseValueText: (raw: string) => number | SpreadsheetError;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Narrow `unknown` to a plain object (not null, not array). */
|
|
2
|
+
export declare function isRecord(value: unknown): value is Record<string, unknown>;
|
|
3
|
+
/** Narrow `unknown` to any object (not null, arrays allowed).
|
|
4
|
+
* Use `isRecord` when you need to access string keys. */
|
|
5
|
+
export declare function isObj(value: unknown): value is object;
|
|
6
|
+
/** The message to show for a thrown value. A non-Error object with a non-empty
|
|
7
|
+
* string `details` (the gRPC convention) or `message` surfaces that field —
|
|
8
|
+
* `details` wins — instead of the `[object Object]` a bare `String(err)`
|
|
9
|
+
* would print. */
|
|
10
|
+
export declare function errorMessage(err: unknown, fallback?: string): string;
|
package/dist/engine/index.d.ts
CHANGED
|
@@ -5,9 +5,20 @@
|
|
|
5
5
|
*/
|
|
6
6
|
export * from "./types";
|
|
7
7
|
export * from "./parser";
|
|
8
|
+
export * from "./condition";
|
|
9
|
+
export * from "./date-locale";
|
|
10
|
+
export * from "./cellEmpty";
|
|
11
|
+
export * from "./datedif";
|
|
8
12
|
export * from "./formatter";
|
|
13
|
+
export * from "./translateFormula";
|
|
14
|
+
export * from "./formulaError";
|
|
15
|
+
export * from "./spreadsheet-errors";
|
|
9
16
|
export * from "./evaluator";
|
|
10
17
|
export * from "./calculator";
|
|
18
|
+
export * from "./formulaRefs";
|
|
19
|
+
export * from "./cellBuilder";
|
|
20
|
+
export * from "./responseDecoder";
|
|
21
|
+
export * from "./jsonCellLocator";
|
|
11
22
|
export * from "./registry";
|
|
12
23
|
import "./functions";
|
|
13
24
|
export { SpreadsheetEngine } from "./engine";
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Locate the start offset of a specific cell value inside a
|
|
3
|
+
* pretty-printed JSON spreadsheet document. Extracted from
|
|
4
|
+
* `handleTableClick` in `src/plugins/spreadsheet/View.vue` where the
|
|
5
|
+
* inline scanner pushed cognitive complexity to 163.
|
|
6
|
+
*
|
|
7
|
+
* The scanner walks the raw editor text character-by-character,
|
|
8
|
+
* tracking string boundaries, bracket depth, and object depth to
|
|
9
|
+
* find the n-th cell of the m-th row within a named sheet's `data`
|
|
10
|
+
* array. We deliberately do NOT parse the JSON — we need the
|
|
11
|
+
* character offset inside the user's text buffer (which may not be
|
|
12
|
+
* valid JSON mid-edit), so a positional scan is required.
|
|
13
|
+
*
|
|
14
|
+
* Pure — no refs, no DOM. Returns -1 if the cell can't be located.
|
|
15
|
+
* Tested in `test/plugins/spreadsheet/engine/test_jsonCellLocator.ts`.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* Given the pretty-printed JSON editor text, a sheet name, and
|
|
19
|
+
* (rowIndex, colIndex), return the character offset of that cell's
|
|
20
|
+
* JSON token. The returned offset can be fed into
|
|
21
|
+
* `textarea.setSelectionRange(offset, offset + cellJsonLength)` to
|
|
22
|
+
* highlight the cell.
|
|
23
|
+
*
|
|
24
|
+
* Returns -1 if the sheet isn't found or the (row, col) is out of
|
|
25
|
+
* range. Never throws.
|
|
26
|
+
*/
|
|
27
|
+
export declare function findCellJsonPosition(editorText: string, sheetName: string, rowIndex: number, colIndex: number): number;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rounding, modulo and significance math for the mathematical functions.
|
|
3
|
+
*
|
|
4
|
+
* Pure number-in / (number | formula-error-value)-out helpers. The rounding
|
|
5
|
+
* direction and the domain rules are exactly where the engine diverged from
|
|
6
|
+
* Excel (rounding toward +∞ instead of away from zero, modulo taking the
|
|
7
|
+
* dividend's sign, negative bases and domains slipping through as NaN/∞), so
|
|
8
|
+
* they are worth testing apart from the argument-reading handlers.
|
|
9
|
+
*/
|
|
10
|
+
import { type SpreadsheetError } from "./spreadsheet-errors";
|
|
11
|
+
/** Excel ROUND: half away from zero (JS `Math.round` breaks half toward +∞, so
|
|
12
|
+
* `ROUND(-2.5, 0)` came back -2 instead of -3). */
|
|
13
|
+
export declare function roundTo(value: number, digits: number): number;
|
|
14
|
+
/** Excel ROUNDUP: away from zero. */
|
|
15
|
+
export declare function roundUpTo(value: number, digits: number): number;
|
|
16
|
+
/** Excel ROUNDDOWN: toward zero. */
|
|
17
|
+
export declare function roundDownTo(value: number, digits: number): number;
|
|
18
|
+
/** Excel FLOOR: nearest multiple of `significance` toward zero; opposite signs
|
|
19
|
+
* are #NUM!, and a zero significance is #DIV/0! — the division by the
|
|
20
|
+
* significance is what FLOOR reports, so it wins over the sign check. */
|
|
21
|
+
export declare function floorToSignificance(value: number, significance: number): number | SpreadsheetError;
|
|
22
|
+
/** Excel CEILING: nearest multiple of `significance` away from zero; opposite
|
|
23
|
+
* signs are #NUM!, and a zero significance is 0 — deliberately NOT FLOOR's
|
|
24
|
+
* #DIV/0!, an asymmetry Excel keeps and this engine has to match. */
|
|
25
|
+
export declare function ceilingToSignificance(value: number, significance: number): number | SpreadsheetError;
|
|
26
|
+
/** Excel MOD: result takes the divisor's sign (`MOD(-3, 2) === 1`); dividing by
|
|
27
|
+
* zero is #DIV/0!, not a silent 0. */
|
|
28
|
+
export declare function modulo(value: number, divisor: number): number | SpreadsheetError;
|
|
29
|
+
/** Excel POWER: a negative base with a non-integer exponent has no real root, so
|
|
30
|
+
* it is #NUM! rather than JS's NaN. */
|
|
31
|
+
export declare function power(base: number, exponent: number): number | SpreadsheetError;
|
|
32
|
+
/** Natural log guarded to its domain: `LN(x)` for `x <= 0` is #NUM!, not
|
|
33
|
+
* -∞ / NaN. Reused by LN and LOG. */
|
|
34
|
+
export declare function safeLog(value: number): number | SpreadsheetError;
|
|
35
|
+
/** Excel LOG(value, base): the base must also be positive and not 1, or the
|
|
36
|
+
* result is #NUM! rather than the ∞ / NaN a bare division would give. */
|
|
37
|
+
export declare function logWithBase(value: number, base: number): number | SpreadsheetError;
|
|
38
|
+
/** Base-10 log guarded to its domain (`LOG10(x)` for `x <= 0` is #NUM!). */
|
|
39
|
+
export declare function safeLog10(value: number): number | SpreadsheetError;
|
|
40
|
+
/** Square root guarded to its domain (`SQRT(x)` for `x < 0` is #NUM!, not NaN). */
|
|
41
|
+
export declare function safeSqrt(value: number): number | SpreadsheetError;
|