From bd6e6312e18943c077328155f0a11a8f344c2d7e Mon Sep 17 00:00:00 2001 From: Maurice Date: Thu, 10 Sep 2026 19:32:19 +0200 Subject: [PATCH] fix(roadtrip): keep a typed range when the parts round away to nothing A consumption typed as 500 rather than 5.0 makes the tank arithmetic round to a range of zero. Zero is a number, so the `??` in effectiveRangeKm took it as the answer and discarded the range the traveller had typed by hand next to it, and useVehicleRange then folded that zero to null. What reached the rail was "no range at all": every fuel and charge warning went quiet, with nothing on screen naming the field that did it. A range that rounds below one kilometre is a typo rather than a car, so it is reported as no answer and the typed figure gets its turn. --- .../components/Roadtrip/vehicleRange.test.ts | 16 ++++++++++++++++ client/src/components/Roadtrip/vehicleRange.ts | 17 +++++++++++++++-- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/client/src/components/Roadtrip/vehicleRange.test.ts b/client/src/components/Roadtrip/vehicleRange.test.ts index 9e5cf4f01..eaabec5e1 100644 --- a/client/src/components/Roadtrip/vehicleRange.test.ts +++ b/client/src/components/Roadtrip/vehicleRange.test.ts @@ -105,6 +105,22 @@ describe('unit round trip', () => { expect(showSpec('batteryKwh', 58, true)).toBe(58) }) + it('FE-ROADTRIP-VEHRANGE-016: a consumption typed a hundred times too high does not silence the typed range', () => { + // A consumption of 500 rather than 5.0 makes the parts round to nothing. Zero used + // to count as an answer, so the `??` in effectiveRangeKm never reached the 500 km + // the traveller had typed by hand, and every range warning went quiet with nothing + // on screen to say which field did it. + expect(rangeFromSpec('combustion', { tankLitres: 1, litresPer100: 500 })).toBeNull() + expect(effectiveRangeKm('combustion', { tankLitres: 1, litresPer100: 500 }, 500)).toBe(500) + + // Same shape on the other kind of car, where a decimal point is just as easy to lose. + expect(rangeFromSpec('electric', { batteryKwh: 0.5, kwhPer100: 200 })).toBeNull() + expect(effectiveRangeKm('electric', { batteryKwh: 0.5, kwhPer100: 200 }, 420)).toBe(420) + + // One kilometre is still a figure, so the cut is below it and not at some round guess. + expect(rangeFromSpec('combustion', { tankLitres: 1, litresPer100: 100 })).toBe(1) + }) + it('FE-ROADTRIP-VEHRANGE-015: an empty field stays empty rather than becoming a zero', () => { expect(showSpec('tankLitres', undefined, false)).toBeUndefined() expect(showSpec('tankLitres', 0, true)).toBeUndefined() diff --git a/client/src/components/Roadtrip/vehicleRange.ts b/client/src/components/Roadtrip/vehicleRange.ts index 44532e47c..cda588664 100644 --- a/client/src/components/Roadtrip/vehicleRange.ts +++ b/client/src/components/Roadtrip/vehicleRange.ts @@ -46,12 +46,25 @@ function positive(value: number | null | undefined): number | null { * Null rather than a guess whenever a piece is missing: half a specification is not a * range, and a made-up figure here becomes a fuel warning that fires at the wrong place. */ +/** + * A rounded range that came out at nothing is a typo, not a car. + * + * A consumption typed as 500 rather than 5.0 rounds to a range of zero, and zero is a + * number: `effectiveRangeKm` would take it as the answer and throw away the range the + * traveller had typed by hand right next to it. What reached the rail was "no range at + * all", so every warning went quiet, with nothing on screen to say which field did it. + * Reported as nothing instead, so the typed figure gets its turn. + */ +function atLeastAKilometre(km: number): number | null { + return km >= 1 ? km : null +} + export function rangeFromSpec(vehicle: VehicleKind | null | undefined, spec: VehicleSpec): number | null { if (vehicle === 'combustion') { const tank = positive(spec.tankLitres) const per100 = positive(spec.litresPer100) if (!tank || !per100) return null - return Math.round((tank / per100) * 100) + return atLeastAKilometre(Math.round((tank / per100) * 100)) } if (vehicle === 'electric') { @@ -63,7 +76,7 @@ export function rangeFromSpec(vehicle: VehicleKind | null | undefined, spec: Veh // the first metre of the trip. const lost = Math.min(90, Math.max(0, positive(spec.degradationPercent) ?? 0)) const usable = battery * (1 - lost / 100) - return Math.round((usable / per100) * 100) + return atLeastAKilometre(Math.round((usable / per100) * 100)) } return null