Skip to content

Conversation

@leonardehrenfried
Copy link
Member

Summary

Implements GTFS FaresV2 timeframes.

In order to manage the complexity a bit better, I have introduced the following changes:

  • Some records where converted to classes
  • GtfsFaresV2Service now has a builder

Lets have a chat about the model classes and the mappers in the dev meeting.

Unit tests

Lots added and adapted.

Documentation

Javadoc.

@leonardehrenfried leonardehrenfried requested a review from a team as a code owner December 16, 2025 07:54
@leonardehrenfried leonardehrenfried added +Sandbox This will be implemented as a Sandbox feature +Bump Serialization Id Add this label if you want the serialization id automatically bumped after merging the PR +Skip Changelog This is not a relevant change for a product owner since last release. labels Dec 16, 2025
@codecov
Copy link

codecov bot commented Dec 16, 2025

Codecov Report

❌ Patch coverage is 96.22642% with 6 lines in your changes missing coverage. Please review.
✅ Project coverage is 72.16%. Comparing base (9aa668b) to head (106cdf9).
⚠️ Report is 1 commits behind head on dev-2.x.

Files with missing lines Patch % Lines
...g/opentripplanner/ext/fares/model/FareLegRule.java 76.47% 3 Missing and 1 partial ⚠️
...er/ext/fares/service/gtfs/v2/TimeframeMatcher.java 90.90% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@              Coverage Diff              @@
##             dev-2.x    #7139      +/-   ##
=============================================
+ Coverage      72.12%   72.16%   +0.03%     
- Complexity     20837    20884      +47     
=============================================
  Files           2268     2273       +5     
  Lines          84338    84470     +132     
  Branches        8415     8424       +9     
=============================================
+ Hits           60827    60955     +128     
- Misses         20538    20542       +4     
  Partials        2973     2973              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Contributor

@binh-dam-ibigroup binh-dam-ibigroup left a comment

Choose a reason for hiding this comment

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

Still need to play with it, however I'm going to request you replace all the Entity.of() methods that return builders with either new EntityBuilder() or Entity.builder().

var t = Timeframe.of()
.withServiceId(serviceId)
.withStart(rhs.getStartTime())
.withEnd(rhs.getEndTime())
Copy link
Contributor

Choose a reason for hiding this comment

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

Not sure if that's an issue with OBA, a blank end_time value will map to midnight "00:00:00" (same as blank start_time values), so this will fail timeframe matching where end_time is omitted.

Copy link
Member Author

Choose a reason for hiding this comment

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

That's a good catch. I had set the default to LocalDate.MIDNIGHT and had assumed that that means 24:00 but it is the same as LocalDate.MIN.

I had seconds thoughts about the OBA default values and have removed them: OneBusAway/onebusaway-gtfs-modules#431

The handling of the missing value now happens in the OTP mapping process: bf0d9a9

Copy link
Contributor

@binh-dam-ibigroup binh-dam-ibigroup left a comment

Choose a reason for hiding this comment

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

I was trying to set up a "free fare Sunday afternoons for cash customers" rule but was not able to make it work smoothly. I'm attaching excerpts below. Basically, in order to override the regular cash fare that would normally apply, I have to populate rule_priority, however, right now, applying rule_priority hides other fare products, unless the corresponding fare leg rules also have the same priority value.

In trying to make fare products appear in the trip response, I also found an issue where leg rules with the default timeframe_regular timeframe were skipped altogether, and that needs to be fixed as well.

Happy to discuss further during the dev call.

Relevant GTFS data:

timeframes.txt (fare_regular is a calendar entry that applies 7 days, FallSunday only applies Sundays)

timeframe_group_id,start_time,end_time,service_id
timeframe_regular,,,fare_regular
timeframe_free_sundays_afternoons,12:00:00,18:00:00,FallSunday

fare_products.txt

fare_product_id,fare_product_name,fare_media_id,amount,currency
prod_free_fare_sundays_afternoons_cash,Free fare Sundays Afternoons Cash,cash,0.00,USD
prod_rapid_transit_cash,Subway cash fare,cash,2.40,USD
prod_rapid_transit_quick_subway,Subway Quick Ticket,charlieticket,2.40,USD

