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()) ),