88 lines
3.9 KiB
Swift
88 lines
3.9 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
|
|
return stations
|
|
.filter { $0.prices[fuel] != nil && $0.distanceKM(to: lat, lng2: lng) <= radiusKM }
|
|
.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)")!
|
|
}
|
|
}
|