467 Stellen. Nicht durch einen anderen Strich ersetzt, sondern durch das
Satzzeichen, das an der jeweiligen Stelle richtig ist: Doppelpunkt, wo eine
Erlaeuterung folgt; Komma, wo der Satz weiterlaeuft; Punkt, wo zwei
Hauptsaetze aneinanderstiessen. Ein Halbgeviertstrich waere nur derselbe
Griff mit kuerzerem Strich gewesen.
Regelbasiert ersetzt und danach nachgeprueft, weil eine Regel allein nicht
reicht. Gefunden und einzeln korrigiert:
- Eingeschobene Saetze mit Strichpaar. Nur der oeffnende Strich passte ins
Muster, der schliessende blieb als "—," stehen. Drei Stellen, jetzt mit
Klammern oder als eigener Satz.
- Doppelte Doppelpunkte, wo beide Striche eines Paars zu Doppelpunkten wurden
("Globale Regeln: Schrift, Groessen, Abstaende: kommen aus HiveGrid").
Sieben Stellen in Doku und Oberflaeche, jetzt in Klammern.
- Pfade, denen ein Komma statt eines Doppelpunkts folgte.
- Ausgerichtete Kommentartabellen, in denen der Strich eine Spalte war.
Nicht angefasst: drei Geviertstriche, die keine Prosa sind, sondern
Tabellenplatzhalter fuer "kein Wert" (Trichter, Auftraege, Kunden). Dort ist
der Strich die Angabe selbst; ihn zu entfernen liesse leere Zellen zurueck.
tsc, lint, build, verify:carbon und verify:layout laufen sauber.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QPeZNwSeU7qG3RRCwaxEZX
159 lines
5.3 KiB
TypeScript
159 lines
5.3 KiB
TypeScript
/**
|
|
* Deutsche Zahlen-, Waehrungs- und Datumsformatierung.
|
|
* Regeln aus UI_UX_GUIDELINES.md (Abschnitt 3, Zahlenformatierung):
|
|
* Punkt als Tausender-Trenner, Komma als Dezimaltrenner, grosse Zahlen abkuerzen.
|
|
*/
|
|
|
|
const NF = new Intl.NumberFormat("de-DE");
|
|
|
|
/** 12450 -> "12.450" · 1250000 -> "1,3 Mio." */
|
|
export function formatZahl(value: number, dezimalstellen = 0): string {
|
|
if (Math.abs(value) >= 1_000_000) {
|
|
return `${(value / 1_000_000).toFixed(1).replace(".", ",")} Mio.`;
|
|
}
|
|
return new Intl.NumberFormat("de-DE", {
|
|
minimumFractionDigits: dezimalstellen,
|
|
maximumFractionDigits: dezimalstellen,
|
|
}).format(value);
|
|
}
|
|
|
|
/** 8420 -> "8.420 €" · 245000 -> "245.000 €" (immer voll, fuer KPI-Werte) */
|
|
export function formatEuro(value: number): string {
|
|
return `${NF.format(Math.round(value))} €`;
|
|
}
|
|
|
|
/**
|
|
* Kompakte Waehrung fuer Achsen und Balken-Labels.
|
|
* 8420 -> "8,4 T€" · 245000 -> "245 T€" · 1250000 -> "1,25 Mio. €"
|
|
*/
|
|
export function formatEuroKompakt(value: number): string {
|
|
const abs = Math.abs(value);
|
|
if (abs >= 1_000_000) {
|
|
return `${(value / 1_000_000).toFixed(2).replace(".", ",")} Mio. €`;
|
|
}
|
|
if (abs >= 1_000) {
|
|
return `${(value / 1_000).toFixed(1).replace(".", ",")} T€`;
|
|
}
|
|
return `${NF.format(Math.round(value))} €`;
|
|
}
|
|
|
|
/**
|
|
* Noch kompakter für Achsenbeschriftungen: dort zählt jede Stelle,
|
|
* und Nachkommastellen sind bei einer Skala ohnehin überflüssig.
|
|
* 34000 -> "34 T€" · 1250000 -> "1,3 Mio. €"
|
|
*/
|
|
export function formatEuroAchse(value: number): string {
|
|
// Die Nulllinie traegt keine Einheit: "0 €" neben "10 T€" liest sich wie ein
|
|
// Einheitenwechsel mitten in der Skala.
|
|
if (value === 0) return "0";
|
|
const abs = Math.abs(value);
|
|
if (abs >= 1_000_000) return `${(value / 1_000_000).toFixed(1).replace(".", ",")} Mio. €`;
|
|
if (abs >= 1_000) return `${Math.round(value / 1_000)} T€`;
|
|
return `${NF.format(Math.round(value))} €`;
|
|
}
|
|
|
|
/**
|
|
* Achsenformat, einmal aus dem Maximum gewaehlt.
|
|
*
|
|
* formatEuroAchse entscheidet die Einheit pro Wert, dadurch steht "0 €"
|
|
* neben "10 T€". Eine Achse traegt aber genau eine Einheit.
|
|
*/
|
|
export function achsenFormat(max: number): (wert: number) => string {
|
|
if (max >= 1_000_000) {
|
|
return (wert) =>
|
|
wert === 0 ? "0" : `${(wert / 1_000_000).toFixed(1).replace(".", ",")} Mio. €`;
|
|
}
|
|
if (max >= 1_000) {
|
|
return (wert) => (wert === 0 ? "0" : `${NF.format(Math.round(wert / 1_000))} T€`);
|
|
}
|
|
return (wert) => (wert === 0 ? "0" : formatEuro(wert));
|
|
}
|
|
|
|
/** 45.32 -> "45,3 %" */
|
|
export function formatProzent(value: number, dezimalstellen = 1): string {
|
|
return `${value.toFixed(dezimalstellen).replace(".", ",")} %`;
|
|
}
|
|
|
|
const NF1 = new Intl.NumberFormat("de-DE", {
|
|
minimumFractionDigits: 1,
|
|
maximumFractionDigits: 1,
|
|
});
|
|
|
|
/** 2.4 -> "2,4 Tage" · 1234.5 -> "1.234,5 Tage" */
|
|
export function formatTage(value: number): string {
|
|
return `${NF1.format(value)} Tage`;
|
|
}
|
|
|
|
/** 2812.5 -> "2.812,5 h" */
|
|
export function formatStunden(value: number): string {
|
|
return `${NF1.format(value)} h`;
|
|
}
|
|
|
|
/**
|
|
* Ganze Tage mit korrektem Numerus: "1 Tag", "3 Tage".
|
|
* Ohne das steht in der Terminspalte "1 Tage ueber".
|
|
*/
|
|
export function tageLabel(anzahl: number, suffix = ""): string {
|
|
const wort = Math.abs(anzahl) === 1 ? "Tag" : "Tage";
|
|
return `${NF.format(anzahl)} ${wort}${suffix ? ` ${suffix}` : ""}`;
|
|
}
|
|
|
|
/** Dativform fuer "seit …": "seit 1 Tag" / "seit 99 Tagen". */
|
|
export function tageLabelDativ(anzahl: number): string {
|
|
const wort = Math.abs(anzahl) === 1 ? "Tag" : "Tagen";
|
|
return `${NF.format(anzahl)} ${wort}`;
|
|
}
|
|
|
|
/** "2026-07-30" -> "30.07.2026" */
|
|
export function formatDatum(iso: string): string {
|
|
const d = new Date(iso);
|
|
return d.toLocaleDateString("de-DE", {
|
|
day: "2-digit",
|
|
month: "2-digit",
|
|
year: "numeric",
|
|
});
|
|
}
|
|
|
|
/** "2026-07-30" -> "30.07." */
|
|
export function formatDatumKurz(iso: string): string {
|
|
const d = new Date(iso);
|
|
return d.toLocaleDateString("de-DE", { day: "2-digit", month: "2-digit" });
|
|
}
|
|
|
|
/** "2026-07" -> "Jul 26" */
|
|
export function formatMonatKurz(iso: string): string {
|
|
const [jahr, monat] = iso.split("-");
|
|
const namen = ["Jan", "Feb", "Mär", "Apr", "Mai", "Jun", "Jul", "Aug", "Sep", "Okt", "Nov", "Dez"];
|
|
return `${namen[Number(monat) - 1]} ${jahr.slice(2)}`;
|
|
}
|
|
|
|
/** Tage zwischen heute und einem ISO-Datum (positiv = in der Vergangenheit). */
|
|
export function tageSeit(iso: string, referenz: Date): number {
|
|
const diff = referenz.getTime() - new Date(iso).getTime();
|
|
return Math.floor(diff / 86_400_000);
|
|
}
|
|
|
|
/**
|
|
* Trend zwischen aktueller und vorheriger Periode.
|
|
* Gibt Prozentpunkte und Richtung zurueck; < 0,5 % gilt als unveraendert.
|
|
*/
|
|
export function berechneTrend(
|
|
aktuell: number,
|
|
vorher: number
|
|
): { wert: number; richtung: "up" | "down" | "flat" } {
|
|
if (vorher === 0) {
|
|
return aktuell > 0 ? { wert: 100, richtung: "up" } : { wert: 0, richtung: "flat" };
|
|
}
|
|
const veraenderung = ((aktuell - vorher) / vorher) * 100;
|
|
if (Math.abs(veraenderung) < 0.5) return { wert: 0, richtung: "flat" };
|
|
return {
|
|
wert: Math.abs(veraenderung),
|
|
richtung: veraenderung > 0 ? "up" : "down",
|
|
};
|
|
}
|
|
|
|
/** Text auf maxLaenge kuerzen, Ellipse anhaengen. */
|
|
export function kuerze(text: string, maxLaenge: number): string {
|
|
return text.length <= maxLaenge ? text : `${text.slice(0, maxLaenge - 1)}…`;
|
|
}
|