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)
}
struct TrainStationLookUp: Sendable {
var trainNumbers: [String]
var timeZone: TimeZone?
}
struct AmtrakClient {
var decoder = JSONDecoder()
@ -42,14 +48,20 @@ struct AmtrakClient {
.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)
guard let meta = reponse[code] else {
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 {

View file

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

View file

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