diff --git a/main/src/main/java/org/mobilitydata/gtfsvalidator/validator/PickupDropOffTypeValidator.java b/main/src/main/java/org/mobilitydata/gtfsvalidator/validator/PickupDropOffTypeValidator.java
new file mode 100644
index 0000000000..233c31bb42
--- /dev/null
+++ b/main/src/main/java/org/mobilitydata/gtfsvalidator/validator/PickupDropOffTypeValidator.java
@@ -0,0 +1,95 @@
+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.GtfsPickupDropOff;
+import org.mobilitydata.gtfsvalidator.table.GtfsStopTime;
+import org.mobilitydata.gtfsvalidator.type.GtfsTime;
+
+/**
+ * Validates that the `start_pickup_drop_off_window` or `end_pickup_drop_off_window` fields are not
+ * set when the `pickup_type` is regularly scheduled (0) or must be coordinated with the driver (3),
+ * and that these fields are not set when the `drop_off_type` is regularly scheduled (0).
+ *
+ *
Generated notices include: - {@link ForbiddenPickupTypeNotice} - {@link
+ * ForbiddenDropOffTypeNotice} if the `drop_off_type` is invalid.
+ */
+@GtfsValidator
+public class PickupDropOffTypeValidator extends SingleEntityValidator {
+ @Override
+ public void validate(GtfsStopTime entity, NoticeContainer noticeContainer) {
+ if ((entity.hasStartPickupDropOffWindow() || entity.hasEndPickupDropOffWindow())
+ && (entity.pickupType().equals(GtfsPickupDropOff.ALLOWED)
+ || entity.pickupType().equals(GtfsPickupDropOff.ON_REQUEST_TO_DRIVER))) {
+ noticeContainer.addValidationNotice(
+ new ForbiddenPickupTypeNotice(
+ entity.csvRowNumber(),
+ entity.startPickupDropOffWindow(),
+ entity.endPickupDropOffWindow()));
+ }
+
+ if ((entity.hasStartPickupDropOffWindow() || entity.hasEndPickupDropOffWindow())
+ && entity.dropOffType().equals(GtfsPickupDropOff.ALLOWED)) {
+ noticeContainer.addValidationNotice(
+ new ForbiddenDropOffTypeNotice(
+ entity.csvRowNumber(),
+ entity.startPickupDropOffWindow(),
+ entity.endPickupDropOffWindow()));
+ }
+ }
+
+ @Override
+ 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);
+ }
+
+ /**
+ * pickup_drop_off_window fields are forbidden when the pickup_type is regularly scheduled (0) or
+ * must be coordinated with the driver (3).
+ */
+ @GtfsValidationNotice(severity = ERROR)
+ public static class ForbiddenPickupTypeNotice 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 ForbiddenPickupTypeNotice(
+ int csvRowNumber, GtfsTime startPickupDropOffWindow, GtfsTime endPickupDropOffWindow) {
+ this.csvRowNumber = csvRowNumber;
+ this.startPickupDropOffWindow = startPickupDropOffWindow;
+ this.endPickupDropOffWindow = endPickupDropOffWindow;
+ }
+ }
+
+ /**
+ * pickup_drop_off_window fields are forbidden when the drop_off_type is regularly scheduled (0).
+ */
+ @GtfsValidationNotice(severity = ERROR)
+ public static class ForbiddenDropOffTypeNotice 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 ForbiddenDropOffTypeNotice(
+ int csvRowNumber, GtfsTime startPickupDropOffWindow, GtfsTime endPickupDropOffWindow) {
+ this.csvRowNumber = csvRowNumber;
+ this.startPickupDropOffWindow = startPickupDropOffWindow;
+ this.endPickupDropOffWindow = endPickupDropOffWindow;
+ }
+ }
+}
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",
diff --git a/main/src/test/java/org/mobilitydata/gtfsvalidator/validator/PickupDropOffTypeValidatorTest.java b/main/src/test/java/org/mobilitydata/gtfsvalidator/validator/PickupDropOffTypeValidatorTest.java
new file mode 100644
index 0000000000..23537fb844
--- /dev/null
+++ b/main/src/test/java/org/mobilitydata/gtfsvalidator/validator/PickupDropOffTypeValidatorTest.java
@@ -0,0 +1,76 @@
+package org.mobilitydata.gtfsvalidator.validator;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mobilitydata.gtfsvalidator.notice.NoticeContainer;
+import org.mobilitydata.gtfsvalidator.notice.ValidationNotice;
+import org.mobilitydata.gtfsvalidator.table.GtfsPickupDropOff;
+import org.mobilitydata.gtfsvalidator.table.GtfsStopTime;
+import org.mobilitydata.gtfsvalidator.type.GtfsTime;
+
+@RunWith(JUnit4.class)
+public class PickupDropOffTypeValidatorTest {
+ static PickupDropOffTypeValidator validator = new PickupDropOffTypeValidator();
+
+ private static List generateNotices(GtfsStopTime stopTime) {
+ NoticeContainer noticeContainer = new NoticeContainer();
+ validator.validate(stopTime, noticeContainer);
+ return noticeContainer.getValidationNotices();
+ }
+
+ @Test
+ public void forbiddenDropOffTypeShouldGenerateNotice() {
+ GtfsStopTime stopTime =
+ new GtfsStopTime.Builder()
+ .setCsvRowNumber(1)
+ .setPickupType(GtfsPickupDropOff.NOT_AVAILABLE)
+ .setDropOffType(GtfsPickupDropOff.ALLOWED)
+ .setStartPickupDropOffWindow(GtfsTime.fromString("00:00:02"))
+ .setEndPickupDropOffWindow(GtfsTime.fromString("00:00:03"))
+ .build();
+ assertThat(generateNotices(stopTime))
+ .containsExactly(
+ new PickupDropOffTypeValidator.ForbiddenDropOffTypeNotice(
+ 1, GtfsTime.fromString("00:00:02"), GtfsTime.fromString("00:00:03")));
+ }
+
+ @Test
+ public void allowedDropOffTypeShouldNotGenerateNotice() {
+ GtfsStopTime stopTime =
+ new GtfsStopTime.Builder()
+ .setCsvRowNumber(2)
+ .setDropOffType(GtfsPickupDropOff.NOT_AVAILABLE)
+ .build();
+ assertThat(generateNotices(stopTime)).isEmpty();
+ }
+
+ @Test
+ public void forbiddenPickupTypeShouldGenerateNotice() {
+ GtfsStopTime stopTime =
+ new GtfsStopTime.Builder()
+ .setCsvRowNumber(3)
+ .setPickupType(GtfsPickupDropOff.ALLOWED)
+ .setDropOffType(GtfsPickupDropOff.NOT_AVAILABLE)
+ .setStartPickupDropOffWindow(GtfsTime.fromString("08:00:00"))
+ .setEndPickupDropOffWindow(GtfsTime.fromString("09:00:00"))
+ .build();
+ assertThat(generateNotices(stopTime))
+ .containsExactly(
+ new PickupDropOffTypeValidator.ForbiddenPickupTypeNotice(
+ 3, GtfsTime.fromString("08:00:00"), GtfsTime.fromString("09:00:00")));
+ }
+
+ @Test
+ public void allowedPickupTypeShouldNotGenerateNotice() {
+ GtfsStopTime stopTime =
+ new GtfsStopTime.Builder()
+ .setCsvRowNumber(4)
+ .setPickupType(GtfsPickupDropOff.NOT_AVAILABLE)
+ .build();
+ assertThat(generateNotices(stopTime)).isEmpty();
+ }
+}