diff --git a/src/layers/firms/model.js b/src/layers/firms/model.js index 4f12593f2..00873c2b2 100644 --- a/src/layers/firms/model.js +++ b/src/layers/firms/model.js @@ -535,15 +535,26 @@ export function createModel({ return `${isolate(latPart)} ${isolate(lonPart)}`; } + /** + * Millisecond delta → `{ unit: '<1h' | 'h' | 'd', count }`, or null for + * invalid input. Both age formatters read it, so they pick the same unit. + */ + + function ageParts(deltaMs) { + if (!Number.isFinite(deltaMs) || deltaMs < 0) return null; + const hours = deltaMs / 3600000; + if (hours < 1) return { unit: '<1h', count: 0 }; + const wholeHours = Math.round(hours); + if (wholeHours < 48) return { unit: 'h', count: wholeHours }; + return { unit: 'd', count: Math.round(hours / 24) }; + } + /** Millisecond delta → "<1h" / "Xh" / "Xd", or '' for invalid input. */ function formatAge(deltaMs) { - if (!Number.isFinite(deltaMs) || deltaMs < 0) return ''; - const hours = deltaMs / 3600000; - if (hours < 1) return '<1h'; - const wholeHours = Math.round(hours); - if (wholeHours < 48) return `${wholeHours}h`; - return `${Math.round(hours / 24)}d`; + const age = ageParts(deltaMs); + if (!age) return ''; + return age.unit === '<1h' ? '<1h' : `${age.count}${age.unit}`; } /** Millisecond delta → "<1m ago" / "Xm ago" / "Xh ago" (fresh-feed readout). */ @@ -574,13 +585,12 @@ export function createModel({ /** formatAge for a card: "<1h", "24h", "3d" in the UI language ("24 ч"). */ function formatAgeLabel(deltaMs) { - if (!Number.isFinite(deltaMs) || deltaMs < 0) return ''; - const hours = deltaMs / 3600000; - if (hours < 1) return t('<1h', { context: 'age' }); - const wholeHours = Math.round(hours); - if (wholeHours < 48) - return t('{count}h', { context: 'age', count: wholeHours }); - return t('{count}d', { context: 'age', count: Math.round(hours / 24) }); + const age = ageParts(deltaMs); + if (!age) return ''; + if (age.unit === '<1h') return t('<1h', { context: 'age' }); + return age.unit === 'h' + ? t('{count}h', { context: 'age', count: age.count }) + : t('{count}d', { context: 'age', count: age.count }); } /** Normalized 0..1 confidence → low/nominal/high display bucket. */ diff --git a/src/layers/firms/model.test.mjs b/src/layers/firms/model.test.mjs index a0df2eb2c..2c42227f8 100644 --- a/src/layers/firms/model.test.mjs +++ b/src/layers/firms/model.test.mjs @@ -24,3 +24,22 @@ test('a fire age that rounds to 48 hours reads in days, as 48 hours does', () => setLocale(previousLocale); } }); + +test('both fire age formatters agree on invalid, sub-hour and multi-day ages', () => { + const previousLocale = getLocale(); + const { formatAge, formatAgeLabel } = createModel({ + services: { anchors: {} }, + }); + try { + setLocale('en'); + for (const format of [formatAge, formatAgeLabel]) { + assert.equal(format(-1), ''); + assert.equal(format(Number.NaN), ''); + assert.equal(format(0.5 * HOUR_MS), '<1h'); + assert.equal(format(1 * HOUR_MS), '1h'); + assert.equal(format(72 * HOUR_MS), '3d'); + } + } finally { + setLocale(previousLocale); + } +});