503 lines
23 KiB
Swift
503 lines
23 KiB
Swift
import XCTest
|
|
#if SWIFT_PACKAGE
|
|
@testable import FuelBoardShared
|
|
#endif
|
|
|
|
/// Worth the Drive calculation audit.
|
|
///
|
|
/// Written against the audit brief (30 MPG imperial, £60 fill-up, baseline
|
|
/// Rontec Shelf at 171.9p, 0.4 miles). The brief's three observed results are
|
|
/// reproduced here exactly, so any future change to the formula breaks a test
|
|
/// instead of silently shifting the numbers on screen.
|
|
///
|
|
/// Deterministic journey distances only — never live MKDirections responses.
|
|
final class FuelSavingsCalculationAuditTests: XCTestCase {
|
|
private func station(_ id: String, price: Double?) -> FuelStation {
|
|
FuelStation(
|
|
id: id,
|
|
name: id,
|
|
brand: "X",
|
|
address: "",
|
|
postcode: "",
|
|
lat: 51.5,
|
|
lng: -0.1,
|
|
prices: price.map { [FuelType.e10: $0] } ?? [:],
|
|
priceUpdated: nil
|
|
)
|
|
}
|
|
|
|
/// Journeys are OUT AND BACK (the app's convention): the brief's baseline is
|
|
/// 0.4 miles away, so its journey is 0.8 miles, and "extra driving" of N
|
|
/// miles means a candidate journey of 0.8 + N.
|
|
private func journey(_ miles: Double, basis: FuelSavingsDistanceBasis = .roadRoute) -> FuelSavingsJourney {
|
|
FuelSavingsJourney(miles: miles, basis: basis)
|
|
}
|
|
|
|
private let baselineJourneyMiles = 0.8
|
|
|
|
private func briefResult(candidatePrice: Double, extraMiles: Double) throws -> FuelSavingsResult {
|
|
try FuelSavingsCalculator.calculate(
|
|
mpg: 30,
|
|
amount: FuelSavingsFillAmount(value: 60, unit: .pounds),
|
|
baselinePricePencePerLitre: 171.9,
|
|
candidatePricePencePerLitre: candidatePrice,
|
|
baselineJourneyMiles: baselineJourneyMiles,
|
|
candidateJourneyMiles: baselineJourneyMiles + extraMiles
|
|
)
|
|
}
|
|
|
|
// MARK: - 1. Unit conversion
|
|
|
|
func testImperialGallonIsUsedNotTheUSGallon() {
|
|
XCTAssertEqual(FuelSavingsCalculator.imperialGallonLitres, 4.54609)
|
|
XCTAssertNotEqual(FuelSavingsCalculator.imperialGallonLitres, 3.785411784,
|
|
"a US gallon must never be used: it would understate every travel cost")
|
|
|
|
// 30 miles at 30 MPG burns exactly one imperial gallon.
|
|
XCTAssertEqual(FuelSavingsCalculator.fuelUsedLitres(distanceMiles: 30, mpg: 30), 4.54609, accuracy: 1e-12)
|
|
// 100 miles at 50 MPG.
|
|
XCTAssertEqual(FuelSavingsCalculator.fuelUsedLitres(distanceMiles: 100, mpg: 50), 9.09218, accuracy: 1e-12)
|
|
// Zero distance burns nothing, and the relationship is linear in distance.
|
|
XCTAssertEqual(FuelSavingsCalculator.fuelUsedLitres(distanceMiles: 0, mpg: 30), 0, accuracy: 1e-12)
|
|
XCTAssertEqual(FuelSavingsCalculator.fuelUsedLitres(distanceMiles: 60, mpg: 30),
|
|
2 * FuelSavingsCalculator.fuelUsedLitres(distanceMiles: 30, mpg: 30),
|
|
accuracy: 1e-12)
|
|
}
|
|
|
|
func testMileAndKilometreFactorsAreTheExactOnes() {
|
|
XCTAssertEqual(FuelSavingsCalculator.kilometresPerMile, 1.609344)
|
|
XCTAssertEqual(FuelSavingsCalculator.metresPerMile, 1609.344)
|
|
XCTAssertEqual(FuelSavingsCalculator.metresPerMile,
|
|
FuelSavingsCalculator.kilometresPerMile * 1000,
|
|
accuracy: 1e-9,
|
|
"the metre and kilometre factors must describe the same mile")
|
|
}
|
|
|
|
func testDistanceLabelUsesTheSameConversionAsTheModel() {
|
|
// One mile, shown in kilometres, goes through the single factor.
|
|
XCTAssertEqual(
|
|
FuelSavingsFormat.distance(1, unit: .kilometers),
|
|
DistanceUnit.kilometers.format(FuelSavingsCalculator.kilometresPerMile)
|
|
)
|
|
XCTAssertEqual(FuelSavingsFormat.distance(1, unit: .miles), DistanceUnit.miles.format(1.609344))
|
|
}
|
|
|
|
// MARK: - 2. Candidate-priced fill-up
|
|
|
|
func testPoundsFillUsesCandidatePriceForLitresAndGrossSaving() throws {
|
|
let result = try FuelSavingsCalculator.calculate(
|
|
mpg: 30,
|
|
amount: FuelSavingsFillAmount(value: 60, unit: .pounds),
|
|
baselinePricePencePerLitre: 171.9,
|
|
candidatePricePencePerLitre: 169.9,
|
|
baselineJourneyMiles: 0.879,
|
|
candidateJourneyMiles: 8.306
|
|
)
|
|
|
|
XCTAssertEqual(result.fillLitres, 60 / 1.699, accuracy: 1e-9)
|
|
XCTAssertEqual(result.grossSavingGBP, (60 / 1.699) * 0.02, accuracy: 1e-9)
|
|
XCTAssertEqual(FuelSavingsFormat.money(result.grossSavingGBP), "£0.71")
|
|
XCTAssertEqual(result.extraFuelLitres, 1.1254603476666667, accuracy: 1e-9)
|
|
XCTAssertEqual(result.extraDrivingCostGBP, 1.9121571306856667, accuracy: 1e-9)
|
|
XCTAssertEqual(result.netSavingGBP, -1.2058593084372853, accuracy: 1e-9)
|
|
XCTAssertEqual(result.headline(), "Costs £1.21 more")
|
|
}
|
|
|
|
func testPoundsExamplesScaleFromThirtyToOneHundredPounds() throws {
|
|
for (budget, expectedLitres, expectedGross) in [
|
|
(30.0, 17.657445556209535, 0.3531489111241907),
|
|
(60.0, 35.31489111241907, 0.7062978222483814),
|
|
(100.0, 58.85815185403179, 1.1771630370806357)
|
|
] {
|
|
let result = try FuelSavingsCalculator.calculate(
|
|
mpg: 30,
|
|
amount: FuelSavingsFillAmount(value: budget, unit: .pounds),
|
|
baselinePricePencePerLitre: 171.9,
|
|
candidatePricePencePerLitre: 169.9,
|
|
baselineJourneyMiles: 0,
|
|
candidateJourneyMiles: 0
|
|
)
|
|
XCTAssertEqual(result.fillLitres, expectedLitres, accuracy: 1e-8)
|
|
XCTAssertEqual(result.grossSavingGBP, expectedGross, accuracy: 1e-8)
|
|
}
|
|
}
|
|
|
|
func testCandidatePriceControlsLitresWhileBaselinePriceOnlyChangesSaving() throws {
|
|
let amount = FuelSavingsFillAmount(value: 60, unit: .pounds)
|
|
let first = try FuelSavingsCalculator.calculate(
|
|
mpg: 30, amount: amount,
|
|
baselinePricePencePerLitre: 171.9,
|
|
candidatePricePencePerLitre: 169.9,
|
|
baselineJourneyMiles: 0, candidateJourneyMiles: 0
|
|
)
|
|
let differentBaseline = try FuelSavingsCalculator.calculate(
|
|
mpg: 30, amount: amount,
|
|
baselinePricePencePerLitre: 179.9,
|
|
candidatePricePencePerLitre: 169.9,
|
|
baselineJourneyMiles: 0, candidateJourneyMiles: 0
|
|
)
|
|
let differentCandidate = try FuelSavingsCalculator.calculate(
|
|
mpg: 30, amount: amount,
|
|
baselinePricePencePerLitre: 179.9,
|
|
candidatePricePencePerLitre: 159.9,
|
|
baselineJourneyMiles: 0, candidateJourneyMiles: 0
|
|
)
|
|
|
|
XCTAssertEqual(first.fillLitres, differentBaseline.fillLitres, accuracy: 1e-12)
|
|
XCTAssertNotEqual(first.grossSavingGBP, differentBaseline.grossSavingGBP)
|
|
XCTAssertNotEqual(differentBaseline.fillLitres, differentCandidate.fillLitres)
|
|
}
|
|
|
|
// MARK: - 3/5. The three stations from the brief
|
|
|
|
func testBriefCaseTescoBradfordIsMarginallyWorseOff() throws {
|
|
let result = try briefResult(candidatePrice: 170.9, extraMiles: 2.0)
|
|
|
|
XCTAssertEqual(result.fillLitres, 35.108250, accuracy: 1e-6)
|
|
XCTAssertEqual(result.grossSavingGBP, 0.351083, accuracy: 1e-6, "£60 buys 35.11 L at the candidate price")
|
|
XCTAssertEqual(result.extraDistanceMiles, 2.0, accuracy: 1e-9)
|
|
XCTAssertEqual(result.extraDrivingCostGBP, 0.517951, accuracy: 1e-6)
|
|
XCTAssertEqual(result.netSavingGBP, -0.166869, accuracy: 1e-6)
|
|
XCTAssertEqual(result.status, .notWorthTheDrive)
|
|
XCTAssertEqual(result.headline(), "Costs £0.17 more")
|
|
}
|
|
|
|
/// The brief recorded "£0.18 worse off" for this row against a displayed
|
|
/// "2.0 miles" of extra driving; the brief also warns that the displayed
|
|
/// mileage is rounded. This pins that explanation instead of treating it as
|
|
/// a calculation bug: at an extra distance the caption still shows as
|
|
/// "2.0 mi" (2.00-2.05), the headline reads £0.17-£0.18, and the difference
|
|
/// is entirely the rounding of the distance, not of the money.
|
|
func testTheReportedEighteenPenceIsRoundedExtraDistanceNotARoundingBug() throws {
|
|
XCTAssertEqual(try briefResult(candidatePrice: 170.9, extraMiles: 2.00).headline(),
|
|
"Costs £0.17 more")
|
|
XCTAssertEqual(try briefResult(candidatePrice: 170.9, extraMiles: 2.05).headline(),
|
|
"Costs £0.18 more")
|
|
|
|
// 2.05 mi shows as "2.0 mi" at one decimal place — the same caption.
|
|
XCTAssertEqual(FuelSavingsFormat.extraTravelLabel(extraDistanceMiles: 2.05, unit: .miles),
|
|
"2.0 mi extra return travel")
|
|
XCTAssertEqual(FuelSavingsFormat.extraTravelLabel(extraDistanceMiles: 2.00, unit: .miles),
|
|
"2.0 mi extra return travel")
|
|
}
|
|
|
|
func testBriefCaseMFGHareAndHoundsIsWorseOff() throws {
|
|
let result = try briefResult(candidatePrice: 172.9, extraMiles: 2.8)
|
|
|
|
XCTAssertEqual(result.fillLitres, 34.702140, accuracy: 1e-6)
|
|
XCTAssertEqual(result.grossSavingGBP, -0.347021, accuracy: 1e-6, "£60 buys 34.70 L at the candidate price")
|
|
XCTAssertEqual(result.extraDistanceMiles, 2.8, accuracy: 1e-9)
|
|
XCTAssertEqual(result.extraDrivingCostGBP, 0.733618, accuracy: 1e-6)
|
|
XCTAssertEqual(result.netSavingGBP, -1.080639, accuracy: 1e-6)
|
|
XCTAssertEqual(result.status, .notWorthTheDrive)
|
|
XCTAssertEqual(result.headline(), "Costs £1.08 more")
|
|
}
|
|
|
|
func testBriefCasePGGNapoleonSamePumpPriceIsTravelCostOnly() throws {
|
|
let result = try briefResult(candidatePrice: 171.9, extraMiles: 2.8)
|
|
|
|
XCTAssertEqual(result.grossSavingGBP, 0, accuracy: 1e-9, "same pump price saves nothing at the pump")
|
|
XCTAssertEqual(result.extraDrivingCostGBP, 0.729375, accuracy: 1e-6)
|
|
XCTAssertEqual(result.netSavingGBP, -0.729375, accuracy: 1e-6,
|
|
"with no price difference the whole result is the extra driving")
|
|
XCTAssertEqual(result.status, .notWorthTheDrive)
|
|
XCTAssertEqual(result.headline(), "Costs £0.73 more")
|
|
}
|
|
|
|
// MARK: - 5/6. Classification, tolerance and rounding
|
|
|
|
func testSamePriceAndSameDistanceIsBreakEven() throws {
|
|
let result = try briefResult(candidatePrice: 171.9, extraMiles: 0)
|
|
XCTAssertEqual(result.grossSavingGBP, 0, accuracy: 1e-9)
|
|
XCTAssertEqual(result.extraDrivingCostGBP, 0, accuracy: 1e-9)
|
|
XCTAssertEqual(result.netSavingGBP, 0, accuracy: 1e-9)
|
|
XCTAssertEqual(result.status, .breakEven)
|
|
XCTAssertEqual(result.headline(), "Break even")
|
|
}
|
|
|
|
func testSmallSavingInsideTheToleranceIsBreakEvenNotPennies() throws {
|
|
// 171.9p vs 170.9p over a 1.23 mi delta: the small saving remains within tolerance.
|
|
let result = try briefResult(candidatePrice: 170.9, extraMiles: 1.23)
|
|
XCTAssertEqual(result.netSavingGBP, 0.03254252417853143, accuracy: 1e-9)
|
|
XCTAssertEqual(result.status, .breakEven, "±5p reads as break even rather than a meaningless £0.03")
|
|
XCTAssertEqual(result.headline(), "Break even")
|
|
}
|
|
|
|
func testBreakEvenToleranceDecidesTheClassificationAtTheBoundary() throws {
|
|
let net = -0.166869
|
|
// A tolerance wider than the loss → break even; narrower → worse off.
|
|
let lenient = try FuelSavingsCalculator.calculate(
|
|
mpg: 30,
|
|
amount: FuelSavingsFillAmount(value: 60, unit: .pounds),
|
|
baselinePricePencePerLitre: 171.9,
|
|
candidatePricePencePerLitre: 170.9,
|
|
baselineJourneyMiles: baselineJourneyMiles,
|
|
candidateJourneyMiles: baselineJourneyMiles + 2.0,
|
|
breakEvenToleranceGBP: 0.17
|
|
)
|
|
XCTAssertEqual(lenient.netSavingGBP, net, accuracy: 1e-6)
|
|
XCTAssertEqual(lenient.status, .breakEven)
|
|
|
|
let strict = try FuelSavingsCalculator.calculate(
|
|
mpg: 30,
|
|
amount: FuelSavingsFillAmount(value: 60, unit: .pounds),
|
|
baselinePricePencePerLitre: 171.9,
|
|
candidatePricePencePerLitre: 170.9,
|
|
baselineJourneyMiles: baselineJourneyMiles,
|
|
candidateJourneyMiles: baselineJourneyMiles + 2.0,
|
|
breakEvenToleranceGBP: 0.16
|
|
)
|
|
XCTAssertEqual(strict.status, .notWorthTheDrive)
|
|
}
|
|
|
|
func testMoneyIsRoundedOnlyForDisplay() throws {
|
|
// Full precision retained internally…
|
|
let result = try briefResult(candidatePrice: 172.9, extraMiles: 2.8)
|
|
XCTAssertEqual(result.netSavingGBP, -1.080639, accuracy: 1e-6)
|
|
XCTAssertNotEqual(result.netSavingGBP, -1.08, "the stored value must not be pre-rounded")
|
|
// …and rounded to two places only when shown.
|
|
XCTAssertEqual(FuelSavingsFormat.money(1.082658), "£1.08")
|
|
XCTAssertEqual(FuelSavingsFormat.money(0.729375), "£0.73")
|
|
XCTAssertEqual(FuelSavingsFormat.money(0.1749), "£0.17")
|
|
XCTAssertEqual(FuelSavingsFormat.money(0.176), "£0.18")
|
|
XCTAssertEqual(FuelSavingsFormat.money(0), "£0.00")
|
|
}
|
|
|
|
func testIndependentlyRoundedFinancialComponentsMayDifferFromRoundedTotalByOnePenny() {
|
|
let gross = 1.234
|
|
let drivingCost = 0.005
|
|
let net = gross - drivingCost
|
|
|
|
XCTAssertEqual(FuelSavingsFormat.money(gross), "£1.23")
|
|
XCTAssertEqual(FuelSavingsFormat.money(drivingCost), "£0.01")
|
|
XCTAssertEqual(FuelSavingsFormat.money(net), "£1.23")
|
|
let independentlyRoundedSubtraction = 1.23 - 0.01
|
|
XCTAssertEqual(independentlyRoundedSubtraction, 1.22, accuracy: 1e-12)
|
|
XCTAssertNotEqual(
|
|
FuelSavingsFormat.money(independentlyRoundedSubtraction),
|
|
FuelSavingsFormat.money(net),
|
|
"the raw total remains authoritative; independently rounded components need not add back exactly"
|
|
)
|
|
}
|
|
|
|
func testSortingUsesTheUnroundedNetSoRoundedTiesStillOrderCorrectly() {
|
|
// Two rows that both DISPLAY "£0.18 worse off" but differ in the raw
|
|
// figure: the sort must use the raw value, never the shown one.
|
|
func ranked(_ id: String, net: Double) -> FuelSavingsRankedStation {
|
|
FuelSavingsRankedStation(
|
|
stationID: id,
|
|
result: .available(FuelSavingsResult(
|
|
fillLitres: 34.9,
|
|
grossSavingGBP: 0,
|
|
extraDrivingCostGBP: abs(net),
|
|
extraDistanceMiles: 2.0,
|
|
netSavingGBP: net,
|
|
status: .notWorthTheDrive
|
|
)),
|
|
distanceMiles: 2.8,
|
|
pricePencePerLitre: 171.9
|
|
)
|
|
}
|
|
let worse = ranked("worse", net: -0.1751)
|
|
let lessWorse = ranked("less-worse", net: -0.1749)
|
|
XCTAssertEqual(FuelSavingsFormat.money(abs(worse.result.result!.netSavingGBP)), "£0.18")
|
|
XCTAssertEqual(FuelSavingsFormat.money(abs(lessWorse.result.result!.netSavingGBP)), "£0.17")
|
|
|
|
let sorted = FuelSavingsCalculator.sortedForWorthTheDrive([worse, lessWorse])
|
|
XCTAssertEqual(sorted.first?.stationID, "less-worse",
|
|
"the smaller loss must rank first even when the money rounds to the same figure")
|
|
}
|
|
|
|
// MARK: - 3. Distances and travel cost
|
|
|
|
func testCheaperFuelFurtherAwayCanStillBeWorthTheDrive() throws {
|
|
// 145.9p nearby vs 138.9p five miles further out, £40 fill, 45 MPG.
|
|
let result = try FuelSavingsCalculator.calculate(
|
|
mpg: 45,
|
|
amount: FuelSavingsFillAmount(value: 40, unit: .pounds),
|
|
baselinePricePencePerLitre: 145.9,
|
|
candidatePricePencePerLitre: 138.9,
|
|
baselineJourneyMiles: 2.0,
|
|
candidateJourneyMiles: 7.0
|
|
)
|
|
XCTAssertEqual(result.fillLitres, 28.797696, accuracy: 1e-6)
|
|
XCTAssertEqual(result.grossSavingGBP, 2.015839, accuracy: 1e-6)
|
|
XCTAssertEqual(result.extraDrivingCostGBP, 0.701613, accuracy: 1e-6)
|
|
XCTAssertEqual(result.netSavingGBP, 1.314226, accuracy: 1e-6)
|
|
XCTAssertEqual(result.status, .worthTheDrive)
|
|
XCTAssertEqual(result.headline(), "Save £1.31")
|
|
}
|
|
|
|
func testNearerCandidateIsNeverChargedExtraTravel() throws {
|
|
// The baseline is the nearest priced station, so this should not happen
|
|
// in the app — but the clamp must hold if it ever does.
|
|
let result = try FuelSavingsCalculator.calculate(
|
|
mpg: 30,
|
|
amount: FuelSavingsFillAmount(value: 60, unit: .pounds),
|
|
baselinePricePencePerLitre: 171.9,
|
|
candidatePricePencePerLitre: 169.9,
|
|
baselineJourneyMiles: 2.0,
|
|
candidateJourneyMiles: 1.0
|
|
)
|
|
XCTAssertEqual(result.extraDistanceMiles, 0, "a shorter journey is never a negative extra distance")
|
|
XCTAssertEqual(result.extraDrivingCostGBP, 0, accuracy: 1e-12)
|
|
XCTAssertEqual(result.netSavingGBP, result.grossSavingGBP, accuracy: 1e-12)
|
|
}
|
|
|
|
func testDifferentMPGValuesScaleOnlyTheTravelCost() throws {
|
|
let sixty = try FuelSavingsCalculator.calculate(
|
|
mpg: 60,
|
|
amount: FuelSavingsFillAmount(value: 60, unit: .pounds),
|
|
baselinePricePencePerLitre: 171.9,
|
|
candidatePricePencePerLitre: 172.9,
|
|
baselineJourneyMiles: 0.8,
|
|
candidateJourneyMiles: 3.6
|
|
)
|
|
XCTAssertEqual(sixty.grossSavingGBP, -0.347021, accuracy: 1e-6, "MPG must not touch the pump saving")
|
|
XCTAssertEqual(sixty.extraDrivingCostGBP, 0.366809, accuracy: 1e-6)
|
|
XCTAssertEqual(sixty.netSavingGBP, -0.713830, accuracy: 1e-6)
|
|
|
|
let fortyFive = try FuelSavingsCalculator.calculate(
|
|
mpg: 45,
|
|
amount: FuelSavingsFillAmount(value: 60, unit: .pounds),
|
|
baselinePricePencePerLitre: 171.9,
|
|
candidatePricePencePerLitre: 172.9,
|
|
baselineJourneyMiles: 0.8,
|
|
candidateJourneyMiles: 3.6
|
|
)
|
|
XCTAssertEqual(fortyFive.extraDrivingCostGBP, 0.489078, accuracy: 1e-6)
|
|
XCTAssertEqual(fortyFive.netSavingGBP, -0.836100, accuracy: 1e-6)
|
|
}
|
|
|
|
func testDifferentFillBudgetsScaleOnlyThePumpSaving() throws {
|
|
let forty = try FuelSavingsCalculator.calculate(
|
|
mpg: 30,
|
|
amount: FuelSavingsFillAmount(value: 40, unit: .pounds),
|
|
baselinePricePencePerLitre: 171.9,
|
|
candidatePricePencePerLitre: 172.9,
|
|
baselineJourneyMiles: 0.8,
|
|
candidateJourneyMiles: 3.6
|
|
)
|
|
XCTAssertEqual(forty.fillLitres, 23.134760, accuracy: 1e-6, "£40 buys 23.13 L at the candidate price")
|
|
XCTAssertEqual(forty.grossSavingGBP, -0.231348, accuracy: 1e-6)
|
|
XCTAssertEqual(forty.extraDrivingCostGBP, 0.733618, accuracy: 1e-6, "the journey cost is budget-independent")
|
|
XCTAssertEqual(forty.netSavingGBP, -0.964965, accuracy: 1e-6)
|
|
|
|
let ninety = try FuelSavingsCalculator.calculate(
|
|
mpg: 30,
|
|
amount: FuelSavingsFillAmount(value: 90, unit: .pounds),
|
|
baselinePricePencePerLitre: 171.9,
|
|
candidatePricePencePerLitre: 172.9,
|
|
baselineJourneyMiles: 0.8,
|
|
candidateJourneyMiles: 3.6
|
|
)
|
|
XCTAssertEqual(ninety.fillLitres, 52.053210, accuracy: 1e-6)
|
|
XCTAssertEqual(ninety.grossSavingGBP, -0.520532, accuracy: 1e-6)
|
|
XCTAssertEqual(ninety.netSavingGBP, -1.254150, accuracy: 1e-6)
|
|
}
|
|
|
|
func testFillUpInLitresIsFixedRegardlessOfPrice() throws {
|
|
// 40 L is 40 L: no baseline price is involved in the quantity.
|
|
let result = try FuelSavingsCalculator.calculate(
|
|
mpg: 30,
|
|
amount: FuelSavingsFillAmount(value: 40, unit: .litres),
|
|
baselinePricePencePerLitre: 171.9,
|
|
candidatePricePencePerLitre: 165.9,
|
|
baselineJourneyMiles: 0.8,
|
|
candidateJourneyMiles: 3.6
|
|
)
|
|
XCTAssertEqual(result.fillLitres, 40, accuracy: 1e-9)
|
|
XCTAssertEqual(result.grossSavingGBP, 40 * 0.06, accuracy: 1e-9)
|
|
}
|
|
|
|
// MARK: - 7. Failures never invent a saving
|
|
|
|
func testUnavailableRouteProducesNoSavingAtAll() {
|
|
let baseline = station("baseline", price: 171.9)
|
|
let candidate = station("candidate", price: 170.9)
|
|
let settings = FuelSavingsSettings(
|
|
enabled: true,
|
|
mpg: 30,
|
|
preferredAmount: FuelSavingsFillAmount(value: 60, unit: .pounds)
|
|
)
|
|
|
|
let noRoute = FuelSavingsEstimator.availability(
|
|
station: candidate,
|
|
fuel: .e10,
|
|
baseline: baseline,
|
|
settings: settings,
|
|
amount: FuelSavingsFillAmount(value: 60, unit: .pounds),
|
|
journey: nil,
|
|
baselineJourney: journey(0.8)
|
|
)
|
|
XCTAssertEqual(noRoute, .unavailable("Distance unavailable"))
|
|
|
|
let noBaselineRoute = FuelSavingsEstimator.availability(
|
|
station: candidate,
|
|
fuel: .e10,
|
|
baseline: baseline,
|
|
settings: settings,
|
|
amount: FuelSavingsFillAmount(value: 60, unit: .pounds),
|
|
journey: journey(2.8),
|
|
baselineJourney: nil
|
|
)
|
|
XCTAssertEqual(noBaselineRoute, .unavailable("No comparison station"))
|
|
|
|
// With both legs present it produces the saving — no silent zero.
|
|
let both = FuelSavingsEstimator.availability(
|
|
station: candidate,
|
|
fuel: .e10,
|
|
baseline: baseline,
|
|
settings: settings,
|
|
amount: FuelSavingsFillAmount(value: 60, unit: .pounds),
|
|
journey: journey(2.8),
|
|
baselineJourney: journey(0.8)
|
|
)
|
|
XCTAssertEqual(both?.result?.netSavingGBP ?? 0, -0.166869, accuracy: 1e-6)
|
|
}
|
|
|
|
func testMixedRouteBasisIsReportedAsAnEstimate() {
|
|
let baseline = station("baseline", price: 171.9)
|
|
let candidate = station("candidate", price: 170.9)
|
|
let settings = FuelSavingsSettings(
|
|
enabled: true,
|
|
mpg: 30,
|
|
preferredAmount: FuelSavingsFillAmount(value: 60, unit: .pounds)
|
|
)
|
|
let mixed = FuelSavingsEstimator.availability(
|
|
station: candidate,
|
|
fuel: .e10,
|
|
baseline: baseline,
|
|
settings: settings,
|
|
amount: FuelSavingsFillAmount(value: 60, unit: .pounds),
|
|
journey: journey(2.8, basis: .straightLine),
|
|
baselineJourney: journey(0.8, basis: .roadRoute)
|
|
)
|
|
XCTAssertEqual(mixed?.result?.distanceBasis, .straightLine,
|
|
"one unmeasured leg makes the whole estimate an estimate")
|
|
XCTAssertEqual(mixed?.result?.journeyConfidence, .estimated)
|
|
XCTAssertEqual(mixed?.result?.headline(compact: true), "Approx. Costs £0.17 more")
|
|
}
|
|
|
|
func testBaselineStationIsLabelledClosestStationNotBreakEven() {
|
|
let baseline = station("baseline", price: 171.9)
|
|
let settings = FuelSavingsSettings(
|
|
enabled: true,
|
|
mpg: 30,
|
|
preferredAmount: FuelSavingsFillAmount(value: 60, unit: .pounds)
|
|
)
|
|
let result = FuelSavingsEstimator.availability(
|
|
station: baseline,
|
|
fuel: .e10,
|
|
baseline: baseline,
|
|
settings: settings,
|
|
amount: FuelSavingsFillAmount(value: 60, unit: .pounds),
|
|
journey: journey(0.8),
|
|
baselineJourney: journey(0.8)
|
|
)?.result
|
|
XCTAssertEqual(result?.status, .baseline)
|
|
XCTAssertEqual(result?.netSavingGBP, 0)
|
|
XCTAssertEqual(result?.headline(), "Closest station")
|
|
}
|
|
}
|