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
+46
View File
@@ -9,11 +9,13 @@
*/
import type {
CellCoord,
CellFormat,
ChartObject,
CustomFunction,
DocumentState,
HAlign,
NamedRange,
Sheet,
SheetObject,
TableObject,
@@ -58,6 +60,46 @@ function normaliseCustomFunctions(x: unknown): CustomFunction[] {
return out;
}
function isCellCoord(x: unknown): x is CellCoord {
return (
!!x &&
typeof x === 'object' &&
typeof (x as CellCoord).col === 'number' &&
typeof (x as CellCoord).row === 'number'
);
}
/** Defaults to `[]` -- absent in every document saved before this feature
* existed. Drops a malformed entry, and any entry whose `tableId` doesn't
* match a real table *in this document* (a dangling reference -- e.g. a
* hand-edited file, or a table that no longer exists) -- rather than
* leaving it to resolve to `#NAME?` forever; the live "delete a table"
* path (`document.ts`) does leave such entries in place, for consistency
* with how a deleted table's ordinary formula references are never
* auto-pruned either -- this is stricter only because *importing* JSON is
* already a validating boundary for everything else in the document. */
function normaliseNamedRanges(x: unknown, tableIds: ReadonlySet<string>): NamedRange[] {
if (!Array.isArray(x)) return [];
const out: NamedRange[] = [];
for (const r of x) {
if (
!r ||
typeof r !== 'object' ||
typeof (r as { id?: unknown }).id !== 'string' ||
typeof (r as { name?: unknown }).name !== 'string' ||
typeof (r as { tableId?: unknown }).tableId !== 'string' ||
!isCellCoord((r as { start?: unknown }).start) ||
!isCellCoord((r as { end?: unknown }).end)
) {
continue;
}
const rec = r as NamedRange;
if (!tableIds.has(rec.tableId)) continue;
out.push({ id: rec.id, name: rec.name, tableId: rec.tableId, start: { ...rec.start }, end: { ...rec.end } });
}
return out;
}
const HEX_COLOR = /^#(?:[0-9a-f]{3}|[0-9a-f]{4}|[0-9a-f]{6}|[0-9a-f]{8})$/i;
function colorOrUndef(x: unknown): string | undefined {
return typeof x === 'string' && HEX_COLOR.test(x) ? x : undefined;
@@ -293,6 +335,7 @@ export function validateDocument(x: unknown): DocumentState {
const sheetIds = new Set<string>();
const objectIds = new Set<string>();
const tableIds = new Set<string>();
for (const s of doc.sheets) {
if (!s || typeof s.id !== 'string' || typeof s.name !== 'string' || !Array.isArray(s.objects))
throw new JsonImportError('Mindestens ein Blatt ist fehlerhaft.');
@@ -303,6 +346,7 @@ export function validateDocument(x: unknown): DocumentState {
throw new JsonImportError('Mindestens ein Objekt ist fehlerhaft.');
if (objectIds.has(o.id)) throw new JsonImportError('Doppelte Objekt-ID.');
objectIds.add(o.id);
if (isTableObject(o)) tableIds.add(o.id);
}
}
if (typeof doc.activeSheetId !== 'string' || !sheetIds.has(doc.activeSheetId))
@@ -330,6 +374,7 @@ export function validateDocument(x: unknown): DocumentState {
locale: isNumberLocale(doc.locale) ? doc.locale : DEFAULT_LOCALE,
csvDelimiter: doc.csvDelimiter === ';' ? ';' : ',',
customFunctions: normaliseCustomFunctions(doc.customFunctions),
namedRanges: normaliseNamedRanges(doc.namedRanges, tableIds),
};
}
@@ -393,6 +438,7 @@ export function migrateToV2(x: unknown): DocumentState {
locale: DEFAULT_LOCALE,
csvDelimiter: ',',
customFunctions: [],
namedRanges: [],
};
}