Files
fuelboard/Shared/SiriCheapestLookup.swift
T

63 lines
2.6 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)
}
}