diff --git a/src/main/java/org/opentripplanner/middleware/utils/DateTimeUtils.java b/src/main/java/org/opentripplanner/middleware/utils/DateTimeUtils.java
index 7565d4415..7aca4b12e 100644
--- a/src/main/java/org/opentripplanner/middleware/utils/DateTimeUtils.java
+++ b/src/main/java/org/opentripplanner/middleware/utils/DateTimeUtils.java
@@ -97,6 +97,30 @@ public static String getStringFromDate(LocalDateTime localDate, String expectedD
return localDate.format(expectedDateFormat);
}
+ /**
+ * Compares two Strings, returning {@code true} if they are equal or if the only difference
+ * is between a space and a narrow no-break space (NNBSP). Intended for strings that have
+ * formatted time in the {@code en_US} locale which includes AM or PM. Older JDKs formatted
+ * these with an ASCII space ({@code U+0020}) before "AM" or "PM", but as of JDK 20 the space
+ * is replaced with NNBSP ({@code U+202F}) by default.
+ *
+ * Use of {@code null}s will return {@code false} but not throw an exception. It is intended
+ * that the first string was made using {@link DateTimeFormatter} and may have either a space
+ * or a NNBSP, whereas the second "test string" to compare it against will have a hardcoded
+ * NNBSP in it regardless of the JDK.
+ *
+ * @param str formatted text to test, e.g. {@code "6:30 PM"} or {@code "6:30\u202fPM"}
+ * @param testStr text to be tested against using NNBSP format, e.g. {@code "6:30\u202fPM"}
+ */
+ public static boolean equalsAmPm(final String str, final String testStr) {
+ if (str == null || testStr == null) {
+ return false;
+ } else {
+ // The first string in `replaceAll()` is an NNBSP, not a space.
+ return str.equals(testStr) || str.equals(testStr.replaceAll(" ", " "));
+ }
+ }
+
/**
* Helper to format a date in short format (e.g. "5:40 PM" - no seconds) in the specified locale.
*/
diff --git a/src/test/java/org/opentripplanner/middleware/tripmonitor/jobs/CheckMonitoredTripTest.java b/src/test/java/org/opentripplanner/middleware/tripmonitor/jobs/CheckMonitoredTripTest.java
index 48a32802e..190592886 100644
--- a/src/test/java/org/opentripplanner/middleware/tripmonitor/jobs/CheckMonitoredTripTest.java
+++ b/src/test/java/org/opentripplanner/middleware/tripmonitor/jobs/CheckMonitoredTripTest.java
@@ -234,7 +234,7 @@ void testDelayNotifications(
int minutesLate,
int previousMinutesLate,
NotificationType notificationType,
- String expectedNotificationPattern,
+ String expectedBody,
String message
) throws Exception {
long previousDelayMillis = TimeUnit.MILLISECONDS.convert(previousMinutesLate, TimeUnit.MINUTES);
@@ -264,12 +264,12 @@ void testDelayNotifications(
}
TripMonitorNotification notification = check.checkTripForDelays();
- if (expectedNotificationPattern == null) {
+ if (expectedBody == null) {
assertNull(notification, message);
} else {
assertNotNull(notification);
assertEquals(notificationType, notification.type);
- assertThat(message, notification.body, matchesPattern(expectedNotificationPattern));
+ assertTrue(DateTimeUtils.equalsAmPm(notification.body, expectedBody), message);
}
}
@@ -318,7 +318,7 @@ private static Stream createDelayNotificationTestCases() {
0,
NotificationType.DEPARTURE_AND_ARRIVAL_DELAY,
STOPWATCH_ICON +
- " Your trip is now predicted to depart 20 minutes late at 9:00[\\u202f ]AM \\(Now arriving at 9:18[\\u202f ]AM\\)\\.",
+ " Your trip is now predicted to depart 20 minutes late at 9:00\u202fAM (Now arriving at 9:18\u202fAM).",
"20m-late trip previously on-time => show dep/arr delay notifications"
),
Arguments.of(
@@ -326,7 +326,7 @@ private static Stream createDelayNotificationTestCases() {
0,
NotificationType.DEPARTURE_DELAY,
STOPWATCH_ICON +
- " Your trip is now predicted to depart 20 minutes late \\(at 9:00[\\u202f ]AM\\)\\.",
+ " Your trip is now predicted to depart 20 minutes late (at 9:00\u202fAM).",
"20m-late departure previously on-time, but still arriving on-time => show departure-only delay notifications"
),
Arguments.of(
@@ -334,7 +334,7 @@ private static Stream createDelayNotificationTestCases() {
0,
NotificationType.ARRIVAL_DELAY,
STOPWATCH_ICON +
- " Your trip is now predicted to arrive 20 minutes late \\(at 9:18[\\u202f ]AM\\)\\.",
+ " Your trip is now predicted to arrive 20 minutes late (at 9:18\u202fAM).",
"20m-late arrival previously on-time, but still departing on-time => show arrival-only delay notifications"
),
Arguments.of(
@@ -342,7 +342,7 @@ private static Stream createDelayNotificationTestCases() {
0,
NotificationType.DEPARTURE_AND_ARRIVAL_DELAY,
STOPWATCH_ICON +
- " Your trip is now predicted to depart 18 minutes early at 8:22[\\u202f ]AM \\(Now arriving at 8:40[\\u202f ]AM\\)\\.",
+ " Your trip is now predicted to depart 18 minutes early at 8:22\u202fAM (Now arriving at 8:40\u202fAM).",
"18m-early trip previously on-time => show delay (early) notifications"
),
Arguments.of(
@@ -357,7 +357,7 @@ private static Stream createDelayNotificationTestCases() {
15,
NotificationType.DEPARTURE_AND_ARRIVAL_DELAY,
STOPWATCH_ICON +
- " Your trip is now predicted to depart about on time at 8:40[\\u202f ]AM \\(Now arriving at 8:58[\\u202f ]AM\\)\\.",
+ " Your trip is now predicted to depart about on time at 8:40\u202fAM (Now arriving at 8:58\u202fAM).",
"On-time trip previously late => show on-time notifications"
)
);
@@ -788,16 +788,15 @@ void canSendDelayNotifications(boolean isOneTime) throws Exception {
mockTrip.itineraryExistence.tuesday = new ItineraryExistence.ItineraryExistenceResult();
List cases = List.of(
- // TODO: fix time separator char
// Add some delays for the trip.
- new DelayCase(300, 420, true, TUESDAY_20200609_0800, 1, "⏱ Your trip is now predicted to depart 5 minutes late (at 8:45 AM)."),
+ new DelayCase(300, 420, true, TUESDAY_20200609_0800, 1, "⏱ Your trip is now predicted to depart 5 minutes late (at 8:45\u202fAM)."),
// Decrease real-time delays (subtract delays) from the OTP response.
- new DelayCase(-100, -60, true, TUESDAY_20200609_0800, 1, "⏱ Your trip is now predicted to arrive 6 minutes late (at 9:04 AM)."),
+ new DelayCase(-100, -60, true, TUESDAY_20200609_0800, 1, "⏱ Your trip is now predicted to arrive 6 minutes late (at 9:04\u202fAM)."),
// Drop real-time updates (subtract delays) from the OTP response.
new DelayCase(-200, -360, false, TUESDAY_20200609_0800, 1, "⏱ Real-time updates for your trip were lost. Monitoring will be based on your originally saved trip."),
// Add back delays for the trip.
- new DelayCase(300, 420, true, TUESDAY_20200609_0800, 1, "⏱ Your trip is now predicted to depart 5 minutes late (at 8:45 AM)."),
+ new DelayCase(300, 420, true, TUESDAY_20200609_0800, 1, "⏱ Your trip is now predicted to depart 5 minutes late (at 8:45\u202fAM)."),
// Drop real-time updates and simulate a time at which the trip is considered over.
// No notifications should be sent when the trip is considered over.
new DelayCase(
diff --git a/src/test/java/org/opentripplanner/middleware/triptracker/ManageLegTraversalTest.java b/src/test/java/org/opentripplanner/middleware/triptracker/ManageLegTraversalTest.java
index 3413072cb..b8167d47a 100644
--- a/src/test/java/org/opentripplanner/middleware/triptracker/ManageLegTraversalTest.java
+++ b/src/test/java/org/opentripplanner/middleware/triptracker/ManageLegTraversalTest.java
@@ -42,6 +42,7 @@
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.opentripplanner.middleware.triptracker.ManageLegTraversal.getSecondsToMilliseconds;
import static org.opentripplanner.middleware.triptracker.ManageLegTraversal.interpolatePoints;
import static org.opentripplanner.middleware.triptracker.TravelerLocator.getNextOrClosestWayPoint;
@@ -471,7 +472,11 @@ void canTrackAtBusStop(String message, Itinerary itinerary, int currentLegIndex,
.build();
travelerPosition.locale = locale;
TripInstruction tripInstruction = TravelerLocator.getInstruction(traceData.tripStatus, travelerPosition);
- assertEquals(traceData.expectedInstruction, tripInstruction != null ? tripInstruction.build() : NO_INSTRUCTION, message);
+ if (tripInstruction == null) {
+ assertEquals(traceData.expectedInstruction, NO_INSTRUCTION, message);
+ } else {
+ assertTrue(DateTimeUtils.equalsAmPm(traceData.expectedInstruction, tripInstruction.build()), message);
+ }
// If a Gwinnett County bus notification was sent, check that the agency, route, and trip id fields are not null.
if (!travelerPosition.trackedJourney.busNotificationMessages.isEmpty() && currentLeg.route != null) {
@@ -532,7 +537,7 @@ private static Stream createBusStopTrace() {
.withPosition(busStopCoords)
.withTripStatus(TripStatus.BEHIND_SCHEDULE)
.withInstant(Instant.now())
- .withExpectedInstruction("Wait for your bus, route 20, scheduled at 7:58 AM (That time has passed)")
+ .withExpectedInstruction("Wait for your bus, route 20, scheduled at 7:58\u202fAM (That time has passed)")
),
Arguments.of(
"Arrive at bus stop well after the bus departure (indicates past departure).",
@@ -542,7 +547,7 @@ private static Stream createBusStopTrace() {
.withPosition(walkToBusTransition.legs.get(0).to.toCoordinates())
.withTripStatus(TripStatus.BEHIND_SCHEDULE)
.withInstant(Instant.now())
- .withExpectedInstruction("Wait for your bus, route 40, scheduled at 6:41 AM (That time has passed)")
+ .withExpectedInstruction("Wait for your bus, route 40, scheduled at 6:41\u202fAM (That time has passed)")
),
Arguments.of(
"Arrive at bus stop well in advance.",
@@ -552,7 +557,7 @@ private static Stream createBusStopTrace() {
.withPosition(walkToBusTransition.legs.get(0).to.toCoordinates())
.withTripStatus(TripStatus.AHEAD_OF_SCHEDULE)
.withInstant(walkToBusTransition.legs.get(1).startTime.toInstant().minus(40, ChronoUnit.MINUTES))
- .withExpectedInstruction("Wait 40 minutes for your bus, route 40, scheduled at 6:41 AM (On time)")
+ .withExpectedInstruction("Wait 40 minutes for your bus, route 40, scheduled at 6:41\u202fAM (On time)")
),
Arguments.of(
"Arrive at bus stop where the walk geometry is so the stop is farther than the last walk shape. Should produce a bus-stop-in-vicinity instruction, not 'destination in vicinity'.",
@@ -615,7 +620,7 @@ private static Stream createTransitRideTrace() {
"If present at the transit stop after the trip departure, instruct to wait (indicate past departure).",
new TraceData()
.withPosition(originCoords)
- .withExpectedInstruction("Wait for your bus, route 27, scheduled at 9:18 AM (That time has passed)")
+ .withExpectedInstruction("Wait for your bus, route 27, scheduled at 9:18\u202fAM (That time has passed)")
.withTripStatus(TripStatus.BEHIND_SCHEDULE)
.withInstant(Instant.now())
),