diff --git a/RULES.md b/RULES.md index 3ef9211289..e52fdcf404 100644 --- a/RULES.md +++ b/RULES.md @@ -79,6 +79,8 @@ Each Notice is associated with a severity: `INFO`, `WARNING`, `ERROR`. | [`start_and_end_range_out_of_order`](#start_and_end_range_out_of_order) | Two date or time fields are out of order. | | [`station_with_parent_station`](#station_with_parent_station) | A station has `parent_station` field set. | | [`stop_time_timepoint_without_times`](#stop_time_timepoint_without_times) | `arrival_time` or `departure_time` not specified for timepoint. | +| [\`stop_time_trip_without_times_notice](#stop_time_trip_without_times) \| `arrival_time` or `departure_time` not specified for stop time trip. | | + | [`stop_time_with_arrival_before_previous_departure_time`](#stop_time_with_arrival_before_previous_departure_time) | Backwards time travel between stops in `stop_times.txt` | | [`stop_time_with_only_arrival_or_departure_time`](#stop_time_with_only_arrival_or_departure_time) | Missing `stop_times.arrival_time` or `stop_times.departure_time`. | | [`stop_without_location`](#stop_without_location) | `stop_lat` and/or `stop_lon` is missing for stop with `location_type` equal to`0`, `1`, or `2` | @@ -196,6 +198,32 @@ Trips with the same block id have overlapping stop times. + + +### stop_time_trip_without_times + +We can't generate the ERROR notice block_trips_with_overlapping_stop_times for the same trip because we are missing time information. + +#### References +* [stops.txt specification](http://gtfs.org/reference/static#stopstxt) +* [trips.txt specification](http://gtfs.org/reference/static#tripstxt) + +
+ +#### Notice fields description + +| Field name | Description | Type | +| ---------------- | --------------------------------------------------- | ------- | +| `csvRowNumber` | The row number from `stops.txt` of the faulty stop. | Long | +| `tripId` | The id of faulty trip. | String | +| `stopSequence` | The faulty record's `stop_times.stop_sequence`. | Integer | +| `specifiedField` | Name of the missing field. | String | + +#### Affected files +* [`stops.txt`](http://gtfs.org/reference/static#stopstxt) + +
+
### csv_parsing_failed diff --git a/main/src/main/java/org/mobilitydata/gtfsvalidator/validator/BlockTripsWithOverlappingStopTimesValidator.java b/main/src/main/java/org/mobilitydata/gtfsvalidator/validator/BlockTripsWithOverlappingStopTimesValidator.java index 01c3029d3a..e92371b66f 100644 --- a/main/src/main/java/org/mobilitydata/gtfsvalidator/validator/BlockTripsWithOverlappingStopTimesValidator.java +++ b/main/src/main/java/org/mobilitydata/gtfsvalidator/validator/BlockTripsWithOverlappingStopTimesValidator.java @@ -34,7 +34,8 @@ *

Both trips have to be operating on the same service day, as determined by comparing the * service dates of the trips. * - *

Generated notices: {@link BlockTripsWithOverlappingStopTimesNotice}. + *

Generated notices: {@link BlockTripsWithOverlappingStopTimesNotice}, {@link + * StopTimeTripWithoutTimesNotice}. */ @GtfsValidator public class BlockTripsWithOverlappingStopTimesValidator extends FileValidator { @@ -87,7 +88,8 @@ public void validate(NoticeContainer noticeContainer) { // properly judge trip overlap. for (GtfsTripOverlap overlap : findOverlapIntervals( - constructOrderedTripIntervals(tripsInBlock), serviceIdIntersectionCache)) { + constructOrderedTripIntervals(tripsInBlock, noticeContainer), + serviceIdIntersectionCache)) { final GtfsTrip tripA = overlap.getTripA(); final GtfsTrip tripB = overlap.getTripB(); noticeContainer.addValidationNotice( @@ -106,7 +108,8 @@ public void validate(NoticeContainer noticeContainer) { * *

Intervals are sorted by increasing first-arrival times, and then last-departure time. */ - private List constructOrderedTripIntervals(List tripsInBlock) { + private List constructOrderedTripIntervals( + List tripsInBlock, NoticeContainer noticeContainer) { ArrayList intervals = new ArrayList<>(); intervals.ensureCapacity(tripsInBlock.size()); for (GtfsTrip trip : tripsInBlock) { @@ -117,10 +120,22 @@ private List constructOrderedTripIntervals(List trip } GtfsStopTime firstStopTime = stopTimes.get(0); GtfsStopTime lastStopTime = stopTimes.get(stopTimes.size() - 1); + if (!firstStopTime.hasArrivalTime() || !firstStopTime.hasDepartureTime() || !lastStopTime.hasArrivalTime() || !lastStopTime.hasDepartureTime()) { + + for (var stop : new GtfsStopTime[] {firstStopTime, lastStopTime}) { + if (!stop.hasArrivalTime()) { + noticeContainer.addValidationNotice( + new StopTimeTripWithoutTimesNotice(stop, GtfsStopTime.ARRIVAL_TIME_FIELD_NAME)); + } + if (!stop.hasDepartureTime()) { + noticeContainer.addValidationNotice( + new StopTimeTripWithoutTimesNotice(stop, GtfsStopTime.DEPARTURE_TIME_FIELD_NAME)); + } + } continue; } intervals.add( @@ -306,4 +321,30 @@ static class BlockTripsWithOverlappingStopTimesNotice extends ValidationNotice { this.intersection = intersection; } } + + /** + * We can't generate the ERROR notice block_trips_with_overlapping_stop_times for the same trip + * because we are missing time information + * + *

Severity: {@code SeverityLevel.ERROR} + */ + @GtfsValidationNotice(severity = ERROR) + static class StopTimeTripWithoutTimesNotice extends ValidationNotice { + /** The row number from `stops.txt` of the faulty stop. */ + private final int csvRowNumber; + /** The id of faulty trip. */ + private final String tripId; + /** The faulty record's `stop_times.stop_sequence`. */ + private final long stopSequence; + /** Name of the missing field. */ + private final String specifiedField; + + StopTimeTripWithoutTimesNotice(GtfsStopTime stopTime, String specifiedField) { + super(ERROR); + this.csvRowNumber = stopTime.csvRowNumber(); + this.tripId = stopTime.tripId(); + this.stopSequence = stopTime.stopSequence(); + this.specifiedField = specifiedField; + } + } } diff --git a/main/src/test/java/org/mobilitydata/gtfsvalidator/validator/BlockTripsWithOverlappingStopTimesValidatorTest.java b/main/src/test/java/org/mobilitydata/gtfsvalidator/validator/BlockTripsWithOverlappingStopTimesValidatorTest.java index 269c54d242..89ff5acad5 100644 --- a/main/src/test/java/org/mobilitydata/gtfsvalidator/validator/BlockTripsWithOverlappingStopTimesValidatorTest.java +++ b/main/src/test/java/org/mobilitydata/gtfsvalidator/validator/BlockTripsWithOverlappingStopTimesValidatorTest.java @@ -27,6 +27,7 @@ import org.mobilitydata.gtfsvalidator.type.GtfsTime; import org.mobilitydata.gtfsvalidator.util.CalendarUtilTest; import org.mobilitydata.gtfsvalidator.validator.BlockTripsWithOverlappingStopTimesValidator.BlockTripsWithOverlappingStopTimesNotice; +import org.mobilitydata.gtfsvalidator.validator.BlockTripsWithOverlappingStopTimesValidator.StopTimeTripWithoutTimesNotice; @RunWith(JUnit4.class) public class BlockTripsWithOverlappingStopTimesValidatorTest { @@ -110,6 +111,23 @@ private static List createStopTimeTable( return stopTimes; } + private static GtfsStopTime createStopTime( + int csvRowNumber, + String tripId, + GtfsTime arrivalTime, + GtfsTime departureTime, + String stopId, + int stopSequence) { + return new GtfsStopTime.Builder() + .setCsvRowNumber(csvRowNumber) + .setTripId(tripId) + .setArrivalTime(arrivalTime) + .setDepartureTime(departureTime) + .setStopSequence(stopSequence) + .setStopId(stopId) + .build(); + } + private static List generateNotices( List calendars, List calendarDates, @@ -209,4 +227,60 @@ public void tripsWith0or1Stop() { new String[][] {new String[] {"08:00:00"}}))) .isEmpty(); } + + @Test + public void stopTimeMissingArrivalTime() { + List trips = createTripTable(new String[] {"t0"}, new String[] {"WEEK"}, "b1"); + + List stopTimes = new ArrayList<>(); + stopTimes.add( + createStopTime( + 1, + "t0", + null, // arrival time + GtfsTime.fromSecondsSinceMidnight(518), // departure time + "s0", + 1)); + stopTimes.add( + createStopTime( + 2, + "t0", + GtfsTime.fromSecondsSinceMidnight(1418), // arrival time + GtfsTime.fromSecondsSinceMidnight(1518), // departure time + "s1", + 2)); + + assertThat(generateNotices(createCalendarTable(), ImmutableList.of(), trips, stopTimes)) + .containsExactly( + new StopTimeTripWithoutTimesNotice( + stopTimes.get(0), GtfsStopTime.ARRIVAL_TIME_FIELD_NAME)); + } + + @Test + public void stopTimeMissingDepartureTime() { + List trips = createTripTable(new String[] {"t0"}, new String[] {"WEEK"}, "b1"); + + List stopTimes = new ArrayList<>(); + stopTimes.add( + createStopTime( + 1, + "t0", + GtfsTime.fromSecondsSinceMidnight(518), // arrival time + null, // Departure Time + "s0", + 1)); + stopTimes.add( + createStopTime( + 2, + "t0", + GtfsTime.fromSecondsSinceMidnight(1418), // arrival time + GtfsTime.fromSecondsSinceMidnight(1518), // departure time + "s1", + 2)); + + assertThat(generateNotices(createCalendarTable(), ImmutableList.of(), trips, stopTimes)) + .containsExactly( + new StopTimeTripWithoutTimesNotice( + stopTimes.get(0), GtfsStopTime.DEPARTURE_TIME_FIELD_NAME)); + } }