astronomical_conversions verbessert
This commit is contained in:
@@ -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);
|
||||||
|
|||||||
@@ -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(' ');
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user