Skip to content

[GTFS Fares v2] Semantics clarification#561

Merged
tzujenchanmbd merged 4 commits intogoogle:masterfrom
MobilityData:faresv2-clarification-2025May
Oct 14, 2025
Merged

[GTFS Fares v2] Semantics clarification#561
tzujenchanmbd merged 4 commits intogoogle:masterfrom
MobilityData:faresv2-clarification-2025May

Conversation

@tzujenchanmbd
Copy link
Collaborator

Based on recent discussions in the gtfs-fares Slack channel and working group, clarify some fares v2 related semantics including:

  • Introduce a Local Time data type to eliminate potential confusion caused by the existing Time data type in timeframes.txt.
  • Add "effective fare leg" to the general description of fare_transfer_rules.txt
  • Add "timer should start from the first matched leg" clarification

Tagging active working group members for review🙏
@felixguendling @westontrillium @jfabi @skinkie @halbertram @evansiroky @miklcct @jll01 @bdferris-v2

@tzujenchanmbd tzujenchanmbd added GTFS-Fares Issues and Pull Requests that focus on GTFS-Fares Extension Change: Clarification labels May 9, 2025
Copy link

@felixguendling felixguendling left a comment

Choose a reason for hiding this comment

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

Thank you for addressing this and improving the standard! 👍

To process the cost of a multi-leg journey:

1. The applicable fare leg groups defined in `fare_leg_rules.txt` should be determined for all individual legs of travel based on the rider’s journey.
1. The applicable fare leg groups defined in `fare_leg_rules.txt` should be determined for all individual legs or effective fare legs of travel based on the rider’s journey.

Choose a reason for hiding this comment

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

Suggested change
1. The applicable fare leg groups defined in `fare_leg_rules.txt` should be determined for all individual legs or effective fare legs of travel based on the rider’s journey.
1. The applicable fare leg groups defined in `fare_leg_rules.txt` should be determined for all effective fare legs of travel based on the rider’s journey.

Depending on how an "effective fare leg" is defined, it could also be an individual leg. It's a list of 1 to N joined legs. So always just referring to "effective fare leg" (without mentioning "individual legs") would be fine and would allow for a shorter documentation (which usually equals to better readability IMO).

Copy link
Contributor

Choose a reason for hiding this comment

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

Effective Fare Leg is currently defined as "A sub-journey of two or more legs" so indeed, this would need to change to support the semantics you've proposed. That said, I think I tend to lean towards the status-quo definition. I think there is value in distinguishing between an individual leg and an effective leg in the spec, as it usually more directly highlights in the text where you need to thing about how an individual leg vs effective leg impacts semantics.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I will keep both individual leg and effect fare leg in the sentence at the moment.

| `transfer_count` | Non-zero integer | **Conditionally Forbidden** | Defines how many consecutive transfers the transfer rule may be applied to.<br><br>Valid options are:<br>`-1` - No limit.<br>`1` or more - Defines how many transfers the transfer rule may span.<br><br>If a sub-journey matches multiple records with different `transfer_count`s, then the rule with the minimum `transfer_count` that is greater than or equal to the current transfer count of the sub-journey is to be selected.<br><br>Conditionally Forbidden:<br>- **Forbidden** if `fare_transfer_rules.from_leg_group_id` does not equal `fare_transfer_rules.to_leg_group_id`.<br>- **Required** if `fare_transfer_rules.from_leg_group_id` equals `fare_transfer_rules.to_leg_group_id`. |
| `duration_limit` | Positive integer | Optional | Defines the duration limit of the transfer.<br><br>Must be expressed in integer increments of seconds.<br><br>If there is no duration limit, `fare_transfer_rules.duration_limit` must be empty. |
| `duration_limit_type` | Enum | **Conditionally Required** | Defines the relative start and end of `fare_transfer_rules.duration_limit`.<br><br>Valid options are:<br>`0` - Between the departure fare validation of the current leg and the arrival fare validation of the next leg.<br>`1` - Between the departure fare validation of the current leg and the departure fare validation of the next leg.<br>`2` - Between the arrival fare validation of the current leg and the departure fare validation of the next leg.<br>`3` - Between the arrival fare validation of the current leg and the arrival fare validation of the next leg.<br><br>Conditionally Required:<br>- **Required** if `fare_transfer_rules.duration_limit` is defined.<br>- **Forbidden** if `fare_transfer_rules.duration_limit` is empty. |
| `duration_limit_type` | Enum | **Conditionally Required** | Defines the relative start and end of `fare_transfer_rules.duration_limit`.<br><br>Valid options are:<br>`0` - Between the departure fare validation of the current leg and the arrival fare validation of the next leg.<br>`1` - Between the departure fare validation of the current leg and the departure fare validation of the next leg.<br>`2` - Between the arrival fare validation of the current leg and the departure fare validation of the next leg.<br>`3` - Between the arrival fare validation of the current leg and the arrival fare validation of the next leg.<br><br>When a transfer rule with the same `from_leg_group_id` and `to_leg_group_id` is matched multiple times consecutively within a multi-leg journey, the `duration_limit` specified by the rule should be measured starting from the first matched leg.<br><br>Conditionally Required:<br>- **Required** if `fare_transfer_rules.duration_limit` is defined.<br>- **Forbidden** if `fare_transfer_rules.duration_limit` is empty. |
Copy link

@felixguendling felixguendling May 9, 2025

Choose a reason for hiding this comment

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

Suggested change
| `duration_limit_type` | Enum | **Conditionally Required** | Defines the relative start and end of `fare_transfer_rules.duration_limit`.<br><br>Valid options are:<br>`0` - Between the departure fare validation of the current leg and the arrival fare validation of the next leg.<br>`1` - Between the departure fare validation of the current leg and the departure fare validation of the next leg.<br>`2` - Between the arrival fare validation of the current leg and the departure fare validation of the next leg.<br>`3` - Between the arrival fare validation of the current leg and the arrival fare validation of the next leg.<br><br>When a transfer rule with the same `from_leg_group_id` and `to_leg_group_id` is matched multiple times consecutively within a multi-leg journey, the `duration_limit` specified by the rule should be measured starting from the first matched leg.<br><br>Conditionally Required:<br>- **Required** if `fare_transfer_rules.duration_limit` is defined.<br>- **Forbidden** if `fare_transfer_rules.duration_limit` is empty. |
| `duration_limit_type` | Enum | **Conditionally Required** | Defines the relative start and end of `fare_transfer_rules.duration_limit`.<br><br>Valid options are:<br>`0` - Between the departure fare validation of the first matched leg and the arrival fare validation of the last matched leg.<br>`1` - Between the departure fare validation of the first matched leg and the departure fare validation of the last matched leg.<br>`2` - Between the arrival fare validation of the first matched leg and the departure fare validation of the last matched leg.<br>`3` - Between the arrival fare validation of the first matched leg and the arrival fare validation of the last matched leg.<br><br>When a transfer rule with the same `from_leg_group_id` and `to_leg_group_id` is matched multiple times consecutively within a multi-leg journey, the `duration_limit` specified by the rule should be measured starting from the first matched leg.<br><br>Conditionally Required:<br>- **Required** if `fare_transfer_rules.duration_limit` is defined.<br>- **Forbidden** if `fare_transfer_rules.duration_limit` is empty. |

I would eliminate all occurrences of "current leg" and replace them with "first matched leg" to make it clear from the beginning.

Copy link
Contributor

Choose a reason for hiding this comment

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

I'd go further and replace both "current"+"next" with "first"+"last". "First matched leg" is ok but wondering if "first leg in transfer sub-journey" would clearer? (Not sure if I feel too strongly on that point)

Choose a reason for hiding this comment

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

I agree. For the duration, it only makes sense to measure time from the first to the last leg of the fare transfer. "current" and "next" are a bit confusing here.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Replaced "current"+"next" with "first"+"last" - 9ff1c18

| `service_id` | Foreign ID referencing `calendar.service_id` or `calendar_dates.service_id` | **Required** | Identifies a set of dates that a timeframe is in effect. |

#### Timeframe Local Time Semantics
- When evaluating a fare event’s time against [timeframes.txt](#timeframestxt), the event time is computed in local time using the local timezone, as determined by the `stop_timezone`, if specified, of the stop or parent station for the fare event. If not specified, the feed’s agency timezone should be used instead.

Choose a reason for hiding this comment

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

This would require that the feed has only one agency or at least all agencies to have the same timezone to be non-ambiguous in case there's no stop timezone.

Copy link
Contributor

Choose a reason for hiding this comment

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

The specification falls back to the agency time zone, so if there is no stop time zone, it should fallback to the actual agency used.

For example, if there are two agencies, A work on UTC+0, B work on UTC+1, and the current time is UTC 00:30 with a timeframe which start at 01:00 local time, my understanding is that it is in the timeframe if you use a service of B, but not a service of A.

Copy link
Contributor

Choose a reason for hiding this comment

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

Per the spec for agency_timezone, if multiple agencies are specified in the dataset, each must have the same agency_timezone. So there should only ever be a single "feed"-level timezone either way.

@eliasmbd eliasmbd added the Former Governance Applies This proposal is subject to the former governance process which predates July 7, 2025. label Jul 7, 2025
@eliasmbd eliasmbd added Change type: Non-Functional Refers to important updates to the specification that do not significantly affect functionalities. and removed Change: Clarification labels Jul 9, 2025
@tzujenchanmbd
Copy link
Collaborator Author

I am initiating a vote for this clarification. Voting ends on 2025-09-15 at 23:59:59 UTC.

@tzujenchanmbd tzujenchanmbd added the Vote to Adopt Community votes to officially adopt the change. label Sep 1, 2025
@felixguendling
Copy link

+1 MOTIS

@jll01
Copy link

jll01 commented Sep 15, 2025

+1 Transit

@bdferris-v2
Copy link
Contributor

+1 Google

@tzujenchanmbd
Copy link
Collaborator Author

Thanks for participating in the vote!

Since the third “+1” vote appears to have been cast a few hours after 2025-09-15 23:59:59 UTC. To avoid any potential governance-related disputes, I am reopening a vote here. Voting ends on 2025-10-06 at 23:59:59 UTC.

@felixguendling @jll01 @bdferris-v2 could you please cast your vote again? Thanks🙏

@felixguendling
Copy link

+1 MOTIS

@jll01
Copy link

jll01 commented Sep 22, 2025

+1 Transit

@bdferris-v2
Copy link
Contributor

+1 Google

@etienne0101 etienne0101 removed the Vote to Adopt Community votes to officially adopt the change. label Oct 8, 2025
@tzujenchanmbd
Copy link
Collaborator Author

The vote passed on 2025-10-06 at 23:59:59 UTC.

3 votes in favour and no votes against.

Votes came from:
MOTIS (@felixguendling)
Transit (@jll01)
Google (@bdferris-v2)

Thank you to everyone who participated!

@tzujenchanmbd tzujenchanmbd merged commit e5d1544 into google:master Oct 14, 2025
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Change type: Non-Functional Refers to important updates to the specification that do not significantly affect functionalities. Former Governance Applies This proposal is subject to the former governance process which predates July 7, 2025. GTFS-Fares Issues and Pull Requests that focus on GTFS-Fares Extension

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants