Skip to content

Commit 4a70dc2

Browse files
authored
feat: 2056 adding a info notice related to unsorted stop times (#2095)
1 parent 22e62eb commit 4a70dc2

File tree

3 files changed

+177
-1
lines changed

3 files changed

+177
-1
lines changed
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
package org.mobilitydata.gtfsvalidator.validator;
2+
3+
import static org.mobilitydata.gtfsvalidator.notice.SeverityLevel.INFO;
4+
5+
import java.util.HashMap;
6+
import java.util.HashSet;
7+
import java.util.Map;
8+
import java.util.Set;
9+
import javax.inject.Inject;
10+
import org.mobilitydata.gtfsvalidator.annotation.GtfsValidationNotice;
11+
import org.mobilitydata.gtfsvalidator.annotation.GtfsValidator;
12+
import org.mobilitydata.gtfsvalidator.notice.NoticeContainer;
13+
import org.mobilitydata.gtfsvalidator.notice.ValidationNotice;
14+
import org.mobilitydata.gtfsvalidator.table.*;
15+
16+
@GtfsValidator
17+
public class StopTimesTripBlockOrderValidator extends FileValidator {
18+
private final GtfsStopTimeTableContainer stopTimeTable;
19+
20+
private final Map<String, Integer> tripRowCount = new HashMap<>();
21+
private final Map<String, Integer> tripMinRow = new HashMap<>();
22+
private final Map<String, Integer> tripMaxRow = new HashMap<>();
23+
// last seen stop_sequence for a given trip (file order)
24+
private final Map<String, Integer> lastStopSequence = new HashMap<>();
25+
26+
// Ensure each kind of notice is emitted once per trip
27+
private final Set<String> contiguityNotified = new HashSet<>();
28+
private final Set<String> unsortedNotified = new HashSet<>();
29+
30+
@Inject
31+
StopTimesTripBlockOrderValidator(GtfsStopTimeTableContainer stopTimeTable) {
32+
this.stopTimeTable = stopTimeTable;
33+
}
34+
35+
@Override
36+
public void validate(NoticeContainer noticeContainer) {
37+
38+
// PASS 1 — Collect statistics
39+
for (GtfsStopTime stopTime : stopTimeTable.getEntities()) {
40+
if (stopTime == null) continue;
41+
42+
String tripId = stopTime.tripId();
43+
if (tripId == null) continue;
44+
45+
int rowNumber = stopTime.csvRowNumber();
46+
int stopSeq = stopTime.stopSequence();
47+
48+
int count = tripRowCount.getOrDefault(tripId, 0) + 1;
49+
tripRowCount.put(tripId, count);
50+
51+
int minRow = tripMinRow.getOrDefault(tripId, rowNumber);
52+
int maxRow = tripMaxRow.getOrDefault(tripId, rowNumber);
53+
54+
if (rowNumber < minRow) minRow = rowNumber;
55+
if (rowNumber > maxRow) maxRow = rowNumber;
56+
57+
tripMinRow.put(tripId, minRow);
58+
tripMaxRow.put(tripId, maxRow);
59+
60+
// Track stop_sequence ordering (file order)
61+
Integer last = lastStopSequence.get(tripId);
62+
if (last != null && stopSeq <= last) {
63+
unsortedNotified.add(tripId); // mark only, emit later
64+
}
65+
lastStopSequence.put(tripId, stopSeq);
66+
}
67+
68+
// PASS 2 — Validate + Emit Notices
69+
for (String tripId : tripRowCount.keySet()) {
70+
71+
int count = tripRowCount.get(tripId);
72+
int minRow = tripMinRow.get(tripId);
73+
int maxRow = tripMaxRow.get(tripId);
74+
75+
int span = maxRow - minRow + 1;
76+
boolean nonContiguous = span > count;
77+
boolean unsortedSequence = unsortedNotified.contains(tripId);
78+
79+
if (nonContiguous || unsortedSequence) {
80+
noticeContainer.addValidationNotice(new UnsortedStopTimesNotice(tripId, minRow, maxRow));
81+
}
82+
}
83+
}
84+
85+
/**
86+
* Stop times are not sorted by trip_id and stop_sequence.
87+
*
88+
* <p>'stop_times.txt' entries for a given trip are not sorted by stop_sequence, or are not
89+
* contiguous in the file.
90+
*/
91+
@GtfsValidationNotice(
92+
severity = INFO,
93+
files = @GtfsValidationNotice.FileRefs({GtfsStopTimeSchema.class}))
94+
static class UnsortedStopTimesNotice extends ValidationNotice {
95+
/** The faulty record's trip_id. */
96+
private final String tripId;
97+
98+
/** CSV row number of the first stop_times entry for this trip. */
99+
private final int startCsvRowNumber;
100+
101+
/** CSV row number of the last stop_times entry for this trip. */
102+
private final int endCsvRowNumber;
103+
104+
public UnsortedStopTimesNotice(String tripId, int startCsvRowNumber, int endCsvRowNumber) {
105+
this.tripId = tripId;
106+
this.startCsvRowNumber = startCsvRowNumber;
107+
this.endCsvRowNumber = endCsvRowNumber;
108+
}
109+
}
110+
}

main/src/test/java/org/mobilitydata/gtfsvalidator/validator/NoticeFieldsTest.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,9 @@ public void testNoticeClassFieldNames() {
231231
"riderCategoryId1",
232232
"riderCategoryId2",
233233
"currencyCode",
234-
"stopAccess");
234+
"stopAccess",
235+
"startCsvRowNumber",
236+
"endCsvRowNumber");
235237
}
236238

