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')
};
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);