Add timezone-aware station lookup and formatting

These changes ensure displayed times respect the station's timezone. Something I mistakenly overlooked when creating this.

Luckily, my screenshot with Ashland isn't affected by this change.
This commit is contained in:
Tony Bark 2026-08-24 01:00:47 -04:00
parent fc45303761
commit dd50d7cd4a
3 changed files with 42 additions and 19 deletions

View file

@ -7,6 +7,12 @@ enum AmtrakError: Error {
case stationNotFound(String) case stationNotFound(String)
} }
struct TrainStationLookUp: Sendable {
var trainNumbers: [String]
var timeZone: TimeZone?
}
struct AmtrakClient { struct AmtrakClient {
var decoder = JSONDecoder() var decoder = JSONDecoder()
@ -41,15 +47,21 @@ struct AmtrakClient {
.first .first
.map(String.init) ?? trainId .map(String.init) ?? trainId
} }
func fetchStationTrainNumbers(stationCode code: String) async throws -> [String] { func trainStationLookup(stationCode code: String) async throws -> TrainStationLookUp {
let reponse = try await fetchStation(code: code) let reponse = try await fetchStation(code: code)
guard let meta = reponse[code] else { guard let meta = reponse[code] else {
throw AmtrakError.stationNotFound(code) throw AmtrakError.stationNotFound(code)
} }
return Array(Set(meta.trains.map(trainNumber(fromTrainId:)))) let trainNumbers = Array(Set(meta.trains.map(trainNumber(fromTrainId:))))
let timeZone = meta.tz.flatMap(TimeZone.init(identifier:))
return TrainStationLookUp(
trainNumbers: trainNumbers,
timeZone: timeZone
)
} }
func get<T: Decodable>(_ urlString: String) async throws -> T { func get<T: Decodable>(_ urlString: String) async throws -> T {

View file

@ -26,12 +26,12 @@ struct AmtrakDateFormatting {
return isoFormatter.date(from: isoString) return isoFormatter.date(from: isoString)
} }
func time(from isoString: String?) -> String? { func time(from isoString: String?, timeZone: 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 = timeZone
return timeFormatter.string(from: date) return timeFormatter.string(from: date)
} }

View file

@ -24,7 +24,7 @@ struct ContentView: View {
Task { Task {
await loadTrainStatus(forStationCode: normalizedStationCode) await loadTrainStatus(forStationCode: normalizedStationCode)
} }
}.disabled(normalizedStationCode.isEmpty || isLoading) }.disabled(normalizedStationCode.isEmpty)
}.padding(10) }.padding(10)
// Most popular Virtual Railfan stations // Most popular Virtual Railfan stations
@ -103,6 +103,12 @@ struct ContentView: View {
return .white return .white
} }
} }
func loadStationStatus(forStationCode code: String) async {
guard !code.isEmpty else {
return
}
}
func loadTrainStatus(forStationCode code: String) async { func loadTrainStatus(forStationCode code: String) async {
guard !code.isEmpty else { guard !code.isEmpty else {
@ -115,25 +121,32 @@ struct ContentView: View {
defer { isLoading = false } defer { isLoading = false }
do { do {
let trainNumbers = try await client.fetchStationTrainNumbers( let lookup = try await client.trainStationLookup(
stationCode: code, stationCode: code,
) )
guard !trainNumbers.isEmpty else { guard !lookup.trainNumbers.isEmpty else {
errorMessage = "\(code) has no trains listed right now" errorMessage = "\(code) has no trains listed right now"
rows = [] rows = []
return return
} }
var newRows: [TrainStatusRow] = [] var newRows: [TrainStatusRow] = []
for number in trainNumbers { for number in lookup.trainNumbers {
let trains = try await client.fetchAllTrainStatus(num: number) let trains = try await client.fetchAllTrainStatus(num: number)
for train in trains { for train in trains {
let platform = train.stations?.first( let platform = train.stations?.first(
where: { $0.code == code where: { $0.code == code
}, },
)?.platform )?.platform
newRows.append(trainStatus(for: train, atStationCode: code)) newRows
.append(
trainStatus(
for: train,
atStationCode: code,
timeZone: lookup.timeZone
)
)
} }
} }
@ -170,18 +183,16 @@ struct ContentView: View {
} }
} }
func trainStatus(for train: Train, atStationCode code: String) -> TrainStatusRow { func trainStatus(for train: Train, atStationCode code: String, timeZone: TimeZone?) -> TrainStatusRow {
let leg = train.stations?.first { $0.code == code } let leg = train.stations?.first { $0.code == code }
let amtrakDate = AmtrakDateFormatting() let amtrakDate = AmtrakDateFormatting()
let stationName = leg?.name ?? unknown let stationName = leg?.name ?? unknown
let arrival = amtrakDate.time(from: leg?.arr) ?? amtrakDate.time( let arrival = amtrakDate.time(from: leg?.arr, timeZone: timeZone) ?? amtrakDate.time(
from: leg?.schArr from: leg?.schArr,
) timeZone: timeZone)
let depature = amtrakDate.time(from: leg?.dep) ?? amtrakDate.time( let depature = amtrakDate.time(from: leg?.dep, timeZone: timeZone) ?? amtrakDate.time(
from: leg?.schDep from: leg?.schDep,
) timeZone: timeZone)
currentStation = "\(stationName) station"
return TrainStatusRow( return TrainStatusRow(
trainID: train.trainID, trainID: train.trainID,