mirror of
https://github.com/tonytins/amtkstat.git
synced 2026-10-24 13:42:51 -04:00
Compare commits
2 commits
dc464de1c4
...
ca60e36ea2
| Author | SHA1 | Date | |
|---|---|---|---|
| ca60e36ea2 | |||
| c699007cbf |
9 changed files with 587 additions and 561 deletions
|
|
@ -3,21 +3,21 @@
|
||||||
import PackageDescription
|
import PackageDescription
|
||||||
|
|
||||||
let package = Package(
|
let package = Package(
|
||||||
name: "AmtrakStatus",
|
name: "AmtrakStatus",
|
||||||
platforms: [.macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .macCatalyst(.v13)],
|
platforms: [.macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .macCatalyst(.v13)],
|
||||||
dependencies: [
|
dependencies: [
|
||||||
.package(
|
.package(
|
||||||
url: "https://github.com/moreSwift/swift-cross-ui",
|
url: "https://github.com/moreSwift/swift-cross-ui",
|
||||||
.upToNextMinor(from: "0.7.0"),
|
.upToNextMinor(from: "0.7.0"),
|
||||||
),
|
)
|
||||||
],
|
],
|
||||||
targets: [
|
targets: [
|
||||||
.executableTarget(
|
.executableTarget(
|
||||||
name: "AmtrakStatus",
|
name: "AmtrakStatus",
|
||||||
dependencies: [
|
dependencies: [
|
||||||
.product(name: "SwiftCrossUI", package: "swift-cross-ui"),
|
.product(name: "SwiftCrossUI", package: "swift-cross-ui"),
|
||||||
.product(name: "DefaultBackend", package: "swift-cross-ui"),
|
.product(name: "DefaultBackend", package: "swift-cross-ui"),
|
||||||
],
|
],
|
||||||
),
|
)
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,87 +1,85 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
enum AmtrakError: Error {
|
enum AmtrakError: Error {
|
||||||
case invalidURL(String)
|
case invalidURL(String)
|
||||||
case invalidResponse
|
case invalidResponse
|
||||||
case httpStatus(Int)
|
case httpStatus(Int)
|
||||||
case stationNotFound(String)
|
case stationNotFound(String)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
struct TrainStationLookUp: Sendable {
|
struct TrainStationLookUp: Sendable {
|
||||||
var trainNumbers: [String]
|
var trainNumbers: [String]
|
||||||
var timeZone: TimeZone?
|
var timeZone: TimeZone?
|
||||||
}
|
}
|
||||||
|
|
||||||
struct AmtrakClient {
|
struct AmtrakClient {
|
||||||
var decoder = JSONDecoder()
|
var decoder = JSONDecoder()
|
||||||
|
|
||||||
let stationBaseURL = "https://api.amtraker.com/v3/stations"
|
let stationBaseURL = "https://api.amtraker.com/v3/stations"
|
||||||
let trainsBaseURL = "https://api.amtraker.com/v3/trains"
|
let trainsBaseURL = "https://api.amtraker.com/v3/trains"
|
||||||
let staleBaseURL = "https://api.amtraker.com/v3/stale"
|
let staleBaseURL = "https://api.amtraker.com/v3/stale"
|
||||||
|
|
||||||
func fetchTrain(number trainid: String) async throws -> TrainResponse {
|
func fetchTrain(number trainid: String) async throws -> TrainResponse {
|
||||||
try await get("\(trainsBaseURL)/\(trainid)")
|
try await get("\(trainsBaseURL)/\(trainid)")
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
return reponse.values.flatMap(\.self)
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchStation(code stationid: String) async throws -> StationResponse {
|
||||||
|
try await get("\(stationBaseURL)/\(stationid)")
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchAllStations() async throws -> StationResponse {
|
||||||
|
try await get(stationBaseURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
func trainNumber(fromTrainId trainId: String) -> String {
|
||||||
|
trainId
|
||||||
|
.split(separator: "-")
|
||||||
|
.first
|
||||||
|
.map(String.init) ?? trainId
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchAllTrains() async throws -> TrainResponse {
|
let trainNumbers = Array(Set(meta.trains.map(trainNumber(fromTrainId:))))
|
||||||
try await get(trainsBaseURL)
|
let timeZone = meta.tz.flatMap(TimeZone.init(identifier:))
|
||||||
|
return TrainStationLookUp(
|
||||||
|
trainNumbers: trainNumbers,
|
||||||
|
timeZone: timeZone
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func get<T: Decodable>(_ urlString: String) async throws -> T {
|
||||||
|
guard let url = URL(string: urlString) else {
|
||||||
|
throw AmtrakError.invalidURL(urlString)
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchStaleStatus() async throws -> StaleData {
|
let (data, reponse) = try await URLSession.shared.data(from: url)
|
||||||
try await get(staleBaseURL)
|
guard let http = reponse as? HTTPURLResponse else {
|
||||||
|
throw AmtrakError.invalidResponse
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchAllTrainStatus(num trainNum: String) async throws -> [Train] {
|
guard (200..<300).contains(http.statusCode) else {
|
||||||
let reponse = try await fetchTrain(number: trainNum)
|
throw AmtrakError.httpStatus(http.statusCode)
|
||||||
return reponse.values.flatMap(\.self)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchStation(code stationid: String) async throws -> StationResponse {
|
return try decoder.decode(T.self, from: data)
|
||||||
try await get("\(stationBaseURL)/\(stationid)")
|
}
|
||||||
}
|
|
||||||
|
|
||||||
func fetchAllStations() async throws -> StationResponse {
|
|
||||||
try await get(stationBaseURL)
|
|
||||||
}
|
|
||||||
|
|
||||||
func trainNumber(fromTrainId trainId: String) -> String {
|
|
||||||
trainId
|
|
||||||
.split(separator: "-")
|
|
||||||
.first
|
|
||||||
.map(String.init) ?? trainId
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
|
||||||
guard let url = URL(string: urlString) else {
|
|
||||||
throw AmtrakError.invalidURL(urlString)
|
|
||||||
}
|
|
||||||
|
|
||||||
let (data, reponse) = try await URLSession.shared.data(from: url)
|
|
||||||
guard let http = reponse as? HTTPURLResponse else {
|
|
||||||
throw AmtrakError.invalidResponse
|
|
||||||
}
|
|
||||||
|
|
||||||
guard (200 ..< 300).contains(http.statusCode) else {
|
|
||||||
throw AmtrakError.httpStatus(http.statusCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
return try decoder.decode(T.self, from: data)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,93 +1,99 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
struct AmtrakDateFormatting {
|
struct AmtrakDateFormatting {
|
||||||
let isoFormatter = ISO8601DateFormatter()
|
let isoFormatter = ISO8601DateFormatter()
|
||||||
|
|
||||||
let timeFormatter: DateFormatter = {
|
let timeFormatter: DateFormatter = {
|
||||||
let formatter = DateFormatter()
|
let formatter = DateFormatter()
|
||||||
formatter.dateStyle = .none
|
formatter.dateStyle = .none
|
||||||
formatter.timeStyle = .short
|
formatter.timeStyle = .short
|
||||||
return formatter
|
return formatter
|
||||||
}()
|
}()
|
||||||
|
|
||||||
let dateFormatter: DateFormatter = {
|
let dateFormatter: DateFormatter = {
|
||||||
let formatter = DateFormatter()
|
let formatter = DateFormatter()
|
||||||
formatter.dateStyle = .medium
|
formatter.dateStyle = .medium
|
||||||
formatter.timeStyle = .none
|
formatter.timeStyle = .none
|
||||||
return formatter
|
return formatter
|
||||||
}()
|
}()
|
||||||
|
|
||||||
func parsedDate(from isoString: String?) -> Date? {
|
func parsedDate(from isoString: String?) -> Date? {
|
||||||
guard let isoString, !isoString.isEmpty, let date = isoFormatter.date(
|
guard let isoString, !isoString.isEmpty,
|
||||||
from: isoString) else {
|
let date = isoFormatter.date(
|
||||||
return nil
|
from: isoString)
|
||||||
}
|
else {
|
||||||
|
return nil
|
||||||
return isoFormatter.date(from: isoString)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func time(from isoString: String?, in zone: TimeZone?, comparedTo originZone: TimeZone? = nil) -> String? {
|
return isoFormatter.date(from: isoString)
|
||||||
guard let isoString, !isoString.isEmpty, let date = isoFormatter.date(
|
}
|
||||||
from: isoString) else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
timeFormatter.timeZone = zone
|
func time(from isoString: String?, in zone: TimeZone?, comparedTo originZone: TimeZone? = nil)
|
||||||
|
-> String?
|
||||||
let formatted = timeFormatter.string(from: date)
|
{
|
||||||
|
guard let isoString, !isoString.isEmpty,
|
||||||
guard
|
let date = isoFormatter.date(
|
||||||
let zone,
|
from: isoString)
|
||||||
let originZone,
|
else {
|
||||||
zone.identifier != originZone.identifier,
|
return nil
|
||||||
let abbreviation = zone.abbreviation(for: date)
|
|
||||||
else {
|
|
||||||
return formatted
|
|
||||||
}
|
|
||||||
|
|
||||||
return "\(formatted) \(abbreviation)"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
timeFormatter.timeZone = zone
|
||||||
|
|
||||||
func isOnTime(actual: String?, scheduled: String?) -> LateTrain {
|
let formatted = timeFormatter.string(from: date)
|
||||||
|
|
||||||
guard
|
guard
|
||||||
let actualDate = parsedDate(from: actual),
|
let zone,
|
||||||
let scheduledDate = parsedDate(from: scheduled)
|
let originZone,
|
||||||
else {
|
zone.identifier != originZone.identifier,
|
||||||
return .unknown
|
let abbreviation = zone.abbreviation(for: date)
|
||||||
}
|
else {
|
||||||
|
return formatted
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return "\(formatted) \(abbreviation)"
|
||||||
|
}
|
||||||
|
|
||||||
func localTime(timeZone: TimeZone?) -> String {
|
func isOnTime(actual: String?, scheduled: String?) -> LateTrain {
|
||||||
let now = Date()
|
|
||||||
timeFormatter.timeZone = timeZone
|
guard
|
||||||
return timeFormatter.string(from: now)
|
let actualDate = parsedDate(from: actual),
|
||||||
|
let scheduledDate = parsedDate(from: scheduled)
|
||||||
|
else {
|
||||||
|
return .unknown
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateTime(timeZone: TimeZone?) {
|
let differenceInMinutes = Int(actualDate.timeIntervalSince(scheduledDate) / 60)
|
||||||
let now = Date()
|
|
||||||
timeFormatter.timeZone = timeZone
|
switch differenceInMinutes {
|
||||||
|
case -1...1:
|
||||||
|
return .onTime
|
||||||
|
case ..<0:
|
||||||
|
return .early(minutes: -differenceInMinutes)
|
||||||
|
default:
|
||||||
|
return .late(minutes: differenceInMinutes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func localTime(timeZone: TimeZone?) -> String {
|
||||||
|
let now = Date()
|
||||||
|
timeFormatter.timeZone = timeZone
|
||||||
|
return timeFormatter.string(from: now)
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateTime(timeZone: TimeZone?) {
|
||||||
|
let now = Date()
|
||||||
|
timeFormatter.timeZone = timeZone
|
||||||
|
}
|
||||||
|
|
||||||
|
func date(from isoString: String?) -> String? {
|
||||||
|
guard let isoString, !isoString.isEmpty,
|
||||||
|
let date = isoFormatter.date(
|
||||||
|
from: isoString)
|
||||||
|
else {
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func date(from isoString: String?) -> String? {
|
return dateFormatter.string(from: date)
|
||||||
guard let isoString, !isoString.isEmpty, let date = isoFormatter.date(
|
}
|
||||||
from: isoString) else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return dateFormatter.string(from: date)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,9 @@ import SwiftCrossUI
|
||||||
|
|
||||||
@main
|
@main
|
||||||
struct AmtrakStatusApp: App {
|
struct AmtrakStatusApp: App {
|
||||||
var body: some Scene {
|
var body: some Scene {
|
||||||
WindowGroup("Amtrak Status") {
|
WindowGroup("AMTK Status") {
|
||||||
ContentView()
|
ContentView()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,288 +1,293 @@
|
||||||
import SwiftCrossUI
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import SwiftCrossUI
|
||||||
|
|
||||||
struct ContentView: View {
|
struct ContentView: View {
|
||||||
@State var client = AmtrakClient()
|
@State var client = AmtrakClient()
|
||||||
|
|
||||||
@State var stationCode = "ASD" // Ashland, VA
|
@State var stationCode = "ASD" // Ashland, VA
|
||||||
@State var rows: [TrainStatusRow] = []
|
@State var rows: [TrainStatusRow] = []
|
||||||
@State var isLoading = false
|
@State var isLoading = false
|
||||||
@State var errorMessage: String?
|
@State var errorMessage: String?
|
||||||
@State var currentStation: String = ""
|
@State var currentStation: String = ""
|
||||||
@State var localTimeZone: TimeZone?
|
@State var localTimeZone: TimeZone?
|
||||||
|
|
||||||
let amtrakDate = AmtrakDateFormatting()
|
let amtrakDate = AmtrakDateFormatting()
|
||||||
let unknown = "Unknown"
|
let unknown = "Unknown"
|
||||||
let staleDataThreshold: TimeInterval = 60 * 60 * 48 // 48 hours
|
let staleDataThreshold: TimeInterval = 60 * 60 * 48 // 48 hours
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack {
|
VStack {
|
||||||
HStack {
|
HStack {
|
||||||
Text("Station Code:")
|
Text("Station Code:")
|
||||||
TextField("e.g. ASD", text: $stationCode)
|
TextField("e.g. ASD", text: $stationCode)
|
||||||
.frame(width: 100)
|
.frame(width: 100)
|
||||||
|
|
||||||
Button("Submit") {
|
Button("Submit") {
|
||||||
Task {
|
Task {
|
||||||
await loadTrainStatus(forStationCode: normalizedStationCode)
|
await loadTrainStatus(forStationCode: normalizedStationCode)
|
||||||
}
|
}
|
||||||
}.disabled(normalizedStationCode.isEmpty)
|
}.disabled(normalizedStationCode.isEmpty)
|
||||||
}.padding(10)
|
}.padding(10)
|
||||||
|
|
||||||
// Most popular Virtual Railfan stations
|
// Most popular Virtual Railfan stations
|
||||||
HStack {
|
HStack {
|
||||||
Button("Ashland, VA") {
|
Button("Ashland, VA") {
|
||||||
Task {
|
Task {
|
||||||
await loadTrainStatus(forStationCode: "ASD")
|
await loadTrainStatus(forStationCode: "ASD")
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Button("San Juan Capistrano, CA") {
|
|
||||||
Task {
|
|
||||||
await loadTrainStatus(forStationCode: "SNC")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Button("La Plata, MO") {
|
|
||||||
Task {
|
|
||||||
await loadTrainStatus(forStationCode: "LAP")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Table(rows) {
|
|
||||||
TableColumn("Time", value: \TrainStatusRow.time)
|
|
||||||
TableColumn("Number", value: \TrainStatusRow.trainNum)
|
|
||||||
TableColumn("", value: \TrainStatusRow.presence.rawValue)
|
|
||||||
TableColumn("Train", value: \TrainStatusRow.train)
|
|
||||||
TableColumn("To", value: \TrainStatusRow.to)
|
|
||||||
TableColumn("From", value: \TrainStatusRow.from)
|
|
||||||
TableColumn("Status") {
|
|
||||||
(row: TrainStatusRow) in
|
|
||||||
ZStack {
|
|
||||||
RoundedRectangle(cornerRadius: 0)
|
|
||||||
.fill(row.status.color)
|
|
||||||
Text(row.status.descrption)
|
|
||||||
.foregroundColor(
|
|
||||||
statusColour(row.status.color)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
TableColumn("Track", value: \TrainStatusRow.track)
|
|
||||||
}.frame(minWidth: 600)
|
|
||||||
.overlay(alignment: .bottomTrailing) {
|
|
||||||
if isLoading {
|
|
||||||
ProgressView()
|
|
||||||
.frame(alignment: .topTrailing)
|
|
||||||
.padding(10)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
HStack {
|
|
||||||
if let errorMessage {
|
|
||||||
Text(errorMessage)
|
|
||||||
} else if !currentStation.isEmpty {
|
|
||||||
Text(
|
|
||||||
"It is \(amtrakDate.localTime(timeZone: localTimeZone)) at \(currentStation) station"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}.padding(10)
|
|
||||||
}
|
}
|
||||||
.frame(idealWidth: 500)
|
|
||||||
.onAppear {
|
Button("San Juan Capistrano, CA") {
|
||||||
Task {
|
Task {
|
||||||
await loadTrainStatus(forStationCode: normalizedStationCode)
|
await loadTrainStatus(forStationCode: "SNC")
|
||||||
}
|
}
|
||||||
}.overlay(alignment: .topTrailing) {
|
|
||||||
Text("Information by Amtraker")
|
|
||||||
.padding(10)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Button("La Plata, MO") {
|
||||||
|
Task {
|
||||||
|
await loadTrainStatus(forStationCode: "LAP")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Table(rows) {
|
||||||
|
TableColumn("Time", value: \TrainStatusRow.time)
|
||||||
|
TableColumn("Number", value: \TrainStatusRow.trainNum)
|
||||||
|
TableColumn("", value: \TrainStatusRow.presence.rawValue)
|
||||||
|
TableColumn("Train", value: \TrainStatusRow.train)
|
||||||
|
TableColumn("To", value: \TrainStatusRow.to)
|
||||||
|
TableColumn("From", value: \TrainStatusRow.from)
|
||||||
|
TableColumn("Status") {
|
||||||
|
(row: TrainStatusRow) in
|
||||||
|
ZStack {
|
||||||
|
RoundedRectangle(cornerRadius: 0)
|
||||||
|
.fill(row.status.color)
|
||||||
|
Text(row.status.descrption)
|
||||||
|
.foregroundColor(
|
||||||
|
statusColour(row.status.color)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TableColumn("Track", value: \TrainStatusRow.track)
|
||||||
|
}.frame(minWidth: 600)
|
||||||
|
.overlay(alignment: .bottomTrailing) {
|
||||||
|
if isLoading {
|
||||||
|
ProgressView()
|
||||||
|
.frame(alignment: .topTrailing)
|
||||||
|
.padding(10)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HStack {
|
||||||
|
if let errorMessage {
|
||||||
|
Text(errorMessage)
|
||||||
|
} else if !currentStation.isEmpty {
|
||||||
|
Text(
|
||||||
|
"It is \(amtrakDate.localTime(timeZone: localTimeZone)) at \(currentStation) station"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}.padding(10)
|
||||||
|
}
|
||||||
|
.frame(idealWidth: 500)
|
||||||
|
.onAppear {
|
||||||
|
Task {
|
||||||
|
await loadTrainStatus(forStationCode: normalizedStationCode)
|
||||||
|
}
|
||||||
|
}.overlay(alignment: .topTrailing) {
|
||||||
|
Text("Information by Amtraker")
|
||||||
|
.padding(10)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var normalizedStationCode: String {
|
||||||
|
stationCode.trimmingCharacters(in: .whitespacesAndNewlines).uppercased()
|
||||||
|
}
|
||||||
|
|
||||||
|
func statusColour(_ color: Color) -> Color {
|
||||||
|
switch color {
|
||||||
|
case .green:
|
||||||
|
return .adaptive(light: .white, dark: .black)
|
||||||
|
default:
|
||||||
|
return .white
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadStationStatus(forStationCode code: String) async {
|
||||||
|
guard !code.isEmpty else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadTrainStatus(forStationCode code: String) async {
|
||||||
|
guard !code.isEmpty else {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var normalizedStationCode: String {
|
isLoading = true
|
||||||
stationCode.trimmingCharacters(in: .whitespacesAndNewlines).uppercased()
|
errorMessage = nil
|
||||||
|
|
||||||
|
defer { isLoading = false }
|
||||||
|
|
||||||
|
do {
|
||||||
|
let lookup = try await client.trainStationLookup(
|
||||||
|
stationCode: code,
|
||||||
|
)
|
||||||
|
|
||||||
|
guard !lookup.trainNumbers.isEmpty else {
|
||||||
|
errorMessage = "\(code) has no trains listed right now"
|
||||||
|
rows = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var zoneCache: [String: TimeZone?] = [code: lookup.timeZone]
|
||||||
|
let staleness = await staleTrainLookup()
|
||||||
|
|
||||||
|
var newRows: [TrainStatusRow] = []
|
||||||
|
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
|
||||||
|
let row = await trainStatus(
|
||||||
|
for: train,
|
||||||
|
code: code,
|
||||||
|
zone: lookup.timeZone,
|
||||||
|
cache: &zoneCache,
|
||||||
|
staleness: staleness
|
||||||
|
)
|
||||||
|
if let row {
|
||||||
|
newRows.append(row)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rows = newRows
|
||||||
|
|
||||||
|
} catch let AmtrakError.stationNotFound(code) {
|
||||||
|
errorMessage = "Station \(code) not found"
|
||||||
|
rows = []
|
||||||
|
} catch {
|
||||||
|
errorMessage = "Couldn't load train status: \(error.localizedDescription)"
|
||||||
|
rows = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func trainStatus(
|
||||||
|
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
|
||||||
|
!isStale(leg, timeZone: timeZone), !isDataStale(train.trainID, staleness: staleness)
|
||||||
|
else {
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func statusColour(_ color: Color) -> Color {
|
let stationName = leg?.name ?? unknown
|
||||||
switch color {
|
let track = leg?.platform ?? ""
|
||||||
case .green:
|
|
||||||
return .adaptive(light: .white, dark: .black)
|
var originZone: TimeZone?
|
||||||
default:
|
if let orgCode = train.origCode {
|
||||||
return .white
|
originZone = await fetchTimeZone(
|
||||||
}
|
forStationCode: orgCode,
|
||||||
|
cache: &zoneCache
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadStationStatus(forStationCode code: String) async {
|
let time =
|
||||||
guard !code.isEmpty else {
|
amtrakDate.time(
|
||||||
return
|
from: leg?.arr,
|
||||||
}
|
in: timeZone,
|
||||||
|
comparedTo: originZone
|
||||||
|
)
|
||||||
|
?? amtrakDate.time(
|
||||||
|
from: leg?.schArr,
|
||||||
|
in: timeZone,
|
||||||
|
comparedTo: originZone
|
||||||
|
)
|
||||||
|
|
||||||
|
if stationName != unknown {
|
||||||
|
currentStation = stationName
|
||||||
|
localTimeZone = timeZone
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadTrainStatus(forStationCode code: String) async {
|
return TrainStatusRow(
|
||||||
guard !code.isEmpty else {
|
trainID: train.trainID,
|
||||||
return
|
time: time ?? "",
|
||||||
}
|
trainNum: train.trainNum,
|
||||||
|
presence: leg?.status ?? .unknown, // Yeah... It's a little confusing
|
||||||
|
train: train.routeName,
|
||||||
|
to: train.destName,
|
||||||
|
from: train.origName,
|
||||||
|
status: amtrakDate.isOnTime(
|
||||||
|
actual: leg?.arr,
|
||||||
|
scheduled: leg?.schArr
|
||||||
|
),
|
||||||
|
track: track.isEmpty ? "" : track
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
isLoading = true
|
func isStale(_ leg: Station?, timeZone: TimeZone?) -> Bool {
|
||||||
errorMessage = nil
|
guard leg?.status == .departed else { return false }
|
||||||
|
|
||||||
defer { isLoading = false }
|
guard
|
||||||
|
let deparatureDate = amtrakDate.parsedDate(
|
||||||
do {
|
from: leg?.dep
|
||||||
let lookup = try await client.trainStationLookup(
|
) ?? amtrakDate.parsedDate(from: leg?.schDep)
|
||||||
stationCode: code,
|
else {
|
||||||
)
|
return false
|
||||||
|
|
||||||
guard !lookup.trainNumbers.isEmpty else {
|
|
||||||
errorMessage = "\(code) has no trains listed right now"
|
|
||||||
rows = []
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var zoneCache: [String: TimeZone?] = [code: lookup.timeZone]
|
|
||||||
let staleness = await staleTrainLookup()
|
|
||||||
|
|
||||||
var newRows: [TrainStatusRow] = []
|
|
||||||
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
|
|
||||||
let row = await trainStatus(
|
|
||||||
for: train,
|
|
||||||
code: code,
|
|
||||||
zone: lookup.timeZone,
|
|
||||||
cache: &zoneCache,
|
|
||||||
staleness: staleness
|
|
||||||
)
|
|
||||||
if let row {
|
|
||||||
newRows.append(row)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
rows = newRows
|
|
||||||
|
|
||||||
} catch let AmtrakError.stationNotFound(code) {
|
|
||||||
errorMessage = "Station \(code) not found"
|
|
||||||
rows = []
|
|
||||||
} catch {
|
|
||||||
errorMessage = "Couldn't load train status: \(error.localizedDescription)"
|
|
||||||
rows = []
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func trainStatus(
|
var calendar = Calendar.current
|
||||||
for train: Train,
|
calendar.timeZone = timeZone ?? .current
|
||||||
code stationCode: String,
|
return
|
||||||
zone timeZone: TimeZone?,
|
calendar
|
||||||
cache zoneCache: inout [String: TimeZone?],
|
.compare(
|
||||||
staleness: [String: TimeInterval]
|
deparatureDate,
|
||||||
) async -> TrainStatusRow? {
|
to: Date(),
|
||||||
let leg = train.stations?.first { $0.code == stationCode }
|
toGranularity: .day
|
||||||
|
) == .orderedAscending
|
||||||
|
}
|
||||||
|
|
||||||
guard
|
func isDataStale(
|
||||||
!isStale(leg, timeZone: timeZone), !isDataStale(train.trainID, staleness: staleness)
|
_ trainId: String,
|
||||||
else {
|
staleness: [String: TimeInterval]
|
||||||
return nil
|
) -> Bool {
|
||||||
}
|
guard let timeSince = staleness[trainId] else { return false }
|
||||||
|
return timeSince > staleDataThreshold
|
||||||
|
}
|
||||||
|
|
||||||
let stationName = leg?.name ?? unknown
|
func staleTrainLookup() async -> [String: TimeInterval] {
|
||||||
let track = leg?.platform ?? ""
|
guard let staleData = try? await client.fetchStaleStatus() else {
|
||||||
|
return [:]
|
||||||
|
}
|
||||||
|
return Dictionary(
|
||||||
|
staleData.lastUpdatedArr.map { ($0.trainID, $0.timeSince) },
|
||||||
|
uniquingKeysWith: { first, _ in first }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
var originZone: TimeZone?
|
func fetchTimeZone(forStationCode code: String, cache: inout [String: TimeZone?]) async
|
||||||
if let orgCode = train.origCode {
|
-> TimeZone?
|
||||||
originZone = await fetchTimeZone(
|
{
|
||||||
forStationCode: orgCode,
|
if let cached = cache[code] {
|
||||||
cache: &zoneCache
|
return cached
|
||||||
)
|
}
|
||||||
}
|
let zone: TimeZone?
|
||||||
|
do {
|
||||||
let time = amtrakDate.time(
|
let response = try await client.fetchStation(code: code)
|
||||||
from: leg?.arr,
|
zone = response[code]?.tz.flatMap(TimeZone.init(identifier:))
|
||||||
in: timeZone,
|
} catch {
|
||||||
comparedTo: originZone
|
zone = nil
|
||||||
) ?? amtrakDate.time(
|
|
||||||
from: leg?.schArr,
|
|
||||||
in: timeZone,
|
|
||||||
comparedTo: originZone
|
|
||||||
)
|
|
||||||
|
|
||||||
if stationName != unknown {
|
|
||||||
currentStation = stationName
|
|
||||||
localTimeZone = timeZone
|
|
||||||
}
|
|
||||||
|
|
||||||
return TrainStatusRow(
|
|
||||||
trainID: train.trainID,
|
|
||||||
time: time ?? "",
|
|
||||||
trainNum: train.trainNum,
|
|
||||||
presence: leg?.status ?? .unknown, // Yeah... It's a little confusing
|
|
||||||
train: train.routeName,
|
|
||||||
to: train.destName,
|
|
||||||
from: train.origName,
|
|
||||||
status: amtrakDate.isOnTime(
|
|
||||||
actual: leg?.arr,
|
|
||||||
scheduled: leg?.schArr
|
|
||||||
),
|
|
||||||
track: track.isEmpty ? "" : track
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func isStale(_ leg: Station?, timeZone: TimeZone?) -> Bool {
|
return zone
|
||||||
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
|
|
||||||
}
|
|
||||||
let zone: TimeZone?
|
|
||||||
do {
|
|
||||||
let response = try await client.fetchStation(code: code)
|
|
||||||
zone = response[code]?.tz.flatMap(TimeZone.init(identifier:))
|
|
||||||
} catch {
|
|
||||||
zone = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return zone
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
extension Int {
|
extension Int {
|
||||||
func pluralized(singular: String, plural: String) -> String {
|
func pluralized(singular: String, plural: String) -> String {
|
||||||
self == 1 ? singular : plural
|
self == 1 ? singular : plural
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,48 +2,48 @@ import Foundation
|
||||||
import SwiftCrossUI
|
import SwiftCrossUI
|
||||||
|
|
||||||
enum LateTrain {
|
enum LateTrain {
|
||||||
case onTime
|
case onTime
|
||||||
case early(minutes: Int)
|
case early(minutes: Int)
|
||||||
case late(minutes: Int)
|
case late(minutes: Int)
|
||||||
case unknown
|
case unknown
|
||||||
|
|
||||||
var descrption: String {
|
var descrption: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .onTime:
|
case .onTime:
|
||||||
return "On Time"
|
return "On Time"
|
||||||
case .early(minutes: let minutes):
|
case .early(let minutes):
|
||||||
return "\(Self.formatted(minutes)) early"
|
return "\(Self.formatted(minutes)) early"
|
||||||
case .late(minutes: let minutes):
|
case .late(let minutes):
|
||||||
return "\(Self.formatted(minutes)) late"
|
return "\(Self.formatted(minutes)) late"
|
||||||
case .unknown:
|
case .unknown:
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var color: Color {
|
||||||
|
switch self {
|
||||||
|
case .onTime:
|
||||||
|
return .green
|
||||||
|
case .early:
|
||||||
|
return .blue
|
||||||
|
case .late:
|
||||||
|
return .red
|
||||||
|
case .unknown:
|
||||||
|
return .gray
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static func formatted(_ minutes: Int) -> String {
|
||||||
|
let hours = minutes / 60
|
||||||
|
let remainder = minutes % 60
|
||||||
|
|
||||||
|
let minutesPart = "\(minutes) \(minutes.pluralized(singular: "min", plural: "mins"))"
|
||||||
|
let hoursPart = "\(hours) \(hours.pluralized(singular: "hr", plural: "hrs"))"
|
||||||
|
|
||||||
|
guard hours > 0 else {
|
||||||
|
return minutesPart
|
||||||
}
|
}
|
||||||
|
|
||||||
var color: Color {
|
return hoursPart
|
||||||
switch self {
|
}
|
||||||
case .onTime:
|
|
||||||
return .green
|
|
||||||
case .early:
|
|
||||||
return .blue
|
|
||||||
case .late:
|
|
||||||
return .red
|
|
||||||
case .unknown:
|
|
||||||
return .gray
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static func formatted(_ minutes: Int) -> String {
|
|
||||||
let hours = minutes / 60
|
|
||||||
let remainder = minutes % 60
|
|
||||||
|
|
||||||
let minutesPart = "\(minutes) \(minutes.pluralized(singular: "min", plural: "mins"))"
|
|
||||||
let hoursPart = "\(hours) \(hours.pluralized(singular: "hr", plural: "hrs"))"
|
|
||||||
|
|
||||||
guard hours > 0 else {
|
|
||||||
return minutesPart
|
|
||||||
}
|
|
||||||
|
|
||||||
return hoursPart
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,110 +1,110 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
struct TrainAlert: Codable {
|
struct TrainAlert: Codable {
|
||||||
var message: String
|
var message: String
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Station: Codable, Sendable {
|
struct Station: Codable, Sendable {
|
||||||
var name: String
|
var name: String
|
||||||
var code: String
|
var code: String
|
||||||
var tz: String
|
var tz: String
|
||||||
var bus: Bool
|
var bus: Bool
|
||||||
var schArr: String
|
var schArr: String
|
||||||
var schDep: String
|
var schDep: String
|
||||||
var arr: String
|
var arr: String
|
||||||
var dep: String
|
var dep: String
|
||||||
var arrCmnt: String
|
var arrCmnt: String
|
||||||
var depCmnt: String
|
var depCmnt: String
|
||||||
var status: StationStatus
|
var status: StationStatus
|
||||||
var stopIconColor: String
|
var stopIconColor: String
|
||||||
var platform: String
|
var platform: String
|
||||||
}
|
}
|
||||||
|
|
||||||
enum StationStatus: String, Codable, Sendable {
|
enum StationStatus: String, Codable, Sendable {
|
||||||
case enroute = "Enroute"
|
case enroute = "Enroute"
|
||||||
case station = "Station"
|
case station = "Station"
|
||||||
case departed = "Departed"
|
case departed = "Departed"
|
||||||
case unknown = "Unknown"
|
case unknown = "Unknown"
|
||||||
|
|
||||||
init(from decoder: any Decoder) throws {
|
init(from decoder: any Decoder) throws {
|
||||||
let container = try decoder.singleValueContainer()
|
let container = try decoder.singleValueContainer()
|
||||||
let rawValue = try container.decode(String.self)
|
let rawValue = try container.decode(String.self)
|
||||||
self = StationStatus(rawValue: rawValue) ?? .unknown
|
self = StationStatus(rawValue: rawValue) ?? .unknown
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Train: Codable {
|
struct Train: Codable {
|
||||||
var routeName: String
|
var routeName: String
|
||||||
var trainNum: String
|
var trainNum: String
|
||||||
var trainNumRaw: String?
|
var trainNumRaw: String?
|
||||||
var trainID: String
|
var trainID: String
|
||||||
var lat: Double?
|
var lat: Double?
|
||||||
var lon: Double?
|
var lon: Double?
|
||||||
var trainTimely: String?
|
var trainTimely: String?
|
||||||
var iconColor: String?
|
var iconColor: String?
|
||||||
var textColor: String?
|
var textColor: String?
|
||||||
var heading: String?
|
var heading: String?
|
||||||
var eventCode: String?
|
var eventCode: String?
|
||||||
var eventTZ: String?
|
var eventTZ: String?
|
||||||
var eventName: String?
|
var eventName: String?
|
||||||
var origCode: String?
|
var origCode: String?
|
||||||
var origName: String
|
var origName: String
|
||||||
var destCode: String?
|
var destCode: String?
|
||||||
var destName: String
|
var destName: String
|
||||||
var trainState: String?
|
var trainState: String?
|
||||||
var velocity: Double?
|
var velocity: Double?
|
||||||
var statusMsg: String?
|
var statusMsg: String?
|
||||||
var createdAt: String?
|
var createdAt: String?
|
||||||
var updatedAt: String?
|
var updatedAt: String?
|
||||||
var lastValTS: String?
|
var lastValTS: String?
|
||||||
var objectID: Int?
|
var objectID: Int?
|
||||||
var provider: String?
|
var provider: String?
|
||||||
var providerShort: String?
|
var providerShort: String?
|
||||||
var onlyOfTrainNum: Bool?
|
var onlyOfTrainNum: Bool?
|
||||||
var alerts: [TrainAlert]?
|
var alerts: [TrainAlert]?
|
||||||
var stations: [Station]?
|
var stations: [Station]?
|
||||||
}
|
}
|
||||||
|
|
||||||
struct StationMeta: Codable {
|
struct StationMeta: Codable {
|
||||||
var name: String
|
var name: String
|
||||||
var code: String
|
var code: String
|
||||||
var tz: String?
|
var tz: String?
|
||||||
var lat: Double?
|
var lat: Double?
|
||||||
var lon: Double?
|
var lon: Double?
|
||||||
var hasAddress: Bool?
|
var hasAddress: Bool?
|
||||||
var address1: String?
|
var address1: String?
|
||||||
var address2: String?
|
var address2: String?
|
||||||
var city: String?
|
var city: String?
|
||||||
var state: String?
|
var state: String?
|
||||||
var zip: String?
|
var zip: String?
|
||||||
var trains: [String]
|
var trains: [String]
|
||||||
}
|
}
|
||||||
|
|
||||||
struct StaleData: Codable {
|
struct StaleData: Codable {
|
||||||
var activeTrains: Int
|
var activeTrains: Int
|
||||||
var avgLastUpdate: Double
|
var avgLastUpdate: Double
|
||||||
var lastUpdatedArr: [TrainFreshness]
|
var lastUpdatedArr: [TrainFreshness]
|
||||||
var medianLastUpdate: Double
|
var medianLastUpdate: Double
|
||||||
}
|
}
|
||||||
|
|
||||||
struct TrainFreshness: Codable, Sendable {
|
struct TrainFreshness: Codable, Sendable {
|
||||||
var trainID: String
|
var trainID: String
|
||||||
var timeSince: Double
|
var timeSince: Double
|
||||||
}
|
}
|
||||||
|
|
||||||
typealias TrainResponse = [String: [Train]]
|
typealias TrainResponse = [String: [Train]]
|
||||||
typealias StationResponse = [String: StationMeta]
|
typealias StationResponse = [String: StationMeta]
|
||||||
|
|
||||||
struct TrainStatusRow: Identifiable {
|
struct TrainStatusRow: Identifiable {
|
||||||
var id: String { trainID }
|
var id: String { trainID }
|
||||||
|
|
||||||
var trainID: String
|
var trainID: String
|
||||||
var time: String
|
var time: String
|
||||||
var trainNum: String
|
var trainNum: String
|
||||||
var presence: StationStatus
|
var presence: StationStatus
|
||||||
var train: String
|
var train: String
|
||||||
var to: String
|
var to: String
|
||||||
var from: String
|
var from: String
|
||||||
var status: LateTrain
|
var status: LateTrain
|
||||||
var track: String
|
var track: String
|
||||||
}
|
}
|
||||||
|
|
|
||||||
17
format.sh
Executable file
17
format.sh
Executable file
|
|
@ -0,0 +1,17 @@
|
||||||
|
#!/bin/bash
|
||||||
|
##===----------------------------------------------------------------------===##
|
||||||
|
##
|
||||||
|
## This source file is from the Swift Argument Parser open source project
|
||||||
|
##
|
||||||
|
## Copyright (c) 2025 Apple Inc. and the Swift project authors
|
||||||
|
## Licensed under Apache License v2.0 with Runtime Library Exception
|
||||||
|
##
|
||||||
|
## See https://swift.org/LICENSE.txt for license information
|
||||||
|
##
|
||||||
|
##===----------------------------------------------------------------------===##
|
||||||
|
|
||||||
|
echo "Formatting Swift sources in $(pwd)"
|
||||||
|
|
||||||
|
# Run the format / lint commands
|
||||||
|
git ls-files -z '*.swift' | xargs -0 swift format format --parallel --in-place
|
||||||
|
git ls-files -z '*.swift' | xargs -0 swift format lint --strict --parallel
|
||||||
Loading…
Add table
Add a link
Reference in a new issue