fare_leg_rules.txt (without priorities)

leg_group_id,network_id,from_area_id,to_area_id,fare_product_id,from_timeframe_group_id,to_timeframe_group_id,transfer_only
leg_rapid_transit_cash,rapid_transit,,,prod_rapid_transit_cash,,,1
leg_rapid_transit_cash,rapid_transit,,,prod_free_fare_sundays_afternoons_cash,timeframe_free_sundays_afternoons,,1
leg_rapid_transit_quick_subway,rapid_transit,area_bl,,prod_rapid_transit_quick_subway,,

@leonardehrenfried
Copy link
Member Author

I was trying to set up a "free fare Sunday afternoons for cash customers" rule but was not able to make it work smoothly. I'm attaching excerpts below. Basically, in order to override the regular cash fare that would normally apply, I have to populate rule_priority, however, right now, applying rule_priority hides other fare products, unless the corresponding fare leg rules also have the same priority value.

In trying to make fare products appear in the trip response, I also found an issue where leg rules with the default timeframe_regular timeframe were skipped altogether, and that needs to be fixed as well.

Happy to discuss further during the dev call.

Relevant GTFS data:

timeframes.txt (fare_regular is a calendar entry that applies 7 days, FallSunday only applies Sundays)

timeframe_group_id,start_time,end_time,service_id
timeframe_regular,,,fare_regular
timeframe_free_sundays_afternoons,12:00:00,18:00:00,FallSunday

fare_products.txt

fare_product_id,fare_product_name,fare_media_id,amount,currency
prod_free_fare_sundays_afternoons_cash,Free fare Sundays Afternoons Cash,cash,0.00,USD
prod_rapid_transit_cash,Subway cash fare,cash,2.40,USD
prod_rapid_transit_quick_subway,Subway Quick Ticket,charlieticket,2.40,USD

fare_leg_rules.txt (without priorities)

leg_group_id,network_id,from_area_id,to_area_id,fare_product_id,from_timeframe_group_id,to_timeframe_group_id,transfer_only
leg_rapid_transit_cash,rapid_transit,,,prod_rapid_transit_cash,,,1
leg_rapid_transit_cash,rapid_transit,,,prod_free_fare_sundays_afternoons_cash,timeframe_free_sundays_afternoons,,1
leg_rapid_transit_quick_subway,rapid_transit,area_bl,,prod_rapid_transit_quick_subway,,

I wrote some tests today to make sure, but I believe that this is how the priorities are supposed to work.

