Files
fuelboard/Shared/SiriCheapestLookup.swift
FuelBoard Contributor 2a2823c180 Accurate distances toggle, Approx. distance markers, and Closest-mode speedup
Distance confidence
- Every distance surface takes the figure AND its "Approx." marker from one
  FuelStore.stationDistanceDisplay call (station rows, detail sheet, widget,
  Live Activity, watch, Siri), so a surface can never mark one figure approximate
  while showing a routed one. Radius filters and Closest ordering test that same
  displayed distance, so no row can read a distance outside the radius that
  filtered it (the reported "14.6 mi inside a 10-mile list").

Accurate distances (Settings -> Distances, default off)
- Off: the browse layer serves the straight line, marked Approx., for every row,
  so two installs agree to the metre and the app makes no route requests at all.
- On: routed distances for the whole search area, unmarked, served only from a
  COMPLETE set anchored within 600 m — a partial fill reverts every row at once
  rather than mixing road and straight-line bases.
- Best Value maths sits deliberately outside the toggle: it keeps using measured
  road routes and its own confidence marking, so money figures never move with
  the switch.
- Fills run only while the toggle is on and the app is frontmost; capacity raised
  to 600 stations per pass; completeness tolerates pins Apple Maps has no route
  for, which otherwise left an area permanently incomplete and the whole list
  stuck on estimates.

Performance (the reported slowdown switching to Closest / launching with it)
- FuelStore.accurateDistancesEnabled was a UserDefaults(suiteName:) read per
  call: a synchronous cfprefsd round-trip measured at ~1.9 us, ~200k calls per
  Closest sort. Memoised, with the setter keeping it in step.
- Both keyed sorts decorate their keys — one distance per station, carried as an
  index instead of copying whole stations into the decorated array — and the
  comparison baseline plus per-station journeys now come from ONE pass instead of
  two full scans per tab. The Stations tab also reuses its single sorted list for
  the count and the TOP badge instead of sorting the country three times.
- Measured on iPhone SE (7,914-station Closest pool): sort 36.6 ms -> 17.1 ms per
  list, Cheapest 6 ms, Best Value 11 ms.
- DEBUG probes: FB_PERF list timing, FB_PERF_BENCH toggle-read benchmark, and
  FB_DIST_FOCUS now reports the served value, marker and mode (and re-prints when
  a fill lands). FB_ACCURATE_DISTANCES / FB_SORT_MODE env forms for devices, where
  devicectl eats a leading-dash argument.

Tests: 198, 0 failures. Device and simulator Debug builds green.
2026-10-13 18:01:45 +01:00

94 lines
4.2 KiB
Swift

import Foundation
/// Pure, testable logic behind the Siri "cheapest fuel near me" intent.
/// Foundation-only on purpose: `Shared/` joins both the app and widget
/// targets, and FuelBoardTests exercises this directly.
enum SiriCheapestLookup {
/// The cheapest station selling `fuel` WITHIN `withinMiles` of the given
/// point, tie-broken by distance. Stations without a price for `fuel`, or
/// farther than the radius, are skipped entirely. Returns nil when no
/// station sells the fuel inside the radius.
///
/// Radius scoping mirrors the app's Stations tab (search radius
/// 5/10/15 mi): without it, "cheapest near me" silently answers the
/// UK-wide minimum — a genuinely cheap station hundreds of miles away
/// (found with real data: GULF HISTON 100.9p was 132 mi from the phone).
static func cheapest(
in stations: [FuelStation],
fuel: FuelType,
fromLat lat: Double,
lng: Double,
withinMiles: Double
) -> FuelStation? {
let radiusKM = withinMiles * 1.60934
// Same radius rule as the app list: the distance the answer would SHOW
// (cached road route where there is one).
return stations
.filter {
$0.prices[fuel] != nil
&& FuelStore.isWithinDisplayRadius(station: $0, radiusKM: radiusKM,
userLat: lat, userLng: lng)
}
.min { lhs, rhs in
guard let lp = lhs.prices[fuel], let rp = rhs.prices[fuel] else {
return false
}
if lp != rp { return lp < rp }
return lhs.distanceKM(to: lat, lng2: lng) < rhs.distanceKM(to: lat, lng2: lng)
}
}
/// A short freshness stamp for the dialog: "as of 6:30 AM".
/// Prefers the GOV.UK `data_updated` stamp carried by the relay (ISO 8601
/// with or without fractional seconds); falls back to the local
/// last-refresh date; returns "" when there is no data at all.
static func freshnessLabel(updated: String?, lastRefresh: Date?, now: Date = Date()) -> String {
let date: Date?
if let updated {
date = Self.parseISO(updated) ?? lastRefresh
} else {
date = lastRefresh
}
guard let date else { return "" }
let formatter = DateFormatter()
formatter.dateFormat = "h:mm a"
formatter.locale = Locale(identifier: "en_GB")
return "as of \(formatter.string(from: date))"
}
private static func parseISO(_ string: String) -> Date? {
let withFraction = ISO8601DateFormatter()
withFraction.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
if let date = withFraction.date(from: string) { return date }
let plain = ISO8601DateFormatter()
plain.formatOptions = [.withInternetDateTime]
return plain.date(from: string)
}
/// The user's TOP favourite for `fuel`: the first entry in the manual
/// favourites order (the Favourites tab's drag-and-drop order IS the
/// widget/priority order — the first station of a fuel is its "single
/// widget" favourite). Favourites are refreshed against the current
/// station dump first so prices are as fresh as the cache allows;
/// stations missing from the dump keep their cached snapshot, so the
/// answer still works fully offline. Returns nil when the user has no
/// favourite for that fuel.
static func topFavourite(
in favourites: [FavouriteEntry],
from stations: [FuelStation],
fuel: FuelType
) -> FavouriteEntry? {
let refreshed = FuelStore.refreshedFavourites(favourites, from: stations)
return refreshed.first { $0.fuel == fuel }
}
/// Apple Maps URL for turn-by-turn directions to a point. MUST be the
/// universal-link form (https://maps.apple.com) — OpenURLIntent rejects
/// custom schemes like `maps://`, and the https form survives copy/paste
/// and the share sheet.
static func mapsURL(latitude: Double, longitude: Double) -> URL {
URL(string: "https://maps.apple.com/?daddr=\(latitude),\(longitude)")!
}
}