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