Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rt validator fix overnight #305

Merged
merged 3 commits into from Feb 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
125 changes: 125 additions & 0 deletions ext/sched/sched.go
@@ -0,0 +1,125 @@
package sched

import (
"time"

"github.com/interline-io/transitland-lib/tl"
"github.com/interline-io/transitland-lib/tl/tt"
)

type tripInfo struct {
FrequencyStarts []int
ServiceID string
StartTime tt.WideTime
EndTime tt.WideTime
}

type ScheduleChecker struct {
tripInfo map[string]tripInfo
services map[string]*tl.Service
}

func NewScheduleChecker() *ScheduleChecker {
return &ScheduleChecker{
tripInfo: map[string]tripInfo{},
services: map[string]*tl.Service{},
}
}

// Validate gets a stream of entities from Copier to build up the cache.
func (fi *ScheduleChecker) Validate(ent tl.Entity) []error {
switch v := ent.(type) {
case *tl.Service:
fi.services[v.ServiceID] = v
case *tl.Trip:
ti := tripInfo{
ServiceID: v.ServiceID,
}
if len(v.StopTimes) > 0 {
ti.StartTime = v.StopTimes[0].DepartureTime
ti.EndTime = v.StopTimes[len(v.StopTimes)-1].ArrivalTime
}
fi.tripInfo[v.TripID] = ti
case *tl.Frequency:
a := fi.tripInfo[v.TripID]
for s := v.StartTime.Seconds; s < v.EndTime.Seconds; s += v.HeadwaySecs {
a.FrequencyStarts = append(a.FrequencyStarts, s)
}
fi.tripInfo[v.TripID] = a
}
return nil
}

type dayOffset struct {
day int
sec int
}

func (fi *ScheduleChecker) ActiveTrips(now time.Time) []string {
var ret []string
dayOffsets := []dayOffset{
{day: -1, sec: 86400},
{day: 0, sec: 0},
}
for _, d := range dayOffsets {
nowSvc := map[string]bool{}
nowOffset := now.AddDate(0, 0, d.day)
nowWt := tt.NewWideTimeFromSeconds(nowOffset.Hour()*3600 + nowOffset.Minute()*60 + nowOffset.Second() + d.sec)
for k, v := range fi.tripInfo {
svc, ok := fi.services[v.ServiceID]
if !ok {
// log.Debug().
// Str("service", v.ServiceID).
// Str("trip", k).
// Msg("no service, skipping")
continue
}
// Cache if we have service on this day
sched, ok := nowSvc[svc.ServiceID]
if !ok {
sched = svc.IsActive(nowOffset)
nowSvc[svc.ServiceID] = sched
}
// Not scheduled
if !sched {
// log.Debug().
// Str("date", now.Format("2006-02-03")).
// Str("service", v.ServiceID).
// Str("trip", k).
// Msg("not scheduled, skipping")
continue
}

// Might be scheduled
found := false
if len(v.FrequencyStarts) == 0 && nowWt.Seconds >= v.StartTime.Seconds && nowWt.Seconds <= v.EndTime.Seconds {
// Check non-frequency based trips
// log.Debug().
// Str("date", now.Format("2006-02-03")).
// Str("cur_time", nowWt.String()).
// Str("trip_start", v.StartTime.String()).
// Str("trip_end", v.EndTime.String()).
// Str("service", v.ServiceID).
// Str("trip", k).
// Msg("outside time, skipping")
found = true
}

// Check frequency based trips
tripDuration := v.EndTime.Seconds - v.StartTime.Seconds
for _, s := range v.FrequencyStarts {
freqStart := s
freqEnd := freqStart + tripDuration
if nowWt.Seconds >= freqStart && nowWt.Seconds <= freqEnd {
found = true
break
}
}
if !found {
continue
}
ret = append(ret, k)
}
}
return ret
}
125 changes: 125 additions & 0 deletions ext/sched/sched_test.go
@@ -0,0 +1,125 @@
package sched