237239
private static List<String> discoverValidationNoticeFieldNames() {
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package org.mobilitydata.gtfsvalidator.validator;
2+
3+
import static org.junit.Assert.assertTrue;
4+
5+
import java.util.List;
6+
import org.junit.Test;
7+
import org.junit.runner.RunWith;
8+
import org.junit.runners.JUnit4;
9+
import org.mobilitydata.gtfsvalidator.notice.NoticeContainer;
10+
import org.mobilitydata.gtfsvalidator.notice.ValidationNotice;
11+
import org.mobilitydata.gtfsvalidator.table.*;
12+
13+
@RunWith(JUnit4.class)
14+
public class StopTimesTripBlockOrderValidatorTest {
15+
public static GtfsStopTime createStopTime(
16+
int csvRowNumber, String tripId, String stopId, int stopSequence) {
17+
var builder =
18+
new GtfsStopTime.Builder()
19+
.setCsvRowNumber(csvRowNumber)
20+
.setTripId(tripId)
21+
.setStopSequence(stopSequence);
22+
if (stopId != null) {
23+
builder.setStopId(stopId);
24+
}
25+
return builder.build();
26+
}
27+
28+
private static List<ValidationNotice> generateNotices(List<GtfsStopTime> stopTimes) {
29+
NoticeContainer noticeContainer = new NoticeContainer();
30+
new StopTimesTripBlockOrderValidator(
31+
GtfsStopTimeTableContainer.forEntities(stopTimes, noticeContainer))
32+
.validate(noticeContainer);
33+
return noticeContainer.getValidationNotices();
34+
}
35+
36+
@Test
37+
public void UnsortedStopTimesNotice_generateNotice_nonContiguousTripBlock() {
38+
// tripId 0914 appears, then 0915, then 0914 again: should trigger contiguity notice.
39+
var stopTimes =
40+
List.of(
41+
createStopTime(1, "0914", "S1", 1),
42+
createStopTime(2, "0914", "S2", 2),
43+
createStopTime(3, "0915", "S3", 1),
44+
createStopTime(4, "0914", "S4", 3));
45+
46+
assertTrue(
47+
generateNotices(stopTimes)
48+
.contains(new StopTimesTripBlockOrderValidator.UnsortedStopTimesNotice("0914", 1, 4)));
49+
}
50+
51+
@Test
52+
public void UnsortedStopTimesNotice_generateNotice_nonIncreasingStopSequence() {
53+
// tripId 0916 has stop_sequence 1, 3, 2 in file order: should trigger sequence notice.
54+
var stopTimes =
55+
List.of(
56+
createStopTime(1, "0916", "S1", 1),
57+
createStopTime(2, "0916", "S2", 3),
58+
createStopTime(3, "0916", "S3", 2));
59+
60+
assertTrue(
61+
generateNotices(stopTimes)
62+
.contains(new StopTimesTripBlockOrderValidator.UnsortedStopTimesNotice("0916", 1, 3)));
63+
}
64+
}

0 commit comments

Comments
 (0)