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)")! } }