Skip to content

Bug: ResultFormatter.temperature / totalAirTemp render "NaN degrees" for non-numeric input — only guard empty string, not NaN #496

Description

@kevinelliott

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:188ResultFormatter.temperature(decodeResult, data[6]) in processPS (variant 7), no upstream validation of data[6].
  • lib/utils/arinc_702_helper.ts:196ResultFormatter.temperature(decodeResult, data[7]) in processPS (variant 2), no upstream validation.
  • lib/utils/arinc_702_helper.ts:212ResultFormatter.temperature(decodeResult, data[7]) in processPosition, no upstream validation.
  • lib/plugins/Label_4A.ts:55ResultFormatter.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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions