Summary
ResultFormatter.temperature (lib/utils/result_formatter.ts:331-344) and ResultFormatter.totalAirTemp (346-359) guard value.length === 0 and then run Number(value.replace('M', '-').replace('P', '+')). That guard covers the empty-string case, but any non-numeric value (which Number(...) converts to NaN) sails past it: raw.outside_air_temperature / raw.total_air_temperature are stored as NaN, and formatted.items picks up the string "NaN degrees".
This is materially the same class of bug as #494 (heading / airspeed / groundspeed / mach / day / month / arrivalDay / departureDay). #494's opening paragraph explicitly claims temperature/totalAirTemp "validate their input before pushing to formatted.items", but the if (value.length === 0) guard only handles emptiness — it does not catch NaN from a non-numeric payload. So temperature belongs on the same fix list.
Affected code
lib/utils/result_formatter.ts:331-344
static temperature(decodeResult: DecodeResult, value: string) {
if (value.length === 0) {
return; // ← only catches empty
}
decodeResult.raw.outside_air_temperature = Number(
value.replace('M', '-').replace('P', '+'),
); // ← Number('***') → NaN, stored as-is
decodeResult.formatted.items.push({
type: 'outside_air_temperature',
code: 'OATEMP',
label: 'Outside Air Temperature (C)',
value: `${decodeResult.raw.outside_air_temperature} degrees`, // ← "NaN degrees"
});
}
totalAirTemp at lines 346-359 has the identical shape and the identical bug.
Reproduction
import { ResultFormatter } from './lib/utils/result_formatter';
const dr: any = { raw: {}, formatted: { items: [] } };
ResultFormatter.temperature(dr, '***');
// dr.raw.outside_air_temperature === NaN
// dr.formatted.items[0].value === 'NaN degrees'
ResultFormatter.totalAirTemp(dr, '----');
// dr.raw.total_air_temperature === NaN
// dr.formatted.items[0].value === 'NaN degrees'
Affected callers
Real call sites already in the codebase that can hit the NaN path with production-shaped payloads (ACARS carriers commonly send ***, ----, ** etc. to mean "unavailable"):
lib/utils/arinc_702_helper.ts:188 — ResultFormatter.temperature(decodeResult, data[6]) in processPS (variant 7), no upstream validation of data[6].
lib/utils/arinc_702_helper.ts:196 — ResultFormatter.temperature(decodeResult, data[7]) in processPS (variant 2), no upstream validation.
lib/utils/arinc_702_helper.ts:212 — ResultFormatter.temperature(decodeResult, data[7]) in processPosition, no upstream validation.
lib/plugins/Label_4A.ts:55 — ResultFormatter.temperature(decodeResult, fields[3]) on 4A variant-2 messages, no upstream validation.
(processPerformanceData at arinc_702_helper.ts:491-493 guards startsWith('M'/'P') || !isNaN(Number(...)) before calling, so it is already safe.)
Suggested fix
Match the pattern used by altitude (line 67-78) and by the proposed fix in #494 — add a Number.isFinite guard after the conversion so unknown values disappear from formatted.items rather than surfacing as literal "NaN":
static temperature(decodeResult: DecodeResult, value: string) {
if (value.length === 0) return;
const t = Number(value.replace('M', '-').replace('P', '+'));
if (!Number.isFinite(t)) return;
decodeResult.raw.outside_air_temperature = t;
decodeResult.formatted.items.push({
type: 'outside_air_temperature',
code: 'OATEMP',
label: 'Outside Air Temperature (C)',
value: `${t} degrees`,
});
}
// Same shape for totalAirTemp.
Test coverage
Add unit tests for each formatter covering the two failure modes:
temperature(dr, '***') → no item pushed, raw.outside_air_temperature left unset
temperature(dr, 'M25') → item pushed with value: '-25 degrees', raw.outside_air_temperature === -25
totalAirTemp(dr, '----') → no item pushed, raw.total_air_temperature left unset
totalAirTemp(dr, 'P12') → item pushed with value: '12 degrees'
An end-to-end test through Arinc702Helper.processPS with data[6] = '**' would also lock the fix in against the real caller.
Summary
ResultFormatter.temperature(lib/utils/result_formatter.ts:331-344) andResultFormatter.totalAirTemp(346-359) guardvalue.length === 0and then runNumber(value.replace('M', '-').replace('P', '+')). That guard covers the empty-string case, but any non-numeric value (whichNumber(...)converts toNaN) sails past it:raw.outside_air_temperature/raw.total_air_temperatureare stored asNaN, andformatted.itemspicks up the string"NaN degrees".This is materially the same class of bug as #494 (heading / airspeed / groundspeed / mach / day / month / arrivalDay / departureDay). #494's opening paragraph explicitly claims temperature/
totalAirTemp"validate their input before pushing toformatted.items", but theif (value.length === 0)guard only handles emptiness — it does not catchNaNfrom a non-numeric payload. So temperature belongs on the same fix list.Affected code
lib/utils/result_formatter.ts:331-344totalAirTempat lines 346-359 has the identical shape and the identical bug.Reproduction
Affected callers
Real call sites already in the codebase that can hit the
NaNpath with production-shaped payloads (ACARS carriers commonly send***,----,**etc. to mean "unavailable"):lib/utils/arinc_702_helper.ts:188—ResultFormatter.temperature(decodeResult, data[6])inprocessPS(variant 7), no upstream validation ofdata[6].lib/utils/arinc_702_helper.ts:196—ResultFormatter.temperature(decodeResult, data[7])inprocessPS(variant 2), no upstream validation.lib/utils/arinc_702_helper.ts:212—ResultFormatter.temperature(decodeResult, data[7])inprocessPosition, no upstream validation.lib/plugins/Label_4A.ts:55—ResultFormatter.temperature(decodeResult, fields[3])on 4A variant-2 messages, no upstream validation.(
processPerformanceDataatarinc_702_helper.ts:491-493guardsstartsWith('M'/'P') || !isNaN(Number(...))before calling, so it is already safe.)Suggested fix
Match the pattern used by
altitude(line 67-78) and by the proposed fix in #494 — add aNumber.isFiniteguard after the conversion so unknown values disappear fromformatted.itemsrather than surfacing as literal"NaN":Test coverage
Add unit tests for each formatter covering the two failure modes:
temperature(dr, '***')→ no item pushed,raw.outside_air_temperatureleft unsettemperature(dr, 'M25')→ item pushed withvalue: '-25 degrees',raw.outside_air_temperature === -25totalAirTemp(dr, '----')→ no item pushed,raw.total_air_temperatureleft unsettotalAirTemp(dr, 'P12')→ item pushed withvalue: '12 degrees'An end-to-end test through
Arinc702Helper.processPSwithdata[6] = '**'would also lock the fix in against the real caller.