Files

107 lines
4.6 KiB
JavaScript
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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)
}