From f8eef909d2dd87e670896e27afdc53871e93b988 Mon Sep 17 00:00:00 2001 From: Tony Bark <35226681+tonytins@users.noreply.github.com> Date: Mon, 24 Aug 2026 22:53:15 -0400 Subject: [PATCH] Added handling for stale trains - Renamed TrainLateness to LateTrain --- Sources/AmtrakStatus/AmtrakClient.swift | 4 ++ .../AmtrakStatus/AmtrakDateFormatting.swift | 2 +- Sources/AmtrakStatus/ContentView.swift | 69 +++++++++++++++---- .../{TrainLateness.swift => LateTrain.swift} | 2 +- Sources/AmtrakStatus/Models.swift | 12 +++- 5 files changed, 72 insertions(+), 17 deletions(-) rename Sources/AmtrakStatus/{TrainLateness.swift => LateTrain.swift} (98%) diff --git a/Sources/AmtrakStatus/AmtrakClient.swift b/Sources/AmtrakStatus/AmtrakClient.swift index 0fbe23f..cd0a00d 100644 --- a/Sources/AmtrakStatus/AmtrakClient.swift +++ b/Sources/AmtrakStatus/AmtrakClient.swift @@ -27,6 +27,10 @@ struct AmtrakClient { func fetchAllTrains() async throws -> TrainResponse { try await get(trainsBaseURL) } + + func fetchStaleStatus() async throws -> StaleData { + try await get(staleBaseURL) + } func fetchAllTrainStatus(num trainNum: String) async throws -> [Train] { let reponse = try await fetchTrain(number: trainNum) diff --git a/Sources/AmtrakStatus/AmtrakDateFormatting.swift b/Sources/AmtrakStatus/AmtrakDateFormatting.swift index 34452cb..d7b379e 100644 --- a/Sources/AmtrakStatus/AmtrakDateFormatting.swift +++ b/Sources/AmtrakStatus/AmtrakDateFormatting.swift @@ -49,7 +49,7 @@ struct AmtrakDateFormatting { } - func isOnTime(actual: String?, scheduled: String?) -> TrainLateness { + func isOnTime(actual: String?, scheduled: String?) -> LateTrain { guard let actualDate = parsedDate(from: actual), diff --git a/Sources/AmtrakStatus/ContentView.swift b/Sources/AmtrakStatus/ContentView.swift index 507cbe7..2a48ffb 100644 --- a/Sources/AmtrakStatus/ContentView.swift +++ b/Sources/AmtrakStatus/ContentView.swift @@ -14,6 +14,7 @@ struct ContentView: View { let amtrakDate = AmtrakDateFormatting() let unknown = "Unknown" + let staleDataThreshold: TimeInterval = 60 * 60 * 24 // 24 hours var body: some View { VStack { @@ -146,15 +147,15 @@ struct ContentView: View { where: { $0.code == code }, )?.platform - await newRows - .append( - trainStatus( + let row = await trainStatus( for: train, - atStationCode: code, - timeZone: lookup.timeZone, - zoneCache: &zoneCache + code: code, + zone: lookup.timeZone, + cache: &zoneCache ) - ) + if let row { + newRows.append(row) + } } } @@ -171,11 +172,16 @@ struct ContentView: View { func trainStatus( for train: Train, - atStationCode code: String, - timeZone: TimeZone?, - zoneCache: inout [String: TimeZone?] - ) async -> TrainStatusRow { - let leg = train.stations?.first { $0.code == code } + code stationCode: String, + zone timeZone: TimeZone?, + cache zoneCache: inout [String: TimeZone?] + ) async -> TrainStatusRow? { + let leg = train.stations?.first { $0.code == stationCode } + + guard !isStale(leg, timeZone: timeZone) else { + return nil + } + let stationName = leg?.name ?? unknown let track = leg?.platform ?? "" @@ -216,6 +222,45 @@ struct ContentView: View { ) } + func isStale(_ leg: Station?, timeZone: TimeZone?) -> Bool { + guard leg?.status == .departed else { return false } + + guard + let deparatureDate = amtrakDate.parsedDate( + from: leg?.dep + ) ?? amtrakDate.parsedDate(from: leg?.schDep) + else { + return false + } + + var calendar = Calendar.current + calendar.timeZone = timeZone ?? .current + return calendar + .compare( + deparatureDate, + to: Date(), + toGranularity: .day + ) == .orderedAscending + } + + func isDataStale( + _ trainId: String, + staleness: [String: TimeInterval] + ) -> Bool { + guard let timeSince = staleness[trainId] else { return false } + return timeSince > staleDataThreshold + } + + func staleTrainLookup() async -> [String: TimeInterval] { + guard let staleData = try? await client.fetchStaleStatus() else { + return [:] + } + return Dictionary( + staleData.lastUpdatedArr.map { ( $0.trainID, $0.timeSince) }, + uniquingKeysWith: { first, _ in first } + ) + } + func fetchTimeZone(forStationCode code: String, cache: inout [String: TimeZone?]) async -> TimeZone? { if let cached = cache[code] { return cached diff --git a/Sources/AmtrakStatus/TrainLateness.swift b/Sources/AmtrakStatus/LateTrain.swift similarity index 98% rename from Sources/AmtrakStatus/TrainLateness.swift rename to Sources/AmtrakStatus/LateTrain.swift index ab81940..c36ac7b 100644 --- a/Sources/AmtrakStatus/TrainLateness.swift +++ b/Sources/AmtrakStatus/LateTrain.swift @@ -1,7 +1,7 @@ import Foundation import SwiftCrossUI -enum TrainLateness { +enum LateTrain { case onTime case early(minutes: Int) case late(minutes: Int) diff --git a/Sources/AmtrakStatus/Models.swift b/Sources/AmtrakStatus/Models.swift index ece20f9..11d5aec 100644 --- a/Sources/AmtrakStatus/Models.swift +++ b/Sources/AmtrakStatus/Models.swift @@ -81,9 +81,15 @@ struct StationMeta: Codable { } struct StaleData: Codable { - var avgLastUpdate: Double var activeTrains: Int - var stale: Bool + var avgLastUpdate: Double + var lastUpdatedArr: [TrainFreshness] + var medianLastUpdate: Double +} + +struct TrainFreshness: Codable, Sendable { + var trainID: String + var timeSince: Double } typealias TrainResponse = [String: [Train]] @@ -99,6 +105,6 @@ struct TrainStatusRow: Identifiable { var train: String var to: String var from: String - var status: TrainLateness + var status: LateTrain var track: String }