Skip to content

Expand exact_times frequency trips into schedule-for-stop stop times - #1387

Open
ARCoder181105 wants to merge 6 commits into
OneBusAway:mainfrom
ARCoder181105:feat/schedule-for-stop-exact-times-expansion
Open

Expand exact_times frequency trips into schedule-for-stop stop times#1387
ARCoder181105 wants to merge 6 commits into
OneBusAway:mainfrom
ARCoder181105:feat/schedule-for-stop-exact-times-expansion

Conversation

@ARCoder181105

Copy link
Copy Markdown
Collaborator

Addresses #1031 (the exact_times=1 half; the headway half is #1386).

Stacked on #1386 — its commits show up in this diff until it merges. Review the last commit here.

What changed

A trip's frequency windows are now split on their exact_times value:

  • exact_times=0 keeps populating scheduleFrequencies as an approximate headway window, unchanged from Report headway-based service in schedule-for-stop frequencies #1386.
  • exact_times=1 expands into individual scheduleStopTimes — one per headway interval from the window's start up to, but not including, its end. Each run repeats the template trip's timings, so the stop keeps its offset from the trip's first arrival.

Stop times are sorted by departure time after grouping, since expanded runs are appended after the ones the query had already ordered.

Spec

Per GTFS, exact_times=1 means the departures are precisely timed and frequencies.txt is only a compact encoding of what is functionally a normal timetable.

This deviates from legacy Java, which routes both kinds into scheduleFrequencies undifferentiated — FrequencyInstanceBean has no exactTimes field to distinguish them. The wiki records the deviation, the trade-off, and the fallback under Implementation Decisions. A client built against Java's behaviour will see more entries in scheduleStopTimes and fewer in scheduleFrequencies for such routes.

The loop is bounded by the schema's own constraints — headway_secs > 0 and start_time < end_time — which is noted in a comment where it is relied on.

Tests

TestScheduleForStopHandlerFrequencies gains a case covering an exact_times=1 trip: four runs from a 06:00-08:00 window at a 30-minute headway, each asserted at its exact departure time five minutes into the trip, and nothing in scheduleFrequencies for that group.

The handler returned an empty references block as soon as a stop had no
routes, skipping the includeReferences check further down. A valid stop
with no routes therefore answered with its own ID in entry.stopId and no
matching record in references.stops.

The early return was only an optimization. GetScheduleForStopOnDate
expands an empty route ID slice to IN (NULL), so the normal path already
returns no schedule rows and builds references correctly.
Route and agency references were collected from the schedule rows of the
queried date, so both lists emptied out whenever no service was active --
a holiday, a date outside the feed's validity period, or an expired feed.
The reference server keeps them populated in that case, since references
describe the stop, not the day.

Build them from the routes serving the stop instead. GetRoutesForStop
already selects every column the route reference needs, so the
GetRoutesByIDs round trip goes away, and agencies now derive from those
routes rather than being seeded with the queried stop's own agency.
@coderabbitai

coderabbitai Bot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@ARCoder181105, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 24 minutes

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

Wait for the limit to reset, then comment @coderabbitai review or push new commits to the PR.

An organization admin can change what happens after included review limits in Billing.

How do review limits work?

CodeRabbit enforces per-developer PR review limits within each organization.

For paid Pro and Pro+ reviews, CodeRabbit uses a developer's included PR review attempts over the past 7 days to set the current hourly allowance. At typical activity levels, the full plan allowance applies. Higher sustained activity can lower the allowance until earlier attempts leave the 7-day window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 82a73ed8-a8ff-4f70-91e8-841d6d12c9b2

📥 Commits

Reviewing files that changed from the base of the PR and between 347da6e and 87bc438.

📒 Files selected for processing (6)
  • internal/models/frequency.go
  • internal/restapi/http_test.go
  • internal/restapi/schedule_for_stop_frequencies_test.go
  • internal/restapi/schedule_for_stop_handler.go
  • internal/restapi/schedule_for_stop_handler_test.go
  • internal/utils/validation.go

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

The no-service case only checked that routes and agencies came back
non-empty, which passes just as well if the handler references the wrong
routes. Derive the expected sets from the stop's own routes and compare
against them, so the test fails if the reference contents drift.
The agency lookup ran first, so an unparseable date on an ID whose agency
prefix does not exist answered 404 instead of the field error the
reference server returns. The spec requires the field error to come back
before any stop lookup is attempted.

Check the date's format up front and keep resolving it to a service date
below, where the agency's timezone is available.
Frequency-based trips were indistinguishable from fixed-schedule ones:
the handler passed nil for a direction group's frequencies, so a stop
served only on a headway answered with scheduleFrequencies empty and its
headway trips listed as if each ran once.

Batch-load each trip's frequency windows and route those trips into
scheduleFrequencies instead of scheduleStopTimes, one entry per window,
sorted by start time. A trip's headsign now weighs as many votes as the
runs it is expected to make, so a frequent route no longer loses the
representative-headsign vote to a single scheduled trip.

The three parallel maps that carried a direction group's stop times and
headsign votes are replaced by one accumulator type, since frequencies
would otherwise have added a fourth.

The RABA fixture has no frequencies.txt, so the test builds a feed that
does and loads it through a new createTestApiWithFeed helper, leaving the
shared fixture untouched.
GTFS treats exact_times=1 as a compact encoding of a fixed timetable, not
an approximate headway, so a rider reading the schedule expects the same
individual departures a fully written-out timetable would show.

Split a trip's frequency windows on exact_times: headway windows keep
populating scheduleFrequencies, while exact ones expand into one stop
time per interval, each repeating the template trip's offset from its
first arrival. Stop times are sorted by departure time afterwards, since
expanded runs are appended after the ones the query already ordered.

This deviates from legacy Java, which routes both kinds into
scheduleFrequencies undifferentiated; the wiki records the reasoning and
the fallback.
@ARCoder181105
ARCoder181105 force-pushed the feat/schedule-for-stop-exact-times-expansion branch from 0ccde65 to 87bc438 Compare August 21, 2026 17:05
@sonarqubecloud

Copy link
Copy Markdown

@ARCoder181105
ARCoder181105 requested review from Ahmedhossamdev and burma-shave and a lite review from Copilot and removed request for Copilot August 21, 2026 17:15

@Ahmedhossamdev Ahmedhossamdev left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. Checked the exact_times/headway split against the Java reference, the divergence
is real but it's the one you already flagged in the description and the wiki, so
nothing to do there.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants