Text-Vergleich, &-Verkettung und benannte Bereiche

- Vergleichsoperatoren (= <> > < >= <=) unterscheiden jetzt Text- von
  Zahlenvergleich statt jede Seite zwangsweise durch toNumber() zu
  schicken (=A1="Nord" gab vorher #VALUE! statt 1); ermöglicht
  Text-Kriterien in selbstgebauten COUNTIF/SUMIF/AVERAGEIF-Funktionen
  (neue README-Sektion "SUMIF/COUNTIF/AVERAGEIF nachbauen").
- Neuer &-Operator (Excel-Präzedenz zwischen Vergleich und +/-) plus
  CONCAT/CONCATENATE als Funktions-Äquivalent.
- Benannte Bereiche: neuer Toolbar-Button "🏷" verwaltet dokumentweite
  Namen für eine feste Zelle/einen festen Bereich (z. B. Umsatz_Q1statt
  Umsatz!B2:B6), unqualifiziert in jeder Top-Level-Formel nutzbar.
  Live gegen die Tabellen-ID aufgelöst (übersteht Tabellen-Umbenennung),
  bewusst nicht innerhalb einer benutzerdefinierten Funktion selbst
  nutzbar (Aufrufer können sie aber als Argument übergeben).
- Nebenbei: COUNT2s Autovervollständigungs-Hint korrigiert (zeigte
  "COUNT(...)" statt "COUNT2(...)").

374 Tests, vue-tsc/eslint/build sauber, Playwright-live-verifiziert.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
wkh
2026-09-08 21:59:47 +02:00
co-authored by Claude Sonnet 5
parent 9015a991aa
commit 2c54537427
22 changed files with 1460 additions and 86 deletions
+31 -5
View File
@@ -31,6 +31,7 @@ import {
parseKey,
type CellValue,
type EngineHost,
type NamedRangeDef,
} from './types';
import { evaluate } from './evaluator';
@@ -81,10 +82,17 @@ export class Workbook implements EngineHost {
private readonly values = new Map<string, CellValue>();
/** user-defined function library for this document, name.toUpperCase() -> def. */
private readonly functionIndex: Map<string, FunctionDef>;
/** document-wide named ranges (see NamedRangeDef's doc comment). */
private readonly namedRanges: NamedRangeDef[];
constructor(tables: TableModel[], functionDefs: FunctionDef[] = []) {
constructor(
tables: TableModel[],
functionDefs: FunctionDef[] = [],
namedRanges: NamedRangeDef[] = [],
) {
this.tables = tables;
this.functionIndex = buildFunctionIndex(functionDefs);
this.namedRanges = namedRanges;
this.rebuild();
}
@@ -112,6 +120,17 @@ export class Workbook implements EngineHost {
return null; // still ambiguous -> #REF, needs an explicit sheet qualifier
}
resolveNamedRange(
name: string,
): { qualifier: string; sheetQualifier?: string; start: { col: number; row: number }; end: { col: number; row: number } } | null {
const needle = name.trim().toLowerCase();
const def = this.namedRanges.find((d) => d.name.trim().toLowerCase() === needle);
if (!def) return null;
const table = this.tableById(def.tableId);
if (!table) return null; // target table was since deleted -- #NAME?, same as a stale table rename
return { qualifier: table.name, sheetQualifier: table.sheetName, start: def.start, end: def.end };
}
tableSize(tableId: string): { cols: number; rows: number } | null {
const t = this.tableById(tableId);
return t ? { cols: t.cols, rows: t.rows } : null;
@@ -201,13 +220,20 @@ export class Workbook implements EngineHost {
this.homeTableId = tableId;
const out: string[] = [];
for (const ref of collectRefs(ast)) {
// a named range isn't itself a qualifier/coords pair -- resolve it to
// one first (same shape as a 'cell'/'range' ref), then fall through to
// the identical edge-wiring below.
const resolved = ref.kind === 'namedref' ? this.resolveNamedRange(ref.name) : ref;
if (!resolved) continue; // unknown name -> no edge; evaluator emits #NAME?
const target =
ref.qualifier == null ? tableId : this.resolveTableName(ref.qualifier, ref.sheetQualifier ?? null);
resolved.qualifier == null
? tableId
: this.resolveTableName(resolved.qualifier, resolved.sheetQualifier ?? null);
if (!target) continue; // unknown table -> no edge; evaluator emits #REF
if (ref.kind === 'cell') {
out.push(keyOf(target, ref.col, ref.row));
if ('col' in resolved) {
out.push(keyOf(target, resolved.col, resolved.row));
} else {
for (const c of expandRange(ref.start, ref.end)) {
for (const c of expandRange(resolved.start, resolved.end)) {
out.push(keyOf(target, c.col, c.row));
}
}