Shop C1: Geld- und Grundpreis-Funktionen (euro, parseEuro, grundpreisText)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-10-05 08:32:38 +00:00
1 parent 308268e901
commit 6782d99960
2 files changed
+185

No files matched your search

+106
View File
@@ -0,0 +1,106 @@
/**
* Geld und Grundpreis fuer den Shop: reine Funktionen ohne Abhaengigkeiten.
* Geld ist immer eine ganze Zahl in Cent, nie Gleitkomma-Euro; Eingaben werden als Ziffernfolge gelesen, nicht gerechnet.
*/
const NBSP = ' '
/** Obergrenze fuer eingegebene Preise: 100.000 € in Cent. */
const MAX_CENT = 10_000_000
/**
* Formatiert ganze Cent als deutschen Eurobetrag: Tausenderpunkt, Dezimalkomma, immer zwei Nachkommastellen,
* geschuetztes Leerzeichen (U+00A0) vor dem Eurozeichen. Beispiel: 123456 => '1.234,56 €', -1250 => '-12,50 €'.
* Wirft einen TypeError, wenn cent keine ganze Zahl ist.
*/
export function euro(cent) {
if (!Number.isInteger(cent)) throw new TypeError('euro: cent muss eine ganze Zahl sein')
const neg = cent < 0
const abs = Math.abs(cent)
const euros = String(Math.floor(abs / 100)).replace(/\B(?=(\d{3})+(?!\d))/g, '.')
const rest = String(abs % 100).padStart(2, '0')
return `${neg ? '-' : ''}${euros},${rest}${NBSP}€`
}
/**
* Liest einen Eurobetrag und liefert ganze Cent, sonst null.
* Erlaubt: Zahl in Euro (12.5), Text wie '12', '12,5', '12,50 €', '1.234,56', '1234.56'.
* Mit Komma sind Punkte nur Tausendertrenner. Ohne Komma ist ein einzelner Punkt mit 1-2 Ziffern dahinter das Dezimaltrennzeichen,
* mit genau 3 Ziffern dahinter ein Tausenderpunkt ('1.234' = 1234 €).
* null bei leer, negativ, keine Zahl, mehr als zwei Nachkommastellen oder ueber 100.000 €.
*/
export function parseEuro(value) {
let s
if (typeof value === 'number') {
if (!Number.isFinite(value)) return null
s = String(value)
// Zahlen nur als schlichte Dezimalzahl (kein Exponent, keine Tausendergruppen)
if (!/^\d+(\.\d{1,2})?$/.test(s)) return null
return zuCent(s, '.')
}
if (typeof value !== 'string') return null
s = value.replace(/€/g, '').trim() // trim entfernt auch geschuetzte Leerzeichen; Leerzeichen mitten in der Zahl sind ungueltig
if (s === '') return null
if (s.includes(',')) {
const m = /^(\d{1,3}(?:\.\d{3})+|\d+),(\d{1,2})$/.exec(s)
if (!m) return null
return zuCent(`${m[1].replace(/\./g, '')}.${m[2]}`, '.')
}
if (/^\d+$/.test(s)) return zuCent(s, '.')
if (/^\d+\.\d{1,2}$/.test(s)) return zuCent(s, '.')
if (/^\d{1,3}(?:\.\d{3})+$/.test(s)) return zuCent(s.replace(/\./g, ''), '.')
return null
}
/** Wandelt eine geprueft schlichte Dezimalzahl ('12.5') per Ziffernarithmetik in Cent; null ueber der Obergrenze. */
function zuCent(text, trenner) {
const [ganz, nach = ''] = text.split(trenner)
const cent = Number(ganz) * 100 + Number(nach.padEnd(2, '0'))
if (!Number.isSafeInteger(cent) || cent > MAX_CENT) return null
return cent
}
/** Unterstuetzte Einheiten: Anzeige-Einheit der Bezugsmenge und Faktor von der Eingabeeinheit auf diese. */
const EINHEITEN = Object.freeze({
g: { anzeige: 'kg', faktor: 1000 },
kg: { anzeige: 'kg', faktor: 1 },
ml: { anzeige: 'l', faktor: 1000 },
l: { anzeige: 'l', faktor: 1 },
m: { anzeige: 'm', faktor: 1 },
m2: { anzeige: 'm²', faktor: 1 },
})
/** Nachkommastellen der Menge, die exakt beruecksichtigt werden (0,75 l, 0,333 kg ...). */
const MENGE_STELLEN = 6n
/**
* Grundpreis nach PAngV, z. B. '(16,67 € / 1 kg)'. Bezug ist 1 kg, 1 l, 1 m oder 1 m² (g => kg, ml => l).
* einheit: g | kg | ml | l | m | m2; menge: positive Zahl (auch Dezimal, z. B. 0.75).
* Rundung kaufmaennisch (halb auf) auf ganze Cent, mit BigInt gerechnet, damit keine Gleitkommafehler entstehen.
* Fehlen Menge, Einheit oder Preis oder sind sie ungueltig, kommt '' zurueck.
*/
export function grundpreisText(angaben) {
const { preisCent, menge, einheit } = angaben ?? {}
const e = typeof einheit === 'string' && Object.hasOwn(EINHEITEN, einheit) ? EINHEITEN[einheit] : null
if (!e) return ''
if (!Number.isInteger(preisCent) || preisCent < 0) return ''
const m = mengeSkaliert(menge)
if (m === null) return ''
// Cent je Bezugsmenge = preisCent * faktor / menge, menge = m / 10^6
const zaehler = BigInt(preisCent) * BigInt(e.faktor) * 10n ** MENGE_STELLEN
const cent = (2n * zaehler + m) / (2n * m)
if (cent > BigInt(Number.MAX_SAFE_INTEGER)) return ''
return `(${euro(Number(cent))} / 1 ${e.anzeige})`
}
/** Menge (Zahl oder Dezimaltext) als BigInt mit 6 Nachkommastellen; null bei <= 0 oder ungueltig. */
function mengeSkaliert(menge) {
let n
if (typeof menge === 'number') n = menge
else if (typeof menge === 'string' && /^\d+(\.\d+)?$/.test(menge.trim())) n = Number(menge.trim())
else return null
if (!Number.isFinite(n) || n <= 0) return null
const skaliert = Math.round(n * 10 ** Number(MENGE_STELLEN))
if (!Number.isSafeInteger(skaliert) || skaliert <= 0) return null
return BigInt(skaliert)
}