Summary
Two position plugins treat the compact NDDMM.mm/WDDDMM.mm coordinate form as plain decimal degrees scaled by 100. The real format is degrees + decimal-minutes (DDMM.mm), so the correct conversion is degrees = floor(value/100) + (value mod 100)/60. Dividing by 100 only works as a wrong-but-close-looking approximation when the minutes portion is small; in general it understates the displacement from the whole-degree boundary by a factor of 60/100, putting the aircraft on the wrong side of the nearest meridian or parallel by up to ~15 nm in latitude and ~30 nm in longitude.
Both plugins have tests that enshrine the buggy output, including a FIXME?: 35.392 comment on the Label 80 test that suggests the maintainers already suspected something is off.
Affected code
1. lib/plugins/Label_16_TOD.ts:52-62 — 2-token branch
} else if (posFields.length === 2) {
ResultFormatter.position(decodeResult, {
latitude:
(CoordinateUtils.getDirection(posFields[0][0]) *
Number(posFields[0].slice(1))) /
100,
longitude:
(CoordinateUtils.getDirection(posFields[1][0]) *
Number(posFields[1].slice(1))) /
100,
});
}
2. lib/plugins/Label_80.ts:110-124 — POS IEI
case 'POS': {
const posResult = POS_REGEX.exec(val);
const lat = Number(posResult?.groups?.lat) *
(posResult?.groups?.latd === 'S' ? -1 : 1);
const lon = Number(posResult?.groups?.lng) *
(posResult?.groups?.lngd === 'W' ? -1 : 1);
const position = {
latitude: Number.isInteger(lat) ? lat / 1000 : lat / 100,
longitude: Number.isInteger(lon) ? lon / 1000 : lon / 100,
};
...
}
The Number.isInteger(lat) ? lat / 1000 : lat / 100 branch suggests the format is "if integer, scale by 1000; if decimal, scale by 100" — but N3539.2 is degrees-and-decimal-minutes (35° 39.2'), not a fixed-point integer.
Reproduction
Label 16 TOD — using the input from Label_16_TOD.test.ts:92 ('decodes variant 3'):
message.text = '001415,20274,0047, 3740,N3835.95 W07858.88';
plugin.decode(message);
// Plugin returns:
// latitude: 38.3595 (test asserts this)
// longitude: -78.5888 (test asserts this)
//
// DDMM.mm interpretation:
// 38° 35.95' = 38 + 35.95/60 = 38.59917 (~14.2 nm north of where the plugin places the aircraft)
// 78° 58.88' = 78 + 58.88/60 = 78.98133 (~29.4 nm west of where the plugin places it)
Label 80 — from Label_80.test.ts:46-57, 'decodes POSRPT variant 2':
message.text = '...\r\n/POS N3539.2W07937.2/FL 360/...';
plugin.decode(message);
// Plugin returns:
// latitude: 35.391999999999996 // test asserts this; comment says "FIXME?: 35.392"
// longitude: -79.372 // test asserts this
//
// DDMM.mm interpretation:
// 35° 39.2' = 35.65333 (~15.7 nm off)
// 79° 37.2' = 79.62000 (~22.7 nm off, at this latitude)
The Label 80 test comment is a strong signal that the existing assertion was written defensively to lock current behavior rather than asserting correctness.
For comparison, the 4-token branch in the same Label_16_TOD plugin (lines 45-51) and the decodeStringCoordinatesDecimalMinutes helper in coordinate_utils.ts already handle the ° + ' math correctly — so the fix is to route the compact form through the same path.
Why it matters
Position reports drive every downstream consumer of the decoder — map overlays, ETA/altitude correlation, separation alerts, archival storage. Putting aircraft 15-30 nm off the correct position is the most visible kind of bug a decoder library can ship, and the tests currently make it look intentional.
Suggested fix
Use the existing CoordinateUtils.decodeStringCoordinatesDecimalMinutes helper (which is already imported in several sibling plugins) for the 2-token branch in Label_16_TOD.ts and for the POS case in Label_80.ts. Both plugins would then produce the same answers as Label_83.ts variant 3, which already handles DDMM.mm correctly.
After the fix, update the assertions in Label_16_TOD.test.ts:101-102 and Label_80.test.ts:56-57 to the corrected values, and drop the FIXME?: 35.392 comment.
Test coverage
Once the math is fixed, add a regression test with a coordinate whose minutes portion is large (e.g. N3859.99 W07759.99) where the difference between /100 and °+min/60 is dramatic — that pins the fix and prevents either approach from silently regressing back to the other.
Summary
Two position plugins treat the compact
NDDMM.mm/WDDDMM.mmcoordinate form as plain decimal degrees scaled by 100. The real format is degrees + decimal-minutes (DDMM.mm), so the correct conversion isdegrees = floor(value/100) + (value mod 100)/60. Dividing by 100 only works as a wrong-but-close-looking approximation when the minutes portion is small; in general it understates the displacement from the whole-degree boundary by a factor of 60/100, putting the aircraft on the wrong side of the nearest meridian or parallel by up to ~15 nm in latitude and ~30 nm in longitude.Both plugins have tests that enshrine the buggy output, including a
FIXME?: 35.392comment on the Label 80 test that suggests the maintainers already suspected something is off.Affected code
1.
lib/plugins/Label_16_TOD.ts:52-62— 2-token branch2.
lib/plugins/Label_80.ts:110-124—POSIEIThe
Number.isInteger(lat) ? lat / 1000 : lat / 100branch suggests the format is "if integer, scale by 1000; if decimal, scale by 100" — butN3539.2is degrees-and-decimal-minutes (35° 39.2'), not a fixed-point integer.Reproduction
Label 16 TOD — using the input from
Label_16_TOD.test.ts:92('decodes variant 3'):Label 80 — from
Label_80.test.ts:46-57,'decodes POSRPT variant 2':The Label 80 test comment is a strong signal that the existing assertion was written defensively to lock current behavior rather than asserting correctness.
For comparison, the 4-token branch in the same
Label_16_TODplugin (lines 45-51) and thedecodeStringCoordinatesDecimalMinuteshelper incoordinate_utils.tsalready handle the°+'math correctly — so the fix is to route the compact form through the same path.Why it matters
Position reports drive every downstream consumer of the decoder — map overlays, ETA/altitude correlation, separation alerts, archival storage. Putting aircraft 15-30 nm off the correct position is the most visible kind of bug a decoder library can ship, and the tests currently make it look intentional.
Suggested fix
Use the existing
CoordinateUtils.decodeStringCoordinatesDecimalMinuteshelper (which is already imported in several sibling plugins) for the 2-token branch inLabel_16_TOD.tsand for thePOScase inLabel_80.ts. Both plugins would then produce the same answers asLabel_83.tsvariant 3, which already handlesDDMM.mmcorrectly.After the fix, update the assertions in
Label_16_TOD.test.ts:101-102andLabel_80.test.ts:56-57to the corrected values, and drop theFIXME?: 35.392comment.Test coverage
Once the math is fixed, add a regression test with a coordinate whose minutes portion is large (e.g.
N3859.99 W07759.99) where the difference between/100and°+min/60is dramatic — that pins the fix and prevents either approach from silently regressing back to the other.