P1: Favourites Trends graph — FuelHistoryStore + TrendsView (Swift Charts)
- Shared/FuelHistory.swift: GitHub mirror read path (latest.json pointer + history/YYYY-MM-DD.json day files); day math (UTC noon keys), slim day decode with the shared price band, series + deltaSeries (vs-cheapest rebase), favourites-only app-group cache (90-day prune), parallel per-day fetches, 404/network = graph gap never error. - FuelPriceProvider: shared priceBand + mapGrades (single source for live + history decoding). - FuelBoard/TrendsView.swift: line chart in the Favourites tab via toolbar button + sheet; fuel capsule (fuels with favourites only), 7/30/90 range, Price/vs-cheapest toggle, per-station legend, price-display style on y-axis, empty/loading/retry states with honest copy; no widget in v1. - Localizable.strings: Trends keys. - 83 tests (13 history + URL regression): day math, band guard, series gaps, delta rebasing, prune, and the appendingPathComponent regression (relative URL resolution dropped /main — all fetches 404'd). - Sim-verified: favourites rows + TOP/deltas; sheet controls + building-up state with live first-snapshot date (archive has 1 day; lines render once >=2 snapshots). - Backlog: P1 Trends DONE (unmerged); P0 REMAINING = live provider chain + telemetry beacon.
This commit is contained in:
@@ -42,6 +42,28 @@ enum FuelPriceProvider {
|
||||
}
|
||||
}
|
||||
|
||||
/// Price sanity band (pence/litre) — anything outside is relay regression
|
||||
/// garbage and must never reach calculations (shared by the live decode
|
||||
/// and the history mirror decoder).
|
||||
static let priceBand: ClosedRange<Double> = 50...500
|
||||
|
||||
/// Maps relay grade keys (E5/E10/DIESEL…) to FuelType with the defensive
|
||||
/// band guard. Shared by the relay decode and the history mirror parser so
|
||||
/// both surfaces apply identical sanitisation.
|
||||
static func mapGrades(_ prices: [String: Double]) -> [FuelType: Double] {
|
||||
var result: [FuelType: Double] = [:]
|
||||
for (grade, value) in prices {
|
||||
guard priceBand.contains(value) else { continue }
|
||||
switch grade.uppercased() {
|
||||
case "E10": result[.e10] = value
|
||||
case "E5": result[.e5] = value
|
||||
case "DIESEL", "B7", "B7S", "B7P", "B10": result[.diesel] = value
|
||||
default: break
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/// Decodes the relay envelope metadata (source, dataset update time) — the
|
||||
/// About section shows these so the user can see which data source is
|
||||
/// live and how fresh the GOV.UK data itself is. The fields are additive
|
||||
@@ -143,20 +165,9 @@ private struct RelayResponse: Codable {
|
||||
/// regression (e.g. the band being removed server-side) can't re-poison
|
||||
/// the nationwide cheapest reference with 1.3p / 1589p garbage.
|
||||
var allPrices: [FuelType: Double] {
|
||||
var result: [FuelType: Double] = [:]
|
||||
if let prices {
|
||||
for (grade, value) in prices {
|
||||
guard (50...500).contains(value) else { continue }
|
||||
switch grade.uppercased() {
|
||||
case "E10": result[.e10] = value
|
||||
case "E5": result[.e5] = value
|
||||
case "DIESEL", "B7", "B7S", "B7P", "B10": result[.diesel] = value
|
||||
default: break
|
||||
}
|
||||
}
|
||||
}
|
||||
var result = FuelPriceProvider.mapGrades(prices ?? [:])
|
||||
// Backwards-compat: relay versions without `prices` still send `price`.
|
||||
if result.isEmpty, let price, (50...500).contains(price) {
|
||||
if result.isEmpty, let price, FuelPriceProvider.priceBand.contains(price) {
|
||||
result[.e10] = price
|
||||
}
|
||||
return result
|
||||
|
||||
Reference in New Issue
Block a user