From 60411251cb3c3d6dfded51046c6075c95e845b53 Mon Sep 17 00:00:00 2001 From: Michael Peters Jr Date: Sat, 19 Apr 2025 20:03:07 -0700 Subject: [PATCH 1/2] fix(ferries): add schedules/routes endpoints --- example/vessels/main.go | 16 +++++ ferries/ferries.go | 117 ---------------------------------- ferries/locations.go | 122 +++++++++++++++++++++++++++++++++++ ferries/schedule.go | 137 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 275 insertions(+), 117 deletions(-) create mode 100644 ferries/locations.go create mode 100644 ferries/schedule.go diff --git a/example/vessels/main.go b/example/vessels/main.go index cd4879f..aec7a96 100644 --- a/example/vessels/main.go +++ b/example/vessels/main.go @@ -51,4 +51,20 @@ func main() { fmt.Println(vesselLocations[1].VesselName) fmt.Printf("%f°N, %f°W\n", vesselLocations[1].Latitude, vesselLocations[1].Longitude) } + + // get the route schedule + schedules, err := ferriesClient.GetRouteSchedules() + if err != nil { + panic(err) + } + + for _, schedule := range schedules { + fmt.Printf("Schedule ID: %d, Route ID: %d, Description: %s\n", schedule.ScheduleID, schedule.RouteID, schedule.Description) + allSailings, err := ferriesClient.GetSchedulesTodayByRouteID(schedule.RouteID, true) + if err != nil { + panic(err) + } + fmt.Printf("last sailing: %s\n", allSailings.TerminalCombos[0].Times[len(allSailings.TerminalCombos[0].Times)-1].DepartingTime) + } + } diff --git a/ferries/ferries.go b/ferries/ferries.go index 5323358..574cc82 100644 --- a/ferries/ferries.go +++ b/ferries/ferries.go @@ -1,18 +1,9 @@ package ferries import ( - "encoding/json" - "fmt" - "net/http" - "alpineworks.io/wsdot" ) -const ( - getVesselBasicsAsJsonURL = "https://www.wsdot.wa.gov/Ferries/API/Vessels/rest/vesselbasics" - getVesselLocationsAsJsonURL = "https://www.wsdot.wa.gov/Ferries/API/Vessels/rest/vessellocations" -) - type FerriesClient struct { wsdot *wsdot.WSDOTClient } @@ -26,111 +17,3 @@ func NewFerriesClient(wsdotClient *wsdot.WSDOTClient) (*FerriesClient, error) { wsdot: wsdotClient, }, nil } - -type VesselBasic struct { - VesselID int `json:"VesselID"` - VesselSubjectID int `json:"VesselSubjectID"` - VesselName string `json:"VesselName"` - VesselAbbrev string `json:"VesselAbbrev"` - Class struct { - ClassID int `json:"ClassID"` - ClassSubjectID int `json:"ClassSubjectID"` - ClassName string `json:"ClassName"` - SortSeq int `json:"SortSeq"` - DrawingImg string `json:"DrawingImg"` - SilhouetteImg string `json:"SilhouetteImg"` - PublicDisplayName string `json:"PublicDisplayName"` - } `json:"Class"` - Status int `json:"Status"` - OwnedByWSF bool `json:"OwnedByWSF"` -} - -func (f *FerriesClient) GetVesselBasics() ([]VesselBasic, error) { - req, err := http.NewRequest(http.MethodGet, getVesselBasicsAsJsonURL, nil) - if err != nil { - return nil, fmt.Errorf("error creating request: %v", err) - } - - q := req.URL.Query() - q.Add(wsdot.ParamFerriesAccessCodeKey, f.wsdot.ApiKey) - req.URL.RawQuery = q.Encode() - req.Header.Set("Content-Type", "application/json") - - resp, err := f.wsdot.Client.Do(req) - if err != nil { - return nil, fmt.Errorf("error making request: %v", err) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode) - } - - var vessels []VesselBasic - if err := json.NewDecoder(resp.Body).Decode(&vessels); err != nil { - return nil, fmt.Errorf("error decoding response: %v", err) - } - - return vessels, nil -} - -type VesselLocation struct { - VesselID int `json:"VesselID"` - VesselName string `json:"VesselName"` - Mmsi int `json:"Mmsi"` - DepartingTerminalID int `json:"DepartingTerminalID"` - DepartingTerminalName string `json:"DepartingTerminalName"` - DepartingTerminalAbbrev string `json:"DepartingTerminalAbbrev"` - ArrivingTerminalID int `json:"ArrivingTerminalID"` - ArrivingTerminalName string `json:"ArrivingTerminalName"` - ArrivingTerminalAbbrev string `json:"ArrivingTerminalAbbrev"` - Latitude float64 `json:"Latitude"` - Longitude float64 `json:"Longitude"` - Speed float64 `json:"Speed"` - Heading int `json:"Heading"` - InService bool `json:"InService"` - AtDock bool `json:"AtDock"` - LeftDock string `json:"LeftDock"` - Eta string `json:"Eta"` - EtaBasis string `json:"EtaBasis"` - ScheduledDeparture string `json:"ScheduledDeparture"` - OpRouteAbbrev []string `json:"OpRouteAbbrev"` - VesselPositionNum int `json:"VesselPositionNum"` - SortSeq int `json:"SortSeq"` - ManagedBy int `json:"ManagedBy"` - TimeStamp string `json:"TimeStamp"` - VesselWatchShutID int `json:"VesselWatchShutID"` - VesselWatchShutMsg string `json:"VesselWatchShutMsg"` - VesselWatchShutFlag string `json:"VesselWatchShutFlag"` - VesselWatchStatus string `json:"VesselWatchStatus"` - VesselWatchMsg string `json:"VesselWatchMsg"` -} - -func (f *FerriesClient) GetVesselLocations() ([]VesselLocation, error) { - req, err := http.NewRequest(http.MethodGet, getVesselLocationsAsJsonURL, nil) - if err != nil { - return nil, fmt.Errorf("error creating request: %v", err) - } - - q := req.URL.Query() - q.Add(wsdot.ParamFerriesAccessCodeKey, f.wsdot.ApiKey) - req.URL.RawQuery = q.Encode() - req.Header.Set("Content-Type", "application/json") - - resp, err := f.wsdot.Client.Do(req) - if err != nil { - return nil, fmt.Errorf("error making request: %v", err) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode) - } - - var vessels []VesselLocation - if err := json.NewDecoder(resp.Body).Decode(&vessels); err != nil { - return nil, fmt.Errorf("error decoding response: %v", err) - } - - return vessels, nil -} diff --git a/ferries/locations.go b/ferries/locations.go new file mode 100644 index 0000000..a41695f --- /dev/null +++ b/ferries/locations.go @@ -0,0 +1,122 @@ +package ferries + +import ( + "encoding/json" + "fmt" + "net/http" + + "alpineworks.io/wsdot" +) + +const ( + getVesselBasicsAsJsonURL = "https://www.wsdot.wa.gov/Ferries/API/Vessels/rest/vesselbasics" + getVesselLocationsAsJsonURL = "https://www.wsdot.wa.gov/Ferries/API/Vessels/rest/vessellocations" +) + +type VesselBasic struct { + VesselID int `json:"VesselID"` + VesselSubjectID int `json:"VesselSubjectID"` + VesselName string `json:"VesselName"` + VesselAbbrev string `json:"VesselAbbrev"` + Class struct { + ClassID int `json:"ClassID"` + ClassSubjectID int `json:"ClassSubjectID"` + ClassName string `json:"ClassName"` + SortSeq int `json:"SortSeq"` + DrawingImg string `json:"DrawingImg"` + SilhouetteImg string `json:"SilhouetteImg"` + PublicDisplayName string `json:"PublicDisplayName"` + } `json:"Class"` + Status int `json:"Status"` + OwnedByWSF bool `json:"OwnedByWSF"` +} + +func (f *FerriesClient) GetVesselBasics() ([]VesselBasic, error) { + req, err := http.NewRequest(http.MethodGet, getVesselBasicsAsJsonURL, nil) + if err != nil { + return nil, fmt.Errorf("error creating request: %v", err) + } + + q := req.URL.Query() + q.Add(wsdot.ParamFerriesAccessCodeKey, f.wsdot.ApiKey) + req.URL.RawQuery = q.Encode() + req.Header.Set("Content-Type", "application/json") + + resp, err := f.wsdot.Client.Do(req) + if err != nil { + return nil, fmt.Errorf("error making request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } + + var vessels []VesselBasic + if err := json.NewDecoder(resp.Body).Decode(&vessels); err != nil { + return nil, fmt.Errorf("error decoding response: %v", err) + } + + return vessels, nil +} + +type VesselLocation struct { + VesselID int `json:"VesselID"` + VesselName string `json:"VesselName"` + Mmsi int `json:"Mmsi"` + DepartingTerminalID int `json:"DepartingTerminalID"` + DepartingTerminalName string `json:"DepartingTerminalName"` + DepartingTerminalAbbrev string `json:"DepartingTerminalAbbrev"` + ArrivingTerminalID int `json:"ArrivingTerminalID"` + ArrivingTerminalName string `json:"ArrivingTerminalName"` + ArrivingTerminalAbbrev string `json:"ArrivingTerminalAbbrev"` + Latitude float64 `json:"Latitude"` + Longitude float64 `json:"Longitude"` + Speed float64 `json:"Speed"` + Heading int `json:"Heading"` + InService bool `json:"InService"` + AtDock bool `json:"AtDock"` + LeftDock string `json:"LeftDock"` + Eta string `json:"Eta"` + EtaBasis string `json:"EtaBasis"` + ScheduledDeparture string `json:"ScheduledDeparture"` + OpRouteAbbrev []string `json:"OpRouteAbbrev"` + VesselPositionNum int `json:"VesselPositionNum"` + SortSeq int `json:"SortSeq"` + ManagedBy int `json:"ManagedBy"` + TimeStamp string `json:"TimeStamp"` + VesselWatchShutID int `json:"VesselWatchShutID"` + VesselWatchShutMsg string `json:"VesselWatchShutMsg"` + VesselWatchShutFlag string `json:"VesselWatchShutFlag"` + VesselWatchStatus string `json:"VesselWatchStatus"` + VesselWatchMsg string `json:"VesselWatchMsg"` +} + +func (f *FerriesClient) GetVesselLocations() ([]VesselLocation, error) { + req, err := http.NewRequest(http.MethodGet, getVesselLocationsAsJsonURL, nil) + if err != nil { + return nil, fmt.Errorf("error creating request: %v", err) + } + + q := req.URL.Query() + q.Add(wsdot.ParamFerriesAccessCodeKey, f.wsdot.ApiKey) + req.URL.RawQuery = q.Encode() + req.Header.Set("Content-Type", "application/json") + + resp, err := f.wsdot.Client.Do(req) + if err != nil { + return nil, fmt.Errorf("error making request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } + + var vessels []VesselLocation + if err := json.NewDecoder(resp.Body).Decode(&vessels); err != nil { + return nil, fmt.Errorf("error decoding response: %v", err) + } + + return vessels, nil +} diff --git a/ferries/schedule.go b/ferries/schedule.go new file mode 100644 index 0000000..e7ba3c4 --- /dev/null +++ b/ferries/schedule.go @@ -0,0 +1,137 @@ +package ferries + +import ( + "encoding/json" + "fmt" + "net/http" + + "alpineworks.io/wsdot" +) + +const ( + getRouteSchedulesAsJsonURL = "https://www.wsdot.wa.gov/Ferries/API/Schedule/rest/schedroutes" + getScheduleTodayByRouteIDAsJsonURL = "https://www.wsdot.wa.gov/Ferries/API/Schedule/rest/scheduletoday/%d/%t" +) + +type RouteSchedule struct { + ScheduleID int `json:"ScheduleID"` + SchedRouteID int `json:"SchedRouteID"` + ContingencyOnly bool `json:"ContingencyOnly"` + RouteID int `json:"RouteID"` + RouteAbbrev string `json:"RouteAbbrev"` + Description string `json:"Description"` + SeasonalRouteNotes string `json:"SeasonalRouteNotes"` + RegionID int `json:"RegionID"` + ServiceDisruptions []ServiceDisruption `json:"ServiceDisruptions"` + ContingencyAdj []ContingencyAdjustment `json:"ContingencyAdj"` +} + +type ServiceDisruption struct { + BulletinID int `json:"BulletinID"` + BulletinFlag bool `json:"BulletinFlag"` + PublishDate string `json:"PublishDate"` + DisruptionDescription string `json:"DisruptionDescription"` +} + +type ContingencyAdjustment struct { + DateFrom string `json:"DateFrom"` + DateThru string `json:"DateThru"` + EventID *int `json:"EventID"` + EventDescription *string `json:"EventDescription"` + AdjType int `json:"AdjType"` + ReplacedBySchedRouteID *int `json:"ReplacedBySchedRouteID"` +} + +func (f *FerriesClient) GetRouteSchedules() ([]RouteSchedule, error) { + req, err := http.NewRequest(http.MethodGet, getRouteSchedulesAsJsonURL, nil) + if err != nil { + return nil, fmt.Errorf("error creating request: %v", err) + } + + q := req.URL.Query() + q.Add(wsdot.ParamFerriesAccessCodeKey, f.wsdot.ApiKey) + req.URL.RawQuery = q.Encode() + req.Header.Set("Content-Type", "application/json") + + resp, err := f.wsdot.Client.Do(req) + if err != nil { + return nil, fmt.Errorf("error making request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } + + var schedules []RouteSchedule + if err := json.NewDecoder(resp.Body).Decode(&schedules); err != nil { + return nil, fmt.Errorf("error decoding response: %v", err) + } + + return schedules, nil +} + +type Schedule struct { + ScheduleID int64 `json:"ScheduleID"` + ScheduleName string `json:"ScheduleName"` + ScheduleSeason int `json:"ScheduleSeason"` + SchedulePDFUrl string `json:"SchedulePDFUrl"` + ScheduleStart string `json:"ScheduleStart"` + ScheduleEnd string `json:"ScheduleEnd"` + AllRoutes []int64 `json:"AllRoutes"` + TerminalCombos []TerminalCombo `json:"TerminalCombos"` +} + +type TerminalCombo struct { + DepartingTerminalID int64 `json:"DepartingTerminalID"` + DepartingTerminalName string `json:"DepartingTerminalName"` + ArrivingTerminalID int64 `json:"ArrivingTerminalID"` + ArrivingTerminalName string `json:"ArrivingTerminalName"` + SailingNotes string `json:"SailingNotes"` + Annotations []string `json:"Annotations"` + Times []Time `json:"Times"` + AnnotationsIVR []string `json:"AnnotationsIVR"` +} + +type Time struct { + DepartingTime string `json:"DepartingTime"` + ArrivingTime string `json:"ArrivingTime"` + LoadingRule int `json:"LoadingRule"` + VesselID int64 `json:"VesselID"` + VesselName string `json:"VesselName"` + VesselHandicapAccessible bool `json:"VesselHandicapAccessible"` + VesselPositionNum int `json:"VesselPositionNum"` + Routes []int64 `json:"Routes"` + AnnotationIndexes []int `json:"AnnotationIndexes"` +} + +func (f *FerriesClient) GetSchedulesTodayByRouteID(routeID int, onlyRemainingTimes bool) (*Schedule, error) { + url := fmt.Sprintf(getScheduleTodayByRouteIDAsJsonURL, routeID, onlyRemainingTimes) + + req, err := http.NewRequest(http.MethodGet, url, nil) + if err != nil { + return nil, fmt.Errorf("error creating request: %v", err) + } + + q := req.URL.Query() + q.Add(wsdot.ParamFerriesAccessCodeKey, f.wsdot.ApiKey) + req.URL.RawQuery = q.Encode() + req.Header.Set("Content-Type", "application/json") + + resp, err := f.wsdot.Client.Do(req) + if err != nil { + return nil, fmt.Errorf("error making request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } + + var schedules Schedule + if err := json.NewDecoder(resp.Body).Decode(&schedules); err != nil { + return nil, fmt.Errorf("error decoding response: %v", err) + } + + return &schedules, nil +} From 6474d1d948ef95f761b80aacd312bc01e8f1908e Mon Sep 17 00:00:00 2001 From: Michael Peters Jr Date: Sat, 19 Apr 2025 21:17:59 -0700 Subject: [PATCH 2/2] fix: add more intelligent timestamp handling --- example/vessels/main.go | 2 +- ferries/schedule.go | 221 ++++++++++++++++++++++++++++++++++++--- ferries/schedule_test.go | 64 ++++++++++++ 3 files changed, 272 insertions(+), 15 deletions(-) create mode 100644 ferries/schedule_test.go diff --git a/example/vessels/main.go b/example/vessels/main.go index aec7a96..b936891 100644 --- a/example/vessels/main.go +++ b/example/vessels/main.go @@ -60,7 +60,7 @@ func main() { for _, schedule := range schedules { fmt.Printf("Schedule ID: %d, Route ID: %d, Description: %s\n", schedule.ScheduleID, schedule.RouteID, schedule.Description) - allSailings, err := ferriesClient.GetSchedulesTodayByRouteID(schedule.RouteID, true) + allSailings, err := ferriesClient.GetSchedulesTodayByRouteID(schedule.RouteID, false) if err != nil { panic(err) } diff --git a/ferries/schedule.go b/ferries/schedule.go index e7ba3c4..7dd6c54 100644 --- a/ferries/schedule.go +++ b/ferries/schedule.go @@ -3,7 +3,11 @@ package ferries import ( "encoding/json" "fmt" + "log/slog" "net/http" + "regexp" + "strconv" + "time" "alpineworks.io/wsdot" ) @@ -71,13 +75,47 @@ func (f *FerriesClient) GetRouteSchedules() ([]RouteSchedule, error) { return schedules, nil } +type inSchedule struct { + ScheduleID int64 `json:"ScheduleID"` + ScheduleName string `json:"ScheduleName"` + ScheduleSeason int `json:"ScheduleSeason"` + SchedulePDFUrl string `json:"SchedulePDFUrl"` + ScheduleStart *string `json:"ScheduleStart"` + ScheduleEnd *string `json:"ScheduleEnd"` + AllRoutes []int64 `json:"AllRoutes"` + TerminalCombos []inTerminalCombo `json:"TerminalCombos"` +} + +type inTerminalCombo struct { + DepartingTerminalID int64 `json:"DepartingTerminalID"` + DepartingTerminalName string `json:"DepartingTerminalName"` + ArrivingTerminalID int64 `json:"ArrivingTerminalID"` + ArrivingTerminalName string `json:"ArrivingTerminalName"` + SailingNotes string `json:"SailingNotes"` + Annotations []string `json:"Annotations"` + Times []inTime `json:"Times"` + AnnotationsIVR []string `json:"AnnotationsIVR"` +} + +type inTime struct { + DepartingTime *string `json:"DepartingTime"` + ArrivingTime *string `json:"ArrivingTime"` + LoadingRule int `json:"LoadingRule"` + VesselID int64 `json:"VesselID"` + VesselName string `json:"VesselName"` + VesselHandicapAccessible bool `json:"VesselHandicapAccessible"` + VesselPositionNum int `json:"VesselPositionNum"` + Routes []int64 `json:"Routes"` + AnnotationIndexes []int `json:"AnnotationIndexes"` +} + type Schedule struct { ScheduleID int64 `json:"ScheduleID"` ScheduleName string `json:"ScheduleName"` ScheduleSeason int `json:"ScheduleSeason"` SchedulePDFUrl string `json:"SchedulePDFUrl"` - ScheduleStart string `json:"ScheduleStart"` - ScheduleEnd string `json:"ScheduleEnd"` + ScheduleStart *time.Time `json:"ScheduleStart"` + ScheduleEnd *time.Time `json:"ScheduleEnd"` AllRoutes []int64 `json:"AllRoutes"` TerminalCombos []TerminalCombo `json:"TerminalCombos"` } @@ -94,15 +132,15 @@ type TerminalCombo struct { } type Time struct { - DepartingTime string `json:"DepartingTime"` - ArrivingTime string `json:"ArrivingTime"` - LoadingRule int `json:"LoadingRule"` - VesselID int64 `json:"VesselID"` - VesselName string `json:"VesselName"` - VesselHandicapAccessible bool `json:"VesselHandicapAccessible"` - VesselPositionNum int `json:"VesselPositionNum"` - Routes []int64 `json:"Routes"` - AnnotationIndexes []int `json:"AnnotationIndexes"` + DepartingTime *time.Time `json:"DepartingTime"` + ArrivingTime *time.Time `json:"ArrivingTime"` + LoadingRule int `json:"LoadingRule"` + VesselID int64 `json:"VesselID"` + VesselName string `json:"VesselName"` + VesselHandicapAccessible bool `json:"VesselHandicapAccessible"` + VesselPositionNum int `json:"VesselPositionNum"` + Routes []int64 `json:"Routes"` + AnnotationIndexes []int `json:"AnnotationIndexes"` } func (f *FerriesClient) GetSchedulesTodayByRouteID(routeID int, onlyRemainingTimes bool) (*Schedule, error) { @@ -128,10 +166,165 @@ func (f *FerriesClient) GetSchedulesTodayByRouteID(routeID int, onlyRemainingTim return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode) } - var schedules Schedule - if err := json.NewDecoder(resp.Body).Decode(&schedules); err != nil { + var inSchedule inSchedule + if err := json.NewDecoder(resp.Body).Decode(&inSchedule); err != nil { return nil, fmt.Errorf("error decoding response: %v", err) } - return &schedules, nil + schedule := inScheduleToSchedule(inSchedule) + + return &schedule, nil +} + +func inScheduleToSchedule(inSchedule inSchedule) Schedule { + var ( + scheduleStart *time.Time = nil + scheduleEnd *time.Time = nil + err error + ) + if inSchedule.ScheduleStart != nil { + scheduleStart, err = wsdotTimeStringToTime(*inSchedule.ScheduleStart) + if err != nil { + slog.Warn("error parsing departing time", "error", err) + scheduleStart = nil + } + } + if inSchedule.ScheduleEnd != nil { + scheduleEnd, err = wsdotTimeStringToTime(*inSchedule.ScheduleEnd) + if err != nil { + slog.Warn("error parsing arriving time", "error", err) + scheduleEnd = nil + } + } + + schedule := Schedule{ + ScheduleID: inSchedule.ScheduleID, + ScheduleName: inSchedule.ScheduleName, + ScheduleSeason: inSchedule.ScheduleSeason, + SchedulePDFUrl: inSchedule.SchedulePDFUrl, + ScheduleStart: scheduleStart, + ScheduleEnd: scheduleEnd, + AllRoutes: inSchedule.AllRoutes, + } + + for _, combo := range inSchedule.TerminalCombos { + schedule.TerminalCombos = append(schedule.TerminalCombos, inTerminalComboToTerminalCombo(combo)) + } + + return schedule +} + +func inTerminalComboToTerminalCombo(inCombo inTerminalCombo) TerminalCombo { + combo := TerminalCombo{ + DepartingTerminalID: inCombo.DepartingTerminalID, + DepartingTerminalName: inCombo.DepartingTerminalName, + ArrivingTerminalID: inCombo.ArrivingTerminalID, + ArrivingTerminalName: inCombo.ArrivingTerminalName, + SailingNotes: inCombo.SailingNotes, + Annotations: inCombo.Annotations, + } + + for _, time := range inCombo.Times { + combo.Times = append(combo.Times, inTimeToTime(time)) + } + + return combo +} + +func inTimeToTime(inTime inTime) Time { + var ( + departingTime *time.Time = nil + arrivingTime *time.Time = nil + err error + ) + if inTime.DepartingTime != nil { + departingTime, err = wsdotTimeStringToTime(*inTime.DepartingTime) + if err != nil { + slog.Warn("error parsing departing time", "error", err) + departingTime = nil + } + } + if inTime.ArrivingTime != nil { + arrivingTime, err = wsdotTimeStringToTime(*inTime.ArrivingTime) + if err != nil { + slog.Warn("error parsing arriving time", "error", err) + arrivingTime = nil + } + } + + return Time{ + DepartingTime: departingTime, + ArrivingTime: arrivingTime, + LoadingRule: inTime.LoadingRule, + VesselID: inTime.VesselID, + VesselName: inTime.VesselName, + VesselHandicapAccessible: inTime.VesselHandicapAccessible, + VesselPositionNum: inTime.VesselPositionNum, + Routes: inTime.Routes, + AnnotationIndexes: inTime.AnnotationIndexes, + } +} + +func wsdotTimeStringToTime(wsdotTime string) (*time.Time, error) { + // /Date(1742713200000-0700)/ + re := regexp.MustCompile(`^/Date\((\d+)([+-]\d{4})\)/$`) + + matches := re.FindStringSubmatch(wsdotTime) + if len(matches) != 3 { + return nil, fmt.Errorf("invalid WSDOT time string format: %s", wsdotTime) + } + + // Parse the timestamp + milliseconds, err := strconv.ParseInt(matches[1], 10, 64) + if err != nil { + return nil, fmt.Errorf("error parsing milliseconds: %v", err) + } + + // WARNING: The offset is not currently used in the conversion, as it's already accounted for + + // // Parse the offset + // offset, err := parseOffsetMilli(matches[2]) + // if err != nil { + // return nil, fmt.Errorf("error parsing offset: %v", err) + // } + + // // Adjust the milliseconds with the offset + // milliseconds += int64(offset) + + // Convert milliseconds to time.Time + t := time.UnixMilli(milliseconds) + + return &t, nil +} + +func parseOffsetMilli(offset string) (int, error) { + if len(offset) != 5 { + return 0, fmt.Errorf("failed to parse offset - length incorrect") + } + + sign := string(offset[0]) + + offsetHours := string(offset[1:3]) + offsetMinutes := string(offset[3:5]) + + hours, err := strconv.ParseInt(offsetHours, 10, 64) + if err != nil || (hours < 0 || hours > 23) { + return 0, fmt.Errorf("error parsing hours: %v", err) + } + + minutes, err := strconv.ParseInt(offsetMinutes, 10, 64) + if err != nil || (minutes < 0 || minutes > 59) { + return 0, fmt.Errorf("error parsing minutes: %v", err) + } + + totalMillis := (hours*60 + minutes) * 60 * 1000 + + switch sign { + case "+": + return int(totalMillis), nil + case "-": + return -int(totalMillis), nil + default: + return 0, fmt.Errorf("invalid sign: %s", string(sign)) + } } diff --git a/ferries/schedule_test.go b/ferries/schedule_test.go new file mode 100644 index 0000000..43f3b36 --- /dev/null +++ b/ferries/schedule_test.go @@ -0,0 +1,64 @@ +package ferries + +import ( + "testing" +) + +func TestParseOffsetMilli(t *testing.T) { + tests := []struct { + name string + offset string + want int + expectErr bool + }{ + { + name: "Valid positive offset", + offset: "+0700", + want: 25200000, // 7 hours in milliseconds + expectErr: false, + }, + { + name: "Valid negative offset", + offset: "-0530", + want: -19800000, // 5 hours 30 minutes in milliseconds + expectErr: false, + }, + { + name: "Invalid offset length", + offset: "+070", + want: 0, + expectErr: true, + }, + { + name: "Invalid sign", + offset: "*0700", + want: 0, + expectErr: true, + }, + { + name: "Invalid hour value", + offset: "+2500", + want: 0, + expectErr: true, + }, + { + name: "Invalid minute value", + offset: "+0760", + want: 0, + expectErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := parseOffsetMilli(tt.offset) + if (err != nil) != tt.expectErr { + t.Errorf("parseOffsetMilli() error = %v, expectErr %v", err, tt.expectErr) + return + } + if got != tt.want { + t.Errorf("parseOffsetMilli() = %v, want %v", got, tt.want) + } + }) + } +}