astronomical_conversions verbessert

This commit is contained in:
2026-04-11 12:13:54 +02:00
parent 8083c731f7
commit 0504c7d03b
2 changed files with 434 additions and 123 deletions
+368 -84
View File
@@ -357,23 +357,73 @@ function h(?string $value): string
unix: document.getElementById('timeUnix') unix: document.getElementById('timeUnix')
}; };
const unitToKm = { const DECIMAL_DIVISION_PRECISION = 32;
km: 1, const pow10Cache = [1n];
au: KM_PER_AU, const decimalConstants = {
lm: KM_PER_LIGHT_MINUTE, zero: null,
lh: KM_PER_LIGHT_HOUR, one: null,
ly: KM_PER_LY, hundred: null,
pc: KM_PER_PC, thousand: null,
mpc: KM_PER_MPC million: null,
speedOfLightKmS: null,
secondsPerGyr: null,
mpcInKm: null,
kmPerAu: null,
kmPerLightMinute: null,
kmPerLightHour: null,
kmPerLy: null,
kmPerPc: null,
kmPerMpc: null,
jdUnixEpoch: null,
mjdOffset: null,
msPerDay: null,
msPerSecond: null,
}; };
const germanNumberFormatter = new Intl.NumberFormat('de-DE', { const getPow10 = function (exponent) {
maximumFractionDigits: 10 for (let i = pow10Cache.length; i <= exponent; i += 1) {
}); pow10Cache[i] = pow10Cache[i - 1] * 10n;
}
const parseGermanNumber = function (value) { return pow10Cache[exponent];
};
const normalizeDecimal = function (decimal) {
let digits = decimal.digits;
let scale = decimal.scale;
if (digits === 0n) {
return { sign: 1, digits: 0n, scale: 0 };
}
while (scale > 0 && digits % 10n === 0n) {
digits /= 10n;
scale -= 1;
}
return {
sign: decimal.sign < 0 ? -1 : 1,
digits,
scale
};
};
const decimalFromParts = function (sign, digits, scale) {
return normalizeDecimal({
sign: sign < 0 ? -1 : 1,
digits: digits < 0n ? -digits : digits,
scale: Math.max(0, scale | 0)
});
};
const decimalFromInteger = function (value) {
const bigintValue = typeof value === 'bigint' ? value : BigInt(value);
return decimalFromParts(bigintValue < 0n ? -1 : 1, bigintValue < 0n ? -bigintValue : bigintValue, 0);
};
const parseDecimalInput = function (value) {
if (typeof value !== 'string') { if (typeof value !== 'string') {
return Number.NaN; return null;
} }
const normalized = value const normalized = value
@@ -383,51 +433,266 @@ function h(?string $value): string
.replace(',', '.'); .replace(',', '.');
if (normalized === '') { if (normalized === '') {
return Number.NaN; return null;
} }
return Number(normalized); const match = normalized.match(/^([+-])?(\d+)(?:\.(\d+))?$/);
if (!match) {
return null;
}
const sign = match[1] === '-' ? -1 : 1;
const integerPart = match[2];
const fractionalPart = match[3] || '';
return decimalFromParts(sign, BigInt(integerPart + fractionalPart), fractionalPart.length);
}; };
const formatValue = function (value) { const parseDecimalConstant = function (value) {
const normalized = String(value).trim();
const match = normalized.match(/^([+-])?(\d+)(?:\.(\d+))?$/);
if (!match) {
return null;
}
const sign = match[1] === '-' ? -1 : 1;
const integerPart = match[2];
const fractionalPart = match[3] || '';
return decimalFromParts(sign, BigInt(integerPart + fractionalPart), fractionalPart.length);
};
const decimalToEnglishString = function (decimal) {
const normalized = normalizeDecimal(decimal);
const digitsText = normalized.digits.toString();
const signText = normalized.sign < 0 && normalized.digits !== 0n ? '-' : '';
if (normalized.scale === 0) {
return signText + digitsText;
}
const splitIndex = digitsText.length - normalized.scale;
if (splitIndex > 0) {
return signText + digitsText.slice(0, splitIndex) + '.' + digitsText.slice(splitIndex);
}
return signText + '0.' + '0'.repeat(-splitIndex) + digitsText;
};
const decimalToNumber = function (decimal) {
return Number(decimalToEnglishString(decimal));
};
const decimalFromNumber = function (value, fractionDigits) {
if (!Number.isFinite(value)) { if (!Number.isFinite(value)) {
return null;
}
const digits = Math.max(0, Number(fractionDigits) || 0);
return parseDecimalConstant(Number(value).toFixed(digits));
};
const decimalIsZero = function (decimal) {
return normalizeDecimal(decimal).digits === 0n;
};
const decimalAdd = function (left, right) {
const scale = Math.max(left.scale, right.scale);
const leftDigits = BigInt(left.sign) * left.digits * getPow10(scale - left.scale);
const rightDigits = BigInt(right.sign) * right.digits * getPow10(scale - right.scale);
const result = leftDigits + rightDigits;
return decimalFromParts(result < 0n ? -1 : 1, result < 0n ? -result : result, scale);
};
const decimalSubtract = function (left, right) {
return decimalAdd(left, {
sign: right.sign * -1,
digits: right.digits,
scale: right.scale
});
};
const decimalMultiply = function (left, right) {
return decimalFromParts(left.sign * right.sign, left.digits * right.digits, left.scale + right.scale);
};
const decimalDivide = function (left, right, precision) {
if (decimalIsZero(right)) {
return null;
}
const digits = Math.max(0, precision | 0);
const numerator = left.digits * getPow10(digits + right.scale);
const denominator = right.digits * getPow10(left.scale);
const quotient = numerator / denominator;
return decimalFromParts(left.sign * right.sign, quotient, digits);
};
const decimalSquare = function (value) {
return decimalMultiply(value, value);
};
const decimalRoundToBigInt = function (decimal) {
const normalized = normalizeDecimal(decimal);
if (normalized.scale === 0) {
return BigInt(normalized.sign) * normalized.digits;
}
const divisor = getPow10(normalized.scale);
let integerPart = normalized.digits / divisor;
const remainder = normalized.digits % divisor;
if (remainder * 2n >= divisor) {
integerPart += 1n;
}
return BigInt(normalized.sign) * integerPart;
};
const formatDecimal = function (decimal, maxFractionDigits) {
if (!decimal) {
return ''; return '';
} }
if (value === 0) { const normalized = normalizeDecimal(decimal);
return '0'; const limit = Math.max(0, Number(maxFractionDigits) || 0);
const sign = normalized.sign < 0 && normalized.digits !== 0n ? '-' : '';
const scale = Math.min(normalized.scale, limit);
const truncatedDigits = normalized.scale > scale
? normalized.digits / getPow10(normalized.scale - scale)
: normalized.digits;
const digitsText = truncatedDigits.toString();
let integerPart = '';
let fractionalPart = '';
if (scale === 0) {
integerPart = digitsText;
} else {
const splitIndex = digitsText.length - scale;
if (splitIndex > 0) {
integerPart = digitsText.slice(0, splitIndex);
fractionalPart = digitsText.slice(splitIndex);
} else {
integerPart = '0';
fractionalPart = '0'.repeat(-splitIndex) + digitsText;
}
} }
return germanNumberFormatter.format(value); integerPart = integerPart.replace(/^0+(?=\d)/, '') || '0';
integerPart = integerPart.replace(/\B(?=(\d{3})+(?!\d))/g, '.');
fractionalPart = fractionalPart.replace(/0+$/, '');
return fractionalPart !== ''
? `${sign}${integerPart},${fractionalPart}`
: `${sign}${integerPart}`;
};
const convertDistanceDecimal = function (value, sourceUnit, targetUnit) {
if (!value) {
return null;
}
if (sourceUnit === targetUnit) {
return value;
}
if (sourceUnit === 'mpc' && targetUnit === 'pc') {
return decimalMultiply(value, decimalConstants.million);
}
if (sourceUnit === 'pc' && targetUnit === 'mpc') {
return decimalDivide(value, decimalConstants.million, DECIMAL_DIVISION_PRECISION);
}
const decimalUnitToKm = {
km: decimalFromInteger(1n),
au: decimalConstants.kmPerAu,
lm: decimalConstants.kmPerLightMinute,
lh: decimalConstants.kmPerLightHour,
ly: decimalConstants.kmPerLy,
pc: decimalConstants.kmPerPc,
mpc: decimalConstants.kmPerMpc
};
const valueInKm = decimalMultiply(value, decimalUnitToKm[sourceUnit]);
return decimalDivide(valueInKm, decimalUnitToKm[targetUnit], DECIMAL_DIVISION_PRECISION);
};
const formatValue = function (value) {
if (!value) {
return '';
}
return formatDecimal(value, 10);
}; };
const cosmologyE = function (z) { const cosmologyE = function (z) {
return Math.sqrt(OMEGA_M * Math.pow(1 + z, 3) + OMEGA_LAMBDA); return Math.sqrt(OMEGA_M * Math.pow(1 + z, 3) + OMEGA_LAMBDA);
}; };
const integrateSimpson = function (fn, start, end, steps) { const adaptiveSimpson = function (fn, start, end, epsilon, maxDepth) {
if (steps % 2 !== 0) { const simpson = function (left, right, leftValue, middleValue, rightValue) {
steps += 1; return (right - left) * (leftValue + (4 * middleValue) + rightValue) / 6;
};
const recurse = function (left, right, eps, whole, leftValue, middleValue, rightValue, depth) {
const middle = (left + right) / 2;
const leftMiddle = (left + middle) / 2;
const rightMiddle = (middle + right) / 2;
const leftMiddleValue = fn(leftMiddle);
const rightMiddleValue = fn(rightMiddle);
const leftArea = simpson(left, middle, leftValue, leftMiddleValue, middleValue);
const rightArea = simpson(middle, right, middleValue, rightMiddleValue, rightValue);
const delta = leftArea + rightArea - whole;
if (depth <= 0 || Math.abs(delta) <= 15 * eps) {
return leftArea + rightArea + (delta / 15);
}
return recurse(left, middle, eps / 2, leftArea, leftValue, leftMiddleValue, middleValue, depth - 1)
+ recurse(middle, right, eps / 2, rightArea, middleValue, rightMiddleValue, rightValue, depth - 1);
};
const leftValue = fn(start);
const rightValue = fn(end);
const middle = (start + end) / 2;
const middleValue = fn(middle);
const whole = simpson(start, end, leftValue, middleValue, rightValue);
if (!Number.isFinite(whole)) {
return Number.NaN;
} }
const h = (end - start) / steps; return recurse(start, end, epsilon, whole, leftValue, middleValue, rightValue, maxDepth);
let sum = fn(start) + fn(end);
for (let i = 1; i < steps; i += 1) {
const x = start + i * h;
sum += fn(x) * (i % 2 === 0 ? 2 : 4);
}
return (h / 3) * sum;
}; };
decimalConstants.zero = decimalFromInteger(0n);
decimalConstants.one = decimalFromInteger(1n);
decimalConstants.hundred = decimalFromInteger(100n);
decimalConstants.thousand = decimalFromInteger(1000n);
decimalConstants.million = decimalFromInteger(1000000n);
decimalConstants.speedOfLightKmS = parseDecimalConstant('299792.458');
decimalConstants.secondsPerGyr = parseDecimalConstant('31557600000000000');
decimalConstants.mpcInKm = parseDecimalConstant('30856775814913673000');
decimalConstants.kmPerAu = parseDecimalConstant('149597870.7');
decimalConstants.kmPerLightMinute = parseDecimalConstant('17987547.48');
decimalConstants.kmPerLightHour = parseDecimalConstant('1079252848.8');
decimalConstants.kmPerLy = parseDecimalConstant('9460730472580.8');
decimalConstants.kmPerPc = parseDecimalConstant('30856775814913.67');
decimalConstants.kmPerMpc = parseDecimalConstant('30856775814913670000');
decimalConstants.jdUnixEpoch = parseDecimalConstant('2440587.5');
decimalConstants.mjdOffset = parseDecimalConstant('2400000.5');
decimalConstants.msPerDay = decimalFromInteger(86400000n);
decimalConstants.msPerSecond = decimalFromInteger(1000n);
const updateTooltip = function (field) { const updateTooltip = function (field) {
const wrapper = field.closest('.astro-conv-field'); const wrapper = field.closest('.astro-conv-field');
const tooltipId = field.getAttribute('data-tooltip-id'); const tooltipId = field.getAttribute('data-tooltip-id');
const tooltip = tooltipId ? document.getElementById(tooltipId) : null; const tooltip = tooltipId ? document.getElementById(tooltipId) : null;
const numericValue = parseGermanNumber(field.value); const rawValue = String(field.value || '').trim();
if (!wrapper || !tooltip || !Number.isFinite(numericValue)) { if (!wrapper || !tooltip || rawValue === '') {
if (wrapper) { if (wrapper) {
wrapper.classList.remove('is-tooltip-open'); wrapper.classList.remove('is-tooltip-open');
} }
@@ -438,7 +703,7 @@ function h(?string $value): string
return; return;
} }
tooltip.textContent = window.numberToGermanText(numericValue); tooltip.textContent = window.numberToGermanText(rawValue);
}; };
const attachTooltip = function (field, key) { const attachTooltip = function (field, key) {
@@ -504,13 +769,12 @@ function h(?string $value): string
return; return;
} }
const numericValue = parseGermanNumber(rawValue); const decimalValue = parseDecimalInput(rawValue);
if (!Number.isFinite(numericValue)) { if (!decimalValue) {
return; return;
} }
const valueInKm = numericValue * unitToKm[sourceUnit]; distanceFields[sourceUnit].value = formatValue(decimalValue);
distanceFields[sourceUnit].value = formatValue(numericValue);
updateTooltip(distanceFields[sourceUnit]); updateTooltip(distanceFields[sourceUnit]);
Object.entries(distanceFields).forEach(function ([unit, field]) { Object.entries(distanceFields).forEach(function ([unit, field]) {
@@ -518,7 +782,7 @@ function h(?string $value): string
return; return;
} }
field.value = formatValue(valueInKm / unitToKm[unit]); field.value = formatValue(convertDistanceDecimal(decimalValue, sourceUnit, unit));
updateTooltip(field); updateTooltip(field);
}); });
}; };
@@ -547,47 +811,55 @@ function h(?string $value): string
}); });
const updateRedshift = function () { const updateRedshift = function () {
const z = parseGermanNumber(redshiftFields.z.value.trim()); const zDecimal = parseDecimalInput(redshiftFields.z.value.trim());
const restNm = parseGermanNumber(redshiftFields.restNm.value.trim()); const restNmDecimal = parseDecimalInput(redshiftFields.restNm.value.trim());
const z = zDecimal ? decimalToNumber(zDecimal) : Number.NaN;
const restNm = restNmDecimal ? decimalToNumber(restNmDecimal) : Number.NaN;
if (!Number.isFinite(z) || !Number.isFinite(restNm) || z <= -1) { if (!zDecimal || !restNmDecimal || !Number.isFinite(z) || !Number.isFinite(restNm) || z <= -1) {
return; return;
} }
const factor = 1 + z; const factorDecimal = decimalAdd(decimalConstants.one, zDecimal);
const scaleFactor = 1 / factor; const factor = decimalToNumber(factorDecimal);
const observedNm = restNm * factor; const scaleFactorDecimal = decimalDivide(decimalConstants.one, factorDecimal, DECIMAL_DIVISION_PRECISION);
const frequencyFactor = 1 / factor; const observedNmDecimal = decimalMultiply(restNmDecimal, factorDecimal);
const velocityApprox = z * SPEED_OF_LIGHT_KM_S; const frequencyFactorDecimal = decimalDivide(decimalConstants.one, factorDecimal, DECIMAL_DIVISION_PRECISION);
const betaRel = ((factor * factor) - 1) / ((factor * factor) + 1); const velocityApproxDecimal = decimalMultiply(zDecimal, decimalConstants.speedOfLightKmS);
const velocityRel = betaRel * SPEED_OF_LIGHT_KM_S; const factorSquaredDecimal = decimalSquare(factorDecimal);
const velocityC = betaRel * 100; const betaRelDecimal = decimalDivide(
const integrationSteps = Math.max(200, Math.ceil(z * 200)); decimalSubtract(factorSquaredDecimal, decimalConstants.one),
const comovingIntegral = integrateSimpson(function (currentZ) { decimalAdd(factorSquaredDecimal, decimalConstants.one),
DECIMAL_DIVISION_PRECISION
);
const velocityRelDecimal = decimalMultiply(betaRelDecimal, decimalConstants.speedOfLightKmS);
const velocityCDecimal = decimalMultiply(betaRelDecimal, decimalConstants.hundred);
const comovingIntegral = adaptiveSimpson(function (currentZ) {
return 1 / cosmologyE(currentZ); return 1 / cosmologyE(currentZ);
}, 0, z, integrationSteps); }, 0, z, 1e-10, 20);
const lookbackIntegral = integrateSimpson(function (currentZ) { const lookbackIntegral = adaptiveSimpson(function (currentZ) {
return 1 / ((1 + currentZ) * cosmologyE(currentZ)); return 1 / ((1 + currentZ) * cosmologyE(currentZ));
}, 0, z, integrationSteps); }, 0, z, 1e-10, 20);
const hubbleTimeSeconds = MPC_IN_KM / H0; const hubbleTimeSeconds = MPC_IN_KM / H0;
const comovingMpc = (SPEED_OF_LIGHT_KM_S / H0) * comovingIntegral; const comovingMpc = (SPEED_OF_LIGHT_KM_S / H0) * comovingIntegral;
const luminosityMpc = comovingMpc * factor; const luminosityMpc = comovingMpc * factor;
const angularDiameterMpc = comovingMpc / factor; const angularDiameterMpc = comovingMpc / factor;
const lookbackGyr = (hubbleTimeSeconds * lookbackIntegral) / SECONDS_PER_GYR; const lookbackGyr = (hubbleTimeSeconds * lookbackIntegral) / SECONDS_PER_GYR;
redshiftFields.z.value = formatValue(z); redshiftFields.z.value = formatValue(zDecimal);
redshiftFields.restNm.value = formatValue(restNm); redshiftFields.restNm.value = formatValue(restNmDecimal);
redshiftFields.factor.value = formatValue(factor); redshiftFields.factor.value = formatValue(factorDecimal);
redshiftFields.scaleFactor.value = formatValue(scaleFactor); redshiftFields.scaleFactor.value = formatValue(scaleFactorDecimal);
redshiftFields.observedNm.value = formatValue(observedNm); redshiftFields.observedNm.value = formatValue(observedNmDecimal);
redshiftFields.frequencyFactor.value = formatValue(frequencyFactor); redshiftFields.frequencyFactor.value = formatValue(frequencyFactorDecimal);
redshiftFields.velocityApprox.value = formatValue(velocityApprox); redshiftFields.velocityApprox.value = formatValue(velocityApproxDecimal);
redshiftFields.velocityRel.value = formatValue(velocityRel); redshiftFields.velocityRel.value = formatValue(velocityRelDecimal);
redshiftFields.velocityC.value = formatValue(velocityC); redshiftFields.velocityC.value = formatValue(velocityCDecimal);
redshiftFields.lookbackGyr.value = formatValue(lookbackGyr); redshiftFields.lookbackGyr.value = formatValue(decimalFromNumber(lookbackGyr, 18));
redshiftFields.comovingMpc.value = formatValue(comovingMpc); redshiftFields.comovingMpc.value = formatValue(decimalFromNumber(comovingMpc, 18));
redshiftFields.luminosityMpc.value = formatValue(luminosityMpc); redshiftFields.luminosityMpc.value = formatValue(decimalFromNumber(luminosityMpc, 18));
redshiftFields.angularDiameterMpc.value = formatValue(angularDiameterMpc); redshiftFields.angularDiameterMpc.value = formatValue(decimalFromNumber(angularDiameterMpc, 18));
Object.values(redshiftFields).forEach(function (field) { Object.values(redshiftFields).forEach(function (field) {
updateTooltip(field); updateTooltip(field);
@@ -654,12 +926,21 @@ function h(?string $value): string
return parts.join('-') + ' ' + timeParts.join(':'); return parts.join('-') + ' ' + timeParts.join(':');
}; };
const dateToJulianDate = function (date) { const dateToJulianDateDecimal = function (date) {
return (date.getTime() / 86400000) + 2440587.5; const unixMsDecimal = decimalFromInteger(BigInt(date.getTime()));
return decimalAdd(
decimalDivide(unixMsDecimal, decimalConstants.msPerDay, DECIMAL_DIVISION_PRECISION),
decimalConstants.jdUnixEpoch
);
}; };
const julianDateToDate = function (jd) { const julianDateDecimalToDate = function (jdDecimal) {
return new Date((jd - 2440587.5) * 86400000); const unixMsDecimal = decimalMultiply(
decimalSubtract(jdDecimal, decimalConstants.jdUnixEpoch),
decimalConstants.msPerDay
);
const unixMs = decimalRoundToBigInt(unixMsDecimal);
return new Date(Number(unixMs));
}; };
const updateTimeFrom = function (sourceUnit) { const updateTimeFrom = function (sourceUnit) {
@@ -668,28 +949,31 @@ function h(?string $value): string
if (sourceUnit === 'utc') { if (sourceUnit === 'utc') {
date = parseUtcDateTime(timeFields.utc.value); date = parseUtcDateTime(timeFields.utc.value);
} else if (sourceUnit === 'jd') { } else if (sourceUnit === 'jd') {
const jd = parseGermanNumber(timeFields.jd.value.trim()); const jdDecimal = parseDecimalInput(timeFields.jd.value.trim());
date = Number.isFinite(jd) ? julianDateToDate(jd) : null; date = jdDecimal ? julianDateDecimalToDate(jdDecimal) : null;
} else if (sourceUnit === 'mjd') { } else if (sourceUnit === 'mjd') {
const mjd = parseGermanNumber(timeFields.mjd.value.trim()); const mjdDecimal = parseDecimalInput(timeFields.mjd.value.trim());
date = Number.isFinite(mjd) ? julianDateToDate(mjd + 2400000.5) : null; date = mjdDecimal ? julianDateDecimalToDate(decimalAdd(mjdDecimal, decimalConstants.mjdOffset)) : null;
} else if (sourceUnit === 'unix') { } else if (sourceUnit === 'unix') {
const unixSeconds = parseGermanNumber(timeFields.unix.value.trim()); const unixSecondsDecimal = parseDecimalInput(timeFields.unix.value.trim());
date = Number.isFinite(unixSeconds) ? new Date(unixSeconds * 1000) : null; if (unixSecondsDecimal) {
const unixMs = decimalRoundToBigInt(decimalMultiply(unixSecondsDecimal, decimalConstants.msPerSecond));
date = new Date(Number(unixMs));
}
} }
if (!(date instanceof Date) || Number.isNaN(date.getTime())) { if (!(date instanceof Date) || Number.isNaN(date.getTime())) {
return; return;
} }
const jd = dateToJulianDate(date); const jdDecimal = dateToJulianDateDecimal(date);
const mjd = jd - 2400000.5; const mjdDecimal = decimalSubtract(jdDecimal, decimalConstants.mjdOffset);
const unixSeconds = Math.floor(date.getTime() / 1000); const unixSecondsDecimal = decimalDivide(decimalFromInteger(BigInt(date.getTime())), decimalConstants.msPerSecond, 10);
timeFields.utc.value = formatUtcDateTime(date); timeFields.utc.value = formatUtcDateTime(date);
timeFields.jd.value = formatValue(jd); timeFields.jd.value = formatValue(jdDecimal);
timeFields.mjd.value = formatValue(mjd); timeFields.mjd.value = formatValue(mjdDecimal);
timeFields.unix.value = formatValue(unixSeconds); timeFields.unix.value = formatValue(unixSecondsDecimal);
Object.values(timeFields).forEach(function (field) { Object.values(timeFields).forEach(function (field) {
updateTooltip(field); updateTooltip(field);
+66 -39
View File
@@ -3,19 +3,18 @@
const ONES_COMPOUND = ['', 'ein', 'zwei', 'drei', 'vier', 'fuenf', 'sechs', 'sieben', 'acht', 'neun']; const ONES_COMPOUND = ['', 'ein', 'zwei', 'drei', 'vier', 'fuenf', 'sechs', 'sieben', 'acht', 'neun'];
const TEENS = ['zehn', 'elf', 'zwoelf', 'dreizehn', 'vierzehn', 'fuenfzehn', 'sechzehn', 'siebzehn', 'achtzehn', 'neunzehn']; const TEENS = ['zehn', 'elf', 'zwoelf', 'dreizehn', 'vierzehn', 'fuenfzehn', 'sechzehn', 'siebzehn', 'achtzehn', 'neunzehn'];
const TENS = ['', '', 'zwanzig', 'dreissig', 'vierzig', 'fuenfzig', 'sechzig', 'siebzig', 'achtzig', 'neunzig']; const TENS = ['', '', 'zwanzig', 'dreissig', 'vierzig', 'fuenfzig', 'sechzig', 'siebzig', 'achtzig', 'neunzig'];
const DIGIT_WORDS = ['null', 'eins', 'zwei', 'drei', 'vier', 'fuenf', 'sechs', 'sieben', 'acht', 'neun'];
const SCALES = [ const SCALES = [
{ singular: '', plural: '', join: true }, { singular: '', plural: '' },
{ singular: 'Tausend', plural: 'Tausend', join: true }, { singular: 'Tausend', plural: 'Tausend' },
{ singular: 'Million', plural: 'Millionen', join: false }, { singular: 'Million', plural: 'Millionen' },
{ singular: 'Milliarde', plural: 'Milliarden', join: false }, { singular: 'Milliarde', plural: 'Milliarden' },
{ singular: 'Billion', plural: 'Billionen', join: false }, { singular: 'Billion', plural: 'Billionen' },
{ singular: 'Billiarde', plural: 'Billiarden', join: false }, { singular: 'Billiarde', plural: 'Billiarden' },
{ singular: 'Trillion', plural: 'Trillionen', join: false }, { singular: 'Trillion', plural: 'Trillionen' },
{ singular: 'Trilliarde', plural: 'Trilliarden', join: false }, { singular: 'Trilliarde', plural: 'Trilliarden' },
{ singular: 'Quadrillion', plural: 'Quadrillionen', join: false } { singular: 'Quadrillion', plural: 'Quadrillionen' }
]; ];
const GROUP_NUMBER_FORMATTER = new Intl.NumberFormat('de-DE');
function chunkToWords(value) { function chunkToWords(value) {
if (value === 0) { if (value === 0) {
@@ -52,67 +51,95 @@
return text; return text;
} }
function integerToWords(value) { function normalizeGermanNumberString(value) {
if (!Number.isFinite(value)) { if (typeof value === 'number') {
if (!Number.isFinite(value)) {
return '';
}
return String(value).replace('.', ',');
}
if (typeof value !== 'string') {
return ''; return '';
} }
if (value === 0) { return value.trim().replace(/\s+/g, '');
}
function splitNormalizedNumber(value) {
const normalized = normalizeGermanNumberString(value);
if (normalized === '') {
return null;
}
const sign = normalized.startsWith('-') ? '-' : '';
const unsigned = sign ? normalized.slice(1) : normalized;
const cleaned = unsigned.replace(/\./g, '');
const parts = cleaned.split(',');
if (parts.length > 2) {
return null;
}
const integerPart = (parts[0] || '0').replace(/^0+(?=\d)/, '') || '0';
const fractionalPart = parts[1] || '';
if (!/^\d+$/.test(integerPart) || (fractionalPart !== '' && !/^\d+$/.test(fractionalPart))) {
return null;
}
return { sign, integerPart, fractionalPart };
}
function integerStringToWords(integerString, sign) {
if (integerString === '0') {
return 'null'; return 'null';
} }
let number = Math.floor(Math.abs(value));
const groups = []; const groups = [];
for (let index = integerString.length; index > 0; index -= 3) {
while (number > 0) { groups.push(integerString.slice(Math.max(0, index - 3), index));
groups.push(number % 1000);
number = Math.floor(number / 1000);
} }
const parts = []; const parts = [];
for (let i = groups.length - 1; i >= 0; i -= 1) { for (let i = groups.length - 1; i >= 0; i -= 1) {
const groupValue = groups[i]; const groupValue = Number(groups[i]);
if (groupValue === 0) { if (groupValue === 0) {
continue; continue;
} }
const scale = SCALES[i] || { singular: '', plural: '', join: false }; const scale = SCALES[i] || { singular: '', plural: '' };
if (i >= 2) { if (i >= 2) {
const scaleLabel = groupValue === 1 ? scale.singular : scale.plural; const prefix = groupValue === 1 ? 'eine' : String(groupValue);
const prefix = groupValue === 1 ? 'eine' : GROUP_NUMBER_FORMATTER.format(groupValue); const label = groupValue === 1 ? scale.singular : scale.plural;
parts.push(prefix + ' ' + scaleLabel); parts.push(prefix + ' ' + label);
} else if (i === 1) { } else if (i === 1) {
parts.push(GROUP_NUMBER_FORMATTER.format(groupValue) + ' ' + scale.singular); parts.push(groupValue === 1 ? 'ein ' + scale.singular : String(groupValue) + ' ' + scale.singular);
} else { } else {
parts.push(GROUP_NUMBER_FORMATTER.format(groupValue)); parts.push(String(groupValue));
} }
} }
const result = parts.join('\n'); const result = parts.join('\n');
return value < 0 ? 'minus ' + result : result; return sign === '-' ? 'minus ' + result : result;
} }
function numberToGermanText(value) { function numberToGermanText(value) {
if (!Number.isFinite(value)) { const parsed = splitNormalizedNumber(value);
if (!parsed) {
return ''; return '';
} }
const integerPart = Math.trunc(value); const integerText = integerStringToWords(parsed.integerPart, parsed.sign);
const fractionalPart = Math.abs(value - integerPart); if (!parsed.fractionalPart || /^0+$/.test(parsed.fractionalPart)) {
const integerText = integerToWords(integerPart);
if (fractionalPart < 1e-10) {
return integerText; return integerText;
} }
const decimalDigits = fractionalPart const decimalDigits = parsed.fractionalPart
.toFixed(10)
.replace(/^0\./, '')
.replace(/0+$/, '')
.split('') .split('')
.map(function (digit) { .map(function (digit) {
return ['null', 'eins', 'zwei', 'drei', 'vier', 'fuenf', 'sechs', 'sieben', 'acht', 'neun'][Number(digit)]; return DIGIT_WORDS[Number(digit)];
}) })
.join(' '); .join(' ');