From af01aa5e53fafe8bca98750279eb16c73695e100 Mon Sep 17 00:00:00 2001 From: Wild Karl-Heinz Date: Wed, 2 Sep 2026 23:31:18 +0200 Subject: [PATCH] =?UTF-8?q?Locale-unabh=C3=A4ngiges=20ISO-Datumsformat=20a?= =?UTF-8?q?ls=20Zellformat?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Neues CellNumFmt 'dateiso' im Zahlenformat-Dropdown ("Datum (ISO) · 2026-09-02"): zeigt ein Datum immer als YYYY-MM-DD, unabhängig von der aktiven Locale (Zelle/Blatt/Dokument) -- anders als "Datum", das der jeweiligen Locale folgt. Nützlich für export-nahe/sortierbare Datumsspalten. - store/types.ts: CellNumFmt um 'dateiso' erweitert. - store/format.ts: die manuelle YYYY-MM-DD-Formatierung, die bisher nur für das 'plain'-Pseudo-Locale galt, in isoDateString() extrahiert und von einem neuen 'dateiso'-Fall in formatValue() wiederverwendet. NUM_FORMATS bekam den neuen Eintrag -- das Zahlenformat-Dropdown (SettingsPanel.vue) und die JSON-Validierung (isCellNumFmt) sind bereits generisch über NUM_FORMATS, keine Änderung nötig. Tests: formatValue mit 'dateiso' über mehrere Locales (inkl. 'plain'). 297 -> 298 Tests, alle grün. vue-tsc/eslint/build sauber. Live per Playwright verifiziert: Zelle auf "Datum (ISO)" umgestellt zeigt 2026-09-02 und bleibt das auch nach Wechsel der Dokument-Locale auf en-US, während eine andere Zelle ohne dieses Format der Locale folgt (09/02/2026). READMEs aktualisiert (Zahlenformat-Liste, "Datum & Zeit"-Abschnitt in engine/README.md, Testzahl). Co-Authored-By: Claude Sonnet 5 --- README.md | 19 +++++++++++++------ src/engine/README.md | 6 +++++- src/store/__tests__/format.spec.ts | 7 +++++++ src/store/format.ts | 20 ++++++++++++++------ src/store/types.ts | 1 + 5 files changed, 40 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 2c880c7..56b9305 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Vorbild von **Apple Numbers**. Zwei Dinge stehen im Mittelpunkt: ```bash npm install npm run dev # Vite-Devserver -npm run test # Vitest-Suite (Engine + Store + IO), 297 Tests +npm run test # Vitest-Suite (Engine + Store + IO), 298 Tests npm run build # Typecheck + Produktionsbuild npm run lint # ESLint (flat config) ``` @@ -264,7 +264,8 @@ Inspector unter **Objekt · Auswahl**: horizontale **Ausrichtung** (links/mitte/rechts), **Zahlenformat** (`Automatisch`, `Ohne`, Ganzzahl, 1/2 Nachkommastellen, **Prozent** 0/2 Stellen, **Datum**/**Zeit**/**Datum + Zeit** — kombiniert mit -der Locale unten), **Zell-Textfarbe**, +der Locale unten — sowie **Datum (ISO)**, das immer `JJJJ-MM-TT` zeigt, +unabhängig von der Locale), **Zell-Textfarbe**, **Zell-Füllfarbe**, **Fett**/**Kursiv**, „Formatierung entfernen". Auch exakte **Spaltenbreite** und **Zeilenhöhe** der Fokuszelle. @@ -440,6 +441,12 @@ ohne Zusatzaufwand: `=fällig - rechnungsdatum` zählt Tage, `=rechnungsdatum `NOW()` liefern das aktuelle Datum bzw. Datum+Uhrzeit. Formel-Ergebnisse bekommen **kein** automatisches Zellformat (wie bei jeder anderen Formel auch) — das Zahlenformat-Dropdown auf „Datum"/„Zeit"/„Datum + Zeit" stellen. + +Das Zahlenformat **„Datum (ISO)"** zeigt ein Datum immer als `JJJJ-MM-TT` +(`2026-09-02`) — unabhängig von der aktiven Locale (Blatt/Dokument oder +Zelle), anders als „Datum", das der jeweiligen Locale folgt (`02.09.2026` +bei de-DE, `09/02/2026` bei en-US). Nützlich für export-nahe oder +sortierbare Datumsspalten. `TODAY`/`NOW` sind **nicht volatil**: diese Engine rechnet rein vorgänger-getrieben neu, ohne Timer — eine Zelle mit `=TODAY()` aktualisiert sich erst bei ihrer eigenen nächsten Neuberechnung, nicht von @@ -561,7 +568,7 @@ Grundrechenarten: `Übersicht!B2 = =SUM(Umsatz!B2:E2)`, `D2 = =B2-C2`, ## Tests -`npm run test` — Vitest, 297 Tests: +`npm run test` — Vitest, 298 Tests: - **lexer / parser** — Präzedenz & Assoziativität (`^` vor `*/` vor `+-`, `^` rechts, `-2^2 = -(2^2)`), lokale/qualifizierte/quoted Bezüge, `!` **und** `::`, @@ -582,9 +589,9 @@ Grundrechenarten: `Übersicht!B2 = =SUM(Umsatz!B2:E2)`, `D2 = =B2-C2`, Schrägstrich-Daten und ungültigen/rollierten Daten; `literalValue` wandelt eine erkannte Eingabe in ihren Serial um; `DATE`/`TIME`/`TODAY`/`NOW` als Formel (Basisfälle, Arity-/Typ-Fehler, Datums-Arithmetik); `formatValue` - über mehrere Locales inkl. `plain`; Store: Auto-Format beim Eintippen - (überschreibt nie ein bereits gesetztes Format), Namenskollision mit den - neuen eingebauten Funktionen. + über mehrere Locales inkl. `plain` und das locale-unabhängige „Datum + (ISO)"; Store: Auto-Format beim Eintippen (überschreibt nie ein bereits + gesetztes Format), Namenskollision mit den neuen eingebauten Funktionen. - **graph** — `affected`, topologische Ordnung, Selbst-/2er-/längerer Zyklus. - **workbook / store** — Seed-Aggregate, Cross-Table-Summen, Edit-Ripple durch die ganze Kette, `#REF` nach Tabellen-/Blatt-Löschung, neues Tabellen-Objekt diff --git a/src/engine/README.md b/src/engine/README.md index 4ed8357..8ca5807 100644 --- a/src/engine/README.md +++ b/src/engine/README.md @@ -364,7 +364,11 @@ eine Zahl ist, funktionieren `+`/`-`/Vergleiche darauf automatisch — keine manuelle, locale-neutrale `YYYY-MM-DD`/`HH:MM`-Formatierung (bewusst nicht `Intl` mit `undefined`-Locale, das würde lautlos die Host-Locale des Browsers durchsickern lassen). Formel-Ergebnisse bekommen **kein** - automatisches Format — wie jedes andere Formelergebnis auch. + automatisches Format — wie jedes andere Formelergebnis auch. Ein viertes + `CellNumFmt` — `dateiso` — rendert dasselbe `YYYY-MM-DD` **unabhängig von + der Locale** (nutzt intern dieselbe Formatier-Funktion wie `'plain'`s + Datumsanteil), für export-nahe/sortierbare Datumsspalten, die sich nicht + mit der Blatt-/Dokument-Locale mitändern sollen. - **Auto-Format beim Eintippen** (`store/document.ts`s `setCellInput`) — eine erkannte Literal-Eingabe (nicht `=`-Formeln) setzt automatisch das passende `numFmt`, aber **nur** solange die Zelle noch `'auto'` ist; ein diff --git a/src/store/__tests__/format.spec.ts b/src/store/__tests__/format.spec.ts index e630f68..4748b95 100644 --- a/src/store/__tests__/format.spec.ts +++ b/src/store/__tests__/format.spec.ts @@ -26,4 +26,11 @@ describe('formatValue: date/time', () => { it('empty cell / error still short-circuit before any date formatting', () => { expect(formatValue(null, 'de-DE', 'date')).toBe(''); }); + + it('"dateiso" renders YYYY-MM-DD regardless of the active locale', () => { + expect(formatValue(day, 'de-DE', 'dateiso')).toBe('2026-09-02'); + expect(formatValue(day, 'en-US', 'dateiso')).toBe('2026-09-02'); + expect(formatValue(day, 'fr-FR', 'dateiso')).toBe('2026-09-02'); + expect(formatValue(day, 'plain', 'dateiso')).toBe('2026-09-02'); + }); }); diff --git a/src/store/format.ts b/src/store/format.ts index 4e755cc..67cb3a5 100644 --- a/src/store/format.ts +++ b/src/store/format.ts @@ -43,6 +43,7 @@ export const NUM_FORMATS: ReadonlyArray<{ id: CellNumFmt; label: string }> = [ { id: 'pct0', label: 'Prozent · 12 %' }, { id: 'pct2', label: 'Prozent · 12,34 %' }, { id: 'date', label: 'Datum · 02.09.2026' }, + { id: 'dateiso', label: 'Datum (ISO) · 2026-09-02' }, { id: 'time', label: 'Zeit · 14:30' }, { id: 'datetime', label: 'Datum + Zeit · 02.09.2026 14:30' }, ]; @@ -63,13 +64,18 @@ function nf(locale: NumberLocale, opts: Intl.NumberFormatOptions): Intl.NumberFo const pad2 = (n: number) => String(n).padStart(2, '0'); -/** `'plain'` renders a fixed, locale-neutral ISO-ish shape by hand -- NOT - * via `Intl` with an `undefined` locale, which would silently fall back to - * the *browser's* locale instead of staying neutral. */ +/** `YYYY-MM-DD`, hand-written rather than via `Intl` -- there's no locale + * that reliably reproduces ISO 8601, and this is also what the `'plain'` + * pseudo-locale uses for a date (an `Intl` call with an `undefined` locale + * would silently fall back to the *browser's* locale instead of staying + * neutral). Shared by the locale-independent `'dateiso'` cell format and + * by `formatDatePart`'s `'plain'` branch below. */ +function isoDateString(d: Date): string { + return `${d.getUTCFullYear()}-${pad2(d.getUTCMonth() + 1)}-${pad2(d.getUTCDate())}`; +} + function formatDatePart(d: Date, locale: NumberLocale): string { - if (locale === 'plain') { - return `${d.getUTCFullYear()}-${pad2(d.getUTCMonth() + 1)}-${pad2(d.getUTCDate())}`; - } + if (locale === 'plain') return isoDateString(d); return new Intl.DateTimeFormat(locale, { year: 'numeric', month: '2-digit', @@ -117,6 +123,8 @@ export function formatValue( }).format(n); case 'date': return formatDatePart(serialToUtcDate(n), locale); + case 'dateiso': + return isoDateString(serialToUtcDate(n)); // same regardless of locale case 'time': return formatTimePart(serialToUtcDate(n), locale); case 'datetime': diff --git a/src/store/types.ts b/src/store/types.ts index 85e7099..25f1251 100644 --- a/src/store/types.ts +++ b/src/store/types.ts @@ -25,6 +25,7 @@ export type CellNumFmt = | 'pct0' // percent, 0 decimals (value 0.5 -> "50 %") | 'pct2' // percent, 2 decimals | 'date' // e.g. 02.09.2026 -- value is a date serial, see engine/dates.ts + | 'dateiso' // e.g. 2026-09-02 -- ISO 8601, same regardless of locale | 'time' // e.g. 14:30 | 'datetime'; // date + time