Skip to content

Commit af13b31

Browse files
authored
Added ServiceExtendsFarInTheFutureNotice (#2127)
1 parent 13b6b2f commit af13b31

3 files changed

Lines changed: 129 additions & 8 deletions

File tree

main/src/main/java/org/mobilitydata/gtfsvalidator/util/ServiceIntervalCache.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
*
2929
* <pre>{@code
3030
* @Inject
31-
* public ServiceGapValidator(ServiceIntervalCache cache) {
31+
* public ServiceSpreadValidator(ServiceIntervalCache cache) {
3232
* this.cache = cache;
3333
* }
3434
* }</pre>

main/src/main/java/org/mobilitydata/gtfsvalidator/validator/ServiceGapValidator.java renamed to main/src/main/java/org/mobilitydata/gtfsvalidator/validator/ServiceSpreadValidator.java

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import org.mobilitydata.gtfsvalidator.annotation.GtfsValidationNotice;
2424
import org.mobilitydata.gtfsvalidator.annotation.GtfsValidationNotice.FileRefs;
2525
import org.mobilitydata.gtfsvalidator.annotation.GtfsValidator;
26+
import org.mobilitydata.gtfsvalidator.input.DateForValidation;
2627
import org.mobilitydata.gtfsvalidator.notice.NoticeContainer;
2728
import org.mobilitydata.gtfsvalidator.notice.ValidationNotice;
2829
import org.mobilitydata.gtfsvalidator.table.GtfsCalendarDateSchema;
@@ -34,29 +35,35 @@
3435
import org.mobilitydata.gtfsvalidator.util.ServiceIntervalCache;
3536

3637
/**
37-
* Validates that no service has a gap of more than 13 days between active service dates.
38+
* Validates data related to the calendar spread of a service. Checks that no service has a gap of
39+
* more than 13 days between active service dates. Also checks the end date of a service is too far
40+
* in the future.
3841
*
3942
* <p>A gap is defined as a period of inactivity between two consecutive active intervals within the
4043
* overall service range. Dates before the first active day or after the last are not considered
4144
* gaps.
4245
*
43-
* <p>Generated notice: {@link BigGapInServiceNotice}.
46+
* <p>Generated notices: {@link BigGapInServiceNotice}, {@link ServiceExtendsFarInTheFutureNotice}
4447
*/
4548
@GtfsValidator
46-
public class ServiceGapValidator extends FileValidator {
49+
public class ServiceSpreadValidator extends FileValidator {
4750

4851
private static final int MAX_GAP_DAYS = 13;
52+
private static final int MAX_FUTURE_EXTENT_DAYS = 2 * 365;
4953

5054
private final ServiceIntervalCache serviceIntervalCache;
55+
private final DateForValidation dateForValidation;
5156
private final GtfsCalendarTableContainer calendarTableContainer;
5257
private final GtfsCalendarDateTableContainer calendarDateTableContainer;
5358

5459
@Inject
55-
ServiceGapValidator(
60+
ServiceSpreadValidator(
5661
ServiceIntervalCache serviceIntervalCache,
62+
DateForValidation dateForValidation,
5763
GtfsCalendarTableContainer calendarTableContainer,
5864
GtfsCalendarDateTableContainer calendarDateTableContainer) {
5965
this.serviceIntervalCache = serviceIntervalCache;
66+
this.dateForValidation = dateForValidation;
6067
this.calendarTableContainer = calendarTableContainer;
6168
this.calendarDateTableContainer = calendarDateTableContainer;
6269
}
@@ -84,6 +91,13 @@ public void validate(NoticeContainer noticeContainer) {
8491
gap.lengthInDays()));
8592
}
8693
}
94+
LocalDate lastActiveDate = intervals.lastActiveDate();
95+
LocalDate now = dateForValidation.getDate();
96+
int diff = (int) (lastActiveDate.toEpochDay() - now.toEpochDay());
97+
if (diff > MAX_FUTURE_EXTENT_DAYS) {
98+
noticeContainer.addValidationNotice(
99+
new ServiceExtendsFarInTheFutureNotice(serviceId, lastActiveDate));
100+
}
87101
});
88102
}
89103

