diff --git a/src/main/java/ch/naviqore/gtfs/schedule/GtfsScheduleParser.java b/src/main/java/ch/naviqore/gtfs/schedule/GtfsScheduleParser.java index 9f469c7c..ce8a5e4f 100644 --- a/src/main/java/ch/naviqore/gtfs/schedule/GtfsScheduleParser.java +++ b/src/main/java/ch/naviqore/gtfs/schedule/GtfsScheduleParser.java @@ -76,9 +76,12 @@ private void parseCalendar(CSVRecord record) { } private void parseCalendarDate(CSVRecord record) { - builder.addCalendarDate(record.get("service_id"), LocalDate.parse(record.get("date"), DATE_FORMATTER), - ExceptionType.parse(record.get("exception_type"))); - + try { + builder.addCalendarDate(record.get("service_id"), LocalDate.parse(record.get("date"), DATE_FORMATTER), + ExceptionType.parse(record.get("exception_type"))); + } catch (IllegalArgumentException e) { + log.warn("Skipping invalid calendar date {}: {}", record.get("date"), e.getMessage()); + } } private void parseStop(CSVRecord record) { @@ -95,11 +98,21 @@ private void parseRoute(CSVRecord record) { } private void parseTrips(CSVRecord record) { - builder.addTrip(record.get("trip_id"), record.get("route_id"), record.get("service_id")); + try { + builder.addTrip(record.get("trip_id"), record.get("route_id"), record.get("service_id")); + } catch (IllegalArgumentException e) { + log.warn("Skipping invalid trip {}: {}", record.get("trip_id"), e.getMessage()); + } } private void parseStopTimes(CSVRecord record) { - builder.addStopTime(record.get("trip_id"), record.get("stop_id"), - ServiceDayTime.parse(record.get("arrival_time")), ServiceDayTime.parse(record.get("departure_time"))); + try { + builder.addStopTime(record.get("trip_id"), record.get("stop_id"), + ServiceDayTime.parse(record.get("arrival_time")), + ServiceDayTime.parse(record.get("departure_time"))); + } catch (IllegalArgumentException e) { + log.warn("Skipping invalid stop time {}-{}: {}", record.get("trip_id"), record.get("stop_id"), + e.getMessage()); + } } } diff --git a/src/main/java/ch/naviqore/gtfs/schedule/GtfsScheduleReader.java b/src/main/java/ch/naviqore/gtfs/schedule/GtfsScheduleReader.java index 696ce9d7..86ce17b6 100644 --- a/src/main/java/ch/naviqore/gtfs/schedule/GtfsScheduleReader.java +++ b/src/main/java/ch/naviqore/gtfs/schedule/GtfsScheduleReader.java @@ -92,7 +92,7 @@ private static void readCsvRecords(InputStreamReader reader, GtfsScheduleParser public GtfsSchedule read(String path) throws IOException { File file = new File(path); - GtfsScheduleBuilder builder = GtfsScheduleBuilder.builder(); + GtfsScheduleBuilder builder = GtfsSchedule.builder(); GtfsScheduleParser parser = new GtfsScheduleParser(builder); if (file.isDirectory()) { diff --git a/src/main/java/ch/naviqore/gtfs/schedule/model/GtfsSchedule.java b/src/main/java/ch/naviqore/gtfs/schedule/model/GtfsSchedule.java index 7706ca7a..cccf5588 100644 --- a/src/main/java/ch/naviqore/gtfs/schedule/model/GtfsSchedule.java +++ b/src/main/java/ch/naviqore/gtfs/schedule/model/GtfsSchedule.java @@ -1,13 +1,23 @@ package ch.naviqore.gtfs.schedule.model; +import ch.naviqore.gtfs.schedule.spatial.Coordinate; import lombok.AccessLevel; import lombok.RequiredArgsConstructor; import java.time.LocalDate; +import java.time.LocalDateTime; import java.util.Collections; +import java.util.List; import java.util.Map; import java.util.stream.Collectors; +/** + * General Transit Feed Specification (GTFS) schedule + *
+ * Use the {@link GtfsScheduleBuilder} to construct a GTFS schedule instance.
+ *
+ * @author munterfi
+ */
@RequiredArgsConstructor(access = AccessLevel.PACKAGE)
public class GtfsSchedule {
@@ -18,20 +28,64 @@ public class GtfsSchedule {
private final Map
- * Represents a daily snapshot of the GTFS schedule, containing only the active services on a specific date.
- */
-@RequiredArgsConstructor(access = AccessLevel.PACKAGE)
-public class GtfsScheduleDay {
-
- @Getter
- private final LocalDate date;
- private final Map
+ * Read GTFS schedule data from a ZIP file and a directory.
+ *
+ * @author munterfi
+ */
class GtfsScheduleReaderIT {
private GtfsScheduleReader gtfsScheduleReader;
@@ -22,13 +31,20 @@ void setUp() {
void readFromZipFile(@TempDir Path tempDir) throws IOException {
File zipFile = GtfsScheduleTestData.prepareZipDataset(tempDir);
GtfsSchedule schedule = gtfsScheduleReader.read(zipFile.getAbsolutePath());
- // assertFalse(records.isEmpty(), "The records map should not be empty");
+ assertScheduleSizes(schedule);
}
@Test
void readFromDirectory(@TempDir Path tempDir) throws IOException {
File unzippedDir = GtfsScheduleTestData.prepareUnzippedDataset(tempDir);
GtfsSchedule schedule = gtfsScheduleReader.read(unzippedDir.getAbsolutePath());
- // assertFalse(records.isEmpty(), "The records map should not be empty");
+ assertScheduleSizes(schedule);
+ }
+
+ private void assertScheduleSizes(GtfsSchedule schedule) {
+ assertThat(schedule.getAgencies()).as("Agencies").hasSize(1);
+ assertThat(schedule.getRoutes()).as("Routes").hasSize(5);
+ assertThat(schedule.getStops()).as("Stops").hasSize(9);
+ assertThat(schedule.getTrips()).as("Trips").hasSize(11);
}
}
\ No newline at end of file
diff --git a/src/test/java/ch/naviqore/gtfs/schedule/GtfsScheduleTestData.java b/src/test/java/ch/naviqore/gtfs/schedule/GtfsScheduleTestData.java
index 5d26b32c..23e5819e 100644
--- a/src/test/java/ch/naviqore/gtfs/schedule/GtfsScheduleTestData.java
+++ b/src/test/java/ch/naviqore/gtfs/schedule/GtfsScheduleTestData.java
@@ -12,7 +12,7 @@
*
* @author munterfi
*/
-public final class GtfsScheduleTestData {
+final class GtfsScheduleTestData {
public static final String SAMPLE_FEED = "sample-feed-1";
public static final String SAMPLE_FEED_ZIP = SAMPLE_FEED + ".zip";
diff --git a/src/test/java/ch/naviqore/gtfs/schedule/model/GtfsScheduleTest.java b/src/test/java/ch/naviqore/gtfs/schedule/model/GtfsScheduleTest.java
new file mode 100644
index 00000000..1d04227d
--- /dev/null
+++ b/src/test/java/ch/naviqore/gtfs/schedule/model/GtfsScheduleTest.java
@@ -0,0 +1,149 @@
+package ch.naviqore.gtfs.schedule.model;
+
+import ch.naviqore.gtfs.schedule.type.RouteType;
+import ch.naviqore.gtfs.schedule.type.ServiceDayTime;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+
+import java.time.DayOfWeek;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.Month;
+import java.util.EnumSet;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+class GtfsScheduleTest {
+
+ private GtfsSchedule schedule;
+
+ @BeforeEach
+ void setUp() {
+ schedule = GtfsSchedule.builder()
+ .addAgency("agency1", "City Transit", "http://citytransit.example.com", "Europe/Zurich")
+ .addStop("stop1", "Main Station", 47.3769, 8.5417)
+ .addStop("stop2", "Central Park", 47.3779, 8.5407)
+ .addStop("stop3", "Hill Valley", 47.3780, 8.5390)
+ .addStop("stop4", "East Side", 47.3785, 8.5350)
+ .addStop("stop5", "West End", 47.3750, 8.5300)
+ .addRoute("route1", "agency1", "101", "Main Line", RouteType.BUS)
+ .addRoute("route2", "agency1", "102", "Cross Town", RouteType.BUS)
+ .addRoute("route3", "agency1", "103", "Circulator", RouteType.BUS)
+ .addCalendar("weekdays", EnumSet.range(DayOfWeek.MONDAY, DayOfWeek.FRIDAY), LocalDate.now(),
+ LocalDate.now().plusMonths(1))
+ .addCalendar("weekends", EnumSet.of(DayOfWeek.SATURDAY), LocalDate.now(), LocalDate.now().plusMonths(1))
+ .addTrip("trip1", "route1", "weekdays")
+ .addTrip("trip2", "route2", "weekdays")
+ .addTrip("trip3", "route3", "weekends")
+ .addStopTime("trip1", "stop1", new ServiceDayTime(8, 0, 0), new ServiceDayTime(8, 5, 0))
+ .addStopTime("trip1", "stop2", new ServiceDayTime(8, 10, 0), new ServiceDayTime(8, 15, 0))
+ .addStopTime("trip2", "stop3", new ServiceDayTime(9, 0, 0), new ServiceDayTime(9, 5, 0))
+ .addStopTime("trip2", "stop4", new ServiceDayTime(9, 10, 0), new ServiceDayTime(9, 15, 0))
+ .addStopTime("trip3", "stop5", new ServiceDayTime(10, 0, 0), new ServiceDayTime(10, 5, 0))
+ .addStopTime("trip3", "stop1", new ServiceDayTime(10, 10, 0), new ServiceDayTime(10, 15, 0))
+ .build();
+ }
+
+ @Nested
+ class Builder {
+
+ @Test
+ void shouldCorrectlyCountAgencies() {
+ assertThat(schedule.getAgencies()).hasSize(1);
+ }
+
+ @Test
+ void shouldCorrectlyCountRoutes() {
+ assertThat(schedule.getRoutes()).hasSize(3);
+ }
+
+ @Test
+ void shouldCorrectlyCountStops() {
+ assertThat(schedule.getStops()).hasSize(5);
+ }
+
+ @Test
+ void shouldCorrectlyCountTrips() {
+ assertThat(schedule.getTrips()).hasSize(3);
+ }
+
+ @Test
+ void shouldCorrectlyCountCalendars() {
+ assertThat(schedule.getCalendars()).hasSize(2);
+ }
+ }
+
+ @Nested
+ class NearestStops {
+
+ @Test
+ void shouldFindStopsWithin500Meters() {
+ assertThat(schedule.getNearestStops(47.3769, 8.5417, 500)).hasSize(3)
+ .extracting("id")
+ .containsOnly("stop1", "stop2", "stop3");
+ }
+
+ @Test
+ void shouldFindNoStopsWhenNoneAreCloseEnough() {
+ assertThat(schedule.getNearestStops(47.3800, 8.5500, 100)).isEmpty();
+ }
+ }
+
+ @Nested
+ class NextDepartures {
+
+ @Test
+ void shouldReturnNextDeparturesOnWeekday() {
+ // 8:00 AM on a weekday
+ LocalDateTime now = LocalDateTime.of(2024, Month.APRIL, 26, 8, 0);
+ assertThat(schedule.getNextDepartures("stop1", now, Integer.MAX_VALUE)).hasSize(1);
+ }
+
+ @Test
+ void shouldReturnNoNextDeparturesOnWeekday() {
+ // 9:00 AM on a weekday
+ LocalDateTime now = LocalDateTime.of(2024, Month.APRIL, 26, 9, 0);
+ assertThat(schedule.getNextDepartures("stop1", now, Integer.MAX_VALUE)).isEmpty();
+ }
+
+ @Test
+ void shouldReturnNextDeparturesOnSaturday() {
+ // 9:00 AM on a Saturday
+ LocalDateTime now = LocalDateTime.of(2024, Month.APRIL, 27, 8, 0);
+ assertThat(schedule.getNextDepartures("stop1", now, Integer.MAX_VALUE)).hasSize(1);
+ }
+
+ @Test
+ void shouldReturnNoDeparturesFromUnknownStop() {
+ LocalDateTime now = LocalDateTime.of(2024, Month.APRIL, 26, 10, 0);
+ assertThatThrownBy(() -> schedule.getNextDepartures("unknown", now, 1)).isInstanceOf(
+ IllegalArgumentException.class).hasMessage("Stop unknown not found");
+ }
+ }
+
+ @Nested
+ class ActiveTrips {
+
+ @Test
+ void shouldReturnActiveTripsOnWeekday() {
+ assertThat(schedule.getActiveTrips(LocalDate.now())).hasSize(2)
+ .extracting("id")
+ .containsOnly("trip1", "trip2");
+ }
+
+ @Test
+ void shouldReturnActiveTripsOnWeekend() {
+ assertThat(schedule.getActiveTrips(LocalDate.now().with(DayOfWeek.SATURDAY))).hasSize(1)
+ .extracting("id")
+ .containsOnly("trip3");
+ }
+
+ @Test
+ void shouldReturnNoActiveTripsForNonServiceDay() {
+ LocalDate nonServiceDay = LocalDate.now().with(DayOfWeek.SUNDAY).plusWeeks(2);
+ assertThat(schedule.getActiveTrips(nonServiceDay)).isEmpty();
+ }
+ }
+}