Kein Formelschutz in einer Datei, die eine Maschine einliest
Some checks failed
CI / Migrationen auf leerer Datenbank (push) Has been cancelled
CI / Lint, Typen, Tests, Build (push) Has been cancelled

Das vorangestellte Hochkomma schuetzt einen Menschen, der eine CSV in Excel
oeffnet: ein Wert mit fuehrendem =, +, - oder @ waere dort sonst eine Formel.
In einer Load-Datei ist es das Gegenteil von Schutz. Jede oesterreichische
Telefonnummer beginnt mit "+", und in LOLYO stuende danach in jedem Konto
'+4366... als Nummer -- ohne dass es beim Erzeugen jemandem auffiele.

toCsv bekommt dafuer formelschutz in der CsvForm, voreingestellt an. Nur LOLYO
schaltet es ab. Die Telefonspalten von Cornerstone sind bisher leer, deshalb
bleibt die Datei dort, wie sie ist.
This commit is contained in:
2026-09-28 13:47:49 +02:00
parent 676cecbf15
commit 38e35e2ff9
3 changed files with 31 additions and 14 deletions

View File

@@ -23,9 +23,10 @@ export function sanitizeForSpreadsheetCell(text: string): string {
return /^[=+\-@]/.test(text) ? `'${text}` : text; return /^[=+\-@]/.test(text) ? `'${text}` : text;
} }
function csvCell(value: string | number | boolean | null, trenner: string): string { function csvCell(value: string | number | boolean | null, trenner: string, formelschutz: boolean): string {
if (value === null || value === undefined) return ""; if (value === null || value === undefined) return "";
const text = typeof value === "boolean" ? (value ? "Ja" : "Nein") : sanitizeForSpreadsheetCell(String(value)); const roh = typeof value === "boolean" ? (value ? "Ja" : "Nein") : String(value);
const text = formelschutz ? sanitizeForSpreadsheetCell(roh) : roh;
// Das Trennzeichen gehört in die Prüfung: mit Komma als Trenner muss ein // Das Trennzeichen gehört in die Prüfung: mit Komma als Trenner muss ein
// Komma im Wert maskiert werden, sonst zerfällt die Zeile in zwei Spalten. // Komma im Wert maskiert werden, sonst zerfällt die Zeile in zwei Spalten.
return text.includes(trenner) || /["\n\r]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text; return text.includes(trenner) || /["\n\r]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text;
@@ -39,15 +40,22 @@ function csvCell(value: string | number | boolean | null, trenner: string): stri
* ankommen. Ein fremdes System will oft das Gegenteil — Cornerstone etwa * ankommen. Ein fremdes System will oft das Gegenteil — Cornerstone etwa
* erwartet Kommas, und ein BOM machte dort aus der ersten Spalte eine, die in * erwartet Kommas, und ein BOM machte dort aus der ersten Spalte eine, die in
* keiner Zuordnung vorkommt, weil das unsichtbare Zeichen im Namen steckt. * keiner Zuordnung vorkommt, weil das unsichtbare Zeichen im Namen steckt.
*
* `formelschutz: false` schaltet das vorangestellte Hochkomma ab (siehe
* sanitizeForSpreadsheetCell). Es gehört in eine Datei, die ein Mensch in Excel
* öffnet, und es verdirbt eine, die eine Maschine einliest: eine österreichische
* Telefonnummer beginnt mit „+", und im Zielsystem stünde danach `'+43…` als
* Nummer. Nur für Dateien setzen, die ausschliesslich ein Fremdsystem liest.
*/ */
export type CsvForm = { trenner?: "," | ";"; bom?: boolean }; export type CsvForm = { trenner?: "," | ";"; bom?: boolean; formelschutz?: boolean };
export function toCsv<T>(rows: T[], columns: ExportColumn<T>[], form: CsvForm = {}): string { export function toCsv<T>(rows: T[], columns: ExportColumn<T>[], form: CsvForm = {}): string {
const trenner = form.trenner ?? ";"; const trenner = form.trenner ?? ";";
const bom = form.bom ?? true; const bom = form.bom ?? true;
const lines = [columns.map((c) => csvCell(c.header, trenner)).join(trenner)]; const schutz = form.formelschutz ?? true;
const lines = [columns.map((c) => csvCell(c.header, trenner, schutz)).join(trenner)];
for (const row of rows) { for (const row of rows) {
lines.push(columns.map((c) => csvCell(c.get(row), trenner)).join(trenner)); lines.push(columns.map((c) => csvCell(c.get(row), trenner, schutz)).join(trenner));
} }
return (bom ? "" : "") + lines.join("\r\n"); return (bom ? "" : "") + lines.join("\r\n");
} }

View File

@@ -17,13 +17,19 @@ import type { CsvForm, ExportColumn } from "./export";
// hiesse, eine Spalte zu liefern, die LOLYO nicht zuordnet. // hiesse, eine Spalte zu liefern, die LOLYO nicht zuordnet.
/** /**
* Semikolon, kein BOM — so sieht die Vorlage aus. * Semikolon, kein BOM, kein Formelschutz — so liest LOLYO die Datei.
* *
* Die Vorgabe von toCsv ist für Excel gemacht und setzt ein BOM voran; das * Die Vorgabe von toCsv ist für Excel gemacht. Das BOM hinge unsichtbar am
* hinge unsichtbar am Namen der ersten Spalte, und „Benutzer/Code" käme beim * Namen der ersten Spalte, und „Benutzer/Code" käme beim Einlesen in keiner
* Einlesen in keiner Zuordnung mehr vor. * Zuordnung mehr vor.
*
* Der Formelschutz stellt jedem Wert, der mit `=`, `+`, `-` oder `@` beginnt,
* ein Hochkomma voran, damit Excel ihn nicht als Formel rechnet. Hier wäre das
* falsch: **jede** österreichische Telefonnummer beginnt mit „+", und in LOLYO
* stünde danach `'+4366…` als Nummer — in jedem Konto, und niemandem fiele es
* beim Erzeugen auf. Die Datei liest eine Maschine, kein Mensch in Excel.
*/ */
export const LOLYO_CSV: CsvForm = { trenner: ";", bom: false }; export const LOLYO_CSV: CsvForm = { trenner: ";", bom: false, formelschutz: false };
/** Für alle dieselbe. Wer eine andere braucht, stellt sie in LOLYO um, nicht hier. */ /** Für alle dieselbe. Wer eine andere braucht, stellt sie in LOLYO um, nicht hier. */
export const LOLYO_SPRACHE = "DE"; export const LOLYO_SPRACHE = "DE";

View File

@@ -121,10 +121,13 @@ describe("Die Datei", () => {
expect(csv.split("\r\n")[0]).toBe(VORGABE); expect(csv.split("\r\n")[0]).toBe(VORGABE);
}); });
it("maskiert eine Telefonnummer, die Excel als Formel läse", () => { it("lässt die Telefonnummer unangetastet", () => {
// „+43…" beginnt mit einem Pluszeichen — ohne Schutz macht Excel daraus // Der Formelschutz von toCsv stellt jedem Wert mit führendem „+" ein
// beim Öffnen eine Formel und zeigt einen Fehler statt der Nummer. // Hochkomma voran, damit Excel ihn nicht rechnet. Diese Datei liest eine
// Maschine: dort stünde danach in **jedem** Konto `'+4366…` als Nummer,
// weil jede österreichische Nummer mit „+" beginnt.
const csv = toCsv([zeile()], lolyoSpalten(), LOLYO_CSV); const csv = toCsv([zeile()], lolyoSpalten(), LOLYO_CSV);
expect(csv).toContain("'+4366412345678"); expect(csv).toContain(";+4366412345678;");
expect(csv).not.toContain("'+43");
}); });
}); });