import (
"testing"
"time"

"github.com/interline-io/transitland-lib/adapters/empty"
"github.com/interline-io/transitland-lib/copier"
"github.com/interline-io/transitland-lib/internal/testutil"
"github.com/interline-io/transitland-lib/tlcsv"
"github.com/stretchr/testify/assert"
)

func newTestScheduleSchecker(path string) (*ScheduleChecker, error) {
ex := NewScheduleChecker()
r, err := tlcsv.NewReader(path)
if err != nil {
return nil, err
}
cp, err := copier.NewCopier(r, &empty.Writer{}, copier.Options{})
if err != nil {
return nil, err
}
cp.AddExtension(ex)
cpResult := cp.Copy()
if cpResult.WriteError != nil {
return nil, err
}
return ex, nil
}

func TestScheduleChecker(t *testing.T) {
ex, err := newTestScheduleSchecker(testutil.RelPath("test/data/rt/ct.zip"))
if err != nil {
t.Fatal(err)
}
tz, _ := time.LoadLocation("America/Los_Angeles")
type tc struct {
name string
when time.Time
exp []string
}
extc := []tc{
{
name: "midday",
when: time.Date(2023, 11, 7, 17, 30, 0, 0, tz),
exp: []string{"412", "411", "410", "309", "308", "710", "311", "312", "127", "310", "125", "126", "709"},
},
{
name: "saturday overnight",
when: time.Date(2023, 11, 11, 0, 30, 0, 0, tz),
exp: []string{"145", "146"},
},
{
name: "sunday overnight",
when: time.Date(2023, 11, 12, 0, 30, 0, 0, tz),
exp: []string{"280", "284", "281"},
},
}
for _, tc := range extc {
t.Run(tc.name, func(t *testing.T) {
stats := ex.ActiveTrips(tc.when)
assert.ElementsMatch(t, tc.exp, stats)
})
}
freqEx, err := newTestScheduleSchecker(testutil.RelPath("test/data/example.zip"))
if err != nil {
t.Fatal(err)
}
freqtc := []tc{
// 6:05 is just after the 6:00 trips for STBA, CITY1, CITY2 started
{
when: time.Date(2007, 01, 10, 6, 5, 0, 0, tz),
exp: []string{"STBA", "CITY2", "CITY1"},
},
// 6:25 is after the first STBA trip ends,
// but within the 26 minute duration of CITY1/CITY2 starting at 6:00am
{
when: time.Date(2007, 01, 10, 6, 25, 0, 0, tz),
exp: []string{"CITY2", "CITY1"},
},
// 6:28 is in between any scheduled trips
{
when: time.Date(2007, 01, 10, 6, 28, 0, 0, tz),
exp: []string{},
},
// 04:00 is before any trips start
{
when: time.Date(2007, 01, 10, 4, 0, 0, 0, tz),
exp: []string{},
},
// 23:00 is after all trips end
{
when: time.Date(2007, 01, 10, 23, 0, 0, 0, tz),
exp: []string{},
},
// 21:30 is the last scheduled trip, so 21:40 will have all three routes
{
when: time.Date(2007, 01, 10, 21, 40, 0, 0, tz),
exp: []string{"STBA", "CITY2", "CITY1"},
},
// 21:30 + 25 minutes, 21:55, should be after STBA ends
{
when: time.Date(2007, 01, 10, 21, 55, 0, 0, tz),
exp: []string{"CITY2", "CITY1"},
},
// 16:15 should have multiple trips of CITY1, CITY2 and 1 trip of STBA
{
when: time.Date(2007, 01, 10, 16, 15, 0, 0, tz),
exp: []string{"STBA", "CITY2", "CITY1"},
},
// Weekend
{
when: time.Date(2007, 01, 13, 16, 0, 0, 0, tz),
exp: []string{"AAMV4", "STBA", "CITY2", "CITY1"},
},
}
for _, tc := range freqtc {
t.Run("frequency "+tc.name, func(t *testing.T) {
stats := freqEx.ActiveTrips(tc.when)
assert.ElementsMatch(t, tc.exp, stats)
})
}

}