Skip to content

Bug: ResultFormatter.heading / airspeed / groundspeed / mach / day / month / arrivalDay / departureDay don't guard NaN — silently render "NaN" values in formatted.items #494

Description

@kevinelliott

Summary

ResultFormatter.position (lib/utils/result_formatter.ts:51-65), ResultFormatter.altitude (67-78), ResultFormatter.flightNumber (80-91), and ResultFormatter.temperature/totalAirTemp (331-359) all validate their input before pushing to formatted.items. Eight sibling formatters do not — they store whatever number they receive (including NaN) and then interpolate it into the formatted string, producing strings like "NaN knots", "NaN mach", and "NaN" in downstream UI:

Formatter Line Output on NaN input
heading 361-369 raw.heading = NaN, value: "NaN"
groundspeed 301-309 raw.groundspeed = NaN, value: "NaN knots"
airspeed 311-319 raw.airspeed = NaN, value: "NaN knots"
mach 321-329 raw.mach = NaN, value: "NaN mach"
day 440-448 raw.day = NaN, value: "NaN"
month 450-458 raw.month = NaN, value: "NaN"
departureDay 460-468 raw.departure_day = NaN, value: "NaN"
arrivalDay 470-478 raw.arrival_day = NaN, value: "NaN"

Because JavaScript's Number(...) returns NaN for common ACARS "unavailable" markers ('***', '---', '****') and any non-numeric text, every caller that passes Number(field) or parseInt(field) straight to one of these formatters propagates NaN all the way to the consumer.

Affected callers (non-exhaustive)

Real call sites already in the codebase that can silently emit NaN:

  • lib/plugins/Label_10_Slash.ts:41ResultFormatter.heading(decodeResult, Number(parts[5])) — any 10 message with parts[5] === '---' (a common "unknown heading" marker) sends NaN into heading.
  • lib/plugins/Label_2P_FM4.ts:53ResultFormatter.heading(decodeResult, Number(parts[7])).
  • lib/plugins/Label_10_Slash.ts:42ResultFormatter.altitude(decodeResult, 100 * Number(parts[6])) — this one is covered because altitude has its own isNaN guard.
  • lib/plugins/Label_44_Base.ts:52-53, Label_44_ETA.ts:37-38, Label_44_POS.ts:41-48ResultFormatter.month/day receive Number(data[4].substring(0, 2)); if data[4] is empty or non-numeric the result is NaN.
  • lib/plugins/Label_4T_ETA.ts:31-33 (before/after the fix for Bug: Label_4T_ETA dereferences etaData[2].substring with no length guard — short ETA payloads throw TypeError and abort MessageDecoder.decode() #482) — ResultFormatter.departureDay(...) / arrivalDay(...) on Number(data[1]) etc. can be NaN.

Reproduction

import { ResultFormatter } from './utils/result_formatter';

const dr: any = { raw: {}, formatted: { items: [] } };
ResultFormatter.heading(dr, Number('---'));
// dr.raw.heading === NaN
// dr.formatted.items[0].value === 'NaN'

ResultFormatter.mach(dr, Number(''));
// dr.raw.mach === 0                            (Number('') → 0, so this one is "silent zero" — see #487)
ResultFormatter.mach(dr, Number('***'));
// dr.raw.mach === NaN, value === 'NaN mach'

ResultFormatter.day(dr, Number('  '));
// dr.raw.day === NaN, value === 'NaN'

End-to-end reproduction via a real plugin (Label_10_Slash needs 17 slash-separated fields; simplest way to hit the heading NaN path):

const decoder = new MessageDecoder();
decoder.decode({
  label: '10',
  text: '/N0000/E0000//-/---/000/KJFK/000000////WPT1/WPT2/000000/WPT3/000000/KLAX',
});
// result.formatted.items includes { code: 'HDG', value: 'NaN' }
// result.raw.heading is NaN, so consumers can't distinguish "unknown" from "0".

Suggested fix

Match the pattern used by altitude (guard isNaN at the top and return without pushing), so callers can pass raw Number(field) results and unknown values disappear from formatted.items rather than surfacing as literal "NaN":

static heading(decodeResult: DecodeResult, value: number) {
  if (isNaN(value)) return;
  decodeResult.raw.heading = value;
  decodeResult.formatted.items.push({
    type: 'heading',
    code: 'HDG',
    label: 'Heading',
    value: `${value}`,
  });
}
// Same shape for airspeed, groundspeed, mach, day, month, departureDay, arrivalDay.

The fuel formatters (currentFuel, burnedFuel, remainingFuel, outFuel, offFuel, onFuel, inFuel, startFuel) have the same shape and are worth guarding while touching this file — some callers do their own isNaN check first (e.g. Label_44_Base.parseFuel, Label_44_ETA:45-48), but not all, and centralising the guard removes the requirement.

Test coverage

Add unit tests for each affected formatter covering:

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions