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 trips; /** - * Retrieves a snapshot of the GTFS schedule active on a specific date. + * Creates a new GTFS schedule builder. * - * @param date the date for which the active schedule is requested. - * @return GtfsScheduleDay containing only the active routes, stops, and trips for the specified date. + * @return A new GTFS schedule builder. + */ + public static GtfsScheduleBuilder builder() { + return new GtfsScheduleBuilder(); + } + + /** + * Retrieves a list of stops within a specified distance from a given location. + * + * @param latitude the latitude of the origin location. + * @param longitude the longitude of the origin location. + * @param maxDistance the maximum distance from the origin location. + * @return A list of stops within the specified distance. + */ + public List getNearestStops(double latitude, double longitude, int maxDistance) { + // TODO: Use a spatial index for efficient nearest neighbor search, e.g. KD-tree or R-tree + Coordinate origin = new Coordinate(latitude, longitude); + return stops.values() + .stream() + .filter(stop -> stop.getCoordinate().distanceTo(origin) <= maxDistance) + .collect(Collectors.toList()); + } + + /** + * Retrieves a list of the next departures from a specific stop. + * + * @param stopId the identifier of the stop. + * @param dateTime the date and time for which the next departures are requested. + * @param limit the maximum number of departures to return. + * @return A list of the next departures from the specified stop. */ - public GtfsScheduleDay getScheduleForDay(LocalDate date) { - Map activeTrips = trips.entrySet() + public List getNextDepartures(String stopId, LocalDateTime dateTime, int limit) { + Stop stop = stops.get(stopId); + if (stop == null) { + throw new IllegalArgumentException("Stop " + stopId + " not found"); + } + return stop.getStopTimes() .stream() - .filter(entry -> entry.getValue().getCalendar().isServiceAvailable(date)) - .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + .filter(stopTime -> stopTime.departure().getTotalSeconds() >= dateTime.toLocalTime().toSecondOfDay()) + .filter(stopTime -> stopTime.trip().getCalendar().isServiceAvailable(dateTime.toLocalDate())) + .limit(limit) + .toList(); + } - // TODO: Implement efficiently without copying. - // return new GtfsScheduleDay(date, activeStops, activeRoutes, activeTrips); - return null; + /** + * Retrieves a snapshot of the trips active on a specific date. + * + * @param date the date for which the active schedule is requested. + * @return A list containing only the active trips. + */ + public List getActiveTrips(LocalDate date) { + return calendars.values() + .stream() + .filter(calendar -> calendar.isServiceAvailable(date)) + .flatMap(calendar -> calendar.getTrips().stream()) + .collect(Collectors.toList()); } public Map getAgencies() { diff --git a/src/main/java/ch/naviqore/gtfs/schedule/model/GtfsScheduleBuilder.java b/src/main/java/ch/naviqore/gtfs/schedule/model/GtfsScheduleBuilder.java index 5488bb50..9decff7a 100644 --- a/src/main/java/ch/naviqore/gtfs/schedule/model/GtfsScheduleBuilder.java +++ b/src/main/java/ch/naviqore/gtfs/schedule/model/GtfsScheduleBuilder.java @@ -1,5 +1,6 @@ package ch.naviqore.gtfs.schedule.model; +import ch.naviqore.gtfs.schedule.spatial.Coordinate; import ch.naviqore.gtfs.schedule.type.ExceptionType; import ch.naviqore.gtfs.schedule.type.RouteType; import ch.naviqore.gtfs.schedule.type.ServiceDayTime; @@ -27,7 +28,7 @@ * * @author munterfi */ -@NoArgsConstructor(access = AccessLevel.PRIVATE) +@NoArgsConstructor(access = AccessLevel.PACKAGE) @Log4j2 public class GtfsScheduleBuilder { @@ -38,10 +39,6 @@ public class GtfsScheduleBuilder { private final Map routes = new HashMap<>(); private final Map trips = new HashMap<>(); - public static GtfsScheduleBuilder builder() { - return new GtfsScheduleBuilder(); - } - public GtfsScheduleBuilder addAgency(String id, String name, String url, String timezone) { if (agencies.containsKey(id)) { throw new IllegalArgumentException("Agency " + id + " already exists"); @@ -56,7 +53,7 @@ public GtfsScheduleBuilder addStop(String id, String name, double lat, double lo throw new IllegalArgumentException("Agency " + id + " already exists"); } log.debug("Adding stop {}", id); - stops.put(id, new Stop(id, name, lat, lon)); + stops.put(id, new Stop(id, name, new Coordinate(lat, lon))); return this; } @@ -136,6 +133,7 @@ public GtfsSchedule build() { trips.values().parallelStream().forEach(Trip::initialize); stops.values().parallelStream().forEach(Stop::initialize); routes.values().parallelStream().forEach(Route::initialize); + // TODO: Build k-d tree for spatial indexing return new GtfsSchedule(agencies, calendars, stops, routes, trips); } diff --git a/src/main/java/ch/naviqore/gtfs/schedule/model/GtfsScheduleDay.java b/src/main/java/ch/naviqore/gtfs/schedule/model/GtfsScheduleDay.java deleted file mode 100644 index 25d93101..00000000 --- a/src/main/java/ch/naviqore/gtfs/schedule/model/GtfsScheduleDay.java +++ /dev/null @@ -1,36 +0,0 @@ -package ch.naviqore.gtfs.schedule.model; - -import lombok.AccessLevel; -import lombok.Getter; -import lombok.RequiredArgsConstructor; - -import java.time.LocalDate; -import java.util.Collections; -import java.util.Map; - -/** - * GTFS Schedule Service Day - *

- * 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 stops; - private final Map routes; - private final Map trips; - - public Map getStops() { - return Collections.unmodifiableMap(stops); - } - - public Map getRoutes() { - return Collections.unmodifiableMap(routes); - } - - public Map getTrips() { - return Collections.unmodifiableMap(trips); - } -} diff --git a/src/main/java/ch/naviqore/gtfs/schedule/model/Stop.java b/src/main/java/ch/naviqore/gtfs/schedule/model/Stop.java index 41a80219..c08f5743 100644 --- a/src/main/java/ch/naviqore/gtfs/schedule/model/Stop.java +++ b/src/main/java/ch/naviqore/gtfs/schedule/model/Stop.java @@ -1,5 +1,6 @@ package ch.naviqore.gtfs.schedule.model; +import ch.naviqore.gtfs.schedule.spatial.Coordinate; import lombok.AccessLevel; import lombok.Getter; import lombok.RequiredArgsConstructor; @@ -14,8 +15,7 @@ public final class Stop implements Initializable { private final String id; private final String name; - private final double lat; - private final double lon; + private final Coordinate coordinate; private final List stopTimes = new ArrayList<>(); void addStopTime(StopTime stopTime) { @@ -42,6 +42,6 @@ public int hashCode() { @Override public String toString() { - return "Stop[" + "id=" + id + ", " + "name=" + name + ", " + "lat=" + lat + ", " + "lon=" + lon + ']'; + return "Stop[" + "id=" + id + ", " + "name=" + name + ']'; } } diff --git a/src/main/java/ch/naviqore/gtfs/schedule/spatial/Coordinate.java b/src/main/java/ch/naviqore/gtfs/schedule/spatial/Coordinate.java new file mode 100644 index 00000000..46b0d9e0 --- /dev/null +++ b/src/main/java/ch/naviqore/gtfs/schedule/spatial/Coordinate.java @@ -0,0 +1,31 @@ +package ch.naviqore.gtfs.schedule.spatial; + +import lombok.EqualsAndHashCode; +import lombok.RequiredArgsConstructor; +import lombok.ToString; + +@RequiredArgsConstructor +@EqualsAndHashCode +@ToString +public class Coordinate { + + private static final int EARTH_RADIUS = 6371000; + private final double latitude; + private final double longitude; + + /** + * Calculates the distance to another Coordinates object using the Haversine formula. + * + * @param other The other Coordinates object to calculate the distance to. + * @return The distance in meters. + */ + public double distanceTo(Coordinate other) { + double latDistance = Math.toRadians(other.latitude - this.latitude); + double lonDistance = Math.toRadians(other.longitude - this.longitude); + double a = Math.sin(latDistance / 2) * Math.sin(latDistance / 2) + Math.cos( + Math.toRadians(this.latitude)) * Math.cos(Math.toRadians(other.latitude)) * Math.sin( + lonDistance / 2) * Math.sin(lonDistance / 2); + double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + return EARTH_RADIUS * c; + } +} diff --git a/src/main/java/ch/naviqore/gtfs/schedule/spatial/KdTree.java b/src/main/java/ch/naviqore/gtfs/schedule/spatial/KdTree.java new file mode 100644 index 00000000..15f36f84 --- /dev/null +++ b/src/main/java/ch/naviqore/gtfs/schedule/spatial/KdTree.java @@ -0,0 +1,5 @@ +package ch.naviqore.gtfs.schedule.spatial; + +public class KdTree { + // TODO: Implement KdTree +} diff --git a/src/main/java/ch/naviqore/raptor/model/RouteTraversal.java b/src/main/java/ch/naviqore/raptor/model/RouteTraversal.java index 0b840f47..78471484 100644 --- a/src/main/java/ch/naviqore/raptor/model/RouteTraversal.java +++ b/src/main/java/ch/naviqore/raptor/model/RouteTraversal.java @@ -6,5 +6,5 @@ public class RouteTraversal { private final StopTime[] stopTimes; private final Route[] routes; - private final Stop[] routeStops; + // private final Stop[] routeStops; } diff --git a/src/test/java/ch/naviqore/gtfs/schedule/GtfsScheduleBenchmark.java b/src/test/java/ch/naviqore/gtfs/schedule/GtfsScheduleBenchmark.java index 3ea9957b..932cc355 100644 --- a/src/test/java/ch/naviqore/gtfs/schedule/GtfsScheduleBenchmark.java +++ b/src/test/java/ch/naviqore/gtfs/schedule/GtfsScheduleBenchmark.java @@ -2,22 +2,23 @@ import ch.naviqore.gtfs.schedule.GtfsScheduleBenchmarkData.Dataset; import ch.naviqore.gtfs.schedule.model.GtfsSchedule; -import ch.naviqore.gtfs.schedule.model.GtfsScheduleDay; +import ch.naviqore.gtfs.schedule.model.Trip; import lombok.AccessLevel; import lombok.NoArgsConstructor; import java.io.IOException; import java.time.LocalDate; +import java.util.List; @NoArgsConstructor(access = AccessLevel.PRIVATE) -public final class GtfsScheduleBenchmark { +final class GtfsScheduleBenchmark { - private static final Dataset DATASET = Dataset.GERMANY; + private static final Dataset DATASET = Dataset.SWITZERLAND; public static void main(String[] args) throws IOException, InterruptedException { String path = GtfsScheduleBenchmarkData.get(DATASET); GtfsSchedule schedule = new GtfsScheduleReader().read(path); - GtfsScheduleDay scheduleDay = schedule.getScheduleForDay(LocalDate.now()); + List activeTrips = schedule.getActiveTrips(LocalDate.now()); // clean heap from reading artifacts System.gc(); // monitor effect diff --git a/src/test/java/ch/naviqore/gtfs/schedule/GtfsScheduleBenchmarkData.java b/src/test/java/ch/naviqore/gtfs/schedule/GtfsScheduleBenchmarkData.java index c072e03a..f214b016 100644 --- a/src/test/java/ch/naviqore/gtfs/schedule/GtfsScheduleBenchmarkData.java +++ b/src/test/java/ch/naviqore/gtfs/schedule/GtfsScheduleBenchmarkData.java @@ -21,7 +21,7 @@ */ @NoArgsConstructor(access = AccessLevel.PRIVATE) @Log4j2 -public final class GtfsScheduleBenchmarkData { +final class GtfsScheduleBenchmarkData { private static final Path DATA_DIRECTORY = Path.of("benchmark/input"); private static final HttpClient httpClient = HttpClient.newBuilder() .followRedirects(HttpClient.Redirect.ALWAYS) diff --git a/src/test/java/ch/naviqore/gtfs/schedule/GtfsScheduleReaderIT.java b/src/test/java/ch/naviqore/gtfs/schedule/GtfsScheduleReaderIT.java index 42937656..59d03da2 100644 --- a/src/test/java/ch/naviqore/gtfs/schedule/GtfsScheduleReaderIT.java +++ b/src/test/java/ch/naviqore/gtfs/schedule/GtfsScheduleReaderIT.java @@ -9,6 +9,15 @@ import java.io.IOException; import java.nio.file.Path; +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration test for {@link GtfsScheduleReader} + *

+ * 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(); + } + } +}