diff --git a/src/cockpitMath.js b/src/cockpitMath.js index a0bd836..4007fdb 100644 --- a/src/cockpitMath.js +++ b/src/cockpitMath.js @@ -80,7 +80,55 @@ export function cockpitGroundSafeHeight( */ export function cockpitAltitudeDisplayFt(altitudeM, onGround) { if (onGround === true) return 0; - return Number.isFinite(altitudeM) ? altitudeM * 3.28084 : null; + return Number.isFinite(altitudeM) ? altitudeM / 0.3048 : null; +} + +/** Round to clean IEEE-754 decimal precision without float representation noise. */ +function cleanPrecision(value, decimals = 6) { + const factor = 10 ** decimals; + return Math.round(value * factor) / factor; +} + +/** Convert Celsius to Fahrenheit with NaN/non-finite guard and IEEE-754 precision smoothing. */ +export function celsiusToFahrenheit(c) { + if (!Number.isFinite(c)) return null; + return cleanPrecision((c * 9) / 5 + 32); +} + +/** Convert Fahrenheit to Celsius with NaN/non-finite guard and IEEE-754 precision smoothing. */ +export function fahrenheitToCelsius(f) { + if (!Number.isFinite(f)) return null; + return cleanPrecision(((f - 32) * 5) / 9); +} + +/** Convert Celsius to Kelvin, clamping unphysical temperatures below absolute zero. */ +export function celsiusToKelvin(c) { + if (!Number.isFinite(c)) return null; + return cleanPrecision(Math.max(0, c + 273.15)); +} + +/** Convert hectopascals (hPa) to inches of mercury (inHg). */ +export function hpaToInHg(hpa) { + if (!Number.isFinite(hpa) || hpa < 0) return null; + return cleanPrecision(hpa * 0.029529983071445, 4); +} + +/** Convert inches of mercury (inHg) to hectopascals (hPa). */ +export function inHgToHpa(inHg) { + if (!Number.isFinite(inHg) || inHg < 0) return null; + return cleanPrecision(inHg / 0.029529983071445, 2); +} + +/** Convert knots to meters per second. */ +export function knotsToMps(knots) { + if (!Number.isFinite(knots) || knots < 0) return null; + return cleanPrecision(knots * 0.514444); +} + +/** Convert meters per second to knots. */ +export function mpsToKnots(mps) { + if (!Number.isFinite(mps) || mps < 0) return null; + return cleanPrecision(mps / 0.514444); } /** Format the cockpit Context scope without overstating installation coverage. */ diff --git a/src/cockpitMath.test.mjs b/src/cockpitMath.test.mjs index ef1e838..85d5983 100644 --- a/src/cockpitMath.test.mjs +++ b/src/cockpitMath.test.mjs @@ -7,6 +7,13 @@ import { bearingBetweenCoordinates, cockpitAnchorCorrectionStep, cockpitAltitudeDisplayFt, + celsiusToFahrenheit, + fahrenheitToCelsius, + celsiusToKelvin, + hpaToInHg, + inHgToHpa, + knotsToMps, + mpsToKnots, cockpitGroundSafeHeight, cockpitSurfaceWaitExpired, cockpitUiUpdateDue, @@ -364,3 +371,21 @@ test('tracked-identity resolution falls back to layer precedence', () => { assert.equal(resolveTrackedAircraftInfo({ trackedId: 'flights:aaa077' }), null); assert.equal(resolveTrackedAircraftInfo(), null); }); + +test('telemetry unit conversions maintain exact precision and guard invalid values', () => { + // Exact foot conversion avoids 0.00032 drift on FL100 + assert.equal(cockpitAltitudeDisplayFt(3048, false), 10000); + + // Temperature conversions + assert.equal(celsiusToFahrenheit(0), 32); + assert.equal(celsiusToFahrenheit(100), 212); + assert.equal(fahrenheitToCelsius(212), 100); + assert.equal(celsiusToKelvin(-300), 0); // clamped at absolute zero + assert.equal(celsiusToFahrenheit(Number.NaN), null); + + // Pressure and speed round-trip accuracy + assert.equal(inHgToHpa(hpaToInHg(1013.25)), 1013.25); + assert.equal(hpaToInHg(-5), null); + assert.ok(Math.abs(mpsToKnots(knotsToMps(250)) - 250) < 1e-5); + assert.equal(knotsToMps(-1), null); +});