@@ -113,4 +127,22 @@ static class BigGapInServiceNotice extends ValidationNotice {
113127
this.gapDurationDays = gapDurationDays;
114128
}
115129
}
130+
131+
/** A service end date is more than 2 years in the future. */
132+
@GtfsValidationNotice(
133+
severity = INFO,
134+
files = @FileRefs({GtfsCalendarSchema.class, GtfsCalendarDateSchema.class}))
135+
static class ServiceExtendsFarInTheFutureNotice extends ValidationNotice {
136+
137+
/** The service_id that ends far in the future. */
138+
private final String serviceId;
139+
140+
/** The end date of the service (YYYY-MM-DD format). */
141+
private final String serviceWindowEndDate;
142+
143+
ServiceExtendsFarInTheFutureNotice(String serviceId, LocalDate serviceWindowEndDate) {
144+
this.serviceId = serviceId;
145+
this.serviceWindowEndDate = serviceWindowEndDate.toString();
146+
}
147+
}
116148
}

main/src/test/java/org/mobilitydata/gtfsvalidator/validator/ServiceGapValidatorTest.java renamed to main/src/test/java/org/mobilitydata/gtfsvalidator/validator/ServiceSpreadValidatorTest.java

Lines changed: 92 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import java.time.LocalDate;
77
import java.util.List;
88
import org.junit.Test;
9+
import org.mobilitydata.gtfsvalidator.input.DateForValidation;
910
import org.mobilitydata.gtfsvalidator.notice.NoticeContainer;
1011
import org.mobilitydata.gtfsvalidator.notice.ValidationNotice;
1112
import org.mobilitydata.gtfsvalidator.table.GtfsCalendar;
@@ -15,9 +16,10 @@
1516
import org.mobilitydata.gtfsvalidator.table.GtfsCalendarTableContainer;
1617
import org.mobilitydata.gtfsvalidator.type.GtfsDate;
1718
import org.mobilitydata.gtfsvalidator.util.ServiceIntervalCache;
18-
import org.mobilitydata.gtfsvalidator.validator.ServiceGapValidator.BigGapInServiceNotice;
19+
import org.mobilitydata.gtfsvalidator.validator.ServiceSpreadValidator.BigGapInServiceNotice;
20+
import org.mobilitydata.gtfsvalidator.validator.ServiceSpreadValidator.ServiceExtendsFarInTheFutureNotice;
1921