So lets say you have three rules (I'm condensing the entities for brevity):

  • Rule 1: Sundays from 12:00-18:00, regular, priority 2
  • Rule 2: Every day from 00:00-23:59, regular, priority 1
  • Rule 3: Every day from 00:00-23:59, youth, priority 1

If rule 1 matches, then both rule 2 and 3 will be excluded. Is this what you made you scratch your head?

If so then I believe that this is the correct behaviour, according to the spec: https://gtfs.org/documentation/schedule/reference/#fare_leg_rulestxt

In particular:

rule_priority: Defines the order of priority in which matching rules are applied to legs, allowing certain rules to take precedence over others. When multiple entries in fare_leg_rules.txt match, the rule or set of rules with the highest value for rule_priority will be selected.
An empty value for rule_priority is treated as zero.

So I believe that you need to increase the rule priority of rule 3 to 2, if you want both 1 and 3 to show up on Sundays.

Let's discuss this because I do think it's a little surprising how this works.

@binh-dam-ibigroup
Copy link
Contributor

So lets say you have three rules (I'm condensing the entities for brevity):

* Rule 1: Sundays from 12:00-18:00, regular, priority 2

* Rule 2: Every day from 00:00-23:59, regular, priority 1

* Rule 3: Every day from 00:00-23:59, youth, priority 1

If rule 1 matches, then both rule 2 and 3 will be excluded. Is this what you made you scratch your head?

Yes, because in this example, I don't think the youth fare should not be affected by what is happening to the regular fare.

rule_priority: Defines the order of priority in which matching rules are applied to legs, allowing certain rules to take precedence over others. When multiple entries in fare_leg_rules.txt match, the rule or set of rules with the highest value for rule_priority will be selected.
An empty value for rule_priority is treated as zero.

The spec is indeed vague about this. How I interpreted was "When multiple entries in the same leg_group_id in fare_leg_rules.txt match" (emphasis mine), because it makes sense to not mess with unrelated rules, but that's up for debate.

@leonardehrenfried
Copy link
Member Author

@binh-dam-ibigroup I'm currently asking about this in the MobilityData Slack: https://mobilitydata-io.slack.com/archives/C01KL7PR170/p1766060365033259

@leonardehrenfried
Copy link
Member Author

@binh-dam-ibigroup I don't think that we will get a definitive answer on our spec question. I believe the best way forward is to come up with our own definition and then remain flexible should the spec become clearer.

@leonardehrenfried leonardehrenfried force-pushed the timeframes branch 2 times, most recently from a7bf48a to cafc4aa Compare December 19, 2025 12:14
# Conflicts:
#	application/src/ext-test/java/org/opentripplanner/ext/fares/service/gtfs/v1/custom/AtlantaFareServiceTest.java
#	application/src/ext-test/java/org/opentripplanner/ext/fares/service/gtfs/v1/custom/OrcaFareServiceTest.java
#	application/src/test-fixtures/java/org/opentripplanner/ext/fares/model/FareModelForTest.java
@leonardehrenfried
Copy link
Member Author

Binh and I had a private chat and we came to conclusion that we will keep the current rule priority logic as is. We are aware that the spec is not precise enough and you could definitely produce conflicting data when combining priorities and timeframes.

However, CTRAN, the only feed that we know of that produces timeframes, handles this problem by producing non-overlapping timeframes. We might have to revisit this when we see this problem re-appear in real-world data.

@binh-dam-ibigroup
Copy link
Contributor

Binh and I had a private chat and we came to conclusion that we will keep the current rule priority logic as is. We are aware that the spec is not precise enough and you could definitely produce conflicting data when combining priorities and timeframes.

However, CTRAN, the only feed that we know of that produces timeframes, handles this problem by producing non-overlapping timeframes. We might have to revisit this when we see this problem re-appear in real-world data.

After our discussion, it should be noted that in order for the timeframe_free_sundays_afternoons Sundays rule to hide the regular cash fare in the example I provided, timeframes for the other Sunday hours must be provided, and those other timeframes must be also referenced in fare_leg_rules.txt. Leg rules that don't have timeframes listed will always apply.

timeframes.txt:

timeframe_group_id,start_time,end_time,service_id
timeframe_regular,00:00:00,11:59:59,FallSunday
timeframe_free_sundays_afternoons,12:00:00,18:00:00,FallSunday
timeframe_regular,18:00:01,23:59:59,FallSunday

fare_leg_rules.txt:

leg_group_id,network_id,from_area_id,to_area_id,fare_product_id,from_timeframe_group_id,to_timeframe_group_id,transfer_only
leg_rapid_transit_cash,rapid_transit,,,prod_rapid_transit_cash,timeframe_regular,,1
leg_rapid_transit_cash,rapid_transit,,,prod_free_fare_sundays_afternoons_cash,timeframe_free_sundays_afternoons,,1
leg_rapid_transit_quick_subway,rapid_transit,,,prod_rapid_transit_quick_subway,,

Copy link
Contributor

@binh-dam-ibigroup binh-dam-ibigroup left a comment

Choose a reason for hiding this comment

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

The basic logic works now, as we discussed. Thanks for the changes.

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

Labels

+Bump Serialization Id Add this label if you want the serialization id automatically bumped after merging the PR +Sandbox This will be implemented as a Sandbox feature +Skip Changelog This is not a relevant change for a product owner since last release.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants