-
Notifications
You must be signed in to change notification settings - Fork 112
fix: fix logic to compute service window #2029
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
base: master
Are you sure you want to change the base?
Changes from all commits
fa5e622
8cec19f
1a60556
04f9e83
c381f84
bd4b3ff
635104b
121733a
e1d542c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,136 @@ | ||
| package org.mobilitydata.gtfsvalidator.reportsummary.model; | ||
|
|
||
| import static java.util.stream.Collectors.*; | ||
|
|
||
| import java.time.LocalDate; | ||
| import java.util.*; | ||
| import org.mobilitydata.gtfsvalidator.table.GtfsCalendar; | ||
| import org.mobilitydata.gtfsvalidator.table.GtfsCalendarDate; | ||
| import org.mobilitydata.gtfsvalidator.table.GtfsCalendarDateExceptionType; | ||
| import org.mobilitydata.gtfsvalidator.table.GtfsCalendarDateTableContainer; | ||
| import org.mobilitydata.gtfsvalidator.table.GtfsCalendarTableContainer; | ||
| import org.mobilitydata.gtfsvalidator.table.GtfsTripTableContainer; | ||
| import org.mobilitydata.gtfsvalidator.util.SetUtil; | ||
|
|
||
| record ServiceWindow(LocalDate startDate, LocalDate endDate) { | ||
| /** | ||
| * Given a list of calendars, get the earliest start date and latest end date for calendars | ||
| * associated with at least one trip. | ||
| * | ||
| * @return The service window if there's at least one calendar, and empty otherwise. | ||
| */ | ||
| static Optional<ServiceWindow> fromCalendars( | ||
| GtfsTripTableContainer tripTable, List<GtfsCalendar> allCalendars) { | ||
| List<GtfsCalendar> calendars = | ||
| allCalendars.stream() | ||
| .filter(calendar -> !tripTable.byServiceId(calendar.serviceId()).isEmpty()) | ||
| .toList(); | ||
|
|
||
| // Only empty if there are no calendars. | ||
| Optional<LocalDate> startDate = | ||
| calendars.stream().map(c -> c.startDate().getLocalDate()).min(LocalDate::compareTo); | ||
| Optional<LocalDate> endDate = | ||
| calendars.stream().map(c -> c.endDate().getLocalDate()).max(LocalDate::compareTo); | ||
| return startDate.map(d -> new ServiceWindow(d, endDate.get())); | ||
| } | ||
|
|
||
| /** | ||
| * Given a list of calendar dates, get the earliest and latest dates on which service is available | ||
| * for at least one route. | ||
| * | ||
| * @return The service window if there's at least one date on which service is available, and | ||
| * empty otherwise. | ||
| */ | ||
| static Optional<ServiceWindow> fromCalendarDates( | ||
| GtfsTripTableContainer tripTable, List<GtfsCalendarDate> allCalendarDates) { | ||
| List<LocalDate> calendarDates = | ||
| allCalendarDates.stream() | ||
| .filter( | ||
| d -> | ||
| d.exceptionType() == GtfsCalendarDateExceptionType.SERVICE_ADDED | ||
| && !tripTable.byServiceId(d.serviceId()).isEmpty()) | ||
| .map(d -> d.date().getLocalDate()) | ||
| .toList(); | ||
|
|
||
| // Only empty if there are no calendar dates. | ||
| Optional<LocalDate> startDate = calendarDates.stream().min(LocalDate::compareTo); | ||
| Optional<LocalDate> endDate = calendarDates.stream().max(LocalDate::compareTo); | ||
| return startDate.map(d -> new ServiceWindow(d, endDate.get())); | ||
| } | ||
|
|
||
| /** | ||
| * Given a list of calendars and a list of calendar dates, get the earliest start date and latest | ||
| * end date for calendars associated with at least one trip. Exceptions are only taken into | ||
| * account if they apply to _all_ services. | ||
| * | ||
| * @return The service window if there's at least one calendar, and empty otherwise. | ||
| */ | ||
| static Optional<ServiceWindow> fromCalendarsAndCalendarDates( | ||
| GtfsTripTableContainer tripTable, | ||
| List<GtfsCalendar> calendars, | ||
| List<GtfsCalendarDate> calendarDates) { | ||
| Optional<ServiceWindow> serviceWindowFromCalendars = | ||
| ServiceWindow.fromCalendars(tripTable, calendars); | ||
| if (serviceWindowFromCalendars.isEmpty()) { | ||
| return Optional.empty(); | ||
| } | ||
|
|
||
| Map<String, Set<LocalDate>> removedDaysByServiceId = | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @sylvansson @davidgamez one question, the function seems to create removedDaysByServiceId by using only GtfsCalendarDate (calendar_dates.txt). However, there might be services in So I think the way to check this is by making sure that for each removed date, it is removed for every service_id that figures either in Eg: calendar_dates.txt: The service window is 2-5-2025 to 31-5-2025 because:
I suggest that for each date, we count the total unique removed service_ids, then compare it with the count of unique service_ids that operate on that date (both from
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks, @skalexch. You are right. The spec doesn't force all service IDs to be defined in the calendar.txt and calendar_dates.txt when both files are defined. In this case, we can have service IDs from calendar.txt that are not present in the calendar_dates and will have a service on a day removed by a calendar_date entry. Following this logic, I agree that we need to keep a total of "all unique service IDs" per day, so we make sure that all services are not present to mark the date as "removed." The "all unique service IDs" can be computed by looking at the trips.txt file.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good catch, I'll try to get this fixed sometime next week.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Thanks! |
||
| calendarDates.stream() | ||
| .filter( | ||
| d -> | ||
| d.exceptionType() == GtfsCalendarDateExceptionType.SERVICE_REMOVED | ||
| && !tripTable.byServiceId(d.serviceId()).isEmpty()) | ||
| .collect( | ||
| groupingBy( | ||
| GtfsCalendarDate::serviceId, mapping(d -> d.date().getLocalDate(), toSet()))); | ||
|
|
||
| // We compute the set of days that are removed across all services in | ||
| // order to shift the start and end dates. | ||
| Set<LocalDate> removedDays = SetUtil.intersectAll(removedDaysByServiceId.values()); | ||
|
|
||
| LocalDate startDate = serviceWindowFromCalendars.get().startDate(); | ||
| LocalDate endDate = serviceWindowFromCalendars.get().endDate(); | ||
|
|
||
| while (removedDays.contains(startDate)) { | ||
| startDate = startDate.plusDays(1); | ||
| } | ||
| while (removedDays.contains(endDate)) { | ||
| endDate = endDate.minusDays(1); | ||
| } | ||
| return Optional.of(new ServiceWindow(startDate, endDate)); | ||
| } | ||
|
|
||
| /** | ||
| * Given a list of calendars and/or a list of calendar dates, get the earliest and latest dates | ||
| * where service is available for at least one route. | ||
| * | ||
| * @return The service window if there's at least one date on which service is available, and | ||
| * empty otherwise. | ||
| */ | ||
| static Optional<ServiceWindow> get( | ||
| GtfsTripTableContainer tripTable, | ||
| Optional<GtfsCalendarTableContainer> calendarTable, | ||
| Optional<GtfsCalendarDateTableContainer> calendarDateTable) { | ||
|
|
||
| Optional<List<GtfsCalendar>> calendars = | ||
| calendarTable.map(GtfsCalendarTableContainer::getEntities); | ||
| Optional<List<GtfsCalendarDate>> calendarDates = | ||
| calendarDateTable.map(GtfsCalendarDateTableContainer::getEntities); | ||
|
|
||
| if (calendarDates.isEmpty() && calendars.isPresent()) { | ||
| return ServiceWindow.fromCalendars(tripTable, calendars.get()); | ||
| } | ||
|
|
||
| if (calendarDates.isPresent() && calendars.isEmpty()) { | ||
| return ServiceWindow.fromCalendarDates(tripTable, calendarDates.get()); | ||
| } | ||
|
|
||
| if (calendars.isPresent() && calendarDates.isPresent()) { | ||
| return ServiceWindow.fromCalendarsAndCalendarDates( | ||
| tripTable, calendars.get(), calendarDates.get()); | ||
| } | ||
|
|
||
| return Optional.empty(); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| package org.mobilitydata.gtfsvalidator.util; | ||
|
|
||
| import static java.util.Collections.emptySet; | ||
|
|
||
| import java.util.Collection; | ||
| import java.util.HashSet; | ||
| import java.util.Set; | ||
|
|
||
| public class SetUtil { | ||
| /** | ||
| * Given a collection of sets, compute their intersection (the set of elements present in all of | ||
| * them). | ||
| */ | ||
| public static <T> Set<T> intersectAll(Collection<Set<T>> sets) { | ||
| if (sets.isEmpty()) { | ||
| return emptySet(); | ||
| } | ||
|
|
||
| Set<T> intersection = null; | ||
| for (Set<T> set : sets) { | ||
| if (intersection == null) { | ||
| intersection = new HashSet<>(set); | ||
| } else { | ||
| intersection.retainAll(set); | ||
| } | ||
| } | ||
| return intersection; | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.