Compare commits

..

No commits in common. "ef98531ca28fe6a2f66d981b0d94aeecf380aef7" and "79176585b441580b2af85b148c0d8bd7e9b468d9" have entirely different histories.

6 changed files with 48 additions and 113 deletions

View file

@ -1,4 +1,3 @@
# https://swiftbundler.dev/documentation/swift-bundler/configuration
format_version = 2 format_version = 2
[apps.AmtrakStatus] [apps.AmtrakStatus]
@ -6,7 +5,6 @@ identifier = 'com.tonybark.AmtrakStatus'
product = 'AmtrakStatus' product = 'AmtrakStatus'
version = '0.2.103' version = '0.2.103'
icon = "./assets/icon.png" icon = "./assets/icon.png"
category = "public.app-category.travel"
[[apps.AmtrakStatus.overlays]] [[apps.AmtrakStatus.overlays]]

View file

@ -28,10 +28,6 @@ struct AmtrakClient {
try await get(trainsBaseURL) try await get(trainsBaseURL)
} }
func fetchStaleStatus() async throws -> StaleData {
try await get(staleBaseURL)
}
func fetchAllTrainStatus(num trainNum: String) async throws -> [Train] { func fetchAllTrainStatus(num trainNum: String) async throws -> [Train] {
let reponse = try await fetchTrain(number: trainNum) let reponse = try await fetchTrain(number: trainNum)
return reponse.values.flatMap(\.self) return reponse.values.flatMap(\.self)

View file

@ -26,21 +26,21 @@ struct AmtrakDateFormatting {
return isoFormatter.date(from: isoString) return isoFormatter.date(from: isoString)
} }
func time(from isoString: String?, in zone: TimeZone?, comparedTo originZone: TimeZone? = nil) -> String? { func time(from isoString: String?, timeZone: TimeZone?, relativeTo otherZone: TimeZone? = nil) -> String? {
guard let isoString, !isoString.isEmpty, let date = isoFormatter.date( guard let isoString, !isoString.isEmpty, let date = isoFormatter.date(
from: isoString) else { from: isoString) else {
return nil return nil
} }
timeFormatter.timeZone = zone timeFormatter.timeZone = timeZone
let formatted = timeFormatter.string(from: date) let formatted = timeFormatter.string(from: date)
guard guard
let zone, let timeZone,
let originZone, let otherZone,
zone.identifier != originZone.identifier, timeZone.identifier != otherZone.identifier,
let abbreviation = zone.abbreviation(for: date) let abbreviation = timeZone.abbreviation(for: date)
else { else {
return formatted return formatted
} }
@ -48,29 +48,6 @@ struct AmtrakDateFormatting {
return "\(formatted) \(abbreviation)" return "\(formatted) \(abbreviation)"
} }
func isOnTime(actual: String?, scheduled: String?) -> LateTrain {
guard
let actualDate = parsedDate(from: actual),
let scheduledDate = parsedDate(from: scheduled)
else {
return .unknown
}
let differenceInMinutes = Int(actualDate.timeIntervalSince(scheduledDate) / 60)
switch differenceInMinutes {
case -1...1:
return .onTime
case ..<0:
return .early(minutes: -differenceInMinutes)
default:
return .late(minutes: differenceInMinutes)
}
}
func localTime(timeZone: TimeZone?) -> String { func localTime(timeZone: TimeZone?) -> String {
let now = Date() let now = Date()
timeFormatter.timeZone = timeZone timeFormatter.timeZone = timeZone

View file

@ -14,7 +14,6 @@ struct ContentView: View {
let amtrakDate = AmtrakDateFormatting() let amtrakDate = AmtrakDateFormatting()
let unknown = "Unknown" let unknown = "Unknown"
let staleDataThreshold: TimeInterval = 60 * 60 * 24 // 24 hours
var body: some View { var body: some View {
VStack { VStack {
@ -138,7 +137,6 @@ struct ContentView: View {
} }
var zoneCache: [String: TimeZone?] = [code: lookup.timeZone] var zoneCache: [String: TimeZone?] = [code: lookup.timeZone]
let staleness = await staleTrainLookup()
var newRows: [TrainStatusRow] = [] var newRows: [TrainStatusRow] = []
for number in lookup.trainNumbers { for number in lookup.trainNumbers {
@ -148,16 +146,15 @@ struct ContentView: View {
where: { $0.code == code where: { $0.code == code
}, },
)?.platform )?.platform
let row = await trainStatus( await newRows
.append(
trainStatus(
for: train, for: train,
code: code, atStationCode: code,
zone: lookup.timeZone, timeZone: lookup.timeZone,
cache: &zoneCache, zoneCache: &zoneCache
staleness: staleness
) )
if let row { )
newRows.append(row)
}
} }
} }
@ -172,22 +169,34 @@ struct ContentView: View {
} }
} }
func trainStatus( func isOnTime(arr: String?, schArr: String?) -> TrainLateness {
for train: Train,
code stationCode: String,
zone timeZone: TimeZone?,
cache zoneCache: inout [String: TimeZone?],
staleness: [String: TimeInterval]
) async -> TrainStatusRow? {
let leg = train.stations?.first { $0.code == stationCode }
guard guard
!isStale(leg, timeZone: timeZone) && let actualDate = amtrakDate.parsedDate(from: arr),
!isDataStale(train.trainID, staleness: staleness) let scheduledDate = amtrakDate.parsedDate(from: schArr)
else { else {
return nil return .unknown
} }
let differenceInMinutes = Int(actualDate.timeIntervalSince(scheduledDate) / 60)
switch differenceInMinutes {
case -1...1:
return .onTime
case ..<0:
return .early(minutes: -differenceInMinutes)
default:
return .late(minutes: differenceInMinutes)
}
}
func trainStatus(
for train: Train,
atStationCode code: String,
timeZone: TimeZone?,
zoneCache: inout [String: TimeZone?]
) async -> TrainStatusRow {
let leg = train.stations?.first { $0.code == code }
let stationName = leg?.name ?? unknown let stationName = leg?.name ?? unknown
let track = leg?.platform ?? "" let track = leg?.platform ?? ""
@ -201,12 +210,12 @@ struct ContentView: View {
let time = amtrakDate.time( let time = amtrakDate.time(
from: leg?.arr, from: leg?.arr,
in: timeZone, timeZone: timeZone,
comparedTo: originZone relativeTo: originZone
) ?? amtrakDate.time( ) ?? amtrakDate.time(
from: leg?.schArr, from: leg?.schArr,
in: timeZone, timeZone: timeZone,
comparedTo: originZone relativeTo: originZone
) )
currentStation = stationName currentStation = stationName
@ -220,53 +229,14 @@ struct ContentView: View {
train: train.routeName, train: train.routeName,
to: train.destName, to: train.destName,
from: train.origName, from: train.origName,
status: amtrakDate.isOnTime( status: isOnTime(
actual: leg?.arr, arr: leg?.arr,
scheduled: leg?.schArr schArr: leg?.schArr
), ),
track: track.isEmpty ? "" : track track: track.isEmpty ? "" : track
) )
} }
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? { func fetchTimeZone(forStationCode code: String, cache: inout [String: TimeZone?]) async -> TimeZone? {
if let cached = cache[code] { if let cached = cache[code] {
return cached return cached

View file

@ -81,15 +81,9 @@ struct StationMeta: Codable {
} }
struct StaleData: Codable { struct StaleData: Codable {
var activeTrains: Int
var avgLastUpdate: Double var avgLastUpdate: Double
var lastUpdatedArr: [TrainFreshness] var activeTrains: Int
var medianLastUpdate: Double var stale: Bool
}
struct TrainFreshness: Codable, Sendable {
var trainID: String
var timeSince: Double
} }
typealias TrainResponse = [String: [Train]] typealias TrainResponse = [String: [Train]]
@ -105,6 +99,6 @@ struct TrainStatusRow: Identifiable {
var train: String var train: String
var to: String var to: String
var from: String var from: String
var status: LateTrain var status: TrainLateness
var track: String var track: String
} }

View file

@ -1,7 +1,7 @@
import Foundation import Foundation
import SwiftCrossUI import SwiftCrossUI
enum LateTrain { enum TrainLateness {
case onTime case onTime
case early(minutes: Int) case early(minutes: Int)
case late(minutes: Int) case late(minutes: Int)