-
Notifications
You must be signed in to change notification settings - Fork 101
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
feat: pickup and drop off window validator #1935
Open
cka-y
wants to merge
5
commits into
master
Choose a base branch
from
feat/1816
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
176 changes: 176 additions & 0 deletions
176
.../src/main/java/org/mobilitydata/gtfsvalidator/validator/PickupDropOffWindowValidator.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,176 @@ | ||
/* | ||
* 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 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; | ||
|
||
/** | ||
* Validates pickup and drop-off windows in the `stop_times.txt` file to ensure compliance with GTFS | ||
* rules. | ||
* | ||
* <p>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. | ||
* | ||
* <p>Generated notices include: - {@link ForbiddenArrivalOrDepartureTimeNotice} - {@link | ||
* MissingPickupOrDropOffWindowNotice} - {@link InvalidPickupDropOffWindowNotice} | ||
*/ | ||
@GtfsValidator | ||
public class PickupDropOffWindowValidator extends SingleEntityValidator<GtfsStopTime> { | ||
|
||
@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()) { | ||
noticeContainer.addValidationNotice( | ||
new ForbiddenArrivalOrDepartureTimeNotice( | ||
stopTime.csvRowNumber(), | ||
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())); | ||
} | ||
} | ||
|
||
@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); | ||
} | ||
|
||
/** | ||
* The arrival or departure times are provided alongside pickup or drop-off windows in | ||
* `stop_times.txt`. | ||
* | ||
* <p>This violates GTFS specification, as both cannot coexist for a single stop time record. | ||
*/ | ||
@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; | ||
} | ||
} | ||
|
||
/** | ||
* Either the start or end pickup/drop-off window is missing in `stop_times.txt`. | ||
* | ||
* <p>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. */ | ||
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; | ||
} | ||
} | ||
|
||
/** | ||
* The pickup/drop-off window in `stop_times.txt` is invalid. | ||
* | ||
* <p>The `end_pickup_drop_off_window` must be strictly later than the | ||
* `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; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
81 changes: 81 additions & 0 deletions
81
.../test/java/org/mobilitydata/gtfsvalidator/validator/PickupDropOffWindowValidatorTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
package org.mobilitydata.gtfsvalidator.validator; | ||
|
||
import static com.google.common.truth.Truth.assertThat; | ||
|
||
import org.junit.Test; | ||
import org.junit.runner.RunWith; | ||
import org.junit.runners.JUnit4; | ||
import org.mobilitydata.gtfsvalidator.notice.NoticeContainer; | ||
import org.mobilitydata.gtfsvalidator.table.GtfsStopTime; | ||
import org.mobilitydata.gtfsvalidator.type.GtfsTime; | ||
|
||
@RunWith(JUnit4.class) | ||
public class PickupDropOffWindowValidatorTest { | ||
@Test | ||
public void shouldGenerateForbiddenArrivalOrDepartureTimeNotice() { | ||
NoticeContainer noticeContainer = new NoticeContainer(); | ||
PickupDropOffWindowValidator validator = new PickupDropOffWindowValidator(); | ||
|
||
GtfsStopTime stopTime = | ||
new GtfsStopTime.Builder() | ||
.setCsvRowNumber(1) | ||
.setArrivalTime(GtfsTime.fromString("00:00:00")) | ||
.setDepartureTime(GtfsTime.fromString("00:00:01")) | ||
.setStartPickupDropOffWindow(GtfsTime.fromString("00:00:02")) | ||
.setEndPickupDropOffWindow(GtfsTime.fromString("00:00:03")) | ||
.build(); | ||
validator.validate(stopTime, noticeContainer); | ||
assertThat(noticeContainer.getValidationNotices()).hasSize(1); | ||
assertThat(noticeContainer.getValidationNotices().stream().findFirst().get()) | ||
.isInstanceOf(PickupDropOffWindowValidator.ForbiddenArrivalOrDepartureTimeNotice.class); | ||
} | ||
|
||
@Test | ||
public void shouldGenerateMissingPickupOrDropOffWindowNotice_missingStart() { | ||
NoticeContainer noticeContainer = new NoticeContainer(); | ||
PickupDropOffWindowValidator validator = new PickupDropOffWindowValidator(); | ||
|
||
GtfsStopTime stopTime = | ||
new GtfsStopTime.Builder() | ||
.setCsvRowNumber(1) | ||
.setEndPickupDropOffWindow(GtfsTime.fromString("00:00:03")) | ||
.build(); | ||
validator.validate(stopTime, noticeContainer); | ||
assertThat(noticeContainer.getValidationNotices()).hasSize(1); | ||
assertThat(noticeContainer.getValidationNotices().stream().findFirst().get()) | ||
.isInstanceOf(PickupDropOffWindowValidator.MissingPickupOrDropOffWindowNotice.class); | ||
} | ||
|
||
@Test | ||
public void shouldGenerateMissingPickupOrDropOffWindowNotice_missingEnd() { | ||
NoticeContainer noticeContainer = new NoticeContainer(); | ||
PickupDropOffWindowValidator validator = new PickupDropOffWindowValidator(); | ||
|
||
GtfsStopTime stopTime = | ||
new GtfsStopTime.Builder() | ||
.setCsvRowNumber(1) | ||
.setStartPickupDropOffWindow(GtfsTime.fromString("00:00:03")) | ||
.build(); | ||
validator.validate(stopTime, noticeContainer); | ||
assertThat(noticeContainer.getValidationNotices()).hasSize(1); | ||
assertThat(noticeContainer.getValidationNotices().stream().findFirst().get()) | ||
.isInstanceOf(PickupDropOffWindowValidator.MissingPickupOrDropOffWindowNotice.class); | ||
} | ||
|
||
@Test | ||
public void shouldGenerateInvalidPickupDropOffWindowNotice() { | ||
NoticeContainer noticeContainer = new NoticeContainer(); | ||
PickupDropOffWindowValidator validator = new PickupDropOffWindowValidator(); | ||
|
||
GtfsStopTime stopTime = | ||
new GtfsStopTime.Builder() | ||
.setCsvRowNumber(1) | ||
.setStartPickupDropOffWindow(GtfsTime.fromString("00:00:03")) | ||
.setEndPickupDropOffWindow(GtfsTime.fromString("00:00:02")) | ||
.build(); | ||
validator.validate(stopTime, noticeContainer); | ||
assertThat(noticeContainer.getValidationNotices()).hasSize(1); | ||
assertThat(noticeContainer.getValidationNotices().stream().findFirst().get()) | ||
.isInstanceOf(PickupDropOffWindowValidator.InvalidPickupDropOffWindowNotice.class); | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