20-
public class ServiceGapValidatorTest {
22+
public class ServiceSpreadValidatorTest {
2123

2224
private record CalendarMetadata(
2325
String serviceId, String startDate, String endDate, boolean activeAllWeek) {}
@@ -74,7 +76,9 @@ private static List<ValidationNotice> generateNotices(
7476
GtfsCalendarDateTableContainer calendarDateTable =
7577
GtfsCalendarDateTableContainer.forEntities(calendarDates, noticeContainer);
7678

77-
new ServiceGapValidator(new ServiceIntervalCache(), calendarTable, calendarDateTable)
79+
DateForValidation dateForValidation = new DateForValidation(LocalDate.of(2024, 1, 1));
80+
new ServiceSpreadValidator(
81+
new ServiceIntervalCache(), dateForValidation, calendarTable, calendarDateTable)
7882
.validate(noticeContainer);
7983

8084
return noticeContainer.getValidationNotices();
@@ -255,4 +259,89 @@ public void singleDayService_noNotice() {
255259
ImmutableList.of(new CalendarMetadata("service_1", "20240101", "20240101", true)));
256260
assertThat(notices).isEmpty();
257261
}
262+
263+
@Test
264+
public void serviceEndFarInFuture_calendarOnly_generatesNotice() {
265+
// calendar.txt defines a service ending more than 2 years after the validation date
266+
// (2024-01-01).
267+
// Here endDate is 2026-02-01 (> 2 * 365 days after 2024-01-01), so exactly one
268+
// ServiceExtendsFarInTheFutureNotice should be emitted. We assert that there is one and only
269+
// one such notice with the expected data, allowing for any additional notices of other types.
270+
List<ValidationNotice> notices =
271+
generateNotices(
272+
ImmutableList.of(
273+
new CalendarMetadata("service_future_cal", "20240101", "20260201", true)));
274+
275+
LocalDate end = LocalDate.of(2026, 2, 1);
276+
ValidationNotice expectedFutureNotice =
277+
new ServiceExtendsFarInTheFutureNotice("service_future_cal", end);
278+
279+
List<ServiceExtendsFarInTheFutureNotice> futureNotices =
280+
notices.stream()
281+
.filter(ServiceExtendsFarInTheFutureNotice.class::isInstance)
282+
.map(ServiceExtendsFarInTheFutureNotice.class::cast)
283+
.toList();
284+
285+
assertThat(futureNotices).containsExactly(expectedFutureNotice);
286+
}
287+
288+
@Test
289+
public void serviceEndFarInFuture_calendarDatesOnly_generatesNotice() {
290+
// No real range in calendar.txt (single day), service effectively defined by far future
291+
// SERVICE_ADDED in calendar_dates.txt.
292+
// Last added date is 2026-02-01 (> 2 years after 2024-01-01), so exactly one
293+
// ServiceExtendsFarInTheFutureNotice should be emitted for this service. There may also be
294+
// other notices of different types (e.g., BigGapInServiceNotice); we only assert that there
295+
// is one and only one future-extent notice with the expected data.
296+
List<ValidationNotice> notices =
297+
generateNotices(
298+
ImmutableList.of(
299+
new CalendarMetadata("service_future_cd", "20240101", "20240101", true)),
300+
ImmutableList.of(
301+
new CalendarDateMetadata(
302+
"service_future_cd", "20260201", GtfsCalendarDateExceptionType.SERVICE_ADDED)));
303+
304+
LocalDate end = LocalDate.of(2026, 2, 1);
305+
ValidationNotice expectedFutureNotice =
306+
new ServiceExtendsFarInTheFutureNotice("service_future_cd", end);
307+
308+
// Filter out only ServiceExtendsFarInTheFutureNotice instances.
309+
List<ServiceExtendsFarInTheFutureNotice> futureNotices =
310+
notices.stream()
311+
.filter(ServiceExtendsFarInTheFutureNotice.class::isInstance)
312+
.map(ServiceExtendsFarInTheFutureNotice.class::cast)
313+
.toList();
314+
315+
assertThat(futureNotices).containsExactly(expectedFutureNotice);
316+
}
317+
318+
@Test
319+
public void serviceEndFarInFuture_calendarAndCalendarDates_generatesNotice() {
320+
// calendar.txt defines regular service through 2025-01-01.
321+
// calendar_dates.txt adds a single far-future SERVICE_ADDED date in 2026. Exactly one
322+
// ServiceExtendsFarInTheFutureNotice should be emitted for this service. There may be other
323+
// notices (e.g., BigGapInServiceNotice); we only assert that there is one and only one
324+
// future-extent notice with the expected data.
325+
List<ValidationNotice> notices =
326+
generateNotices(
327+
ImmutableList.of(
328+
new CalendarMetadata("service_future_both", "20240101", "20250101", true)),
329+
ImmutableList.of(
330+
new CalendarDateMetadata(
331+
"service_future_both",
332+
"20260201",
333+
GtfsCalendarDateExceptionType.SERVICE_ADDED)));
334+
335+
LocalDate end = LocalDate.of(2026, 2, 1);
336+
ValidationNotice expectedFutureNotice =
337+
new ServiceExtendsFarInTheFutureNotice("service_future_both", end);
338+
339+
List<ServiceExtendsFarInTheFutureNotice> futureNotices =
340+
notices.stream()
341+
.filter(ServiceExtendsFarInTheFutureNotice.class::isInstance)
342+
.map(ServiceExtendsFarInTheFutureNotice.class::cast)
343+
.toList();
344+
345+
assertThat(futureNotices).containsExactly(expectedFutureNotice);
346+
}
258347
}

0 commit comments

Comments
 (0)