diff --git a/main/src/main/java/org/mobilitydata/gtfsvalidator/table/GtfsFareLegRuleSchema.java b/main/src/main/java/org/mobilitydata/gtfsvalidator/table/GtfsFareLegRuleSchema.java index b2297b6596..ef871fe875 100644 --- a/main/src/main/java/org/mobilitydata/gtfsvalidator/table/GtfsFareLegRuleSchema.java +++ b/main/src/main/java/org/mobilitydata/gtfsvalidator/table/GtfsFareLegRuleSchema.java @@ -47,6 +47,16 @@ public interface GtfsFareLegRuleSchema extends GtfsEntity { @ForeignKey(table = "areas.txt", field = "area_id") String toAreaId(); + @FieldType(FieldTypeEnum.ID) + @PrimaryKey(translationRecordIdType = UNSUPPORTED) + @ForeignKey(table = "timeframes.txt", field = "timeframe_group_id") + String fromTimeframeGroupId(); + + @FieldType(FieldTypeEnum.ID) + @PrimaryKey(translationRecordIdType = UNSUPPORTED) + @ForeignKey(table = "timeframes.txt", field = "timeframe_group_id") + String toTimeframeGroupId(); + @FieldType(FieldTypeEnum.ID) @Required @PrimaryKey(translationRecordIdType = UNSUPPORTED) diff --git a/main/src/main/java/org/mobilitydata/gtfsvalidator/table/GtfsTimeframeSchema.java b/main/src/main/java/org/mobilitydata/gtfsvalidator/table/GtfsTimeframeSchema.java new file mode 100644 index 0000000000..3d64f242fd --- /dev/null +++ b/main/src/main/java/org/mobilitydata/gtfsvalidator/table/GtfsTimeframeSchema.java @@ -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(); +} diff --git a/main/src/main/java/org/mobilitydata/gtfsvalidator/validator/TimeframeOverlapValidator.java b/main/src/main/java/org/mobilitydata/gtfsvalidator/validator/TimeframeOverlapValidator.java new file mode 100644 index 0000000000..ff0df11400 --- /dev/null +++ b/main/src/main/java/org/mobilitydata/gtfsvalidator/validator/TimeframeOverlapValidator.java @@ -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> timeframesByKey = + timeframeContainer.getEntities().stream() + .collect(Collectors.groupingBy(TimeframeKey::create, Collectors.toList())); + for (Map.Entry> entry : timeframesByKey.entrySet()) { + List 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. + * + *

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; + } + } +} diff --git a/main/src/main/java/org/mobilitydata/gtfsvalidator/validator/TimeframeServiceIdForeignKeyValidator.java b/main/src/main/java/org/mobilitydata/gtfsvalidator/validator/TimeframeServiceIdForeignKeyValidator.java new file mode 100644 index 0000000000..0afcce36e7 --- /dev/null +++ b/main/src/main/java/org/mobilitydata/gtfsvalidator/validator/TimeframeServiceIdForeignKeyValidator.java @@ -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(); + } +} diff --git a/main/src/main/java/org/mobilitydata/gtfsvalidator/validator/TimeframeStartAndEndTimeValidator.java b/main/src/main/java/org/mobilitydata/gtfsvalidator/validator/TimeframeStartAndEndTimeValidator.java new file mode 100644 index 0000000000..e0b2cbe3f4 --- /dev/null +++ b/main/src/main/java/org/mobilitydata/gtfsvalidator/validator/TimeframeStartAndEndTimeValidator.java @@ -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 { + + 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. + * + *

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; + } + } +} 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 3fed46aaf8..939269a0c7 100644 --- a/main/src/test/java/org/mobilitydata/gtfsvalidator/validator/NoticeFieldsTest.java +++ b/main/src/test/java/org/mobilitydata/gtfsvalidator/validator/NoticeFieldsTest.java @@ -182,6 +182,8 @@ public void testNoticeClassFieldNames() { "stopUrl", "suggestedExpirationDate", "tableName", + "time", + "timeframeGroupId", "transferCount", "tripCsvRowNumber", "tripFieldName", diff --git a/main/src/test/java/org/mobilitydata/gtfsvalidator/validator/TimeframeOverlapValidatorTest.java b/main/src/test/java/org/mobilitydata/gtfsvalidator/validator/TimeframeOverlapValidatorTest.java new file mode 100644 index 0000000000..901e37afb8 --- /dev/null +++ b/main/src/test/java/org/mobilitydata/gtfsvalidator/validator/TimeframeOverlapValidatorTest.java @@ -0,0 +1,154 @@ +package org.mobilitydata.gtfsvalidator.validator; + +import static com.google.common.truth.Truth.assertThat; + +import java.util.Arrays; +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.GtfsTimeframe; +import org.mobilitydata.gtfsvalidator.table.GtfsTimeframeTableContainer; +import org.mobilitydata.gtfsvalidator.type.GtfsTime; +import org.mobilitydata.gtfsvalidator.validator.TimeframeOverlapValidator.TimeframeOverlapNoice; + +@RunWith(JUnit4.class) +public class TimeframeOverlapValidatorTest { + + @Test + public void testSingleTimeframe() { + assertThat( + validate( + new GtfsTimeframe.Builder() + .setTimeframeGroupId("PEAK") + .setServiceId("WEEKDAY") + .setStartTime(GtfsTime.fromString("00:00:00")) + .setEndTime(GtfsTime.fromString("24:00:00")) + .setCsvRowNumber(2) + .build())) + .isEmpty(); + } + + @Test + public void testNoOverlap() { + assertThat( + validate( + new GtfsTimeframe.Builder() + .setTimeframeGroupId("PEAK") + .setServiceId("WEEKDAY") + .setStartTime(GtfsTime.fromString("08:00:00")) + .setEndTime(GtfsTime.fromString("09:00:00")) + .setCsvRowNumber(2) + .build(), + new GtfsTimeframe.Builder() + .setTimeframeGroupId("PEAK") + .setServiceId("WEEKDAY") + .setStartTime(GtfsTime.fromString("17:00:00")) + .setEndTime(GtfsTime.fromString("18:00:00")) + .setCsvRowNumber(3) + .build())) + .isEmpty(); + } + + @Test + public void testNoOverlapButAdjacent() { + assertThat( + validate( + new GtfsTimeframe.Builder() + .setTimeframeGroupId("PEAK") + .setServiceId("WEEKDAY") + .setStartTime(GtfsTime.fromString("08:00:00")) + .setEndTime(GtfsTime.fromString("09:00:00")) + .setCsvRowNumber(2) + .build(), + new GtfsTimeframe.Builder() + .setTimeframeGroupId("PEAK") + .setServiceId("WEEKDAY") + .setStartTime(GtfsTime.fromString("09:00:00")) + .setEndTime(GtfsTime.fromString("10:00:00")) + .setCsvRowNumber(3) + .build())) + .isEmpty(); + } + + @Test + public void testOverlap() { + assertThat( + validate( + new GtfsTimeframe.Builder() + .setTimeframeGroupId("PEAK") + .setServiceId("WEEKDAY") + .setStartTime(GtfsTime.fromString("08:00:00")) + .setEndTime(GtfsTime.fromString("09:00:00")) + .setCsvRowNumber(2) + .build(), + new GtfsTimeframe.Builder() + .setTimeframeGroupId("PEAK") + .setServiceId("WEEKDAY") + .setStartTime(GtfsTime.fromString("08:30:00")) + .setEndTime(GtfsTime.fromString("09:30:00")) + .setCsvRowNumber(3) + .build())) + .containsExactly( + new TimeframeOverlapNoice( + 2, + GtfsTime.fromString("09:00:00"), + 3, + GtfsTime.fromString("08:30:00"), + "PEAK", + "WEEKDAY")); + } + + @Test + public void testWithDifferentServiceIds() { + assertThat( + validate( + new GtfsTimeframe.Builder() + .setTimeframeGroupId("PEAK") + .setServiceId("WEEKDAY") + .setStartTime(GtfsTime.fromString("08:00:00")) + .setEndTime(GtfsTime.fromString("09:00:00")) + .setCsvRowNumber(2) + .build(), + new GtfsTimeframe.Builder() + .setTimeframeGroupId("PEAK") + .setServiceId("WEEKEND") + .setStartTime(GtfsTime.fromString("08:00:00")) + .setEndTime(GtfsTime.fromString("09:00:00")) + .setCsvRowNumber(3) + .build())) + .isEmpty(); + } + + @Test + public void testWithDifferentGroupIds() { + assertThat( + validate( + new GtfsTimeframe.Builder() + .setTimeframeGroupId("PEAK") + .setServiceId("WEEKDAY") + .setStartTime(GtfsTime.fromString("08:00:00")) + .setEndTime(GtfsTime.fromString("09:00:00")) + .setCsvRowNumber(2) + .build(), + new GtfsTimeframe.Builder() + .setTimeframeGroupId("NON-PEAK") + .setServiceId("WEEKDAY") + .setStartTime(GtfsTime.fromString("08:00:00")) + .setEndTime(GtfsTime.fromString("09:00:00")) + .setCsvRowNumber(3) + .build())) + .isEmpty(); + } + + private List validate(GtfsTimeframe... timeframes) { + NoticeContainer noticeContainer = new NoticeContainer(); + GtfsTimeframeTableContainer container = + GtfsTimeframeTableContainer.forEntities(Arrays.asList(timeframes), noticeContainer); + TimeframeOverlapValidator validator = new TimeframeOverlapValidator(container); + validator.validate(noticeContainer); + return noticeContainer.getValidationNotices(); + } +} diff --git a/main/src/test/java/org/mobilitydata/gtfsvalidator/validator/TimeframeServiceIdForeignKeyValidatorTest.java b/main/src/test/java/org/mobilitydata/gtfsvalidator/validator/TimeframeServiceIdForeignKeyValidatorTest.java new file mode 100644 index 0000000000..3dd7db616f --- /dev/null +++ b/main/src/test/java/org/mobilitydata/gtfsvalidator/validator/TimeframeServiceIdForeignKeyValidatorTest.java @@ -0,0 +1,107 @@ +/* + * Copyright 2023 Google LLC, MobilityData IO + * + * 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 com.google.common.truth.Truth.assertThat; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import java.time.DayOfWeek; +import java.time.LocalDate; +import java.util.List; +import org.junit.Test; +import org.mobilitydata.gtfsvalidator.notice.ForeignKeyViolationNotice; +import org.mobilitydata.gtfsvalidator.notice.NoticeContainer; +import org.mobilitydata.gtfsvalidator.notice.ValidationNotice; +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; +import org.mobilitydata.gtfsvalidator.type.GtfsDate; +import org.mobilitydata.gtfsvalidator.util.CalendarUtilTest; + +public class TimeframeServiceIdForeignKeyValidatorTest { + + @Test + public void timeframeServiceIdInCalendarTableShouldNotGenerateNotice() { + assertThat( + generateNotices( + ImmutableList.of(new GtfsTimeframe.Builder().setServiceId("WEEK").build()), + ImmutableList.of( + CalendarUtilTest.createGtfsCalendar( + "WEEK", + LocalDate.of(2021, 1, 14), + LocalDate.of(2021, 1, 24), + ImmutableSet.of( + DayOfWeek.MONDAY, + DayOfWeek.TUESDAY, + DayOfWeek.WEDNESDAY, + DayOfWeek.THURSDAY, + DayOfWeek.FRIDAY))), + ImmutableList.of())) + .isEmpty(); + } + + @Test + public void tripServiceIdInCalendarDateTableShouldNotGenerateNotice() { + assertThat( + generateNotices( + ImmutableList.of(new GtfsTimeframe.Builder().setServiceId("WEEK").build()), + ImmutableList.of(), + ImmutableList.of( + new GtfsCalendarDate.Builder() + .setCsvRowNumber(2) + .setServiceId("WEEK") + .setDate(GtfsDate.fromEpochDay(24354)) + .setExceptionType(2) + .build()))) + .isEmpty(); + } + + @Test + public void tripServiceIdNotInDataShouldGenerateNotice() { + assertThat( + generateNotices( + ImmutableList.of( + new GtfsTimeframe.Builder().setServiceId("WEEK").setCsvRowNumber(1).build()), + ImmutableList.of(), + ImmutableList.of())) + .containsExactly( + new ForeignKeyViolationNotice( + "timeframes.txt", + "service_id", + "calendar.txt or calendar_dates.txt", + "service_id", + "WEEK", + 1)); + } + + private static List generateNotices( + List timeframes, + List calendars, + List calendarDates) { + NoticeContainer noticeContainer = new NoticeContainer(); + new TimeframeServiceIdForeignKeyValidator( + GtfsTimeframeTableContainer.forEntities(timeframes, noticeContainer), + GtfsCalendarTableContainer.forEntities(calendars, noticeContainer), + GtfsCalendarDateTableContainer.forEntities(calendarDates, noticeContainer)) + .validate(noticeContainer); + return noticeContainer.getValidationNotices(); + } +} diff --git a/main/src/test/java/org/mobilitydata/gtfsvalidator/validator/TimeframeStartAndEndTimeValidatorTest.java b/main/src/test/java/org/mobilitydata/gtfsvalidator/validator/TimeframeStartAndEndTimeValidatorTest.java new file mode 100644 index 0000000000..a3f38f19b0 --- /dev/null +++ b/main/src/test/java/org/mobilitydata/gtfsvalidator/validator/TimeframeStartAndEndTimeValidatorTest.java @@ -0,0 +1,92 @@ +/* + * 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 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.GtfsTimeframe; +import org.mobilitydata.gtfsvalidator.type.GtfsTime; +import org.mobilitydata.gtfsvalidator.validator.TimeframeStartAndEndTimeValidator.TimeframeOnlyStartOrEndTimeSpecifiedNotice; +import org.mobilitydata.gtfsvalidator.validator.TimeframeStartAndEndTimeValidator.TimeframeStartOrEndTimeGreaterThanTwentyFourHoursNotice; + +@RunWith(JUnit4.class) +public class TimeframeStartAndEndTimeValidatorTest { + + @Test + public void testExplicitFullDayInterval() { + assertThat( + validate( + new GtfsTimeframe.Builder() + .setStartTime(GtfsTime.fromString("00:00:00")) + .setEndTime(GtfsTime.fromString("24:00:00")) + .build())) + .isEmpty(); + } + + @Test + public void testImplicitFullDayInterval() { + assertThat(validate(new GtfsTimeframe.Builder().build())).isEmpty(); + } + + @Test + public void testBeyondTwentyFourHours() { + assertThat( + validate( + new GtfsTimeframe.Builder() + .setCsvRowNumber(2) + .setStartTime(GtfsTime.fromString("00:00:00")) + .setEndTime(GtfsTime.fromString("24:00:01")) + .build())) + .containsExactly( + new TimeframeStartOrEndTimeGreaterThanTwentyFourHoursNotice( + 2, "end_time", GtfsTime.fromString("24:00:01"))); + } + + @Test + public void testOnlyStartTimeSpecified() { + assertThat( + validate( + new GtfsTimeframe.Builder() + .setStartTime(GtfsTime.fromString("00:00:00")) + .setCsvRowNumber(2) + .build())) + .containsExactly(new TimeframeOnlyStartOrEndTimeSpecifiedNotice(2)); + } + + @Test + public void testOnlyEndTimeSpecified() { + assertThat( + validate( + new GtfsTimeframe.Builder() + .setEndTime(GtfsTime.fromString("10:00:00")) + .setCsvRowNumber(2) + .build())) + .containsExactly(new TimeframeOnlyStartOrEndTimeSpecifiedNotice(2)); + } + + private List validate(GtfsTimeframe timeframe) { + NoticeContainer container = new NoticeContainer(); + new TimeframeStartAndEndTimeValidator().validate(timeframe, container); + return container.getValidationNotices(); + } +}