Files
fuelboard/FuelBoardTests/Tests/FuelBoardSharedTests/FuelSavingsTests.swift
T
FuelBoard Contributor fcd1fb8572 Use the location pointer for the Closest label
The Closest label was drawing location.north.fill. A north arrow reads as a
bearing — it implies direction of travel toward something, which is the wrong
idea for a label that means "this is the station nearest you". The plain
location pointer says where a place is without implying a heading, and it is
already the glyph the app's other location affordances draw in Settings and on
the map.

One constant, as before: StationLabelKind.closest.symbol. The badge, the Key
legend and both verdict symbols reference it, so nothing else needed touching.

The test that pins each label's glyph now expects "location"; its north-arrow
expectation was the only failure in the suite. The comment above the verdict
symbol still described a north arrow and now describes the pointer.

Also bumps CURRENT_PROJECT_VERSION 30 -> 31 for this device install round.

Tests: 278 pass.
2026-10-19 15:20:44 +01:00

536 lines
26 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 testPoundsBudgetConvertsToLitresUsingGivenPrice() throws {
let amount = FuelSavingsFillAmount(value: 40, unit: .pounds)
let litres = try FuelSavingsCalculator.fillLitres(amount: amount, pricePencePerLitre: 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, pricePencePerLitre: 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, 26.6667, accuracy: 0.0001)
XCTAssertEqual(result.grossSavingGBP, 2.6667, accuracy: 0.0001)
XCTAssertEqual(result.extraDistanceMiles, 6, accuracy: 0.0001)
XCTAssertEqual(result.extraDrivingCostGBP, 0.9092, accuracy: 0.0001)
XCTAssertEqual(result.netSavingGBP, 1.7574, 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), pricePencePerLitre: 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"])
}
func testBestValueAlertCandidateRequiresConfiguredSavingsAndThreshold() {
let stations = [
station("near", lat: 0, lng: 0, price: 160),
station("cheap", lat: 0, lng: 0, price: 150),
]
let journeys = ["near": 2.0, "cheap": 4.0]
let off = FuelSavingsSettings(enabled: false, mpg: 45, preferredAmount: .init(value: 40, unit: .pounds))
XCTAssertNil(FuelSavingsCalculator.bestValueAlertCandidate(
stations: stations,
fuel: .e10,
settings: off,
amount: off.preferredAmount,
minimumNetSavingGBP: 1.0,
journeyMiles: { journeys[$0.id].map { FuelSavingsJourney(miles: $0, basis: .roadRoute) } }
))
let on = FuelSavingsSettings(enabled: true, mpg: 45, preferredAmount: .init(value: 40, unit: .pounds))
XCTAssertNil(FuelSavingsCalculator.bestValueAlertCandidate(
stations: stations,
fuel: .e10,
settings: on,
amount: on.preferredAmount,
minimumNetSavingGBP: 5.0,
journeyMiles: { journeys[$0.id].map { FuelSavingsJourney(miles: $0, basis: .roadRoute) } }
))
}
func testBestValueAlertCandidatePicksHighestNetSavingAboveThreshold() throws {
let stations = [
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))
let candidate = FuelSavingsCalculator.bestValueAlertCandidate(
stations: stations,
fuel: .e10,
settings: settings,
amount: settings.preferredAmount,
minimumNetSavingGBP: 1.0,
journeyMiles: { journeys[$0.id].map { FuelSavingsJourney(miles: $0, basis: .roadRoute) } }
)
XCTAssertEqual(candidate?.station.id, "best")
XCTAssertEqual(candidate?.result.status, .worthTheDrive)
XCTAssertGreaterThan(candidate?.result.netSavingGBP ?? 0, 1.0)
}
func testBestValueAlertCandidateBreaksTiesByDistanceThenPrice() throws {
let first = FuelSavingsResult(fillLitres: 25, grossSavingGBP: 3, extraDrivingCostGBP: 1, extraDistanceMiles: 2, netSavingGBP: 2, status: .worthTheDrive)
let nearer = FuelSavingsRankedStation(stationID: "nearer", result: .available(first), distanceMiles: 4, pricePencePerLitre: 149)
let farther = FuelSavingsRankedStation(stationID: "farther", result: .available(first), distanceMiles: 8, pricePencePerLitre: 145)
XCTAssertEqual(FuelSavingsCalculator.sortedForWorthTheDrive([farther, nearer]).first?.stationID, "nearer")
}
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)
}
}
final class FuelSavingsSettingsTests: XCTestCase {
func testSwitchingFillUnitStartsAnExplicitNewComparison() {
let budget = FuelSavingsFillAmount(value: 60, unit: .pounds)
let litres = FuelSavingsCalculator.newComparisonAmount(afterSwitching: budget, to: .litres)
XCTAssertEqual(litres, FuelSavingsFillAmount(value: 30, unit: .litres))
XCTAssertEqual(FuelSavingsCalculator.newComparisonAmount(afterSwitching: litres, to: .pounds),
FuelSavingsFillAmount(value: 40, unit: .pounds))
XCTAssertEqual(FuelSavingsCalculator.newComparisonAmount(afterSwitching: budget, to: .pounds), budget)
}
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)
}
}
/// The row's single recommendation badge. Presentation only, but the rules are
/// worth pinning: they decide which of two true statements a row makes, and
/// "no badge" is itself an answer.
final class StationRecommendationBadgeTests: XCTestCase {
private let threshold = 1.0
func testSavingAtTheThresholdEarnsBestValue() {
XCTAssertEqual(
StationRecommendationBadge.badge(netSavingGBP: 1.0, isCheapest: false,
bestValueThresholdGBP: threshold),
.bestValue
)
}
func testSavingBelowTheThresholdDoesNot() {
XCTAssertNil(
StationRecommendationBadge.badge(netSavingGBP: 0.42, isCheapest: false,
bestValueThresholdGBP: threshold)
)
}
func testCheapestWithoutAWorthwhileSavingStillSaysCheapest() {
XCTAssertEqual(
StationRecommendationBadge.badge(netSavingGBP: 0.42, isCheapest: true,
bestValueThresholdGBP: threshold),
.cheapest
)
}
/// The cheapest station keeps its badge even when the drive costs more than
/// the saving buys — it IS the cheapest price; the capsule underneath is
/// what says the trip is not worth it.
func testCheapestKeepsItsBadgeWhenTheDriveCostsMoreThanItSaves() {
XCTAssertEqual(
StationRecommendationBadge.badge(netSavingGBP: -14.30, isCheapest: true,
bestValueThresholdGBP: threshold),
.cheapest
)
}
/// One badge per row: the stronger statement wins.
func testBestValueOutranksCheapestWhenBothApply() {
XCTAssertEqual(
StationRecommendationBadge.badge(netSavingGBP: 3.15, isCheapest: true,
bestValueThresholdGBP: threshold),
.bestValue
)
}
/// A station with no saving, not the cheapest and not the closest states
/// nothing a badge could carry.
func testNoSavingNotCheapestAndNotClosestMeansNoBadge() {
XCTAssertNil(
StationRecommendationBadge.badge(netSavingGBP: 0.0, isCheapest: false,
isClosest: false,
bestValueThresholdGBP: threshold)
)
XCTAssertNil(
StationRecommendationBadge.badge(netSavingGBP: -2.50, isCheapest: false,
isClosest: false,
bestValueThresholdGBP: threshold)
)
}
func testTheClosestStationWearsClosestWhenNothingElseApplies() {
XCTAssertEqual(
StationRecommendationBadge.badge(netSavingGBP: 0.0, isCheapest: false,
isClosest: true,
bestValueThresholdGBP: threshold),
.closest
)
// Closest is a position, so it does not need a savings figure to hold.
XCTAssertEqual(
StationRecommendationBadge.badge(netSavingGBP: nil, isCheapest: false,
isClosest: true,
bestValueThresholdGBP: threshold),
.closest
)
}
/// The nearest forecourt is usually also the cheapest, so without the
/// ordering the Closest label would win by accident and hide the price news.
func testCheapestOutranksClosestWhenBothApply() {
XCTAssertEqual(
StationRecommendationBadge.badge(netSavingGBP: 0.42, isCheapest: true,
isClosest: true,
bestValueThresholdGBP: threshold),
.cheapest
)
}
func testBestValueOutranksBothCheapestAndClosest() {
XCTAssertEqual(
StationRecommendationBadge.badge(netSavingGBP: 3.15, isCheapest: true,
isClosest: true,
bestValueThresholdGBP: threshold),
.bestValue
)
}
/// The whole ladder in one place, so a future case cannot slide in between
/// two rungs unnoticed: Best Value, then Cheapest, then Closest, then none.
func testLabelPrecedenceIsTotalAcrossEveryCombination() {
let cases: [(saving: Double, cheapest: Bool, closest: Bool,
badge: StationRecommendationBadge?)] = [
(3.15, true, true, .bestValue),
(3.15, false, true, .bestValue),
(3.15, false, false, .bestValue),
(0.42, true, true, .cheapest),
(0.42, true, false, .cheapest),
(0.42, false, true, .closest),
(0.42, false, false, nil)
]
for c in cases {
XCTAssertEqual(
StationRecommendationBadge.badge(
netSavingGBP: c.saving, isCheapest: c.cheapest, isClosest: c.closest,
bestValueThresholdGBP: threshold
),
c.badge,
"saving \(c.saving), cheapest \(c.cheapest), closest \(c.closest)"
)
}
}
func testUnavailableSavingsLeaveOnlyTheCheapestBadge() {
XCTAssertEqual(
StationRecommendationBadge.badge(netSavingGBP: nil, isCheapest: true,
bestValueThresholdGBP: threshold),
.cheapest
)
XCTAssertNil(
StationRecommendationBadge.badge(netSavingGBP: nil, isCheapest: false,
bestValueThresholdGBP: threshold)
)
}
/// A rubbish figure must not become a recommendation.
func testNonFiniteFiguresProduceNoBadge() {
XCTAssertNil(
StationRecommendationBadge.badge(netSavingGBP: .nan, isCheapest: false,
bestValueThresholdGBP: threshold)
)
XCTAssertNil(
StationRecommendationBadge.badge(netSavingGBP: 5.0, isCheapest: false,
bestValueThresholdGBP: .nan)
)
}
/// The threshold is the user's alert setting: set it to zero and any saving
/// that is not a loss counts.
func testZeroThresholdAwardsBestValueToAnyNonNegativeSaving() {
XCTAssertEqual(
StationRecommendationBadge.badge(netSavingGBP: 0, isCheapest: false,
bestValueThresholdGBP: 0),
.bestValue
)
}
}
final class KeySheetContentTests: XCTestCase {
func testPriceRatingsExplainTheImplementedRAGBoundaries() {
XCTAssertEqual(KeySheetContent.priceRatings, [
.init(label: "Good price", description: "Within 1.5p/L of the cheapest."),
.init(label: "Okay", description: "More than 1.5p/L, up to 4p/L above the cheapest."),
.init(label: "Pricey", description: "More than 4p/L above the cheapest.")
])
}
func testSavingsExplainVerdictsWithoutImplyingMinimumSavingQualification() {
XCTAssertEqual(KeySheetContent.savings, [
.init(label: "Saves", description: "You’ll save money compared with your closest station, including travel costs."),
.init(label: "Break even", description: "The saving or extra cost is negligible."),
// "Costs" — the listing capsule's own headline, not "Costs more".
.init(label: "Costs", description: "Filling up here costs more than at your closest station, including travel.")
])
XCTAssertFalse(KeySheetContent.savings.contains { $0.label == "Small saving" })
}
/// Every Key label is sentence case: the capsules are read as phrases, and an
/// all-caps set reads as shouting beside the sentence-case copy around it.
func testEveryKeyLabelIsSentenceCase() {
let labels = (KeySheetContent.priceRatings
+ KeySheetContent.savings
+ KeySheetContent.stationLabels).map(\.label)
for label in labels {
XCTAssertNotEqual(label, label.uppercased(),
"\(label) must be sentence case, not all caps")
}
XCTAssertEqual(KeySheetContent.stationLabels.map(\.label),
["Best value", "Cheapest", "Closest"])
}
/// The Key dispatches its indicators on `StationLabelKind`, so every kind must
/// have exactly one entry in the legend — and each entry's label must be the
/// kind's own, or the row would describe one label and draw another.
func testEveryStationLabelKindHasOneMatchingLegendEntry() {
XCTAssertEqual(StationLabelKind.allCases.count, KeySheetContent.stationLabels.count)
for kind in StationLabelKind.allCases {
let matches = KeySheetContent.stationLabels.filter { $0.label == kind.label }
XCTAssertEqual(matches.count, 1, "\(kind) must own exactly one legend entry")
}
}
/// Recommended symbols, pinned so a rename cannot silently blank a badge.
func testStationLabelKindsUseTheirRecommendedSymbols() {
XCTAssertEqual(StationLabelKind.bestValue.symbol, "medal")
XCTAssertEqual(StationLabelKind.cheapest.symbol, "tag")
XCTAssertEqual(StationLabelKind.closest.symbol, "location")
// No trophy: "Top" is gone. One state, one label.
XCTAssertFalse(StationLabelKind.allCases.map(\.symbol).contains("trophy"))
}
func testStationLabelsDescribeTheCurrentQualificationAndRangeRules() {
XCTAssertEqual(KeySheetContent.stationLabels, [
.init(label: "Best value", description: "Meets your minimum saving after travel costs."),
.init(label: "Cheapest", description: "The lowest fuel price per litre within your selected range."),
.init(label: "Closest", description: "The closest station by journey distance.")
])
}
/// "Top" is retired. It and "Cheapest" were two words for the same flag, and the
/// app drifted between them by device: the phone's rows said Cheapest while the
/// watch said Top for the identical state. Nothing may reintroduce the synonym.
func testTopLabelIsRetired() {
XCTAssertFalse(StationLabelKind.allCases.contains { $0.label == "Top" })
XCTAssertFalse(KeySheetContent.stationLabels.contains { $0.label == "Top" })
// The concept is not lost — it is called Cheapest, which is also the word
// every piece of favourites copy uses.
XCTAssertTrue(StationLabelKind.allCases.contains { $0.label == "Cheapest" })
}
/// The closest label is one word everywhere: the badge, the verdict capsule the
/// baseline station wears, and the Key all read it from `baselineHeadline`.
func testClosestLabelIsTheSameWordEverywhere() {
XCTAssertEqual(FuelSavingsResult.baselineHeadline, "Closest")
XCTAssertEqual(StationLabelKind.closest.label, "Closest")
let baseline = FuelSavingsResult(
fillLitres: 30,
grossSavingGBP: 0,
extraDrivingCostGBP: 0,
extraDistanceMiles: 0,
netSavingGBP: 0,
status: .baseline,
journeyConfidence: .confirmed
)
XCTAssertEqual(baseline.headline(), "Closest")
XCTAssertEqual(baseline.headline(), StationLabelKind.closest.label)
}
func testFavouriteAndHelpCopyRemainPinned() {
XCTAssertEqual(
KeySheetContent.favourite,
.init(label: "Favourites", description: "Star a station to save it to your Favourites.")
)
// The link names the calculation, not the feature: the sheet explains the
// fill-up, the return trip and the total, of which the Best Value gate is
// only the last step.
XCTAssertEqual(KeySheetContent.helpTitle, "How does FuelBoard calculate value?")
// The sheet title is a short form of the question, NOT the question itself:
// a nav title clamps to the bar, and the full question truncated to
// "How does FuelBoard…" at both display modes.
XCTAssertEqual(KeySheetContent.helpSheetTitle, "How value is calculated")
XCTAssertLessThan(KeySheetContent.helpSheetTitle.count, KeySheetContent.helpTitle.count)
}
}
/// The More info sheet's ranking capsules. Every rule must come from the shared
/// badge logic — the sheet may show more labels than a row (which shows one), but
/// it must not invent a qualification of its own.
final class StationRankingTests: XCTestCase {
private let threshold = 2.50
func testNoQualificationProducesNoCapsules() {
XCTAssertEqual(
StationRanking.kinds(netSavingGBP: 0.40, isCheapestInRange: false,
bestValueThresholdGBP: threshold),
[]
)
}
func testBestValueUsesTheSameGateAsTheListingBadge() {
// Exactly at the threshold qualifies, matching StationRecommendationBadge.
XCTAssertEqual(
StationRanking.kinds(netSavingGBP: threshold, isCheapestInRange: false,
bestValueThresholdGBP: threshold),
[.bestValue]
)
// A hair under does not.
XCTAssertEqual(
StationRanking.kinds(netSavingGBP: threshold - 0.01, isCheapestInRange: false,
bestValueThresholdGBP: threshold),
[]
)
}
/// Cheapest is the only ranking label besides Best value. Whichever tab opened
/// the sheet — Stations or Favourites — it means the lowest price among the
/// stations on that screen, so there is no second cheapest-flavoured capsule.
func testCheapestIsTheOnlyCheapestFlavouredLabel() {
XCTAssertEqual(
StationRanking.kinds(netSavingGBP: nil, isCheapestInRange: true,
bestValueThresholdGBP: threshold),
[.cheapest]
)
XCTAssertEqual(
StationRanking.kinds(netSavingGBP: nil, isCheapestInRange: false,
bestValueThresholdGBP: threshold),
[]
)
}
func testCapsulesAreOrderedBestValueThenCheapest() {
XCTAssertEqual(
StationRanking.kinds(netSavingGBP: 9.99, isCheapestInRange: true,
bestValueThresholdGBP: threshold),
[.bestValue, .cheapest]
)
}
/// The sheet has room for every qualification, so a station holding all three
/// shows all three — in the Key's order, which is also the row's precedence,
/// so the sheet reads left-to-right the same way a row resolves top-to-bottom.
func testCapsulesAreOrderedBestValueCheapestClosest() {
XCTAssertEqual(
StationRanking.kinds(netSavingGBP: 9.99, isCheapestInRange: true,
isClosestInRange: true, bestValueThresholdGBP: threshold),
[.bestValue, .cheapest, .closest]
)
}
/// The closest station with no saving and no price advantage still earns its
/// label on the sheet — the one case a listing row cannot show, because there
/// Best Value and Cheapest are both unavailable and Closest wins the slot.
func testClosestAloneEarnsOneCapsule() {
XCTAssertEqual(
StationRanking.kinds(netSavingGBP: nil, isCheapestInRange: false,
isClosestInRange: true, bestValueThresholdGBP: threshold),
[.closest]
)
XCTAssertEqual(
StationRanking.kinds(netSavingGBP: 0.0, isCheapestInRange: false,
isClosestInRange: true, bestValueThresholdGBP: threshold),
[.closest]
)
}
/// Closest must not sneak in when the host did not flag it — the sheet never
/// invents a rule of its own.
func testNoClosestFlagMeansNoClosestCapsule() {
XCTAssertEqual(
StationRanking.kinds(netSavingGBP: nil, isCheapestInRange: true,
isClosestInRange: false, bestValueThresholdGBP: threshold),
[.cheapest]
)
}
/// A row shows ONE badge; the sheet may show several. Both must agree on the
/// Best Value decision, or the sheet would claim a label the list behind it
/// withheld.
func testSheetAgreesWithTheRowOnBestValue() {
for netSaving in [-3.0, 0.0, 2.49, 2.50, 12.0, .nan] as [Double] {
let rowSaysBestValue = StationRecommendationBadge.badge(
netSavingGBP: netSaving, isCheapest: false, bestValueThresholdGBP: threshold
) == .bestValue
let sheetSaysBestValue = StationRanking.kinds(
netSavingGBP: netSaving, isCheapestInRange: false,
bestValueThresholdGBP: threshold
).contains(.bestValue)
XCTAssertEqual(rowSaysBestValue, sheetSaysBestValue, "disagreed on net saving \(netSaving)")
}
}
}