From 6185a8bb13e9a21c60081494413a922ea3146074 Mon Sep 17 00:00:00 2001 From: cka-y Date: Tue, 24 Dec 2024 11:40:50 -0500 Subject: [PATCH 1/5] feat: created class --- .../PickupDropOffWindowValidator.java | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 main/src/main/java/org/mobilitydata/gtfsvalidator/validator/PickupDropOffWindowValidator.java diff --git a/main/src/main/java/org/mobilitydata/gtfsvalidator/validator/PickupDropOffWindowValidator.java b/main/src/main/java/org/mobilitydata/gtfsvalidator/validator/PickupDropOffWindowValidator.java new file mode 100644 index 0000000000..f7ee7c7f74 --- /dev/null +++ b/main/src/main/java/org/mobilitydata/gtfsvalidator/validator/PickupDropOffWindowValidator.java @@ -0,0 +1,44 @@ +/* + * Copyright 2024 MobilityData + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.mobilitydata.gtfsvalidator.validator; + +import org.mobilitydata.gtfsvalidator.annotation.GtfsValidator; +import org.mobilitydata.gtfsvalidator.notice.NoticeContainer; +import org.mobilitydata.gtfsvalidator.table.GtfsStopTime; + + +/** + * TODO + * + *

Generated notice: TODO + */ +@GtfsValidator +public class PickupDropOffWindowValidator + extends SingleEntityValidator { + + @Override + public void validate(GtfsStopTime stopTime, NoticeContainer noticeContainer) { + // TODO: Implement this validator + } + + @Override + public boolean shouldCallValidate(ColumnInspector header) { + // No point in validating if there is no start_pickup_drop_off_window column + // and no end_pickup_drop_off_window column + return header.hasColumn(GtfsStopTime.START_PICKUP_DROP_OFF_WINDOW_FIELD_NAME) + || header.hasColumn(GtfsStopTime.END_PICKUP_DROP_OFF_WINDOW_FIELD_NAME); + } +} From a78887de9418b8549d2cf111c33986df1abbc9f0 Mon Sep 17 00:00:00 2001 From: cka-y Date: Tue, 24 Dec 2024 14:07:41 -0500 Subject: [PATCH 2/5] feat: pickup drop odd window validator --- .../report/HtmlReportGenerator.java | 26 ++++ .../PickupDropOffWindowValidator.java | 123 +++++++++++++++++- main/src/main/resources/report.html | 12 +- .../validator/NoticeFieldsTest.java | 2 + 4 files changed, 151 insertions(+), 12 deletions(-) diff --git a/main/src/main/java/org/mobilitydata/gtfsvalidator/report/HtmlReportGenerator.java b/main/src/main/java/org/mobilitydata/gtfsvalidator/report/HtmlReportGenerator.java index 3d72409b8a..5c482aebf2 100644 --- a/main/src/main/java/org/mobilitydata/gtfsvalidator/report/HtmlReportGenerator.java +++ b/main/src/main/java/org/mobilitydata/gtfsvalidator/report/HtmlReportGenerator.java @@ -19,8 +19,13 @@ import java.io.FileWriter; import java.io.IOException; import java.nio.file.Path; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; import org.mobilitydata.gtfsvalidator.notice.NoticeContainer; import org.mobilitydata.gtfsvalidator.report.model.FeedMetadata; +import org.mobilitydata.gtfsvalidator.report.model.NoticeView; import org.mobilitydata.gtfsvalidator.report.model.ReportSummary; import org.mobilitydata.gtfsvalidator.runner.ValidationRunnerConfig; import org.mobilitydata.gtfsvalidator.util.VersionInfo; @@ -56,9 +61,30 @@ public void generateReport( context.setVariable("config", config); context.setVariable("date", date); context.setVariable("is_different_date", is_different_date); + context.setVariable( + "uniqueFieldsByCode", + getUniqueFieldsForCodes( + summary.getNoticesMap().values().stream() + .flatMap(map -> map.entrySet().stream()) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)))); try (FileWriter writer = new FileWriter(reportPath.toFile())) { templateEngine.process("report.html", context, writer); } } + + private Map> getUniqueFieldsForCodes( + Map> noticesByCode) { + return noticesByCode.entrySet().stream() + .collect( + Collectors.toMap( + Map.Entry::getKey, // Notice code + entry -> { + // Find the notice with the most fields + return entry.getValue().stream() + .max(Comparator.comparingInt(notice -> notice.getFields().size())) + .map(NoticeView::getFields) // Extract fields from that notice + .orElse(List.of()); // Default to an empty list if no notices + })); + } } diff --git a/main/src/main/java/org/mobilitydata/gtfsvalidator/validator/PickupDropOffWindowValidator.java b/main/src/main/java/org/mobilitydata/gtfsvalidator/validator/PickupDropOffWindowValidator.java index f7ee7c7f74..e1bfd053ce 100644 --- a/main/src/main/java/org/mobilitydata/gtfsvalidator/validator/PickupDropOffWindowValidator.java +++ b/main/src/main/java/org/mobilitydata/gtfsvalidator/validator/PickupDropOffWindowValidator.java @@ -15,23 +15,53 @@ */ package org.mobilitydata.gtfsvalidator.validator; +import static org.mobilitydata.gtfsvalidator.notice.SeverityLevel.ERROR; + +import org.mobilitydata.gtfsvalidator.annotation.GtfsValidationNotice; import org.mobilitydata.gtfsvalidator.annotation.GtfsValidator; import org.mobilitydata.gtfsvalidator.notice.NoticeContainer; +import org.mobilitydata.gtfsvalidator.notice.ValidationNotice; import org.mobilitydata.gtfsvalidator.table.GtfsStopTime; - +import org.mobilitydata.gtfsvalidator.type.GtfsTime; /** - * TODO + * Validates the pickup and drop off windows in stop_times.txt. * - *

Generated notice: TODO + *

Generated notice: {@link ForbiddenArrivalOrDepartureTimeNotice}, {@link + * MissingPickupOrDropOffWindowNotice}, {@link InvalidPickupDropOffWindowNotice} */ @GtfsValidator -public class PickupDropOffWindowValidator - extends SingleEntityValidator { +public class PickupDropOffWindowValidator extends SingleEntityValidator { @Override public void validate(GtfsStopTime stopTime, NoticeContainer noticeContainer) { - // TODO: Implement this validator + if (stopTime.hasArrivalTime() || stopTime.hasDepartureTime()) { + // forbidden_arrival_or_departure_time + noticeContainer.addValidationNotice( + new ForbiddenArrivalOrDepartureTimeNotice( + stopTime.csvRowNumber(), + stopTime.arrivalTime(), + stopTime.departureTime(), + stopTime.startPickupDropOffWindow(), + stopTime.endPickupDropOffWindow())); + } + if (!stopTime.hasStartPickupDropOffWindow() || !stopTime.hasEndPickupDropOffWindow()) { + noticeContainer.addValidationNotice( + new MissingPickupOrDropOffWindowNotice( + stopTime.csvRowNumber(), + stopTime.startPickupDropOffWindow(), + stopTime.endPickupDropOffWindow())); + } + if (stopTime.hasStartPickupDropOffWindow() && stopTime.hasEndPickupDropOffWindow()) { + if (stopTime.startPickupDropOffWindow().isAfter(stopTime.endPickupDropOffWindow()) + || stopTime.startPickupDropOffWindow().equals(stopTime.endPickupDropOffWindow())) { + noticeContainer.addValidationNotice( + new InvalidPickupDropOffWindowNotice( + stopTime.csvRowNumber(), + stopTime.startPickupDropOffWindow(), + stopTime.endPickupDropOffWindow())); + } + } } @Override @@ -41,4 +71,85 @@ public boolean shouldCallValidate(ColumnInspector header) { return header.hasColumn(GtfsStopTime.START_PICKUP_DROP_OFF_WINDOW_FIELD_NAME) || header.hasColumn(GtfsStopTime.END_PICKUP_DROP_OFF_WINDOW_FIELD_NAME); } + + /** + * Arrival and departure times are forbidden in stop_times.txt when pickup and drop off windows + * are provided. + */ + @GtfsValidationNotice(severity = ERROR) + public static class ForbiddenArrivalOrDepartureTimeNotice extends ValidationNotice { + + /** The row of the faulty record. */ + private final int csvRowNumber; + + /** The arrival time of the faulty record. */ + private final GtfsTime arrivalTime; + + /** The departure time of the faulty record. */ + private final GtfsTime departureTime; + + /** The start pickup drop off window of the faulty record. */ + private final GtfsTime startPickupDropOffWindow; + + /** The end pickup drop off window of the faulty record. */ + private final GtfsTime endPickupDropOffWindow; + + public ForbiddenArrivalOrDepartureTimeNotice( + int csvRowNumber, + GtfsTime arrivalTime, + GtfsTime departureTime, + GtfsTime startPickupDropOffWindow, + GtfsTime endPickupDropOffWindow) { + this.csvRowNumber = csvRowNumber; + this.arrivalTime = arrivalTime; + this.departureTime = departureTime; + this.startPickupDropOffWindow = startPickupDropOffWindow; + this.endPickupDropOffWindow = endPickupDropOffWindow; + } + } + + /** Start or end pickup drop off window is missing in stop_times.txt. */ + @GtfsValidationNotice(severity = ERROR) + public static class MissingPickupOrDropOffWindowNotice extends ValidationNotice { + /** The row of the faulty record. */ + private final int csvRowNumber; + + /** The start pickup drop off window of the faulty record. */ + private final GtfsTime startPickupDropOffWindow; + + /** The end pickup drop off window of the faulty record. */ + private final GtfsTime endPickupDropOffWindow; + + public MissingPickupOrDropOffWindowNotice( + int csvRowNumber, GtfsTime startPickupDropOffWindow, GtfsTime endPickupDropOffWindow) { + this.csvRowNumber = csvRowNumber; + this.startPickupDropOffWindow = startPickupDropOffWindow; + this.endPickupDropOffWindow = endPickupDropOffWindow; + } + } + + /** + * Start or end pickup drop off window is invalid in stop_times.txt. + * + *

The value of `end_pickup_drop_off_window` must be strictly greater than the value of + * `start_pickup_drop_off_window`. + */ + @GtfsValidationNotice(severity = ERROR) + public static class InvalidPickupDropOffWindowNotice extends ValidationNotice { + /** The row of the faulty record. */ + private final int csvRowNumber; + + /** The start pickup drop off window of the faulty record. */ + private final GtfsTime startPickupDropOffWindow; + + /** The end pickup drop off window of the faulty record. */ + private final GtfsTime endPickupDropOffWindow; + + public InvalidPickupDropOffWindowNotice( + int csvRowNumber, GtfsTime startPickupDropOffWindow, GtfsTime endPickupDropOffWindow) { + this.csvRowNumber = csvRowNumber; + this.startPickupDropOffWindow = startPickupDropOffWindow; + this.endPickupDropOffWindow = endPickupDropOffWindow; + } + } } diff --git a/main/src/main/resources/report.html b/main/src/main/resources/report.html index f01ae6f30a..78c73bba71 100644 --- a/main/src/main/resources/report.html +++ b/main/src/main/resources/report.html @@ -368,9 +368,9 @@

- + - - - + +
- + (?) @@ -379,10 +379,10 @@

- +
+
diff --git a/main/src/test/java/org/mobilitydata/gtfsvalidator/validator/NoticeFieldsTest.java b/main/src/test/java/org/mobilitydata/gtfsvalidator/validator/NoticeFieldsTest.java index 6a1a55ae91..0507ec5147 100644 --- a/main/src/test/java/org/mobilitydata/gtfsvalidator/validator/NoticeFieldsTest.java +++ b/main/src/test/java/org/mobilitydata/gtfsvalidator/validator/NoticeFieldsTest.java @@ -80,6 +80,7 @@ public void testNoticeClassFieldNames() { "departureTime1", "distanceKm", "endFieldName", + "endPickupDropOffWindow", "endValue", "entityCount", "entityId", @@ -182,6 +183,7 @@ public void testNoticeClassFieldNames() { "specifiedField", "speedKph", "startFieldName", + "startPickupDropOffWindow", "startValue", "stopCsvRowNumber", "stopDesc", From 97fa89014cf51a49966629c300fba8a8266f1e2a Mon Sep 17 00:00:00 2001 From: cka-y Date: Tue, 24 Dec 2024 14:44:48 -0500 Subject: [PATCH 3/5] test: added coverage --- .../PickupDropOffWindowValidator.java | 4 + .../PickupDropOffWindowValidatorTest.java | 93 +++++++++++++++++++ 2 files changed, 97 insertions(+) create mode 100644 main/src/test/java/org/mobilitydata/gtfsvalidator/validator/PickupDropOffWindowValidatorTest.java diff --git a/main/src/main/java/org/mobilitydata/gtfsvalidator/validator/PickupDropOffWindowValidator.java b/main/src/main/java/org/mobilitydata/gtfsvalidator/validator/PickupDropOffWindowValidator.java index e1bfd053ce..7541a83234 100644 --- a/main/src/main/java/org/mobilitydata/gtfsvalidator/validator/PickupDropOffWindowValidator.java +++ b/main/src/main/java/org/mobilitydata/gtfsvalidator/validator/PickupDropOffWindowValidator.java @@ -35,6 +35,10 @@ public class PickupDropOffWindowValidator extends SingleEntityValidator Date: Tue, 24 Dec 2024 14:48:28 -0500 Subject: [PATCH 4/5] fix: GJF --- .../PickupDropOffWindowValidatorTest.java | 20 ++++--------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/main/src/test/java/org/mobilitydata/gtfsvalidator/validator/PickupDropOffWindowValidatorTest.java b/main/src/test/java/org/mobilitydata/gtfsvalidator/validator/PickupDropOffWindowValidatorTest.java index 844c47954e..722015190a 100644 --- a/main/src/test/java/org/mobilitydata/gtfsvalidator/validator/PickupDropOffWindowValidatorTest.java +++ b/main/src/test/java/org/mobilitydata/gtfsvalidator/validator/PickupDropOffWindowValidatorTest.java @@ -25,10 +25,7 @@ public void shouldGenerateForbiddenArrivalOrDepartureTimeNotice() { .setEndPickupDropOffWindow(GtfsTime.fromString("00:00:03")) .build(); validator.validate(stopTime, noticeContainer); - assertThat(noticeContainer.getValidationNotices()) - .hasSize( - 1); // - // assertThat(Optional.ofNullable(noticeContainer.getValidationNotices().stream().collect(Collectors.toList()).get(0))).isInstanceOf(PickupDropOffWindowValidator.ForbiddenArrivalOrDepartureTimeNotice.class); + assertThat(noticeContainer.getValidationNotices()).hasSize(1); assertThat(noticeContainer.getValidationNotices().stream().findFirst().get()) .isInstanceOf(PickupDropOffWindowValidator.ForbiddenArrivalOrDepartureTimeNotice.class); } @@ -44,10 +41,7 @@ public void shouldGenerateMissingPickupOrDropOffWindowNotice_missingStart() { .setEndPickupDropOffWindow(GtfsTime.fromString("00:00:03")) .build(); validator.validate(stopTime, noticeContainer); - assertThat(noticeContainer.getValidationNotices()) - .hasSize( - 1); // - // assertThat(Optional.ofNullable(noticeContainer.getValidationNotices().stream().collect(Collectors.toList()).get(0))).isInstanceOf(PickupDropOffWindowValidator.ForbiddenArrivalOrDepartureTimeNotice.class); + assertThat(noticeContainer.getValidationNotices()).hasSize(1); assertThat(noticeContainer.getValidationNotices().stream().findFirst().get()) .isInstanceOf(PickupDropOffWindowValidator.MissingPickupOrDropOffWindowNotice.class); } @@ -63,10 +57,7 @@ public void shouldGenerateMissingPickupOrDropOffWindowNotice_missingEnd() { .setStartPickupDropOffWindow(GtfsTime.fromString("00:00:03")) .build(); validator.validate(stopTime, noticeContainer); - assertThat(noticeContainer.getValidationNotices()) - .hasSize( - 1); // - // assertThat(Optional.ofNullable(noticeContainer.getValidationNotices().stream().collect(Collectors.toList()).get(0))).isInstanceOf(PickupDropOffWindowValidator.ForbiddenArrivalOrDepartureTimeNotice.class); + assertThat(noticeContainer.getValidationNotices()).hasSize(1); assertThat(noticeContainer.getValidationNotices().stream().findFirst().get()) .isInstanceOf(PickupDropOffWindowValidator.MissingPickupOrDropOffWindowNotice.class); } @@ -83,10 +74,7 @@ public void shouldGenerateInvalidPickupDropOffWindowNotice() { .setEndPickupDropOffWindow(GtfsTime.fromString("00:00:02")) .build(); validator.validate(stopTime, noticeContainer); - assertThat(noticeContainer.getValidationNotices()) - .hasSize( - 1); // - // assertThat(Optional.ofNullable(noticeContainer.getValidationNotices().stream().collect(Collectors.toList()).get(0))).isInstanceOf(PickupDropOffWindowValidator.ForbiddenArrivalOrDepartureTimeNotice.class); + assertThat(noticeContainer.getValidationNotices()).hasSize(1); assertThat(noticeContainer.getValidationNotices().stream().findFirst().get()) .isInstanceOf(PickupDropOffWindowValidator.InvalidPickupDropOffWindowNotice.class); } From e3927f60d9a9e238595e4da865debbf2d00d2a58 Mon Sep 17 00:00:00 2001 From: cka-y Date: Tue, 24 Dec 2024 15:51:37 -0500 Subject: [PATCH 5/5] fix: polish descriptions --- .../gtfsvalidator/notice/Notice.java | 10 +++ .../report/HtmlReportGenerator.java | 20 ++++-- .../report/model/NoticeView.java | 9 +++ .../PickupDropOffWindowValidator.java | 63 ++++++++++++------- 4 files changed, 73 insertions(+), 29 deletions(-) diff --git a/core/src/main/java/org/mobilitydata/gtfsvalidator/notice/Notice.java b/core/src/main/java/org/mobilitydata/gtfsvalidator/notice/Notice.java index ac7622b469..2433de5c85 100644 --- a/core/src/main/java/org/mobilitydata/gtfsvalidator/notice/Notice.java +++ b/core/src/main/java/org/mobilitydata/gtfsvalidator/notice/Notice.java @@ -25,7 +25,11 @@ import com.google.gson.JsonPrimitive; import com.google.gson.JsonSerializationContext; import com.google.gson.JsonSerializer; +import java.lang.reflect.Field; import java.lang.reflect.Type; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; import org.apache.commons.lang3.StringUtils; import org.mobilitydata.gtfsvalidator.type.GtfsColor; import org.mobilitydata.gtfsvalidator.type.GtfsDate; @@ -48,6 +52,12 @@ public JsonElement toJsonTree() { return GSON.toJsonTree(this); } + public List getAllFields() { + return Arrays.stream(this.getClass().getDeclaredFields()) + .map(Field::getName) // Extract the name of each field + .collect(Collectors.toList()); // Collect as a list of strings + } + /** * Returns a descriptive type-specific name for this notice based on the class simple name. * diff --git a/main/src/main/java/org/mobilitydata/gtfsvalidator/report/HtmlReportGenerator.java b/main/src/main/java/org/mobilitydata/gtfsvalidator/report/HtmlReportGenerator.java index 5c482aebf2..41452e1590 100644 --- a/main/src/main/java/org/mobilitydata/gtfsvalidator/report/HtmlReportGenerator.java +++ b/main/src/main/java/org/mobilitydata/gtfsvalidator/report/HtmlReportGenerator.java @@ -19,7 +19,7 @@ import java.io.FileWriter; import java.io.IOException; import java.nio.file.Path; -import java.util.Comparator; +import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @@ -80,11 +80,19 @@ private Map> getUniqueFieldsForCodes( Collectors.toMap( Map.Entry::getKey, // Notice code entry -> { - // Find the notice with the most fields - return entry.getValue().stream() - .max(Comparator.comparingInt(notice -> notice.getFields().size())) - .map(NoticeView::getFields) // Extract fields from that notice - .orElse(List.of()); // Default to an empty list if no notices + // Collect unique fields from all notices for this code + List uniqueFields = + entry.getValue().stream() + .flatMap(notice -> notice.getFields().stream()) + .distinct() + .collect(Collectors.toList()); + + // Start with all fields from the first notice and filter based on unique fields + List filteredFields = + new ArrayList<>(entry.getValue().get(0).getAllFields()); + filteredFields.removeIf(field -> !uniqueFields.contains(field)); + + return filteredFields; })); } } diff --git a/main/src/main/java/org/mobilitydata/gtfsvalidator/report/model/NoticeView.java b/main/src/main/java/org/mobilitydata/gtfsvalidator/report/model/NoticeView.java index b6047f0d90..d86c3431e2 100644 --- a/main/src/main/java/org/mobilitydata/gtfsvalidator/report/model/NoticeView.java +++ b/main/src/main/java/org/mobilitydata/gtfsvalidator/report/model/NoticeView.java @@ -97,4 +97,13 @@ public String getDescription() { public String getCode() { return notice.getContext().getCode(); } + + /** + * Returns a list of all fields in the notice. + * + * @return list of all fields in the notice. + */ + public List getAllFields() { + return notice.getContext().getAllFields(); + } } diff --git a/main/src/main/java/org/mobilitydata/gtfsvalidator/validator/PickupDropOffWindowValidator.java b/main/src/main/java/org/mobilitydata/gtfsvalidator/validator/PickupDropOffWindowValidator.java index 7541a83234..0bcb0dd807 100644 --- a/main/src/main/java/org/mobilitydata/gtfsvalidator/validator/PickupDropOffWindowValidator.java +++ b/main/src/main/java/org/mobilitydata/gtfsvalidator/validator/PickupDropOffWindowValidator.java @@ -25,47 +25,57 @@ import org.mobilitydata.gtfsvalidator.type.GtfsTime; /** - * Validates the pickup and drop off windows in stop_times.txt. + * Validates pickup and drop-off windows in the `stop_times.txt` file to ensure compliance with GTFS + * rules. * - *

Generated notice: {@link ForbiddenArrivalOrDepartureTimeNotice}, {@link - * MissingPickupOrDropOffWindowNotice}, {@link InvalidPickupDropOffWindowNotice} + *

This validator checks for: - Forbidden use of arrival or departure times when pickup or + * drop-off windows are provided. - Missing start or end pickup/drop-off windows when one of them is + * present. - Invalid pickup/drop-off windows where the end time is not strictly later than the + * start time. + * + *

Generated notices include: - {@link ForbiddenArrivalOrDepartureTimeNotice} - {@link + * MissingPickupOrDropOffWindowNotice} - {@link InvalidPickupDropOffWindowNotice} */ @GtfsValidator public class PickupDropOffWindowValidator extends SingleEntityValidator { @Override public void validate(GtfsStopTime stopTime, NoticeContainer noticeContainer) { + // Skip validation if neither start nor end pickup/drop-off window is present if (!stopTime.hasStartPickupDropOffWindow() && !stopTime.hasEndPickupDropOffWindow()) { return; } + // Check for forbidden coexistence of arrival/departure times with pickup/drop-off windows if (stopTime.hasArrivalTime() || stopTime.hasDepartureTime()) { - // forbidden_arrival_or_departure_time noticeContainer.addValidationNotice( new ForbiddenArrivalOrDepartureTimeNotice( stopTime.csvRowNumber(), - stopTime.arrivalTime(), - stopTime.departureTime(), - stopTime.startPickupDropOffWindow(), - stopTime.endPickupDropOffWindow())); + stopTime.hasArrivalTime() ? stopTime.arrivalTime() : null, + stopTime.hasDepartureTime() ? stopTime.departureTime() : null, + stopTime.hasStartPickupDropOffWindow() ? stopTime.startPickupDropOffWindow() : null, + stopTime.hasEndPickupDropOffWindow() ? stopTime.endPickupDropOffWindow() : null)); } + + // Check for missing start or end pickup/drop-off window if (!stopTime.hasStartPickupDropOffWindow() || !stopTime.hasEndPickupDropOffWindow()) { noticeContainer.addValidationNotice( new MissingPickupOrDropOffWindowNotice( + stopTime.csvRowNumber(), + stopTime.hasStartPickupDropOffWindow() ? stopTime.startPickupDropOffWindow() : null, + stopTime.hasEndPickupDropOffWindow() ? stopTime.endPickupDropOffWindow() : null)); + return; + } + + // Check for invalid pickup/drop-off window (start time must be strictly before end time) + if (stopTime.startPickupDropOffWindow().isAfter(stopTime.endPickupDropOffWindow()) + || stopTime.startPickupDropOffWindow().equals(stopTime.endPickupDropOffWindow())) { + noticeContainer.addValidationNotice( + new InvalidPickupDropOffWindowNotice( stopTime.csvRowNumber(), stopTime.startPickupDropOffWindow(), stopTime.endPickupDropOffWindow())); } - if (stopTime.hasStartPickupDropOffWindow() && stopTime.hasEndPickupDropOffWindow()) { - if (stopTime.startPickupDropOffWindow().isAfter(stopTime.endPickupDropOffWindow()) - || stopTime.startPickupDropOffWindow().equals(stopTime.endPickupDropOffWindow())) { - noticeContainer.addValidationNotice( - new InvalidPickupDropOffWindowNotice( - stopTime.csvRowNumber(), - stopTime.startPickupDropOffWindow(), - stopTime.endPickupDropOffWindow())); - } - } } @Override @@ -77,8 +87,10 @@ public boolean shouldCallValidate(ColumnInspector header) { } /** - * Arrival and departure times are forbidden in stop_times.txt when pickup and drop off windows - * are provided. + * The arrival or departure times are provided alongside pickup or drop-off windows in + * `stop_times.txt`. + * + *

This violates GTFS specification, as both cannot coexist for a single stop time record. */ @GtfsValidationNotice(severity = ERROR) public static class ForbiddenArrivalOrDepartureTimeNotice extends ValidationNotice { @@ -112,7 +124,12 @@ public ForbiddenArrivalOrDepartureTimeNotice( } } - /** Start or end pickup drop off window is missing in stop_times.txt. */ + /** + * Either the start or end pickup/drop-off window is missing in `stop_times.txt`. + * + *

GTFS specification requires both the start and end pickup/drop-off windows to be provided + * together, if used. + */ @GtfsValidationNotice(severity = ERROR) public static class MissingPickupOrDropOffWindowNotice extends ValidationNotice { /** The row of the faulty record. */ @@ -133,9 +150,9 @@ public MissingPickupOrDropOffWindowNotice( } /** - * Start or end pickup drop off window is invalid in stop_times.txt. + * The pickup/drop-off window in `stop_times.txt` is invalid. * - *

The value of `end_pickup_drop_off_window` must be strictly greater than the value of + *

The `end_pickup_drop_off_window` must be strictly later than the * `start_pickup_drop_off_window`. */ @GtfsValidationNotice(severity = ERROR)