Files
fuelboard/FuelBoardTests/Tests/FuelBoardSharedTests/FuelSavingsTests.swift
T
FuelBoard Contributor d7a0ecb79a Add Worth the Drive fuel savings with road-routed estimates
Optional Fuel Savings feature (off by default): vehicle MPG in UK imperial gallons, fill amount in pounds or litres, persisted through the existing keychain/app-group store. Ranks the WHOLE station pool by estimated net saving (fuel-price saving at each pump minus the extra return-trip driving cost) using MapKit road distances with a straight-line fallback marked (est.).

The closest station with a valid price for the selected fuel is the comparison baseline and is labelled "Closest station"; other rows read "Save £X.XX after travel" / "£X.XX worse off after travel" / "Break even". Labels live in the shared model so they are unit-tested. The TOP badge still marks the cheapest pump price and reads "CHEAPEST" when the list is ranked by net saving.

Road routing now prioritises the Worth-the-Drive candidates plus the baseline and merges cache entries instead of replacing them, so far-out stations get real routes rather than permanent straight-line estimates. Numeric fields gain a Done button above the keypad (decimalPad has no Return key); the fill-up control is hidden unless the feature is configured; Favourites shares one session fill amount with the station list. 131 tests.
2026-10-11 14:34:53 +01:00

94 lines
5.2 KiB
Swift

import XCTest
#if SWIFT_PACKAGE
@testable import FuelBoardShared
#endif
final class FuelSavingsCalculatorTests: XCTestCase {
func testUKMPGConversionUsesImperialGallons() {
XCTAssertEqual(FuelSavingsCalculator.fuelUsedLitres(distanceMiles: 10, mpg: 45), 1.0102, accuracy: 0.0001)
}
func testPoundsBudgetConvertsToLitresUsingBaselinePrice() throws {
let amount = FuelSavingsFillAmount(value: 40, unit: .pounds)
let litres = try FuelSavingsCalculator.fillLitres(amount: amount, baselinePricePencePerLitre: 160)
XCTAssertEqual(litres, 25.0, accuracy: 0.0001)
}
func testLitresAmountPassesThrough() throws {
let amount = FuelSavingsFillAmount(value: 35, unit: .litres)
let litres = try FuelSavingsCalculator.fillLitres(amount: amount, baselinePricePencePerLitre: 160)
XCTAssertEqual(litres, 35, accuracy: 0.0001)
}
func testNetSavingSubtractsAdditionalDrivingFuelCost() throws {
let result = try FuelSavingsCalculator.calculate(
mpg: 45,
amount: FuelSavingsFillAmount(value: 40, unit: .pounds),
baselinePricePencePerLitre: 160,
candidatePricePencePerLitre: 150,
baselineJourneyMiles: 2,
candidateJourneyMiles: 8
)
XCTAssertEqual(result.fillLitres, 25, accuracy: 0.0001)
XCTAssertEqual(result.grossSavingGBP, 2.50, accuracy: 0.0001)
XCTAssertEqual(result.extraDistanceMiles, 6, accuracy: 0.0001)
XCTAssertEqual(result.extraDrivingCostGBP, 0.9092, accuracy: 0.0001)
XCTAssertEqual(result.netSavingGBP, 1.5908, accuracy: 0.0001)
XCTAssertEqual(result.status, .worthTheDrive)
}
func testNegativeAndBreakEvenOutcomes() throws {
let negative = try FuelSavingsCalculator.calculate(
mpg: 45,
amount: FuelSavingsFillAmount(value: 10, unit: .litres),
baselinePricePencePerLitre: 150,
candidatePricePencePerLitre: 149,
baselineJourneyMiles: 1,
candidateJourneyMiles: 20
)
XCTAssertEqual(negative.status, .notWorthTheDrive)
XCTAssertLessThan(negative.netSavingGBP, 0)
let even = try FuelSavingsCalculator.calculate(
mpg: 45,
amount: FuelSavingsFillAmount(value: 10, unit: .litres),
baselinePricePencePerLitre: 150,
candidatePricePencePerLitre: 150,
baselineJourneyMiles: 2,
candidateJourneyMiles: 2
)
XCTAssertEqual(even.status, .breakEven)
}
func testInvalidInputsRejected() {
XCTAssertThrowsError(try FuelSavingsCalculator.fillLitres(amount: FuelSavingsFillAmount(value: 40, unit: .pounds), baselinePricePencePerLitre: 0))
XCTAssertThrowsError(try FuelSavingsCalculator.calculate(mpg: 0, amount: FuelSavingsFillAmount(value: 40, unit: .pounds), baselinePricePencePerLitre: 160, candidatePricePencePerLitre: 150, baselineJourneyMiles: 1, candidateJourneyMiles: 2))
XCTAssertThrowsError(try FuelSavingsCalculator.calculate(mpg: 500, amount: FuelSavingsFillAmount(value: 40, unit: .pounds), baselinePricePencePerLitre: 160, candidatePricePencePerLitre: 150, baselineJourneyMiles: 1, candidateJourneyMiles: 2))
}
func testSwitchingPoundsToLitresPreservesPhysicalQuantity() throws {
let pounds = FuelSavingsFillAmount(value: 40, unit: .pounds)
let litres = try FuelSavingsCalculator.convert(amount: pounds, to: .litres, referencePricePencePerLitre: 160)
XCTAssertEqual(litres.unit, .litres)
XCTAssertEqual(litres.value, 25, accuracy: 0.0001)
let back = try FuelSavingsCalculator.convert(amount: litres, to: .pounds, referencePricePencePerLitre: 160)
XCTAssertEqual(back.value, 40, accuracy: 0.0001)
}
func testSortingPlacesUnavailableAfterSavingsAndBreaksTies() {
let a = FuelSavingsRankedStation(stationID: "a", result: .available(.init(fillLitres: 25, grossSavingGBP: 2, extraDrivingCostGBP: 1, extraDistanceMiles: 3, netSavingGBP: 1, status: .worthTheDrive)), distanceMiles: 5, pricePencePerLitre: 150)
let b = FuelSavingsRankedStation(stationID: "b", result: .available(.init(fillLitres: 25, grossSavingGBP: 3, extraDrivingCostGBP: 1, extraDistanceMiles: 2, netSavingGBP: 2, status: .worthTheDrive)), distanceMiles: 8, pricePencePerLitre: 149)
let c = FuelSavingsRankedStation(stationID: "c", result: .unavailable("No route"), distanceMiles: 1, pricePencePerLitre: 140)
XCTAssertEqual(FuelSavingsCalculator.sortedForWorthTheDrive([a, c, b]).map(\.stationID), ["b", "a", "c"])
}
}
final class FuelSavingsSettingsTests: XCTestCase {
func testSettingsValidationRequiresEnabledMPGAndFillAmount() {
XCTAssertFalse(FuelSavingsSettings(enabled: false, mpg: 45, preferredAmount: .init(value: 40, unit: .pounds)).isConfigured)
XCTAssertFalse(FuelSavingsSettings(enabled: true, mpg: 0, preferredAmount: .init(value: 40, unit: .pounds)).isConfigured)
XCTAssertFalse(FuelSavingsSettings(enabled: true, mpg: 45, preferredAmount: .init(value: 0, unit: .pounds)).isConfigured)
XCTAssertTrue(FuelSavingsSettings(enabled: true, mpg: 45, preferredAmount: .init(value: 40, unit: .pounds)).isConfigured)
}
}