Worth the Drive: calculation audit + listing/detail polish

Calculation audit (brief config: 30 MPG imperial, £60 fill-up, baseline
Rontec Shelf 171.9p at 0.4 mi):

- One mile<->kilometre constant pair (kilometresPerMile 1.609344,
  metresPerMile 1609.344) replaces the mixed 1609.34 / 0.621371 / 1.60934
  used in the savings path. Effect ~2 ppm (invisible), but the conversions
  are now auditable in one place.
- New FuelSavingsCalculationAuditTests (20 deterministic tests, no live
  MKDirections) pins the brief's three stations - Tesco 170.9p/2.0 mi,
  MFG Hare & Hounds 172.9p/2.8 mi, PGG Napoleon 171.9p/2.8 mi - plus
  imperial-vs-US gallon, MPG and budget scaling, the break-even tolerance
  boundary, the nearer-candidate clamp, route-unavailable, mixed-basis
  estimates and display-rounding boundaries.
- Formula unchanged, and verified correct: fixed quantity (budget / baseline
  price), pump saving, round-trip extra distance clamped at zero, travel fuel
  priced at the candidate station, +/-5p break even, baseline labelled
  "Closest station", sorting on the unrounded net saving. The brief's "£0.18"
  for Tesco is the rounded 1 dp extra-distance caption (2.02-2.05 mi reads as
  "2.0 mi"), not a rounding bug in the money.

UI (uncommitted since d7a0ecb):

- Done bar drawn in the bottom safe area from keyboard notifications:
  .toolbar(placement: .keyboard) is device-dependent (present on the 15 Pro,
  absent on the SE on the same iOS build).
- Favourite star in the More-info heading, wired to the same fuel-scoped
  toggle as the listing rows at all three call sites.
- CHEAPEST/TOP badge moved above the price so it reads as part of it.
- Listing rows carry the real session fill-up amount instead of a hard zero
  (the compact banner does not print it today; a future row layout would).

153 tests, 0 failures. Simulator and device builds succeed.
This commit is contained in:
FuelBoard Contributor
2026-10-11 17:19:10 +01:00
parent d7a0ecb79a
commit d0a89d97d5
10 changed files with 780 additions and 51 deletions
@@ -374,4 +374,62 @@ final class FuelSavingsEstimateTests: XCTestCase {
"net saving decides the order, not the pump price")
XCTAssertEqual(ranked.last?.id, "cheap-but-far")
}
// MARK: Row copy
/// The row's extra-driving caption must follow the DISPLAY unit: the stored
/// figure is always miles, and converting by the current unit instead would
/// leave a km user's figure unlabelled and 1.6× too large.
func testExtraTravelLabelFollowsTheDisplayUnit() {
XCTAssertEqual(
FuelSavingsFormat.extraTravelLabel(extraDistanceMiles: 19.9, unit: .miles),
"19.9 mi more driving than closest"
)
XCTAssertEqual(
FuelSavingsFormat.extraTravelLabel(extraDistanceMiles: 19.9, unit: .kilometers),
"32.0 km more driving than closest"
)
XCTAssertNil(FuelSavingsFormat.extraTravelLabel(extraDistanceMiles: 0, unit: .miles),
"the baseline row has no extra travel to report")
// The same conversion backs the detail sheet's extra-distance figure,
// which used to be hardcoded miles ("%.1f miles") for km users too.
XCTAssertEqual(FuelSavingsFormat.distance(19.9, unit: .miles), "19.9 mi")
XCTAssertEqual(FuelSavingsFormat.distance(19.9, unit: .kilometers), "32.0 km")
}
/// The baseline capsule has to say what it IS: it previously repeated the
/// status word above it ("Closest station" twice) and never explained that
/// the savings are measured against it.
func testCapsuleHeadlineMarksTheBaselineAndKeepsTheEstimateMarker() {
let baseline = FuelSavingsResult(
fillLitres: 30,
grossSavingGBP: 0,
extraDrivingCostGBP: 0,
extraDistanceMiles: 0,
netSavingGBP: 0,
status: .baseline
)
XCTAssertEqual(baseline.capsuleHeadline(), "Closest station · your baseline")
let saving = FuelSavingsResult(
fillLitres: 30,
grossSavingGBP: 2.40,
extraDrivingCostGBP: 1.40,
extraDistanceMiles: 5.2,
netSavingGBP: 1.00,
status: .worthTheDrive,
distanceBasis: .straightLine
)
XCTAssertEqual(saving.capsuleHeadline(), "Save £1.00 after travel (est.)")
let worse = FuelSavingsResult(
fillLitres: 30,
grossSavingGBP: 1.40,
extraDrivingCostGBP: 1.70,
extraDistanceMiles: 6.7,
netSavingGBP: -0.31,
status: .notWorthTheDrive
)
XCTAssertEqual(worse.capsuleHeadline(), "£0.31 worse off after travel")
}
}