diff --git a/realtime.go b/realtime.go index fbe6757..82d950a 100644 --- a/realtime.go +++ b/realtime.go @@ -266,6 +266,41 @@ func resolveStopReferences(updates []*parse.StopTimeUpdate, events []*storage.St } } +// Computes delay by comparing a static time given as offset, and +// timestamp from realtime. +func delayFromOffsetAndTime(tz *time.Location, eventOffset time.Duration, updateTime time.Time) time.Duration { + + // The eventOffset (from static GTFS) gives a time of + // day, as an offset from noon-12h in the local + // timezone. It's possible to apply to multiple dates, + // but most likely it's from the same day, or the day + // before the realtime update. Whichever's closer in + // time is what we pick. + + sameNoonLocal := time.Date(updateTime.Year(), updateTime.Month(), updateTime.Day(), 12, 0, 0, 0, tz) + sameNoonUTC := sameNoonLocal.UTC() + prevNoonUTC := sameNoonLocal.AddDate(0, 0, -1).UTC() + + sameTime := sameNoonUTC.Add(-12 * time.Hour).Add(eventOffset) + prevTime := prevNoonUTC.Add(-12 * time.Hour).Add(eventOffset) + + sameDiff := updateTime.Sub(sameTime) + prevDiff := updateTime.Sub(prevTime) + + sameDiffAbs := sameDiff + if sameDiffAbs < 0 { + sameDiffAbs *= -1 + } + prevDiffAbs := prevDiff + if prevDiffAbs < 0 { + prevDiffAbs *= -1 + } + if sameDiffAbs < prevDiffAbs { + return sameDiff + } + return prevDiff +} + // Construct RealtimeUpdates from StopTimeUpdates and // StopTimeEvents. Groups them by trip and stop. func (rt *Realtime) buildRealtimeUpdates( @@ -296,41 +331,6 @@ func (rt *Realtime) buildRealtimeUpdates( }) } - // Computes delay of an update, given the correspnding time - // from static schedule. - updateDelay := func(eventOffset time.Duration, updateTime time.Time) time.Duration { - - // The eventOffset (from static GTFS) gives a time of - // day, as an offset from noon-12h in the local - // timezone. It's possible to apply to multiple dates, - // but most likely it's from the same day, or the day - // before the realtime update. Whichever's closer in - // time is what we pick. - - sameNoonLocal := time.Date(updateTime.Year(), updateTime.Month(), updateTime.Day(), 12, 0, 0, 0, timezone) - sameNoonUTC := sameNoonLocal.UTC() - prevNoonUTC := sameNoonLocal.AddDate(0, 0, -1).UTC() - - sameTime := sameNoonUTC.Add(-12 * time.Hour).Add(eventOffset) - prevTime := prevNoonUTC.Add(-12 * time.Hour).Add(eventOffset) - - sameDiff := updateTime.Sub(sameTime) - prevDiff := updateTime.Sub(prevTime) - - sameDiffAbs := sameDiff - if sameDiffAbs < 0 { - sameDiffAbs *= -1 - } - prevDiffAbs := prevDiff - if prevDiffAbs < 0 { - prevDiffAbs *= -1 - } - if sameDiffAbs < prevDiffAbs { - return sameDiff - } - return prevDiff - } - // Combine static schedule and realtime updates for tripID, tripUpdates := range updatesByTrip { events, found := eventsByTrip[tripID] @@ -375,7 +375,8 @@ func (rt *Realtime) buildRealtimeUpdates( // Feeds can use the timestamp to communicate delays rtUp.ArrivalDelay = u.ArrivalDelay if !u.ArrivalTime.IsZero() && u.ArrivalDelay == 0 { - rtUp.ArrivalDelay = updateDelay( + rtUp.ArrivalDelay = delayFromOffsetAndTime( + timezone, events[ei].StopTime.ArrivalTime(), u.ArrivalTime, ) @@ -385,7 +386,8 @@ func (rt *Realtime) buildRealtimeUpdates( // Same thing here rtUp.DepartureDelay = u.DepartureDelay if !u.DepartureTime.IsZero() { - rtUp.DepartureDelay = updateDelay( + rtUp.DepartureDelay = delayFromOffsetAndTime( + timezone, events[ei].StopTime.DepartureTime(), u.DepartureTime, ) @@ -396,12 +398,12 @@ func (rt *Realtime) buildRealtimeUpdates( // departure. If the arrival is early, // interpret it as a return to regular // schedule. - rtUp.DepartureDelay = max(u.ArrivalDelay, 0) + rtUp.DepartureDelay = max(rtUp.ArrivalDelay, 0) } if !u.ArrivalIsSet { // Lacking Arrival data, assume // departure delay applies to arrival - rtUp.ArrivalDelay = u.DepartureDelay + rtUp.ArrivalDelay = rtUp.DepartureDelay } // Track the min and max delays observed. This diff --git a/realtime_test.go b/realtime_test.go index 639b2f4..f4191a6 100644 --- a/realtime_test.go +++ b/realtime_test.go @@ -2026,3 +2026,83 @@ func TestRealtimeDelayCrossingDSTBoundaryFromPreviousDay(t *testing.T) { require.Equal(t, tc.ExpectedDepartureTime, departures[0].Time, tc.Name) } } + +func TestRealtimeArrivalDelayOnly(t *testing.T) { + + // Some agencies (e.g. WMATA) prefer passing Arrival Delay + // instead of Departure Delay (or dito with Time.) When we + // encounter a delayed arrival, and no departure information + // is available, we should assume the departure is similarly + // delayed. + feed := buildFeed(t, []TripUpdate{ + { + TripID: "t1", + StopUpdates: []StopUpdate{ + { // 55s delay + StopID: "s2", + ArrivalSet: true, + ArrivalDelay: 55, + }, + }, + }, + { + TripID: "t2", + StopUpdates: []StopUpdate{ + { // arrival delay, but departure delay shows it recovers + StopID: "s1", + ArrivalSet: true, + ArrivalDelay: 55, + DepartureSet: true, + DepartureDelay: 0, + }, + { // 25s delay (via time) + StopSequence: 2, + ArrivalSet: true, + ArrivalTime: time.Date(2020, 1, 15, 23, 11, 25, 0, time.UTC), + }, + }, + }, + { + TripID: "t3", + StopUpdates: []StopUpdate{ + { // 35s delay (via time) + StopID: "z1", + ArrivalSet: true, + ArrivalTime: time.Date(2020, 1, 15, 23, 5, 35, 0, time.UTC), + }, + { // recovery via early arrival + StopID: "z2", + ArrivalSet: true, + ArrivalTime: time.Date(2020, 1, 15, 23, 5, 55, 0, time.UTC), + }, + }, + }, + }) + static := SimpleStaticFixture(t) + rt, err := gtfs.NewRealtime(context.Background(), static, feed) + require.NoError(t, err) + assert.Equal(t, uint64(time.Date(2020, 1, 15, 23, 0, 0, 0, time.UTC).Unix()), rt.Timestamp) + + for _, tc := range []struct { + TripID string + StopID string + Time time.Time + Delay time.Duration + }{ + {"t1", "s1", time.Date(2020, 1, 15, 23, 0, 0, 0, time.UTC), 0}, + {"t1", "s2", time.Date(2020, 1, 15, 23, 1, 55, 0, time.UTC), 55 * time.Second}, + {"t1", "s3", time.Date(2020, 1, 15, 23, 2, 55, 0, time.UTC), 55 * time.Second}, + {"t2", "s1", time.Date(2020, 1, 15, 23, 0, 0, 0, time.UTC), 0}, + {"t2", "s2", time.Date(2020, 1, 15, 23, 11, 25, 0, time.UTC), 25 * time.Second}, + {"t2", "s3", time.Date(2020, 1, 15, 23, 12, 25, 0, time.UTC), 25 * time.Second}, + {"t3", "z1", time.Date(2020, 1, 15, 23, 5, 35, 0, time.UTC), 35 * time.Second}, + {"t3", "z2", time.Date(2020, 1, 15, 23, 6, 0, 0, time.UTC), 0}, + } { + deps, err := rt.Departures(tc.StopID, tc.Time.Add(-time.Minute), 2*time.Minute, -1, "", -1, nil) + require.NoError(t, err) + require.Equal(t, 1, len(deps)) + assert.Equal(t, tc.Delay, deps[0].Delay, "trip %s stop %s time %s", tc.TripID, tc.StopID, tc.Time) + assert.Equal(t, tc.Time, deps[0].Time, "trip %s stop %s time %s", tc.TripID, tc.StopID, tc.Time) + } + +} diff --git a/whitebox_test.go b/whitebox_test.go index 6a2a774..aa33070 100644 --- a/whitebox_test.go +++ b/whitebox_test.go @@ -1,6 +1,7 @@ package gtfs import ( + "fmt" "testing" "time" @@ -148,3 +149,34 @@ func TestStaticRangePerDate(t *testing.T) { }) } } + +func TestRealtimeDelayFromOffsetAndTime(t *testing.T) { + + // TODO: Would be great to flesh this out. + + for _, tc := range []struct { + tz *time.Location + eventOffset string + updateTime string + expectedDelay string + }{ + {time.UTC, "23h6m", "2020-01-15 23:05:55 +0000 UTC", "-5s"}, + {time.UTC, "23h11m", "2020-01-15 23:11:25 +0000 UTC", "25s"}, + } { + offset, err := time.ParseDuration(tc.eventOffset) + require.NoError(t, err) + time_, err := time.Parse("2006-01-02 15:04:05 -0700 MST", tc.updateTime) + require.NoError(t, err) + delay, err := time.ParseDuration(tc.expectedDelay) + require.NoError(t, err) + + actual := delayFromOffsetAndTime(tc.tz, offset, time_) + assert.Equal( + t, delay, actual, + fmt.Sprintf( + "%s %s, expect %s, got %s", + tc.eventOffset, tc.updateTime, tc.expectedDelay, actual, + ), + ) + } +}