astronomical_conversions verbessert
This commit is contained in:
@@ -357,23 +357,73 @@ function h(?string $value): string
|
||||
unix: document.getElementById('timeUnix')
|
||||
};
|
||||
|
||||
const unitToKm = {
|
||||
km: 1,
|
||||
au: KM_PER_AU,
|
||||
lm: KM_PER_LIGHT_MINUTE,
|
||||
lh: KM_PER_LIGHT_HOUR,
|
||||
ly: KM_PER_LY,
|
||||
pc: KM_PER_PC,
|
||||
mpc: KM_PER_MPC
|
||||
const DECIMAL_DIVISION_PRECISION = 32;
|
||||
const pow10Cache = [1n];
|
||||
const decimalConstants = {
|
||||
zero: null,
|
||||
one: null,
|
||||
hundred: null,
|
||||
thousand: null,
|
||||
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', {
|
||||
maximumFractionDigits: 10
|
||||
});
|
||||
const getPow10 = function (exponent) {
|
||||
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') {
|
||||
return Number.NaN;
|
||||
return null;
|
||||
}
|
||||
|
||||
const normalized = value
|
||||
@@ -383,51 +433,266 @@ function h(?string $value): string
|
||||
.replace(',', '.');
|
||||
|
||||
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)) {
|
||||
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 '';
|
||||
}
|
||||
|
||||
if (value === 0) {
|
||||
return '0';
|
||||
const normalized = normalizeDecimal(decimal);
|
||||
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) {
|
||||
return Math.sqrt(OMEGA_M * Math.pow(1 + z, 3) + OMEGA_LAMBDA);
|
||||
};
|
||||
|
||||
const integrateSimpson = function (fn, start, end, steps) {
|
||||
if (steps % 2 !== 0) {
|
||||
steps += 1;
|
||||
const adaptiveSimpson = function (fn, start, end, epsilon, maxDepth) {
|
||||
const simpson = function (left, right, leftValue, middleValue, rightValue) {
|
||||
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;
|
||||
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;
|
||||
return recurse(start, end, epsilon, whole, leftValue, middleValue, rightValue, maxDepth);
|
||||
};
|
||||
|
||||
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 wrapper = field.closest('.astro-conv-field');
|
||||
const tooltipId = field.getAttribute('data-tooltip-id');
|
||||
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) {
|
||||
wrapper.classList.remove('is-tooltip-open');
|
||||
}
|
||||
@@ -438,7 +703,7 @@ function h(?string $value): string
|
||||
return;
|
||||
}
|
||||
|
||||
tooltip.textContent = window.numberToGermanText(numericValue);
|
||||
tooltip.textContent = window.numberToGermanText(rawValue);
|
||||
};
|
||||
|
||||
const attachTooltip = function (field, key) {
|
||||
@@ -504,13 +769,12 @@ function h(?string $value): string
|
||||
return;
|
||||
}
|
||||
|
||||
const numericValue = parseGermanNumber(rawValue);
|
||||
if (!Number.isFinite(numericValue)) {
|
||||
const decimalValue = parseDecimalInput(rawValue);
|
||||
if (!decimalValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
const valueInKm = numericValue * unitToKm[sourceUnit];
|
||||
distanceFields[sourceUnit].value = formatValue(numericValue);
|
||||
distanceFields[sourceUnit].value = formatValue(decimalValue);
|
||||
updateTooltip(distanceFields[sourceUnit]);
|
||||
|
||||
Object.entries(distanceFields).forEach(function ([unit, field]) {
|
||||
@@ -518,7 +782,7 @@ function h(?string $value): string
|
||||
return;
|
||||
}
|
||||
|
||||
field.value = formatValue(valueInKm / unitToKm[unit]);
|
||||
field.value = formatValue(convertDistanceDecimal(decimalValue, sourceUnit, unit));
|
||||
updateTooltip(field);
|
||||
});
|
||||
};
|
||||
@@ -547,47 +811,55 @@ function h(?string $value): string
|
||||
});
|
||||
|
||||
const updateRedshift = function () {
|
||||
const z = parseGermanNumber(redshiftFields.z.value.trim());
|
||||
const restNm = parseGermanNumber(redshiftFields.restNm.value.trim());
|
||||
const zDecimal = parseDecimalInput(redshiftFields.z.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;
|
||||
}
|
||||
|
||||
const factor = 1 + z;
|
||||
const scaleFactor = 1 / factor;
|
||||
const observedNm = restNm * factor;
|
||||
const frequencyFactor = 1 / factor;
|
||||
const velocityApprox = z * SPEED_OF_LIGHT_KM_S;
|
||||
const betaRel = ((factor * factor) - 1) / ((factor * factor) + 1);
|
||||
const velocityRel = betaRel * SPEED_OF_LIGHT_KM_S;
|
||||
const velocityC = betaRel * 100;
|
||||
const integrationSteps = Math.max(200, Math.ceil(z * 200));
|
||||
const comovingIntegral = integrateSimpson(function (currentZ) {
|
||||
const factorDecimal = decimalAdd(decimalConstants.one, zDecimal);
|
||||
const factor = decimalToNumber(factorDecimal);
|
||||
const scaleFactorDecimal = decimalDivide(decimalConstants.one, factorDecimal, DECIMAL_DIVISION_PRECISION);
|
||||
const observedNmDecimal = decimalMultiply(restNmDecimal, factorDecimal);
|
||||
const frequencyFactorDecimal = decimalDivide(decimalConstants.one, factorDecimal, DECIMAL_DIVISION_PRECISION);
|
||||
const velocityApproxDecimal = decimalMultiply(zDecimal, decimalConstants.speedOfLightKmS);
|
||||
const factorSquaredDecimal = decimalSquare(factorDecimal);
|
||||
const betaRelDecimal = decimalDivide(
|
||||
decimalSubtract(factorSquaredDecimal, decimalConstants.one),
|
||||
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);
|
||||
}, 0, z, integrationSteps);
|
||||
const lookbackIntegral = integrateSimpson(function (currentZ) {
|
||||
}, 0, z, 1e-10, 20);
|
||||
const lookbackIntegral = adaptiveSimpson(function (currentZ) {
|
||||
return 1 / ((1 + currentZ) * cosmologyE(currentZ));
|
||||
}, 0, z, integrationSteps);
|
||||
}, 0, z, 1e-10, 20);
|
||||
const hubbleTimeSeconds = MPC_IN_KM / H0;
|
||||
const comovingMpc = (SPEED_OF_LIGHT_KM_S / H0) * comovingIntegral;
|
||||
const luminosityMpc = comovingMpc * factor;
|
||||
const angularDiameterMpc = comovingMpc / factor;
|
||||
const lookbackGyr = (hubbleTimeSeconds * lookbackIntegral) / SECONDS_PER_GYR;
|
||||
|
||||
redshiftFields.z.value = formatValue(z);
|
||||
redshiftFields.restNm.value = formatValue(restNm);
|
||||
redshiftFields.factor.value = formatValue(factor);
|
||||
redshiftFields.scaleFactor.value = formatValue(scaleFactor);
|
||||
redshiftFields.observedNm.value = formatValue(observedNm);
|
||||
redshiftFields.frequencyFactor.value = formatValue(frequencyFactor);
|
||||
redshiftFields.velocityApprox.value = formatValue(velocityApprox);
|
||||
redshiftFields.velocityRel.value = formatValue(velocityRel);
|
||||
redshiftFields.velocityC.value = formatValue(velocityC);
|
||||
redshiftFields.lookbackGyr.value = formatValue(lookbackGyr);
|
||||
redshiftFields.comovingMpc.value = formatValue(comovingMpc);
|
||||
redshiftFields.luminosityMpc.value = formatValue(luminosityMpc);
|
||||
redshiftFields.angularDiameterMpc.value = formatValue(angularDiameterMpc);
|
||||
redshiftFields.z.value = formatValue(zDecimal);
|
||||
redshiftFields.restNm.value = formatValue(restNmDecimal);
|
||||
redshiftFields.factor.value = formatValue(factorDecimal);
|
||||
redshiftFields.scaleFactor.value = formatValue(scaleFactorDecimal);
|
||||
redshiftFields.observedNm.value = formatValue(observedNmDecimal);
|
||||
redshiftFields.frequencyFactor.value = formatValue(frequencyFactorDecimal);
|
||||
redshiftFields.velocityApprox.value = formatValue(velocityApproxDecimal);
|
||||
redshiftFields.velocityRel.value = formatValue(velocityRelDecimal);
|
||||
redshiftFields.velocityC.value = formatValue(velocityCDecimal);
|
||||
redshiftFields.lookbackGyr.value = formatValue(decimalFromNumber(lookbackGyr, 18));
|
||||
redshiftFields.comovingMpc.value = formatValue(decimalFromNumber(comovingMpc, 18));
|
||||
redshiftFields.luminosityMpc.value = formatValue(decimalFromNumber(luminosityMpc, 18));
|
||||
redshiftFields.angularDiameterMpc.value = formatValue(decimalFromNumber(angularDiameterMpc, 18));
|
||||
|
||||
Object.values(redshiftFields).forEach(function (field) {
|
||||
updateTooltip(field);
|
||||
@@ -654,12 +926,21 @@ function h(?string $value): string
|
||||
return parts.join('-') + ' ' + timeParts.join(':');
|
||||
};
|
||||
|
||||
const dateToJulianDate = function (date) {
|
||||
return (date.getTime() / 86400000) + 2440587.5;
|
||||
const dateToJulianDateDecimal = function (date) {
|
||||
const unixMsDecimal = decimalFromInteger(BigInt(date.getTime()));
|
||||
return decimalAdd(
|
||||
decimalDivide(unixMsDecimal, decimalConstants.msPerDay, DECIMAL_DIVISION_PRECISION),
|
||||
decimalConstants.jdUnixEpoch
|
||||
);
|
||||
};
|
||||
|
||||
const julianDateToDate = function (jd) {
|
||||
return new Date((jd - 2440587.5) * 86400000);
|
||||
const julianDateDecimalToDate = function (jdDecimal) {
|
||||
const unixMsDecimal = decimalMultiply(
|
||||
decimalSubtract(jdDecimal, decimalConstants.jdUnixEpoch),
|
||||
decimalConstants.msPerDay
|
||||
);
|
||||
const unixMs = decimalRoundToBigInt(unixMsDecimal);
|
||||
return new Date(Number(unixMs));
|
||||
};
|
||||
|
||||
const updateTimeFrom = function (sourceUnit) {
|
||||
@@ -668,28 +949,31 @@ function h(?string $value): string
|
||||
if (sourceUnit === 'utc') {
|
||||
date = parseUtcDateTime(timeFields.utc.value);
|
||||
} else if (sourceUnit === 'jd') {
|
||||
const jd = parseGermanNumber(timeFields.jd.value.trim());
|
||||
date = Number.isFinite(jd) ? julianDateToDate(jd) : null;
|
||||
const jdDecimal = parseDecimalInput(timeFields.jd.value.trim());
|
||||
date = jdDecimal ? julianDateDecimalToDate(jdDecimal) : null;
|
||||
} else if (sourceUnit === 'mjd') {
|
||||
const mjd = parseGermanNumber(timeFields.mjd.value.trim());
|
||||
date = Number.isFinite(mjd) ? julianDateToDate(mjd + 2400000.5) : null;
|
||||
const mjdDecimal = parseDecimalInput(timeFields.mjd.value.trim());
|
||||
date = mjdDecimal ? julianDateDecimalToDate(decimalAdd(mjdDecimal, decimalConstants.mjdOffset)) : null;
|
||||
} else if (sourceUnit === 'unix') {
|
||||
const unixSeconds = parseGermanNumber(timeFields.unix.value.trim());
|
||||
date = Number.isFinite(unixSeconds) ? new Date(unixSeconds * 1000) : null;
|
||||
const unixSecondsDecimal = parseDecimalInput(timeFields.unix.value.trim());
|
||||
if (unixSecondsDecimal) {
|
||||
const unixMs = decimalRoundToBigInt(decimalMultiply(unixSecondsDecimal, decimalConstants.msPerSecond));
|
||||
date = new Date(Number(unixMs));
|
||||
}
|
||||
}
|
||||
|
||||
if (!(date instanceof Date) || Number.isNaN(date.getTime())) {
|
||||
return;
|
||||
}
|
||||
|
||||
const jd = dateToJulianDate(date);
|
||||
const mjd = jd - 2400000.5;
|
||||
const unixSeconds = Math.floor(date.getTime() / 1000);
|
||||
const jdDecimal = dateToJulianDateDecimal(date);
|
||||
const mjdDecimal = decimalSubtract(jdDecimal, decimalConstants.mjdOffset);
|
||||
const unixSecondsDecimal = decimalDivide(decimalFromInteger(BigInt(date.getTime())), decimalConstants.msPerSecond, 10);
|
||||
|
||||
timeFields.utc.value = formatUtcDateTime(date);
|
||||
timeFields.jd.value = formatValue(jd);
|
||||
timeFields.mjd.value = formatValue(mjd);
|
||||
timeFields.unix.value = formatValue(unixSeconds);
|
||||
timeFields.jd.value = formatValue(jdDecimal);
|
||||
timeFields.mjd.value = formatValue(mjdDecimal);
|
||||
timeFields.unix.value = formatValue(unixSecondsDecimal);
|
||||
|
||||
Object.values(timeFields).forEach(function (field) {
|
||||
updateTooltip(field);
|
||||
|
||||
@@ -3,19 +3,18 @@
|
||||
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 TENS = ['', '', 'zwanzig', 'dreissig', 'vierzig', 'fuenfzig', 'sechzig', 'siebzig', 'achtzig', 'neunzig'];
|
||||
const DIGIT_WORDS = ['null', 'eins', 'zwei', 'drei', 'vier', 'fuenf', 'sechs', 'sieben', 'acht', 'neun'];
|
||||
const SCALES = [
|
||||
{ singular: '', plural: '', join: true },
|
||||
{ singular: 'Tausend', plural: 'Tausend', join: true },
|
||||
{ singular: 'Million', plural: 'Millionen', join: false },
|
||||
{ singular: 'Milliarde', plural: 'Milliarden', join: false },
|
||||
{ singular: 'Billion', plural: 'Billionen', join: false },
|
||||
{ singular: 'Billiarde', plural: 'Billiarden', join: false },
|
||||
{ singular: 'Trillion', plural: 'Trillionen', join: false },
|
||||
{ singular: 'Trilliarde', plural: 'Trilliarden', join: false },
|
||||
{ singular: 'Quadrillion', plural: 'Quadrillionen', join: false }
|
||||
|
||||
{ singular: '', plural: '' },
|
||||
{ singular: 'Tausend', plural: 'Tausend' },
|
||||
{ singular: 'Million', plural: 'Millionen' },
|
||||
{ singular: 'Milliarde', plural: 'Milliarden' },
|
||||
{ singular: 'Billion', plural: 'Billionen' },
|
||||
{ singular: 'Billiarde', plural: 'Billiarden' },
|
||||
{ singular: 'Trillion', plural: 'Trillionen' },
|
||||
{ singular: 'Trilliarde', plural: 'Trilliarden' },
|
||||
{ singular: 'Quadrillion', plural: 'Quadrillionen' }
|
||||
];
|
||||
const GROUP_NUMBER_FORMATTER = new Intl.NumberFormat('de-DE');
|
||||
|
||||
function chunkToWords(value) {
|
||||
if (value === 0) {
|
||||
@@ -52,67 +51,95 @@
|
||||
return text;
|
||||
}
|
||||
|
||||
function integerToWords(value) {
|
||||
if (!Number.isFinite(value)) {
|
||||
function normalizeGermanNumberString(value) {
|
||||
if (typeof value === 'number') {
|
||||
if (!Number.isFinite(value)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return String(value).replace('.', ',');
|
||||
}
|
||||
|
||||
if (typeof value !== 'string') {
|
||||
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';
|
||||
}
|
||||
|
||||
let number = Math.floor(Math.abs(value));
|
||||
const groups = [];
|
||||
|
||||
while (number > 0) {
|
||||
groups.push(number % 1000);
|
||||
number = Math.floor(number / 1000);
|
||||
for (let index = integerString.length; index > 0; index -= 3) {
|
||||
groups.push(integerString.slice(Math.max(0, index - 3), index));
|
||||
}
|
||||
|
||||
const parts = [];
|
||||
|
||||
for (let i = groups.length - 1; i >= 0; i -= 1) {
|
||||
const groupValue = groups[i];
|
||||
const groupValue = Number(groups[i]);
|
||||
if (groupValue === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const scale = SCALES[i] || { singular: '', plural: '', join: false };
|
||||
const scale = SCALES[i] || { singular: '', plural: '' };
|
||||
if (i >= 2) {
|
||||
const scaleLabel = groupValue === 1 ? scale.singular : scale.plural;
|
||||
const prefix = groupValue === 1 ? 'eine' : GROUP_NUMBER_FORMATTER.format(groupValue);
|
||||
parts.push(prefix + ' ' + scaleLabel);
|
||||
const prefix = groupValue === 1 ? 'eine' : String(groupValue);
|
||||
const label = groupValue === 1 ? scale.singular : scale.plural;
|
||||
parts.push(prefix + ' ' + label);
|
||||
} 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 {
|
||||
parts.push(GROUP_NUMBER_FORMATTER.format(groupValue));
|
||||
parts.push(String(groupValue));
|
||||
}
|
||||
}
|
||||
|
||||
const result = parts.join('\n');
|
||||
return value < 0 ? 'minus ' + result : result;
|
||||
return sign === '-' ? 'minus ' + result : result;
|
||||
}
|
||||
|
||||
function numberToGermanText(value) {
|
||||
if (!Number.isFinite(value)) {
|
||||
const parsed = splitNormalizedNumber(value);
|
||||
if (!parsed) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const integerPart = Math.trunc(value);
|
||||
const fractionalPart = Math.abs(value - integerPart);
|
||||
const integerText = integerToWords(integerPart);
|
||||
|
||||
if (fractionalPart < 1e-10) {
|
||||
const integerText = integerStringToWords(parsed.integerPart, parsed.sign);
|
||||
if (!parsed.fractionalPart || /^0+$/.test(parsed.fractionalPart)) {
|
||||
return integerText;
|
||||
}
|
||||
|
||||
const decimalDigits = fractionalPart
|
||||
.toFixed(10)
|
||||
.replace(/^0\./, '')
|
||||
.replace(/0+$/, '')
|
||||
const decimalDigits = parsed.fractionalPart
|
||||
.split('')
|
||||
.map(function (digit) {
|
||||
return ['null', 'eins', 'zwei', 'drei', 'vier', 'fuenf', 'sechs', 'sieben', 'acht', 'neun'][Number(digit)];
|
||||
return DIGIT_WORDS[Number(digit)];
|
||||
})
|
||||
.join(' ');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user