Make station price age informational only

The relay's price_updated is the maximum price_last_updated across the grades it maps, so it cannot vouch for the selected grade: stop using it to score, gate or warn about Best Value.

- FuelPriceFreshness is now station-level and display-only: one age per station, fresh/outdated/unknown/invalid, "May be outdated" past 30 days, with future-tolerance and non-finite guards.
- Best Value restores the closest-station-selling-the-selected-fuel baseline and its savings ranking; eligibility gating, eligible-first sorting and the unavailable banner are gone.
- Price age appears in the More info sheet only - no listing-row warnings, no extra banners.
- Copy reverted from "closest eligible station" to "closest station"; stale price-confidence strings removed, "May be outdated" added.
- Dormant per-grade timestamp plumbing removed, and regression tests added for every fuel type, missing/invalid/future stamps and the bundled offline dump.
This commit is contained in:
FuelBoard Contributor
2026-10-16 09:48:01 +01:00
parent 5900a81473
commit e960ef85c6
19 changed files with 530 additions and 732 deletions
@@ -3,28 +3,41 @@ import XCTest
@testable import FuelBoardShared
#endif
/// Station-level price age — one timestamp per station, reported in More info
/// and nowhere else.
///
/// The relay's `price_updated` is the newest `price_last_updated` across every
/// grade a station maps, so it is not evidence about the selected grade. These
/// tests pin what the app *reports*; `StationPriceAgePolicyTests` pins what the
/// app does about it (nothing).
final class FuelPriceFreshnessTests: XCTestCase {
private let now = Date(timeIntervalSince1970: 1_792_022_400)
private let now = Date(timeIntervalSince1970: 1_800_000_000)
private func station(
priceUpdated: TimeInterval? = nil,
priceUpdatedByFuel: [FuelType: TimeInterval]? = nil
) -> FuelStation {
FuelStation(
id: "freshness-station",
name: "Freshness Station",
brand: "Test",
address: "1 High Street",
postcode: "BD1 1AA",
lat: 53.79,
lng: -1.75,
prices: [.diesel: 129.9, .e10: 169.9],
priceUpdated: priceUpdated,
priceUpdatedByFuel: priceUpdatedByFuel
)
private func at(_ offset: TimeInterval) -> TimeInterval {
now.addingTimeInterval(offset).timeIntervalSince1970
}
func testPerGradeTimestampIsDecodedAndEligibleWhenFresh() throws {
private func station(priceUpdated: TimeInterval?) -> FuelStation {
FuelStation(id: "freshness-station", name: "Freshness Station", brand: "Test",
address: "1 High Street", postcode: "BD1 1AA", lat: 53.79, lng: -1.75,
prices: [.e10: 169.9], priceUpdated: priceUpdated)
}
// MARK: - One figure per station, whatever fuel is on screen
func testOneAgeIsReportedForEveryFuelAStationSells() {
let sold = FuelStation(id: "multi", name: "Multi", brand: "", address: "", postcode: "",
lat: 53, lng: -1, prices: [.e5: 165.9, .e10: 169.9, .diesel: 129.9],
priceUpdated: at(-28 * 60 * 60))
for fuel in FuelType.allCases {
XCTAssertEqual(FuelPriceFreshness.assessment(for: sold, now: now).updateAgeLabel,
"28h ago",
"\(fuel.rawValue): every grade reads the same station timestamp")
}
}
func testStationTimestampIsDecodedAndNoPerGradeTimestampIsInvented() throws {
let json = """
{
"stations": [{
@@ -36,207 +49,122 @@ final class FuelPriceFreshnessTests: XCTestCase {
"lat": 53.78,
"lng": -1.75,
"prices": {"E10": 169.9, "DIESEL": 129.9},
"price_updated": "2026-09-25T07:45:52Z",
"price_updated": "2026-10-14T07:45:52Z",
"price_updated_by_grade": {
"E10": "2026-10-14T07:45:52Z",
"DIESEL": "2026-10-14T07:45:52Z"
"E10": "2026-01-01T00:00:00Z",
"DIESEL": "2026-01-01T00:00:00Z"
}
}]
}
"""
let stations = try FuelPriceProvider.decodeStations(from: Data(json.utf8))
let decoded = try XCTUnwrap(stations.first)
let decoded = try XCTUnwrap(try FuelPriceProvider.decodeStations(from: Data(json.utf8)).first)
XCTAssertEqual(decoded.priceUpdatedByFuel?[.diesel],
ISO8601DateFormatter.fuelBoardDate("2026-10-14T07:45:52Z")?.timeIntervalSince1970)
let assessment = FuelPriceFreshness.assessment(for: decoded, fuel: .diesel, now: now)
XCTAssertEqual(assessment.source, .gradeSpecific)
XCTAssertEqual(decoded.priceUpdated,
ISO8601DateFormatter.fuelBoardDate("2026-10-14T07:45:52Z")?.timeIntervalSince1970,
"the station-level field is what the app records")
// A per-grade map in the payload is ignored: the app has one age, and it
// is the station's.
let assessment = FuelPriceFreshness.assessment(for: decoded, now: now)
XCTAssertEqual(assessment.status, .outdated,
"the station field is used, not the 2026-01-01 grade entries")
}
// MARK: - Reading the age
func testFreshAgeReadsInMinutesThenHours() {
XCTAssertEqual(FuelPriceFreshness.assessment(priceUpdated: at(-59 * 60), now: now).updateAgeLabel,
"59m ago")
XCTAssertEqual(FuelPriceFreshness.assessment(priceUpdated: at(-60 * 60), now: now).updateAgeLabel,
"1h ago")
XCTAssertEqual(FuelPriceFreshness.assessment(priceUpdated: at(-47 * 60 * 60), now: now).updateAgeLabel,
"47h ago")
XCTAssertEqual(FuelPriceFreshness.assessment(priceUpdated: at(-48 * 60 * 60), now: now).updateAgeLabel,
"2d ago")
}
func testFreshInformationIsNeverFlagged() {
let assessment = FuelPriceFreshness.assessment(for: station(priceUpdated: at(-28 * 60 * 60)), now: now)
XCTAssertEqual(assessment.status, .fresh)
XCTAssertTrue(assessment.isEligibleForBestValue)
XCTAssertFalse(assessment.isOutdated)
XCTAssertNil(assessment.outdatedNotice)
XCTAssertEqual(assessment.age, 28 * 60 * 60)
}
func testStationWideTimestampIsNotPresentedAsGradeSpecificOrEligible() {
let updated = now.addingTimeInterval(-FuelPriceFreshness.warningAge / 2).timeIntervalSince1970
let assessment = FuelPriceFreshness.assessment(
for: station(priceUpdated: updated), fuel: .diesel, now: now
)
// MARK: - The thirty-day display threshold
XCTAssertEqual(assessment.source, .stationWide)
func testInformationExactlyThirtyDaysOldIsNotYetFlagged() {
let assessment = FuelPriceFreshness.assessment(priceUpdated: at(-FuelPriceFreshness.outdatedAge), now: now)
XCTAssertEqual(assessment.status, .fresh)
XCTAssertEqual(assessment.confidence, .provisional)
XCTAssertTrue(assessment.recommendationEligibility == .eligible)
XCTAssertTrue(assessment.isEligibleForBestValue)
XCTAssertTrue(assessment.warning)
XCTAssertTrue(assessment.displayLabel.contains("station-wide"))
XCTAssertNil(assessment.outdatedNotice)
}
func testMoreInfoReportsOneStationPriceAge() {
let updated = now.addingTimeInterval(-28 * 60 * 60).timeIntervalSince1970
let assessment = FuelPriceFreshness.assessment(for: station(priceUpdated: updated), fuel: .e10, now: now)
XCTAssertEqual(assessment.updateAgeLabel, "28h ago")
XCTAssertEqual(assessment.source, .stationWide)
XCTAssertEqual(assessment.status, .fresh)
func testInformationJustPastThirtyDaysIsFlaggedButStillReportsItsAge() {
let assessment = FuelPriceFreshness.assessment(priceUpdated: at(-FuelPriceFreshness.outdatedAge - 1), now: now)
XCTAssertEqual(assessment.status, .outdated)
XCTAssertTrue(assessment.isOutdated)
XCTAssertEqual(assessment.outdatedNotice, "May be outdated")
XCTAssertEqual(assessment.updateAgeLabel, "30d ago")
}
func testOlderFuelSpecificTimestampStillReportsItsOwnAgeOnce() {
let updated = now.addingTimeInterval(-10 * 86_400).timeIntervalSince1970
let priced = station(priceUpdatedByFuel: [.diesel: updated])
let assessment = FuelPriceFreshness.assessment(for: priced, fuel: .diesel, now: now)
XCTAssertEqual(assessment.updateAgeLabel, "10d ago")
XCTAssertEqual(assessment.source, .gradeSpecific)
func testLongOldInformationIsFlaggedWithItsDayCount() {
// The oldest figure the mirror carries for a real station is ~250 days.
let assessment = FuelPriceFreshness.assessment(priceUpdated: at(-250 * 86_400), now: now)
XCTAssertEqual(assessment.status, .outdated)
XCTAssertEqual(assessment.updateAgeLabel, "250d ago")
XCTAssertEqual(assessment.outdatedNotice, "May be outdated")
}
func testMissingUpdateTimeReadsNotProvided() {
let assessment = FuelPriceFreshness.assessment(for: station(), fuel: .e10, now: now)
// MARK: - Missing, malformed, future and absurd timestamps
func testMissingTimestampReadsNotProvidedAndIsNeverFlagged() {
let assessment = FuelPriceFreshness.assessment(for: station(priceUpdated: nil), now: now)
XCTAssertEqual(assessment.status, .unknown)
XCTAssertNil(assessment.age)
XCTAssertEqual(assessment.updateAgeLabel, "Not provided")
XCTAssertFalse(assessment.isOutdated)
XCTAssertNil(assessment.outdatedNotice)
}
func testProvisionalSavingAddsNoSecondWarningLine() {
let recent = now.addingTimeInterval(-28 * 60 * 60).timeIntervalSince1970
let stationWide = station(priceUpdated: recent)
let gradeSpecific = station(priceUpdatedByFuel: [.e10: recent])
XCTAssertEqual(FuelPriceFreshness.comparisonConfidence(
station: gradeSpecific, fuel: .e10, baseline: stationWide, now: now), .provisional)
XCTAssertNil(FuelPriceFreshness.comparisonWarning(
station: stationWide, fuel: .e10, baseline: stationWide, now: now))
XCTAssertNil(FuelPriceFreshness.comparisonWarning(
station: gradeSpecific, fuel: .e10, baseline: stationWide, now: now))
func testUnparseableSourceTextIsDroppedRatherThanGuessed() throws {
XCTAssertNil(FuelPriceTimestamp.parse("not-a-date"))
XCTAssertNil(FuelPriceTimestamp.parse(""))
XCTAssertNil(FuelPriceTimestamp.parse(" "))
let json = """
{"stations": [{"id": "bad", "name": "Bad", "brand": "", "address": "", "postcode": "",
"lat": 53, "lng": -1, "prices": {"E10": 169.9}, "price_updated": "2026-99-99T99:99:99Z"}]}
"""
let decoded = try XCTUnwrap(try FuelPriceProvider.decodeStations(from: Data(json.utf8)).first)
XCTAssertNil(decoded.priceUpdated)
XCTAssertEqual(FuelPriceFreshness.assessment(for: decoded, now: now).updateAgeLabel, "Not provided")
}
func testIneligibleComparisonStillExplainsItself() {
let old = now.addingTimeInterval(-FuelPriceFreshness.staleAge - 1).timeIntervalSince1970
let stale = station(priceUpdatedByFuel: [.diesel: old])
XCTAssertEqual(FuelPriceFreshness.comparisonWarning(
station: stale, fuel: .diesel, baseline: stale, now: now),
"Price age unavailable or too old — not a Best Value recommendation.")
func testSmallClockSkewReadsAsJustNowNotAsAFutureDate() {
let assessment = FuelPriceFreshness.assessment(priceUpdated: at(60), now: now)
XCTAssertEqual(assessment.status, .fresh)
XCTAssertEqual(assessment.age, 0)
XCTAssertEqual(assessment.updateAgeLabel, "0m ago")
}
func testStationWideTimestampAtWarningBoundaryChangesRecommendationEligibility() {
let updated = now.addingTimeInterval(-FuelPriceFreshness.warningAge).timeIntervalSince1970
let onBoundary = FuelPriceFreshness.assessment(for: station(priceUpdated: updated), fuel: .diesel, now: now)
XCTAssertEqual(onBoundary.status, .fresh)
XCTAssertTrue(onBoundary.isEligibleForBestValue)
let pastBoundary = FuelPriceFreshness.assessment(
for: station(priceUpdated: updated - 1), fuel: .diesel, now: now)
XCTAssertEqual(pastBoundary.status, .warning)
XCTAssertFalse(pastBoundary.isEligibleForBestValue)
func testTimestampBeyondTheSkewToleranceIsUnusableNotOptimistic() {
let assessment = FuelPriceFreshness.assessment(priceUpdated: at(86_400), now: now)
XCTAssertEqual(assessment.status, .invalid)
XCTAssertNil(assessment.age)
XCTAssertEqual(assessment.updateAgeLabel, "Not provided")
XCTAssertNil(assessment.outdatedNotice)
}
func testStationWideTimestampOlderThanWarningThresholdCannotRecommendSuspiciousDiesel() {
let now = Date(timeIntervalSince1970: 1_800_000_000)
let shell = station(priceUpdated: now.addingTimeInterval(-20 * 86_400).timeIntervalSince1970)
let age = FuelPriceFreshness.assessment(for: shell, fuel: .diesel, now: now)
XCTAssertEqual(age.status, .warning)
XCTAssertFalse(age.isEligibleForBestValue)
XCTAssertEqual(shell.prices[.diesel], 129.9)
}
func testComparisonConfidenceRejectsMissingSelectedFuelPriceEvenWithFreshTimestamp() {
let now = Date(timeIntervalSince1970: 1_800_000_000)
let updated = now.addingTimeInterval(-86_400).timeIntervalSince1970
let dieselOnly = FuelStation(id: "diesel", name: "Diesel", brand: "", address: "", postcode: "",
lat: 53, lng: -1, prices: [.diesel: 129.9], priceUpdated: nil,
priceUpdatedByFuel: [.e10: updated])
let e10 = FuelStation(id: "petrol", name: "Petrol", brand: "", address: "", postcode: "",
lat: 53, lng: -1, prices: [.e10: 169.9], priceUpdated: nil,
priceUpdatedByFuel: [.e10: updated])
XCTAssertEqual(FuelPriceFreshness.comparisonConfidence(station: dieselOnly, fuel: .e10,
baseline: e10, now: now), .ineligible)
XCTAssertEqual(FuelPriceFreshness.comparisonConfidence(station: e10, fuel: .e10,
baseline: dieselOnly, now: now), .ineligible)
}
func testComparisonConfidenceRequiresBothSelectedGradeTimestamps() {
let now = Date(timeIntervalSince1970: 1_800_000_000)
let recent = now.addingTimeInterval(-86_400).timeIntervalSince1970
let old = now.addingTimeInterval(-20 * 86_400).timeIntervalSince1970
let verified = station(priceUpdatedByFuel: [.diesel: recent])
let provisional = station(priceUpdated: recent)
let oldStationWide = station(priceUpdated: old)
let oldGrade = station(priceUpdatedByFuel: [.diesel: old])
XCTAssertEqual(FuelPriceFreshness.comparisonConfidence(station: verified, fuel: .diesel,
baseline: verified, now: now), .verified)
XCTAssertEqual(FuelPriceFreshness.comparisonConfidence(station: verified, fuel: .diesel,
baseline: provisional, now: now), .provisional)
XCTAssertEqual(FuelPriceFreshness.comparisonConfidence(station: oldGrade, fuel: .diesel,
baseline: verified, now: now), .provisional)
XCTAssertEqual(FuelPriceFreshness.comparisonConfidence(station: verified, fuel: .diesel,
baseline: oldStationWide, now: now), .ineligible)
XCTAssertEqual(FuelPriceFreshness.comparisonConfidence(station: provisional, fuel: .diesel,
baseline: verified, now: now), .provisional)
XCTAssertEqual(FuelPriceFreshness.comparisonConfidence(station: verified, fuel: .e10,
baseline: verified, now: now), .ineligible)
}
func testNonFiniteAndExtremeEpochsCannotBecomeTrusted() {
let now = Date(timeIntervalSince1970: 1_800_000_000)
for value in [Double.nan, .infinity, -.infinity, Double.greatestFiniteMagnitude, -Double.greatestFiniteMagnitude] {
let assessment = FuelPriceFreshness.assessment(
priceUpdated: value, priceUpdatedByFuel: nil, fuel: .diesel, now: now)
XCTAssertFalse(assessment.isEligibleForBestValue)
XCTAssertTrue(assessment.warning)
func testNonFiniteAndAbsurdEpochsAreUnusableAndCannotTrap() {
let values: [TimeInterval] = [.nan, .infinity, -.infinity,
.greatestFiniteMagnitude, -.greatestFiniteMagnitude]
for value in values {
let assessment = FuelPriceFreshness.assessment(priceUpdated: value, now: now)
XCTAssertEqual(assessment.status, .invalid, "\(value)")
XCTAssertNil(assessment.age, "\(value)")
XCTAssertEqual(assessment.updateAgeLabel, "Not provided", "\(value)")
XCTAssertFalse(assessment.isOutdated, "\(value)")
}
}
func testMissingAndMalformedTimestampsAreUnknownAndIneligible() {
let missing = FuelPriceFreshness.assessment(for: station(), fuel: .diesel, now: now)
XCTAssertEqual(missing.status, .unknown)
XCTAssertEqual(missing.source, .unknown)
XCTAssertFalse(missing.isEligibleForBestValue)
let malformed = FuelPriceFreshness.assessment(
priceUpdated: "not-a-date", priceUpdatedByGrade: nil, fuel: .diesel, now: now
)
XCTAssertEqual(malformed.status, .invalid)
XCTAssertFalse(malformed.isEligibleForBestValue)
}
func testStaleDisplayLabelDoesNotCallIneligiblePriceProvisional() {
let stale = station(priceUpdatedByFuel: [
.diesel: now.addingTimeInterval(-FuelPriceFreshness.staleAge - 1).timeIntervalSince1970
])
let assessment = FuelPriceFreshness.assessment(for: stale, fuel: .diesel, now: now)
XCTAssertFalse(assessment.isEligibleForBestValue)
XCTAssertFalse(assessment.displayLabel.contains("provisional"))
XCTAssertTrue(assessment.displayLabel.contains("outdated"))
}
func testStaleAndFutureGradeTimestampsAreRejected() {
let stale = station(priceUpdatedByFuel: [
.diesel: now.addingTimeInterval(-FuelPriceFreshness.staleAge - 1).timeIntervalSince1970
])
let staleAssessment = FuelPriceFreshness.assessment(for: stale, fuel: .diesel, now: now)
XCTAssertEqual(staleAssessment.status, .stale)
XCTAssertFalse(staleAssessment.isEligibleForBestValue)
XCTAssertTrue(staleAssessment.warning)
let future = station(priceUpdatedByFuel: [
.diesel: now.addingTimeInterval(FuelPriceFreshness.futureTolerance + 1).timeIntervalSince1970
])
let futureAssessment = FuelPriceFreshness.assessment(for: future, fuel: .diesel, now: now)
XCTAssertEqual(futureAssessment.status, .invalid)
XCTAssertFalse(futureAssessment.isEligibleForBestValue)
}
func testMalformedAndFutureSourceValuesDoNotCrashOrProduceNegativeAge() {
let malformed = FuelPriceFreshness.assessment(
priceUpdated: "2026-99-99T99:99:99Z",
priceUpdatedByGrade: ["DIESEL": "2026-99-99T99:99:99Z"],
fuel: .diesel,
now: now
)
XCTAssertNil(malformed.age)
XCTAssertFalse(malformed.isEligibleForBestValue)
let future = FuelPriceFreshness.assessment(
priceUpdated: now.addingTimeInterval(86_400).timeIntervalSince1970,
priceUpdatedByFuel: nil,
fuel: .diesel,
now: now
)
XCTAssertNil(future.age)
XCTAssertFalse(future.isEligibleForBestValue)
}
}
private extension ISO8601DateFormatter {
@@ -533,6 +533,6 @@ final class FuelSavingsCalculationAuditTests: XCTestCase {
)?.result
XCTAssertEqual(result?.status, .baseline)
XCTAssertEqual(result?.netSavingGBP, 0)
XCTAssertEqual(result?.headline(), "Closest eligible station")
XCTAssertEqual(result?.headline(), "Closest station")
}
}
@@ -41,6 +41,8 @@ final class FuelSavingsEstimateTests: XCTestCase {
FuelSavingsJourney(miles: miles, basis: .straightLine)
}
/// Fuel types are never mixed: a station that does not sell the selected
/// fuel cannot be its baseline, and cannot be compared with it.
func testDifferentFuelNeverUsesAnotherGradeAsItsBaseline() {
let petrolOnly = FuelStation(id: "petrol", name: "Petrol", brand: "", address: "", postcode: "", lat: 53, lng: -1,
prices: [.e10: 199.9], priceUpdated: nil)
@@ -55,6 +57,26 @@ final class FuelSavingsEstimateTests: XCTestCase {
XCTAssertEqual(unavailable, .unavailable("No comparison station"))
}
/// Ranking must still work when the list being ordered is filtered tighter
/// than the pool the baseline was chosen from.
func testBaselineOutsideFilteredPoolStillRanksByNetSaving() {
let baseline = station("nearest-outside-radius", price: 199.9)
let cheap = station("z-cheap", price: 129.9)
let expensive = station("a-expensive", price: 198.9)
let ranked = FuelSavingsEstimator.ranked(
[expensive, cheap], fuel: .e10, baseline: baseline,
settings: settings, amount: amount,
journeyMiles: { s in self.straightLine(s.id == baseline.id ? 0.8 : 4.0) }
)
XCTAssertEqual(ranked.map(\.id), ["z-cheap", "a-expensive"])
let expected = FuelSavingsEstimator.availability(
station: cheap, fuel: .e10, baseline: baseline,
settings: settings, amount: amount,
journey: straightLine(4.0), baselineJourney: straightLine(0.8)
)?.result
XCTAssertGreaterThan(expected?.netSavingGBP ?? -1, 0)
}
func testCheaperStationThreePointFiveMilesAwayIsRankedFirstWithASaving() {
// Nearest station: 1 mile away at 145.9p. The station the user can see
// in the Cheapest tab: 3.5 miles away at 138.9p.
@@ -99,24 +121,6 @@ final class FuelSavingsEstimateTests: XCTestCase {
"unmeasured routes are reported as straight-line estimates")
}
func testBaselineOutsideFilteredPoolStillRanksByNetSaving() {
let baseline = station("nearest-outside-radius", price: 199.9)
let cheap = station("z-cheap", price: 129.9)
let expensive = station("a-expensive", price: 198.9)
let ranked = FuelSavingsEstimator.ranked(
[expensive, cheap], fuel: .e10, baseline: baseline,
settings: settings, amount: amount,
journeyMiles: { s in self.straightLine(s.id == baseline.id ? 0.8 : 4.0) }
)
XCTAssertEqual(ranked.map(\.id), ["z-cheap", "a-expensive"])
let expected = FuelSavingsEstimator.availability(
station: cheap, fuel: .e10, baseline: baseline,
settings: settings, amount: amount,
journey: straightLine(4.0), baselineJourney: straightLine(0.8)
)?.result
XCTAssertGreaterThan(expected?.netSavingGBP ?? -1, 0)
}
func testEveryPricedStationInThePoolIsEstimatedNotJustAShortlist() {
// 40 stations, but only the first one has a measured road route — the
// old implementation capped the whole ranking to 12 shortlisted
@@ -191,56 +195,6 @@ final class FuelSavingsEstimateTests: XCTestCase {
XCTAssertEqual(result?.distanceBasis, .straightLine)
}
func testBestValueBaselineSkipsClosestStalePriceButKeepsThatStationVisible() {
let now = Date(timeIntervalSince1970: 1_800_000_000)
func priced(_ id: String, _ price: Double, _ daysOld: Double) -> FuelStation {
FuelStation(id: id, name: id, brand: "", address: "", postcode: "", lat: 53, lng: -1,
prices: [.e10: price], priceUpdated: now.addingTimeInterval(-daysOld * 86_400).timeIntervalSince1970)
}
let staleClosest = priced("stale-closest", 199.9, 27)
let eligibleClosest = priced("eligible-closest", 170.9, 1)
let eligibleFurther = priced("eligible-further", 150.9, 1)
let pool = [staleClosest, eligibleClosest, eligibleFurther]
let journeys: (FuelStation) -> FuelSavingsJourney? = { station in
self.straightLine(station.id == "stale-closest" ? 0.8 : station.id == "eligible-closest" ? 1.6 : 3.0)
}
let baseline = FuelSavingsEstimator.bestValueBaselineStation(pool, fuel: .e10,
journeyMiles: journeys, now: now)
XCTAssertEqual(baseline?.id, "eligible-closest")
XCTAssertEqual(FuelSavingsEstimator.baselineStation(pool, fuel: .e10, journeyMiles: journeys)?.id,
"stale-closest", "non-Best-Value closest behaviour must stay unchanged")
let ranked = FuelSavingsEstimator.ranked(pool, fuel: .e10, baseline: baseline,
settings: briefSettings, amount: briefAmount, journeyMiles: journeys, now: now)
XCTAssertEqual(ranked.first?.id, "eligible-further")
XCTAssertTrue(ranked.contains(where: { $0.id == "stale-closest" }))
XCTAssertFalse(FuelSavingsEstimator.recommendationEligible(station: staleClosest, fuel: .e10,
baseline: baseline, now: now))
}
func testBestValueBaselineRequiresEligibleSelectedFuelAndUsableJourney() {
let now = Date(timeIntervalSince1970: 1_800_000_000)
let recent = now.addingTimeInterval(-86_400).timeIntervalSince1970
let stale = now.addingTimeInterval(-40 * 86_400).timeIntervalSince1970
let stations = [
FuelStation(id: "wrong-fuel", name: "Wrong", brand: "", address: "", postcode: "", lat: 0, lng: 0,
prices: [.diesel: 140], priceUpdated: recent),
FuelStation(id: "stale", name: "Stale", brand: "", address: "", postcode: "", lat: 0, lng: 0,
prices: [.e10: 140], priceUpdated: stale),
FuelStation(id: "bad-journey", name: "Bad journey", brand: "", address: "", postcode: "", lat: 0, lng: 0,
prices: [.e10: 141], priceUpdated: recent),
FuelStation(id: "eligible", name: "Eligible", brand: "", address: "", postcode: "", lat: 0, lng: 0,
prices: [.e10: 145], priceUpdated: recent)
]
let journeys: (FuelStation) -> FuelSavingsJourney? = { station in
self.straightLine(station.id == "wrong-fuel" ? 0.1 : station.id == "stale" ? 0.2 :
station.id == "bad-journey" ? .infinity : 2)
}
XCTAssertEqual(FuelSavingsEstimator.bestValueBaselineStation(stations, fuel: .e10,
journeyMiles: journeys, now: now)?.id, "eligible")
XCTAssertNil(FuelSavingsEstimator.bestValueBaselineStation(Array(stations.prefix(2)), fuel: .e10,
journeyMiles: journeys, now: now))
}
func testBaselineStationUsesTheSameJourneyValuesAsSavings() {
let geographicallyNearest = station("geographically-nearest", price: 145.9)
let roadNearest = station("road-nearest", price: 146.9)
@@ -432,8 +386,8 @@ final class FuelSavingsEstimateTests: XCTestCase {
)
XCTAssertEqual(availability?.result?.status, .baseline)
XCTAssertEqual(availability?.result?.headline(compact: true), "Closest eligible station")
XCTAssertEqual(FuelSavingsStatus.baseline.displayName, "Closest eligible station")
XCTAssertEqual(availability?.result?.headline(compact: true), "Closest station")
XCTAssertEqual(FuelSavingsStatus.baseline.displayName, "Closest station")
XCTAssertEqual(availability?.result?.netSavingGBP, 0)
XCTAssertEqual(availability?.result?.extraDistanceMiles, 0)
}
@@ -485,7 +439,7 @@ final class FuelSavingsEstimateTests: XCTestCase {
/// The exact strings the brief asks for.
func testHeadlineWordingMatchesTheBrief() {
XCTAssertEqual(FuelSavingsResult.baselineHeadline, "Closest eligible station")
XCTAssertEqual(FuelSavingsResult.baselineHeadline, "Closest station")
XCTAssertEqual(FuelSavingsResult.breakEvenHeadline, "Break even")
let saving = FuelSavingsResult(fillLitres: 34.9,
@@ -509,87 +463,10 @@ final class FuelSavingsEstimateTests: XCTestCase {
/// The sort must order by NET saving, not pump price: a pricier station can
/// win when the cheaper one is far enough away.
func testBestValueAlertUsesClosestEligiblePriceAsBaseline() {
let now = Date(timeIntervalSince1970: 1_800_000_000)
func priced(_ id: String, _ price: Double, _ daysOld: Double) -> FuelStation {
FuelStation(id: id, name: id, brand: "", address: "", postcode: "", lat: 53, lng: -1,
prices: [.e10: price], priceUpdated: now.addingTimeInterval(-daysOld * 86_400).timeIntervalSince1970)
}
let stations = [priced("stale-closest", 199.9, 27), priced("eligible-closest", 170.9, 1),
priced("eligible-cheaper", 150.9, 1)]
let settings = FuelSavingsSettings(enabled: true, mpg: 30,
preferredAmount: .init(value: 60, unit: .pounds))
let alert = FuelSavingsCalculator.bestValueAlertCandidate(
stations: stations, fuel: .e10, settings: settings, amount: settings.preferredAmount,
minimumNetSavingGBP: 1,
journeyMiles: { self.straightLine($0.id == "stale-closest" ? 0.8 : $0.id == "eligible-closest" ? 1.6 : 3) },
now: now)
XCTAssertEqual(alert?.station.id, "eligible-cheaper")
}
func testBestValueAlertDoesNotRecommendStaleCandidateOrStaleBaseline() {
let now = Date(timeIntervalSince1970: 1_800_000_000)
func priced(_ id: String, _ price: Double, _ daysOld: Double) -> FuelStation {
FuelStation(id: id, name: id, brand: "", address: "", postcode: "", lat: 53, lng: -1,
prices: [.e10: price], priceUpdated: now.addingTimeInterval(-daysOld * 86_400).timeIntervalSince1970)
}
let base = priced("base", 199.9, 1)
let stale = priced("stale", 129.9, 40)
let fresh = priced("fresh", 159.9, 1)
let settings = FuelSavingsSettings(enabled: true, mpg: 30, preferredAmount: .init(value: 60, unit: .pounds))
let choice = FuelSavingsCalculator.bestValueAlertCandidate(
stations: [base, stale, fresh], fuel: .e10, settings: settings,
amount: .init(value: 60, unit: .pounds), minimumNetSavingGBP: 1,
journeyMiles: { station in self.straightLine(station.id == "base" ? 0.8 : 2) }, now: now)
XCTAssertEqual(choice?.station.id, "fresh")
let noTrustedBaseline = FuelSavingsCalculator.bestValueAlertCandidate(
stations: [priced("base", 199.9, 40), fresh], fuel: .e10, settings: settings,
amount: .init(value: 60, unit: .pounds), minimumNetSavingGBP: 1,
journeyMiles: { station in self.straightLine(station.id == "base" ? 0.8 : 2) }, now: now)
XCTAssertNil(noTrustedBaseline)
}
func testStalePriceCannotLeadBestValueButRemainsVisible() {
let now = Date(timeIntervalSince1970: 1_800_000_000)
let baseline = FuelStation(id: "base", name: "Baseline", brand: "", address: "", postcode: "", lat: 53, lng: -1,
prices: [.e10: 199.9], priceUpdated: now.addingTimeInterval(-86_400).timeIntervalSince1970)
let stale = FuelStation(id: "stale-cheap", name: "Stale", brand: "", address: "", postcode: "", lat: 53, lng: -1,
prices: [.e10: 129.9], priceUpdated: now.addingTimeInterval(-40 * 86_400).timeIntervalSince1970)
let recent = FuelStation(id: "recent", name: "Recent", brand: "", address: "", postcode: "", lat: 53, lng: -1,
prices: [.e10: 170.9], priceUpdated: now.addingTimeInterval(-86_400).timeIntervalSince1970)
let ranked = FuelSavingsEstimator.ranked([stale, recent], fuel: .e10, baseline: baseline,
settings: briefSettings, amount: briefAmount,
journeyMiles: { _ in self.straightLine(0.8) }, now: now)
XCTAssertEqual(ranked.map(\.id), ["recent", "stale-cheap"])
}
func testNoFreshPriceComparisonUsesExplicitCheapestPriceFallback() {
let now = Date(timeIntervalSince1970: 1_800_000_000)
let staleTime = now.addingTimeInterval(-40 * 86_400).timeIntervalSince1970
let baseline = FuelStation(id: "baseline", name: "Baseline", brand: "", address: "", postcode: "",
lat: 53, lng: -1, prices: [.diesel: 199.9], priceUpdated: staleTime)
let cheapest = FuelStation(id: "cheapest", name: "Cheapest", brand: "", address: "", postcode: "",
lat: 53, lng: -1, prices: [.diesel: 129.9], priceUpdated: staleTime)
let near = FuelStation(id: "near", name: "Near", brand: "", address: "", postcode: "",
lat: 53, lng: -1, prices: [.diesel: 170.9], priceUpdated: staleTime)
let ordered = FuelSavingsEstimator.ranked([near, baseline, cheapest], fuel: .diesel,
baseline: baseline, settings: briefSettings, amount: briefAmount,
journeyMiles: { station in self.straightLine(station.id == "cheapest" ? 250 : 1) }, now: now)
XCTAssertEqual(ordered.map(\.id), ["cheapest", "near", "baseline"])
XCTAssertFalse(FuelSavingsEstimator.recommendationEligible(station: ordered[0], fuel: .diesel,
baseline: baseline, now: now))
}
func testSortingRanksByNetSavingNotPumpPrice() {
let now = Date(timeIntervalSince1970: 1_800_000_000)
func pricedStation(_ id: String, price: Double) -> FuelStation {
FuelStation(id: id, name: id, brand: "X", address: "", postcode: "",
lat: 51.5, lng: -0.1, prices: [.e10: price],
priceUpdated: nil, priceUpdatedByFuel: [.e10: now.timeIntervalSince1970])
}
let baseline = pricedStation("baseline", price: 171.9)
let cheapButFar = pricedStation("cheap-but-far", price: 167.9)
let pricierButNear = pricedStation("pricier-but-near", price: 169.9)
let baseline = station("baseline", price: 171.9)
let cheapButFar = station("cheap-but-far", price: 167.9)
let pricierButNear = station("pricier-but-near", price: 169.9)
let ranked = FuelSavingsEstimator.ranked(
[baseline, cheapButFar, pricierButNear],
@@ -603,7 +480,7 @@ final class FuelSavingsEstimateTests: XCTestCase {
case "cheap-but-far": return self.straightLine(7.6)
default: return self.straightLine(1.2)
}
}, now: now
}
)
XCTAssertEqual(ranked.first?.id, "pricier-but-near",
@@ -613,18 +490,18 @@ final class FuelSavingsEstimateTests: XCTestCase {
// MARK: Row copy
func testCompactComparisonLabelUsesClosestEligibleStationWording() {
func testCompactComparisonLabelUsesGenericNearestStationWording() {
XCTAssertEqual(
FuelSavingsFormat.comparisonLabel(status: .worthTheDrive),
"vs closest eligible station"
"vs closest station"
)
XCTAssertEqual(
FuelSavingsFormat.comparisonLabel(status: .notWorthTheDrive),
"vs closest eligible station"
"vs closest station"
)
XCTAssertEqual(
FuelSavingsFormat.comparisonLabel(status: .breakEven),
"vs closest eligible station"
"vs closest station"
)
XCTAssertEqual(
FuelSavingsFormat.comparisonLabel(status: .baseline),
@@ -653,7 +530,7 @@ final class FuelSavingsEstimateTests: XCTestCase {
}
/// The baseline capsule has to say what it IS: it previously repeated the
/// status word above it ("Closest eligible station" twice) and never explained that
/// status word above it ("Closest station" twice) and never explained that
/// the savings are measured against it.
func testCapsuleHeadlineMarksTheBaselineAndKeepsTheEstimateMarker() {
let baseline = FuelSavingsResult(
@@ -664,7 +541,7 @@ final class FuelSavingsEstimateTests: XCTestCase {
netSavingGBP: 0,
status: .baseline
)
XCTAssertEqual(baseline.capsuleHeadline(), "Closest eligible station")
XCTAssertEqual(baseline.capsuleHeadline(), "Closest station")
let saving = FuelSavingsResult(
fillLitres: 30,
@@ -110,11 +110,10 @@ final class FuelSavingsCalculatorTests: XCTestCase {
}
func testBestValueAlertCandidatePicksHighestNetSavingAboveThreshold() throws {
let now = Date(timeIntervalSince1970: 1_800_000_000)
let stations = [
station("baseline", lat: 0, lng: 0, price: 160, priceUpdated: now.timeIntervalSince1970),
station("small", lat: 0, lng: 0, price: 154, priceUpdated: now.timeIntervalSince1970),
station("best", lat: 0, lng: 0, price: 148, priceUpdated: now.timeIntervalSince1970),
station("baseline", lat: 0, lng: 0, price: 160),
station("small", lat: 0, lng: 0, price: 154),
station("best", lat: 0, lng: 0, price: 148),
]
let journeys = ["baseline": 2.0, "small": 4.0, "best": 6.0]
let settings = FuelSavingsSettings(enabled: true, mpg: 45, preferredAmount: .init(value: 40, unit: .pounds))
@@ -124,8 +123,7 @@ final class FuelSavingsCalculatorTests: XCTestCase {
settings: settings,
amount: settings.preferredAmount,
minimumNetSavingGBP: 1.0,
journeyMiles: { journeys[$0.id].map { FuelSavingsJourney(miles: $0, basis: .roadRoute) } },
now: now
journeyMiles: { journeys[$0.id].map { FuelSavingsJourney(miles: $0, basis: .roadRoute) } }
)
XCTAssertEqual(candidate?.station.id, "best")
XCTAssertEqual(candidate?.result.status, .worthTheDrive)
@@ -139,10 +137,8 @@ final class FuelSavingsCalculatorTests: XCTestCase {
XCTAssertEqual(FuelSavingsCalculator.sortedForWorthTheDrive([farther, nearer]).first?.stationID, "nearer")
}
private func station(_ id: String, lat: Double, lng: Double, price: Double,
priceUpdated: TimeInterval? = nil) -> FuelStation {
FuelStation(id: id, name: id, brand: id, address: "", postcode: "", lat: lat, lng: lng,
prices: [.e10: price], priceUpdated: priceUpdated)
private func station(_ id: String, lat: Double, lng: Double, price: Double) -> FuelStation {
FuelStation(id: id, name: id, brand: id, address: "", postcode: "", lat: lat, lng: lng, prices: [.e10: price], priceUpdated: nil)
}
}
@@ -0,0 +1,219 @@
import XCTest
#if SWIFT_PACKAGE
@testable import FuelBoardShared
#endif
/// Regression: a station's price age never removes it from Best Value.
///
/// The relay publishes one `price_updated` per station — the newest
/// `price_last_updated` across the grades it maps — which cannot vouch for the
/// selected grade. It is therefore display-only: Best Value, Cheapest and
/// Closest rank stations on their prices and distances exactly as before,
/// whether the timestamp is minutes, months or absent.
final class StationPriceAgePolicyTests: XCTestCase {
private let now = Date(timeIntervalSince1970: 1_800_000_000)
private let settings = FuelSavingsSettings(
enabled: true,
mpg: 30,
preferredAmount: FuelSavingsFillAmount(value: 60, unit: .pounds)
)
private func station(_ id: String,
price: Double,
fuel: FuelType = .e10,
ageDays: Double?,
lat: Double = 53,
lng: Double = -1) -> FuelStation {
FuelStation(
id: id, name: id, brand: "X", address: "", postcode: "", lat: lat, lng: lng,
prices: [fuel: price],
priceUpdated: ageDays.map { now.addingTimeInterval(-$0 * 86_400).timeIntervalSince1970 }
)
}
private func straightLine(_ miles: Double) -> FuelSavingsJourney {
FuelSavingsJourney(miles: miles, basis: .straightLine)
}
private func distanceProvider(_ miles: [String: Double]) -> (FuelStation) -> FuelSavingsJourney? {
{ station in miles[station.id].map { self.straightLine($0) } }
}
// MARK: - The baseline is the closest seller, whatever its age
func testClosestSellerIsTheBaselineEvenWhenItsTimestampIsAncient() {
let ancientClosest = station("ancient-closest", price: 199.9, ageDays: 250)
let freshFurther = station("fresh-further", price: 170.9, ageDays: 1)
let journeys = distanceProvider(["ancient-closest": 0.8, "fresh-further": 3.0])
XCTAssertEqual(
FuelSavingsEstimator.baselineStation([ancientClosest, freshFurther], fuel: .e10,
journeyMiles: journeys)?.id,
"ancient-closest",
"the baseline is the closest station selling the selected fuel — age is not a criterion"
)
}
func testBaselineStillRequiresTheSelectedFuelAndAUsableJourney() {
let wrongFuel = station("wrong-fuel", price: 140, fuel: .diesel, ageDays: 1)
let unusableJourney = station("bad-journey", price: 141, ageDays: 1)
let sellsFuel = station("sells-fuel", price: 145, ageDays: 1)
let journeys = distanceProvider(["wrong-fuel": 0.1, "sells-fuel": 2.0]) // bad-journey has none
XCTAssertEqual(
FuelSavingsEstimator.baselineStation([wrongFuel, unusableJourney, sellsFuel], fuel: .e10,
journeyMiles: journeys)?.id,
"sells-fuel"
)
}
// MARK: - Old, missing and invalid timestamps still rank
func testOldPriceTimestampsRankByNetSavingForEveryFuel() {
for fuel in FuelType.allCases {
let base = station("base", price: 199.9, fuel: fuel, ageDays: 250)
let cheap = station("cheap", price: 129.9, fuel: fuel, ageDays: 250)
let dear = station("dear", price: 198.9, fuel: fuel, ageDays: 250)
let journeys = distanceProvider(["base": 0.8, "cheap": 1.2, "dear": 1.0])
let ranked = FuelSavingsEstimator.ranked([dear, cheap, base], fuel: fuel, baseline: base,
settings: settings, amount: settings.preferredAmount,
journeyMiles: journeys)
XCTAssertEqual(ranked.map(\.id), ["cheap", "dear", "base"], fuel.rawValue)
let saving = FuelSavingsEstimator.availability(
station: cheap, fuel: fuel, baseline: base, settings: settings,
amount: settings.preferredAmount, journey: straightLine(1.2), baselineJourney: straightLine(0.8)
)?.result
XCTAssertGreaterThan(saving?.netSavingGBP ?? -1, 0,
"\(fuel.rawValue): a 250-day-old price still produces a saving")
}
}
func testNetSavingStillOutranksPumpPriceWhenTimestampsAreOld() {
// Net saving decides the order: the cheapest station is 30 miles away,
// so a pricier one a mile from home wins. Nothing here consults the age.
let base = station("base", price: 171.9, ageDays: 120)
let cheapFar = station("cheap-far", price: 169.9, ageDays: 250)
let pricierNear = station("pricier-near", price: 170.9, ageDays: 200)
let journeys = distanceProvider(["base": 0.8, "cheap-far": 30, "pricier-near": 1.2])
let ranked = FuelSavingsEstimator.ranked([cheapFar, pricierNear, base], fuel: .e10, baseline: base,
settings: settings, amount: settings.preferredAmount,
journeyMiles: journeys)
XCTAssertEqual(ranked.map(\.id), ["pricier-near", "base", "cheap-far"])
XCTAssertEqual(ranked.first?.prices[.e10], 170.9, "the winner is chosen on net saving, not pump price")
}
func testMissingFutureAndAbsurdTimestampsDoNotRemoveStations() {
let invalid = FuelStation(id: "absurd", name: "Absurd", brand: "", address: "", postcode: "",
lat: 53, lng: -1, prices: [.e10: 150.9],
priceUpdated: -Double.greatestFiniteMagnitude)
let pool = [station("base", price: 199.9, ageDays: 250),
station("cheap", price: 129.9, ageDays: 100),
FuelStation(id: "future", name: "Future", brand: "", address: "", postcode: "",
lat: 53, lng: -1, prices: [.e10: 160.9],
priceUpdated: now.addingTimeInterval(86_400).timeIntervalSince1970),
station("undated", price: 170.9, ageDays: nil),
invalid]
let journeys = distanceProvider(["base": 0.8, "cheap": 1.2, "future": 1.6,
"undated": 2.0, "absurd": 2.8])
let ranked = FuelSavingsEstimator.ranked(pool, fuel: .e10, baseline: pool[0],
settings: settings, amount: settings.preferredAmount,
journeyMiles: journeys)
XCTAssertEqual(Set(ranked.map(\.id)), Set(pool.map(\.id)),
"every station stays in the pool: no timestamp can filter a row out")
XCTAssertEqual(ranked.first?.id, "cheap", "the cheapest station still wins")
}
func testEveryPricedStationKeepsAResultWhateverItsTimestamp() {
let pool = [
station("base", price: 199.9, ageDays: 1),
station("fresh", price: 180.9, ageDays: 1),
station("old", price: 175.9, ageDays: 40),
station("ancient", price: 170.9, ageDays: 250),
station("undated", price: 165.9, ageDays: nil)
]
let journeys = distanceProvider(["base": 0.8, "fresh": 1.0, "old": 1.4, "ancient": 1.8, "undated": 2.2])
let ranked = FuelSavingsEstimator.ranked(pool, fuel: .e10, baseline: pool[0],
settings: settings, amount: settings.preferredAmount,
journeyMiles: journeys)
XCTAssertEqual(ranked.map(\.id).sorted(), ["ancient", "base", "fresh", "old", "undated"])
for station in pool {
let result = FuelSavingsEstimator.availability(
station: station, fuel: .e10, baseline: pool[0], settings: settings,
amount: settings.preferredAmount,
journey: journeys(station), baselineJourney: straightLine(0.8)
)?.result
XCTAssertNotNil(result, "\(station.id) still shows a Best Value row")
}
}
func testCheapestAndClosestOrdersAreUnaffectedByTimestampAge() {
// Cheapest sorts on pump price, Closest on distance — the only inputs are
// prices and distances, so no age combination changes either order.
let specs: [(String, Double, Double)] = [("a", 199.9, 0.8), ("b", 129.9, 30), ("c", 170.9, 1.2)]
for ages in [[1.0, 1.0, 1.0], [250.0, 250.0, 250.0]] {
let pool = zip(specs, ages).map { spec, age in
station(spec.0, price: spec.1, ageDays: age)
}
let journeys = distanceProvider(Dictionary(uniqueKeysWithValues:
zip(specs.map(\.0), specs.map(\.2))))
let cheapestFirst = pool.sorted { lhs, rhs in
let left = lhs.prices[.e10] ?? .infinity
let right = rhs.prices[.e10] ?? .infinity
if left != right { return left < right }
return (journeys(lhs)?.miles ?? .infinity) < (journeys(rhs)?.miles ?? .infinity)
}
XCTAssertEqual(cheapestFirst.map(\.id), ["b", "c", "a"])
let closestFirst = pool.sorted { lhs, rhs in
let left = journeys(lhs)?.miles ?? .infinity
let right = journeys(rhs)?.miles ?? .infinity
if left != right { return left < right }
return (lhs.prices[.e10] ?? .infinity) < (rhs.prices[.e10] ?? .infinity)
}
XCTAssertEqual(closestFirst.map(\.id), ["a", "c", "b"])
}
}
// MARK: - The bundled / offline dataset
func testBundledDatasetWithMonthOldTimestampsStillRecommendsBestValue() {
// The bundled dump ships one pull of the whole UK feed, so every station
// arrives with the same months-old timestamp. Best Value must still
// recommend — it used to read "unavailable" for the entire offline list.
var specs: [(String, Double, Double, Double?)] = [("base", 171.9, 0.8, 90),
("pricier-near", 170.9, 1.2, 200),
("cheap-far", 149.9, 40, 250),
("dear-near", 179.9, 1.6, nil),
("old-undated", 160.9, 5.0, 240)]
let pool = specs.map { station($0.0, price: $0.1, ageDays: $0.3) }
let journeys = distanceProvider(Dictionary(uniqueKeysWithValues:
specs.map { ($0.0, $0.2) }))
let ranked = FuelSavingsEstimator.ranked(pool, fuel: .e10, baseline: pool[0],
settings: settings, amount: settings.preferredAmount,
journeyMiles: journeys)
let savings = pool.map { station in
(station.id, FuelSavingsEstimator.availability(
station: station, fuel: .e10, baseline: pool[0], settings: settings,
amount: settings.preferredAmount, journey: journeys(station), baselineJourney: straightLine(0.8)
)?.result?.netSavingGBP ?? -Double.infinity)
}
let expectedWinner = savings.max { $0.1 < $1.1 }?.0
XCTAssertEqual(ranked.first?.id, expectedWinner,
"the offline list is still ordered by net saving")
XCTAssertNotNil(ranked.first?.prices[.e10], "the top station has a price to show")
XCTAssertGreaterThan(savings.first { $0.0 == expectedWinner }?.1 ?? -1, 0,
"and that recommendation carries a real saving")
}
}