/** * Shared types for the formula engine. * * The engine is intentionally framework-agnostic: it knows nothing about Vue, * the store or the DOM. The host application implements {@link EngineHost} to * expose the current document to the evaluator. */ /** All error values the engine can produce. They propagate to dependent cells. */ export type ErrorCode = | '#REF' // invalid / deleted reference or unknown sheet name | '#DIV/0!' // division by zero | '#NAME?' // unknown function | '#ERR' // parse error | '#CIRC' // circular reference | '#VALUE!' // wrong value type in arithmetic (documented extension) | '#N/A'; // XLOOKUP: no matching entry and no if_not_found given (documented extension) export const ERROR_CODES: ErrorCode[] = [ '#REF', '#DIV/0!', '#NAME?', '#ERR', '#CIRC', '#VALUE!', '#N/A', ]; /** A formula error. Instances are immutable and comparable by `code`. */ export class FormulaError { constructor( public readonly code: ErrorCode, public readonly detail?: string, ) {} toString(): string { return this.code; } } export function isError(v: unknown): v is FormulaError { return v instanceof FormulaError; } /** A plain scalar a cell can hold once evaluated. `null` means "empty cell". */ export type Scalar = number | string | null; /** The fully evaluated value of a cell. */ export type CellValue = Scalar | FormulaError; /** 0-based cell coordinate inside a single table. */ export interface Coord { col: number; row: number; } /** A cell address that also carries the id of the table it lives on. */ export interface CellRef extends Coord { tableId: string; } /** A document-defined name resolving to a fixed cell/range on a specific * table -- fed into {@link Workbook} (see workbook.ts) so it can implement * {@link EngineHost.resolveNamedRange}. Unlike a custom function, there is * no expression/formula of its own, just fixed coordinates -- so there is * nothing that could cyclically reference itself. `tableId` (not a table * *name*) so a named range survives its target table being renamed; the * host resolves it to that table's *current* name on lookup, the same * qualifier a formula's own `Tabelle!Zelle` reference would carry. */ export interface NamedRangeDef { id: string; name: string; tableId: string; start: Coord; end: Coord; } /** Stable string key for a cell across the whole document. */ export function keyOf(tableId: string, col: number, row: number): string { return `${tableId}␟${col}␟${row}`; } export function parseKey(key: string): CellRef { const [tableId, col, row] = key.split('␟'); return { tableId, col: Number(col), row: Number(row) }; } /** * The document as seen by the engine. Implemented by the Vue store. * * The addressable unit is a **table** (a table object on a Numbers-style * canvas). Table names are only unique *per sheet* -- two different sheets * may each hold a table called "Umsatz". An unqualified table reference * (`Umsatz!B2` / `Umsatz::B2`, no sheet given) resolves to whichever table * named "Umsatz" lives on the *same sheet* as the formula doing the * referencing, falling back to the single document-wide match when the name * isn't ambiguous there. The explicit 3-part form (`Analyse!Umsatz::B2`) * disambiguates when it would otherwise be genuinely ambiguous (name used on * two *other* sheets, neither of them the formula's own). */ export interface EngineHost { /** Id of the table the formula currently being evaluated lives on. */ homeTableId: string; /** * Resolve a table *name* (as written in a formula) to its id, optionally * narrowed to a specific *sheet* name (the "Blatt!" part of a 3-part * `Blatt!Tabelle::Zelle` reference). Matching is case-insensitive and * tolerant of surrounding whitespace. * * - `sheetName` given: only a table with that name **on that sheet** * matches; otherwise `null` (including when the sheet itself is unknown). * - `sheetName` omitted/`null`: matches a table by name anywhere in the * document. Exactly one match resolves as before. Multiple matches (the * name exists on more than one sheet) prefer a table on the same sheet * as {@link homeTableId}; if that still doesn't narrow it to one, `null` * (`#REF` -- the formula needs an explicit sheet qualifier). */ resolveTableName(name: string, sheetName?: string | null): string | null; /** * Resolve a *named range* (as written, unqualified, in a formula -- e.g. * `Umsatz_Q1`) to the table-qualified cell/range it currently stands for. * Matching is case-insensitive and tolerant of surrounding whitespace, * same as {@link resolveTableName}. `null` when the name is unknown (or * its target table has since been deleted) -- the caller turns that into * `#NAME?`, the same error class an unknown function name gets. * * Resolved by *name*, live, every time -- never baked into the AST -- so a * named range survives its target table being renamed, the same way a * `Tabelle!Zelle` reference's table name is looked up fresh on every * evaluation rather than resolved once at parse time. */ resolveNamedRange( name: string, ): { qualifier: string; sheetQualifier?: string; start: Coord; end: Coord } | null; /** Dimensions of a table, used for range clamping and #REF detection. */ tableSize(tableId: string): { cols: number; rows: number } | null; /** * Already-computed value of another cell. The recalculation driver guarantees * precedents are evaluated first (topological order), so this never triggers * a nested evaluation. */ getComputed(tableId: string, col: number, row: number): CellValue; } /** * The document's WRITE surface, as seen by a running macro (see * evaluator.ts's `runMacroActions`/`execAction` and engine/README.md's * "Makros" section). Deliberately separate from {@link EngineHost} -- that * interface stays pure/read-only, implemented by `Workbook` alone, used by * every ordinary formula evaluation; `MacroWriteHost` is only ever * implemented by the store (`document.ts#runMacro`), only ever passed to * the interpreter for an explicitly-triggered macro run, and never reaches * the passive recalculation pass at all -- a macro's write is * indistinguishable, from the engine's point of view, from the same edit * made by hand through the UI. * * Every table/sheet argument below is a plain qualifier STRING (e.g. * `"Umsatz"` or `"Blatt2!Kosten"`, split on the first `!`) rather than a * resolved id or a dedicated reference AST node -- resolved fresh, by the * implementation, exactly like a formula's own `Tabelle!Zelle` qualifier * is resolved fresh on every read (never cached), so a macro that renames * or adds a table earlier in its own run can address it by its new name * later in the same run. */ export interface MacroWriteHost { /** Write the same raw cell text (auto-detected as a formula if it starts * with "=", exactly like typing it manually) into every given cell -- * one cell for a `SETVALUE` on a single reference, several for a range * target (broadcasting the same value, Excel's Ctrl+Enter semantics). */ setValue(cells: CellRef[], text: string): void; insertRow(tableRef: string, at: number): void; deleteRow(tableRef: string, at: number): void; insertCol(tableRef: string, at: number): void; deleteCol(tableRef: string, at: number): void; /** `sheetRef` is a plain sheet name (not table-qualified). */ addTable(sheetRef: string, name: string): void; renameTable(tableRef: string, newName: string): void; deleteTable(tableRef: string): void; addSheet(name: string): void; renameSheet(sheetRef: string, newName: string): void; deleteSheet(sheetRef: string): void; } /** * The document's NETWORK surface, as seen by a running macro's `GET`/`POST` * assignment form (`x = GET(url)` / `x = POST(url; body)`; see evaluator. * ts's `execStatementAsync`). Kept separate from {@link MacroWriteHost} for * the same reason that one is kept separate from {@link EngineHost}: this * engine module stays framework/DOM-agnostic (no `fetch`, no browser APIs * anywhere in `src/engine/`) -- the store (`document.ts#runMacro`) is the * only implementer, and it's the one place a network confirmation prompt * and the actual `fetch()` call belong. * * A single `request()` method (not separate `get`/`post` methods) because * both share the exact same "confirm this host once, then fetch and parse * JSON" flow in the implementation -- only the HTTP method and whether a * body is sent differ. */ export interface MacroNetworkHost { /** Resolves with the parsed JSON response body, or rejects with a plain * `Error` (network failure, a non-2xx status, a response that isn't * valid JSON, or the user declining a host-confirmation prompt) -- * caught the same way as any other macro action failure, aborting the * run and surfacing the message as the result's `.message`. */ request(method: 'GET' | 'POST', url: string, body?: string): Promise; }