-
Notifications
You must be signed in to change notification settings - Fork 101
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Validation for timeframes.txt (#1518)
- Loading branch information
1 parent
d0cfd80
commit 784a664
Showing
9 changed files
with
700 additions
and
0 deletions.
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
51 changes: 51 additions & 0 deletions
51
main/src/main/java/org/mobilitydata/gtfsvalidator/table/GtfsTimeframeSchema.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,51 @@ | ||
/* | ||
* Copyright 2023 Google LLC | ||
* | ||
* 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.table; | ||
|
||
import static org.mobilitydata.gtfsvalidator.annotation.TranslationRecordIdType.UNSUPPORTED; | ||
|
||
import org.mobilitydata.gtfsvalidator.annotation.ConditionallyRequired; | ||
import org.mobilitydata.gtfsvalidator.annotation.EndRange; | ||
import org.mobilitydata.gtfsvalidator.annotation.FieldType; | ||
import org.mobilitydata.gtfsvalidator.annotation.FieldTypeEnum; | ||
import org.mobilitydata.gtfsvalidator.annotation.GtfsTable; | ||
import org.mobilitydata.gtfsvalidator.annotation.Index; | ||
import org.mobilitydata.gtfsvalidator.annotation.PrimaryKey; | ||
import org.mobilitydata.gtfsvalidator.annotation.Required; | ||
import org.mobilitydata.gtfsvalidator.type.GtfsTime; | ||
|
||
@GtfsTable("timeframes.txt") | ||
public interface GtfsTimeframeSchema extends GtfsEntity { | ||
|
||
@FieldType(FieldTypeEnum.ID) | ||
@PrimaryKey(translationRecordIdType = UNSUPPORTED) | ||
@Index | ||
String timeframeGroupId(); | ||
|
||
@PrimaryKey(translationRecordIdType = UNSUPPORTED) | ||
@ConditionallyRequired | ||
@EndRange(field = "end_time", allowEqual = false) | ||
GtfsTime startTime(); | ||
|
||
@PrimaryKey(translationRecordIdType = UNSUPPORTED) | ||
@ConditionallyRequired | ||
GtfsTime endTime(); | ||
|
||
@PrimaryKey(translationRecordIdType = UNSUPPORTED) | ||
@Required | ||
String serviceId(); | ||
} |
120 changes: 120 additions & 0 deletions
120
main/src/main/java/org/mobilitydata/gtfsvalidator/validator/TimeframeOverlapValidator.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,120 @@ | ||
package org.mobilitydata.gtfsvalidator.validator; | ||
|
||
import static org.mobilitydata.gtfsvalidator.notice.SeverityLevel.ERROR; | ||
|
||
import com.google.auto.value.AutoValue; | ||
import java.util.ArrayList; | ||
import java.util.Collections; | ||
import java.util.Comparator; | ||
import java.util.List; | ||
import java.util.Map; | ||
import java.util.stream.Collectors; | ||
import javax.inject.Inject; | ||
import org.mobilitydata.gtfsvalidator.annotation.GtfsValidationNotice; | ||
import org.mobilitydata.gtfsvalidator.annotation.GtfsValidationNotice.FileRefs; | ||
import org.mobilitydata.gtfsvalidator.annotation.GtfsValidator; | ||
import org.mobilitydata.gtfsvalidator.notice.NoticeContainer; | ||
import org.mobilitydata.gtfsvalidator.notice.ValidationNotice; | ||
import org.mobilitydata.gtfsvalidator.table.GtfsFrequencySchema; | ||
import org.mobilitydata.gtfsvalidator.table.GtfsTimeframe; | ||
import org.mobilitydata.gtfsvalidator.table.GtfsTimeframeTableContainer; | ||
import org.mobilitydata.gtfsvalidator.type.GtfsTime; | ||
|
||
/** | ||
* Validates that two entries from `timesframes.txt` with the same `timeframe_group_id` and | ||
* `service_id` do not have overlapping time intervals. | ||
*/ | ||
@GtfsValidator | ||
public class TimeframeOverlapValidator extends FileValidator { | ||
|
||
private final GtfsTimeframeTableContainer timeframeContainer; | ||
|
||
@Inject | ||
public TimeframeOverlapValidator(GtfsTimeframeTableContainer timeframeContainer) { | ||
this.timeframeContainer = timeframeContainer; | ||
} | ||
|
||
@Override | ||
public void validate(NoticeContainer noticeContainer) { | ||
Map<TimeframeKey, List<GtfsTimeframe>> timeframesByKey = | ||
timeframeContainer.getEntities().stream() | ||
.collect(Collectors.groupingBy(TimeframeKey::create, Collectors.toList())); | ||
for (Map.Entry<TimeframeKey, List<GtfsTimeframe>> entry : timeframesByKey.entrySet()) { | ||
List<GtfsTimeframe> timeframes = new ArrayList<>(entry.getValue()); | ||
Collections.sort( | ||
timeframes, | ||
Comparator.comparing(GtfsTimeframe::startTime).thenComparing(GtfsTimeframe::endTime)); | ||
for (int i = 1; i < timeframes.size(); ++i) { | ||
GtfsTimeframe prev = timeframes.get(i - 1); | ||
GtfsTimeframe curr = timeframes.get(i); | ||
if (curr.startTime().isBefore(prev.endTime())) { | ||
noticeContainer.addValidationNotice( | ||
new TimeframeOverlapNoice( | ||
prev.csvRowNumber(), | ||
prev.endTime(), | ||
curr.csvRowNumber(), | ||
curr.startTime(), | ||
entry.getKey().timeframeGroupId(), | ||
entry.getKey().serviceId())); | ||
} | ||
} | ||
} | ||
} | ||
|
||
@AutoValue | ||
abstract static class TimeframeKey { | ||
abstract String timeframeGroupId(); | ||
|
||
abstract String serviceId(); | ||
|
||
static TimeframeKey create(GtfsTimeframe timeframe) { | ||
return new AutoValue_TimeframeOverlapValidator_TimeframeKey( | ||
timeframe.timeframeGroupId(), timeframe.serviceId()); | ||
} | ||
} | ||
|
||
/** | ||
* Two entries in `timeframes.txt` with the same `timeframe_group_id` and `service_id` have | ||
* overlapping time intervals. | ||
* | ||
* <p>Timeframes with the same group and service dates must not overlap in time. Two entries X and | ||
* Y are considered to directly overlap if `X.start_time <= Y.start_time` and `Y.start_time | ||
* < X.end_time`. | ||
*/ | ||
@GtfsValidationNotice(severity = ERROR, files = @FileRefs(GtfsFrequencySchema.class)) | ||
static class TimeframeOverlapNoice extends ValidationNotice { | ||
|
||
/** The row number of the first timeframe entry. */ | ||
private final long prevCsvRowNumber; | ||
|
||
/** The first timeframe end time. */ | ||
private final GtfsTime prevEndTime; | ||
|
||
/** The row number of the second timeframe entry. */ | ||
private final long currCsvRowNumber; | ||
|
||
/** The start time of the second timeframe entry. */ | ||
private final GtfsTime currStartTime; | ||
|
||
/** The timeframe group id associated with the two entries. */ | ||
private final String timeframeGroupId; | ||
|
||
/** The service id associated with the two entries. */ | ||
private final String serviceId; | ||
|
||
TimeframeOverlapNoice( | ||
long prevCsvRowNumber, | ||
GtfsTime prevEndTime, | ||
long currCsvRowNumber, | ||
GtfsTime currStartTime, | ||
String timeframeGroupId, | ||
String serviceId) { | ||
this.prevCsvRowNumber = prevCsvRowNumber; | ||
this.prevEndTime = prevEndTime; | ||
this.currCsvRowNumber = currCsvRowNumber; | ||
this.currStartTime = currStartTime; | ||
this.timeframeGroupId = timeframeGroupId; | ||
this.serviceId = serviceId; | ||
} | ||
} | ||
} |
74 changes: 74 additions & 0 deletions
74
.../java/org/mobilitydata/gtfsvalidator/validator/TimeframeServiceIdForeignKeyValidator.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,74 @@ | ||
/* | ||
* Copyright 2023 Google LLC | ||
* | ||
* 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 javax.inject.Inject; | ||
import org.mobilitydata.gtfsvalidator.annotation.GtfsValidator; | ||
import org.mobilitydata.gtfsvalidator.notice.ForeignKeyViolationNotice; | ||
import org.mobilitydata.gtfsvalidator.notice.NoticeContainer; | ||
import org.mobilitydata.gtfsvalidator.table.GtfsCalendar; | ||
import org.mobilitydata.gtfsvalidator.table.GtfsCalendarDate; | ||
import org.mobilitydata.gtfsvalidator.table.GtfsCalendarDateTableContainer; | ||
import org.mobilitydata.gtfsvalidator.table.GtfsCalendarTableContainer; | ||
import org.mobilitydata.gtfsvalidator.table.GtfsTimeframe; | ||
import org.mobilitydata.gtfsvalidator.table.GtfsTimeframeTableContainer; | ||
|
||
/** | ||
* Validates that `service_id` field in `timeframes.txt` references a valid `service_id` in | ||
* `calendar.txt` or `calendar_date.txt`. | ||
*/ | ||
@GtfsValidator | ||
public class TimeframeServiceIdForeignKeyValidator extends FileValidator { | ||
private final GtfsTimeframeTableContainer timeframeContainer; | ||
private final GtfsCalendarTableContainer calendarContainer; | ||
private final GtfsCalendarDateTableContainer calendarDateContainer; | ||
|
||
@Inject | ||
TimeframeServiceIdForeignKeyValidator( | ||
GtfsTimeframeTableContainer timeframeContainer, | ||
GtfsCalendarTableContainer calendarContainer, | ||
GtfsCalendarDateTableContainer calendarDateContainer) { | ||
this.timeframeContainer = timeframeContainer; | ||
this.calendarContainer = calendarContainer; | ||
this.calendarDateContainer = calendarDateContainer; | ||
} | ||
|
||
@Override | ||
public void validate(NoticeContainer noticeContainer) { | ||
for (GtfsTimeframe timeframe : timeframeContainer.getEntities()) { | ||
String childKey = timeframe.serviceId(); | ||
if (!hasReferencedKey(childKey, calendarContainer, calendarDateContainer)) { | ||
noticeContainer.addValidationNotice( | ||
new ForeignKeyViolationNotice( | ||
GtfsTimeframe.FILENAME, | ||
GtfsTimeframe.SERVICE_ID_FIELD_NAME, | ||
GtfsCalendar.FILENAME + " or " + GtfsCalendarDate.FILENAME, | ||
GtfsCalendar.SERVICE_ID_FIELD_NAME, | ||
childKey, | ||
timeframe.csvRowNumber())); | ||
} | ||
} | ||
} | ||
|
||
private boolean hasReferencedKey( | ||
String childKey, | ||
GtfsCalendarTableContainer calendarContainer, | ||
GtfsCalendarDateTableContainer calendarDateContainer) { | ||
return calendarContainer.byServiceId(childKey).isPresent() | ||
|| !calendarDateContainer.byServiceId(childKey).isEmpty(); | ||
} | ||
} |
90 changes: 90 additions & 0 deletions
90
...main/java/org/mobilitydata/gtfsvalidator/validator/TimeframeStartAndEndTimeValidator.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,90 @@ | ||
/* | ||
* Copyright 2023 Google LLC | ||
* | ||
* 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.GtfsValidationNotice.FileRefs; | ||
import org.mobilitydata.gtfsvalidator.annotation.GtfsValidator; | ||
import org.mobilitydata.gtfsvalidator.notice.NoticeContainer; | ||
import org.mobilitydata.gtfsvalidator.notice.ValidationNotice; | ||
import org.mobilitydata.gtfsvalidator.table.GtfsTimeframe; | ||
import org.mobilitydata.gtfsvalidator.table.GtfsTimeframeSchema; | ||
import org.mobilitydata.gtfsvalidator.type.GtfsTime; | ||
|
||
/** | ||
* Validates the `start_time` and `end_time` values from `timeframes.txt`, checking that either both | ||
* are present or neither. Also checks that no value is greater than 24-hours. | ||
*/ | ||
@GtfsValidator | ||
public class TimeframeStartAndEndTimeValidator extends SingleEntityValidator<GtfsTimeframe> { | ||
|
||
private static final GtfsTime TWENTY_FOUR_HOURS = GtfsTime.fromHourMinuteSecond(24, 0, 0); | ||
|
||
@Override | ||
public void validate(GtfsTimeframe entity, NoticeContainer noticeContainer) { | ||
if (entity.hasStartTime() ^ entity.hasEndTime()) { | ||
noticeContainer.addValidationNotice( | ||
new TimeframeOnlyStartOrEndTimeSpecifiedNotice(entity.csvRowNumber())); | ||
} | ||
if (entity.hasStartTime() && entity.startTime().isAfter(TWENTY_FOUR_HOURS)) { | ||
noticeContainer.addValidationNotice( | ||
new TimeframeStartOrEndTimeGreaterThanTwentyFourHoursNotice( | ||
entity.csvRowNumber(), GtfsTimeframe.START_TIME_FIELD_NAME, entity.startTime())); | ||
} | ||
if (entity.hasEndTime() && entity.endTime().isAfter(TWENTY_FOUR_HOURS)) { | ||
noticeContainer.addValidationNotice( | ||
new TimeframeStartOrEndTimeGreaterThanTwentyFourHoursNotice( | ||
entity.csvRowNumber(), GtfsTimeframe.END_TIME_FIELD_NAME, entity.endTime())); | ||
} | ||
} | ||
|
||
/** | ||
* A row from `timeframes.txt` was found with only one of `start_time` and `end_time` specified. | ||
* | ||
* <p>Either both must be specified or neither must be specified. | ||
*/ | ||
@GtfsValidationNotice(severity = ERROR, files = @FileRefs(GtfsTimeframeSchema.class)) | ||
static class TimeframeOnlyStartOrEndTimeSpecifiedNotice extends ValidationNotice { | ||
|
||
/** The row number for the faulty record. */ | ||
private final int csvRowNumber; | ||
|
||
public TimeframeOnlyStartOrEndTimeSpecifiedNotice(int csvRowNumber) { | ||
this.csvRowNumber = csvRowNumber; | ||
} | ||
} | ||
|
||
/** A time in `timeframes.txt` is greater than `24:00:00`. */ | ||
@GtfsValidationNotice(severity = ERROR, files = @FileRefs(GtfsTimeframeSchema.class)) | ||
static class TimeframeStartOrEndTimeGreaterThanTwentyFourHoursNotice extends ValidationNotice { | ||
/** The row number for the faulty record. */ | ||
private final int csvRowNumber; | ||
/** The time field name for the faulty record. */ | ||
private final String fieldName; | ||
/** The invalid time value. */ | ||
private final GtfsTime time; | ||
|
||
TimeframeStartOrEndTimeGreaterThanTwentyFourHoursNotice( | ||
int csvRowNumber, String fieldName, GtfsTime time) { | ||
this.csvRowNumber = csvRowNumber; | ||
this.fieldName = fieldName; | ||
this.time = time; | ||
} | ||
} | ||
} |
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
Oops, something went wrong.