Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import static org.opentripplanner.transit.model._data.TimetableRepositoryForTest.id;

import gnu.trove.set.hash.TIntHashSet;
import java.time.Instant;
import java.time.LocalDate;
import java.util.Collection;
import java.util.List;
Expand Down Expand Up @@ -37,7 +38,8 @@ class ClosestTripTest {
private static final FlexServiceDate FSD = new FlexServiceDate(
DATE,
ServiceDateUtils.secondsSinceStartOfTime(DATE.atStartOfDay(ZoneIds.BERLIN), DATE),
10,
Instant.ofEpochSecond(10),
ZoneIds.BERLIN,
new TIntHashSet()
);
private static final StopLocation STOP = FLEX_TRIP.getStop(0);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package org.opentripplanner.ext.flex.template;

import static org.junit.jupiter.api.Assertions.assertEquals;

import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import org.junit.jupiter.api.Test;
import org.opentripplanner.transit.model.timetable.booking.RoutingBookingInfo;

/**
* Tests for {@link FlexServiceDate#requestedBookingTime()} to verify that booking time
* is correctly calculated relative to each service date's start-of-service.
*/
class FlexServiceDateBookingTimeTest {

private static final ZoneId ZONE = ZoneId.of("Europe/Oslo");

@Test
void testRequestedBookingTimeSameDay() {
// Booking at 14:00 on Jan 13, service date is Jan 13
LocalDate serviceDate = LocalDate.of(2026, 1, 13);
Instant bookingTime = ZonedDateTime.of(serviceDate, LocalTime.of(14, 0), ZONE).toInstant();

FlexServiceDate flexDate = new FlexServiceDate(serviceDate, 0, bookingTime, ZONE, null);

// Expected: 14:00 = 14 * 3600 = 50400 seconds from start of service
int expected = 14 * 3600;
assertEquals(expected, flexDate.requestedBookingTime());
}

@Test
void testRequestedBookingTimePreviousDay() {
// Booking at 14:40 on Jan 12, service date is Jan 13
// This is the bug scenario: booking time should be negative relative to Jan 13
LocalDate bookingDate = LocalDate.of(2026, 1, 12);
LocalDate serviceDate = LocalDate.of(2026, 1, 13);

Instant bookingTime = ZonedDateTime.of(bookingDate, LocalTime.of(14, 40), ZONE).toInstant();

FlexServiceDate flexDate = new FlexServiceDate(serviceDate, 0, bookingTime, ZONE, null);

// Booking is at 14:40 on Jan 12
// Service date start is midnight Jan 13 (NOON - 12h)
// 14:40 on Jan 12 is 9 hours 20 minutes before midnight Jan 13
// = -(9*3600 + 20*60) = -33600 seconds
int expected = -(9 * 3600 + 20 * 60);
assertEquals(expected, flexDate.requestedBookingTime());
}

@Test
void testRequestedBookingTimeMultipleDaysAhead() {
// Booking at 10:00 on Jan 10, service date is Jan 13
LocalDate bookingDate = LocalDate.of(2026, 1, 10);
LocalDate serviceDate = LocalDate.of(2026, 1, 13);

Instant bookingTime = ZonedDateTime.of(bookingDate, LocalTime.of(10, 0), ZONE).toInstant();

FlexServiceDate flexDate = new FlexServiceDate(serviceDate, 0, bookingTime, ZONE, null);

// 10:00 on Jan 10 is 2 days + 14 hours before midnight Jan 13
// = -(2*24 + 14) hours = -62 hours = -62 * 3600 seconds = -223200
int expected = -((2 * 24 + 14) * 3600);
assertEquals(expected, flexDate.requestedBookingTime());
}

@Test
void testRequestedBookingTimeNull() {
LocalDate serviceDate = LocalDate.of(2026, 1, 13);

FlexServiceDate flexDate = new FlexServiceDate(serviceDate, 0, null, ZONE, null);

assertEquals(RoutingBookingInfo.NOT_SET, flexDate.requestedBookingTime());
}

@Test
void testBookingTimeOnDifferentDatesProducesDifferentResults() {
// Same booking instant should produce different requestedBookingTime values
// for different service dates - this is the core fix
Instant bookingTime = ZonedDateTime.of(
LocalDate.of(2026, 1, 12),
LocalTime.of(14, 40),
ZONE
).toInstant();

FlexServiceDate jan12 = new FlexServiceDate(
LocalDate.of(2026, 1, 12),
0,
bookingTime,
ZONE,
null
);

FlexServiceDate jan13 = new FlexServiceDate(
LocalDate.of(2026, 1, 13),
0,
bookingTime,
ZONE,
null
);

// On Jan 12: booking at 14:40 = 14*3600 + 40*60 = 52800 seconds
assertEquals(14 * 3600 + 40 * 60, jan12.requestedBookingTime());

// On Jan 13: booking at 14:40 on Jan 12 = -9*3600 - 20*60 = -33600 seconds
assertEquals(-(9 * 3600 + 20 * 60), jan13.requestedBookingTime());

// They must be different!
assertEquals(
86400,
jan12.requestedBookingTime() - jan13.requestedBookingTime(),
"Booking time difference should equal one day in seconds"
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import java.time.Duration;
import java.time.LocalDate;
import java.time.Month;
import java.time.ZoneId;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
Expand All @@ -34,7 +35,6 @@
import org.opentripplanner.transit.model.site.RegularStop;
import org.opentripplanner.transit.model.site.StopLocation;
import org.opentripplanner.transit.model.timetable.Trip;
import org.opentripplanner.transit.model.timetable.booking.RoutingBookingInfo;

class FlexTemplateFactoryTest {

Expand Down Expand Up @@ -62,7 +62,8 @@ class FlexTemplateFactoryTest {
private static final FlexServiceDate DATE = new FlexServiceDate(
LocalDate.of(2024, Month.MAY, 17),
SERVICE_TIME_OFFSET,
RoutingBookingInfo.NOT_SET,
null,
ZoneId.of("Europe/Oslo"),
new TIntHashSet()
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@
import org.opentripplanner.transit.model.filter.transit.TripMatcherFactory;
import org.opentripplanner.transit.model.site.StopLocation;
import org.opentripplanner.transit.model.timetable.Trip;
import org.opentripplanner.transit.model.timetable.booking.RoutingBookingInfo;
import org.opentripplanner.transit.service.TransitService;
import org.opentripplanner.utils.time.ServiceDateUtils;

Expand All @@ -59,7 +58,8 @@ public class FlexRouter {
/* Request data */
private final ZonedDateTime startOfTime;
private final int requestedTime;
private final int requestedBookingTime;
private final Instant requestedBookingTimeInstant;
private final ZoneId timeZone;
private final List<FlexServiceDate> dates;
private final Matcher<Trip> matcher;

Expand Down Expand Up @@ -117,9 +117,8 @@ public FlexRouter(
LocalDate searchDate = LocalDate.ofInstant(requestedTime, tz);
this.startOfTime = ServiceDateUtils.asStartOfService(searchDate, tz);
this.requestedTime = ServiceDateUtils.secondsSinceStartOfTime(startOfTime, requestedTime);
this.requestedBookingTime = requestedBookingTime == null
? RoutingBookingInfo.NOT_SET
: ServiceDateUtils.secondsSinceStartOfTime(startOfTime, requestedBookingTime);
this.requestedBookingTimeInstant = requestedBookingTime;
this.timeZone = tz;
this.dates = createFlexServiceDates(
transitService,
additionalPastSearchDays,
Expand Down Expand Up @@ -190,7 +189,8 @@ private List<FlexServiceDate> createFlexServiceDates(
new FlexServiceDate(
date,
ServiceDateUtils.secondsSinceStartOfTime(startOfTime, date),
requestedBookingTime,
requestedBookingTimeInstant,
timeZone,
transitService.getServiceCodesRunningForDate(date)
)
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
package org.opentripplanner.ext.flex.template;

import gnu.trove.set.TIntSet;
import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import javax.annotation.Nullable;
import org.opentripplanner.transit.model.timetable.booking.RoutingBookingInfo;
import org.opentripplanner.utils.time.ServiceDateUtils;

/**
* This class contains information used in a flex router, and depends on the date the search was
Expand All @@ -21,18 +27,39 @@ public class FlexServiceDate {
/** Which services are running on the date. */
private final TIntSet servicesRunning;

/**
* The requested booking time as seconds since the start of service for this date.
* Calculated relative to this specific service date's start-of-service.
*/
private final int requestedBookingTime;

public FlexServiceDate(
LocalDate serviceDate,
int secondsFromStartOfTime,
int requestedBookingTime,
@Nullable Instant requestedBookingTimeInstant,
ZoneId timeZone,
TIntSet servicesRunning
) {
this.serviceDate = serviceDate;
this.secondsFromStartOfTime = secondsFromStartOfTime;
this.requestedBookingTime = requestedBookingTime;
this.servicesRunning = servicesRunning;
this.requestedBookingTime = calculateRequestedBookingTime(
serviceDate,
timeZone,
requestedBookingTimeInstant
);
}

private static int calculateRequestedBookingTime(
LocalDate serviceDate,
ZoneId timeZone,
@Nullable Instant requestedBookingTimeInstant
) {
if (requestedBookingTimeInstant == null) {
return RoutingBookingInfo.NOT_SET;
}
ZonedDateTime startOfService = ServiceDateUtils.asStartOfService(serviceDate, timeZone);
return ServiceDateUtils.secondsSinceStartOfTime(startOfService, requestedBookingTimeInstant);
}

LocalDate serviceDate() {
Expand All @@ -43,6 +70,9 @@ int secondsFromStartOfTime() {
return secondsFromStartOfTime;
}

/**
* Get the requested booking time as seconds since the start of service for this date.
*/
int requestedBookingTime() {
return requestedBookingTime;
}
Expand Down
Loading