From d7bcc9377f8dcc37a8558bddf0a52da24344a346 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathieu=20M=C3=A9a?= Date: Tue, 24 Feb 2026 15:05:39 -0500 Subject: [PATCH 1/2] Allow ignore invalid times --- .../mtransit/parser/DefaultAgencyTools.java | 26 ++++++++++------- .../parser/config/gtfs/data/RouteConfig.kt | 14 ++++++++- .../mtransit/parser/gtfs/GAgencyTools.java | 4 ++- .../org/mtransit/parser/gtfs/data/GSpec.java | 3 ++ .../mtransit/parser/gtfs/data/GStopTime.kt | 29 ++++++++++++++----- .../parser/mt/GenerateMObjectsTask.java | 14 +++++---- 6 files changed, 66 insertions(+), 24 deletions(-) diff --git a/src/main/java/org/mtransit/parser/DefaultAgencyTools.java b/src/main/java/org/mtransit/parser/DefaultAgencyTools.java index 4ef9ef1..38c3741 100644 --- a/src/main/java/org/mtransit/parser/DefaultAgencyTools.java +++ b/src/main/java/org/mtransit/parser/DefaultAgencyTools.java @@ -591,7 +591,7 @@ public String cleanRouteLongName(@NotNull String routeLongName) { } routeLongName = Configs.getRouteConfig().cleanRouteLongName(routeLongName); if (defaultStringsCleanerEnabled()) { - return StringsCleaner.cleanRouteLongName(routeLongName, getSupportedLanguages(), lowerUCStrings(), lowerUCWords(), getIgnoreUCWords()); + return StringsCleaner.cleanRouteLongName(routeLongName, getSupportedLanguages(), lowerUCStrings(), lowerUCWords(), getIgnoreUCWords()); } return org.mtransit.commons.CleanUtils.cleanLabel(getFirstLanguageNN(), routeLongName); } @@ -612,7 +612,6 @@ public boolean lowerUCStrings() { return false; // OPT-IN feature } - @Override public @NotNull String[] getIgnoreUCWords() { if (Configs.getAgencyConfig() != null) { @@ -1284,26 +1283,31 @@ public int getThreadPoolSize() { return THREAD_POOL_SIZE; } - @NotNull + @Nullable @Override public Pair getTimes(@NotNull GStopTime gStopTime, @NotNull List tripStopTimes, @NotNull DateFormat timeFormat) { - if (!gStopTime.hasArrivalTime() || !gStopTime.hasDepartureTime()) { - return extractTimes(gStopTime, tripStopTimes, timeFormat); - } else { + if (gStopTime.hasArrivalTime() && gStopTime.hasDepartureTime()) { return new Pair<>( TimeUtils.cleanExtraSeconds(gStopTime.getArrivalTime()), TimeUtils.cleanExtraSeconds(gStopTime.getDepartureTime())); } + return extractTimes(gStopTime, tripStopTimes, timeFormat); } - @NotNull + @Override + public boolean allowIgnoreInvalidStopTimes() { + return Configs.getRouteConfig().allowIgnoreInvalidStopTimes(getTodayDateInt()); // this is bad, some transit agency data can NOT be fixed :( + } + + @Nullable private static Pair extractTimes(GStopTime gStopTime, @NotNull List tripStopTimes, DateFormat timeFormat) { try { Pair timesInMs = extractTimeInMs(gStopTime, tripStopTimes); + if (timesInMs == null) return null; long arrivalTimeInMs = timesInMs.first; Calendar calendar = Calendar.getInstance(); calendar.setTimeInMillis(arrivalTimeInMs); @@ -1326,7 +1330,7 @@ private static Pair extractTimes(GStopTime gStopTime, } } - @NotNull + @Nullable public static Pair extractTimeInMs(@NotNull GStopTime gStopTime, @NotNull List tripGStopTimes) { int previousArrivalTime = -1; @@ -1393,7 +1397,8 @@ public static Pair extractTimeInMs(@NotNull GStopTime gStopTime, MTLog.log("- %s", aStopTime.toStringPlus(true)); } //noinspection DiscouragedApi - throw new MTLog.Fatal("Invalid stop time trip ID '%s' > no previous stop for %s!", gStopTime.getTripId(), gStopTime.toStringPlus(true)); + MTLog.log("Invalid stop time trip ID '%s' > no previous stop for %s!", gStopTime.getTripId(), gStopTime.toStringPlus(true)); + return null; } long previousArrivalTimeInMs = GTime.toMs(previousArrivalTime); long previousDepartureTimeInMs = GTime.toMs(previousDepartureTime); @@ -1404,7 +1409,8 @@ public static Pair extractTimeInMs(@NotNull GStopTime gStopTime, MTLog.log("- %s", aStopTime.toStringPlus(true)); } //noinspection DiscouragedApi - throw new MTLog.Fatal("Invalid stop time trip ID '%s' > no next stop for %s!", gStopTime.getTripId(), gStopTime.toStringPlus(true)); + MTLog.log("Invalid stop time trip ID '%s' > no next stop for %s!", gStopTime.getTripId(), gStopTime.toStringPlus(true)); + return null; } long nextArrivalTimeInMs = GTime.toMs(nextArrivalTime); long nextDepartureTimeInMs = GTime.toMs(nextDepartureTime); diff --git a/src/main/java/org/mtransit/parser/config/gtfs/data/RouteConfig.kt b/src/main/java/org/mtransit/parser/config/gtfs/data/RouteConfig.kt index 41613a3..d2b5f0f 100644 --- a/src/main/java/org/mtransit/parser/config/gtfs/data/RouteConfig.kt +++ b/src/main/java/org/mtransit/parser/config/gtfs/data/RouteConfig.kt @@ -79,7 +79,11 @@ data class RouteConfig( val stopHeadsignRemoveRouteLongName: Boolean = false, // OPT-IN feature @SerialName("stop_headsign_cleanup_regex") val stopHeadsignCleanupRegex: String? = null, // optional - + // STOP TIMES + @SerialName("allow_invalid_stop_times") + val allowInvalidStopTimes: Boolean = false, // OPT-IN feature + @SerialName("allow_invalid_stop_times_until") + val allowInvalidStopTimesUntil: String? = null, // OPT-IN feature ) { @Serializable @@ -215,4 +219,12 @@ data class RouteConfig( } return string } + + fun allowIgnoreInvalidStopTimes(todayDate: Int): Boolean { + if (allowInvalidStopTimes) return true // ALLOW + allowInvalidStopTimesUntil?.toIntOrNull()?.let { until -> + if (todayDate <= until) return true // ALLOW + } + return false // DO NOT allow + } } diff --git a/src/main/java/org/mtransit/parser/gtfs/GAgencyTools.java b/src/main/java/org/mtransit/parser/gtfs/GAgencyTools.java index cfb8939..92bbac0 100644 --- a/src/main/java/org/mtransit/parser/gtfs/GAgencyTools.java +++ b/src/main/java/org/mtransit/parser/gtfs/GAgencyTools.java @@ -326,11 +326,13 @@ public interface GAgencyTools { boolean excludeCalendarDate(@NotNull GCalendarDate gCalendarDate); // SCHEDULE - @NotNull + @Nullable Pair getTimes(@NotNull GStopTime gStopTime, @NotNull List tripStopTimes, @NotNull DateFormat timeFormat); + boolean allowIgnoreInvalidStopTimes(); + // FREQUENCY int getStartTime(@NotNull GFrequency gFrequency); diff --git a/src/main/java/org/mtransit/parser/gtfs/data/GSpec.java b/src/main/java/org/mtransit/parser/gtfs/data/GSpec.java index ae295df..01822d3 100644 --- a/src/main/java/org/mtransit/parser/gtfs/data/GSpec.java +++ b/src/main/java/org/mtransit/parser/gtfs/data/GSpec.java @@ -798,6 +798,9 @@ private void setDepartureTimeCal(@NotNull Calendar calendar, calendar.setTimeInMillis(gStopTime.getDepartureTimeMs()); } else { final Pair arrivalAndDeparture = DefaultAgencyTools.extractTimeInMs(gStopTime, tripStopTimes); + if (arrivalAndDeparture == null) { + throw new MTLog.Fatal("setDepartureTimeCal() > cannot extract arrival & departure time from '%s'!", gStopTime); + } final long departureTimeInMs = arrivalAndDeparture.second; calendar.setTimeInMillis(departureTimeInMs); } diff --git a/src/main/java/org/mtransit/parser/gtfs/data/GStopTime.kt b/src/main/java/org/mtransit/parser/gtfs/data/GStopTime.kt index 3d1620f..f060427 100644 --- a/src/main/java/org/mtransit/parser/gtfs/data/GStopTime.kt +++ b/src/main/java/org/mtransit/parser/gtfs/data/GStopTime.kt @@ -157,13 +157,28 @@ data class GStopTime( @JvmOverloads @Suppress("unused") - fun toStringPlus(debug: Boolean = Constants.DEBUG): String { - return if (debug) { // longer - return toString() + - "+(tripId:$_tripId)" + - "+(stopId:$_stopId)" - } else { // shorter #CI - "{t:$_tripId,s:$_stopId,#:$stopSequence}" + fun toStringPlus(debug: Boolean = Constants.DEBUG) = if (debug) { // longer + toString() + + "+(tripId:$_tripId)" + + "+(stopId:$_stopId)" + } else { // shorter #CI + buildString { + append("{") + append("t:$_tripId,") + append("s:$_stopId,") + append("#:$stopSequence,") + if (hasDepartureTime()) { + append("d:${GTime.toString(_departureTime)},") + } else if (hasArrivalTime()) { + append("a:${GTime.toString(_arrivalTime)},") + } + if (pickupType != GPickupType.REGULAR) { + append("$pickupType,") + } + if (dropOffType != GDropOffType.REGULAR) { + append("$dropOffType,") + } + append("}") } } diff --git a/src/main/java/org/mtransit/parser/mt/GenerateMObjectsTask.java b/src/main/java/org/mtransit/parser/mt/GenerateMObjectsTask.java index bf9378e..26dbb0d 100644 --- a/src/main/java/org/mtransit/parser/mt/GenerateMObjectsTask.java +++ b/src/main/java/org/mtransit/parser/mt/GenerateMObjectsTask.java @@ -772,16 +772,20 @@ private String parseGStopTimes(HashMap mSchedules, // TODO later, when UI can display multiple times same stop/POI & schedules are affected to a specific sequence, keep both } } + final Pair times = this.agencyTools.getTimes(gStopTime, gTripStopTimes, TIME_FORMAT); + if (times == null) { + if (this.agencyTools.allowIgnoreInvalidStopTimes()) { + continue; // this is bad, some transit agency data can NOT be fixed :( + } else { + throw new MTLog.Fatal("Invalid stop time '%s'!", gStopTime.toStringPlus()); + } + } mSchedule = new MSchedule( this.routeId, serviceIdInt, mDirectionId, mStopId, - this.agencyTools.getTimes( - gStopTime, - gTripStopTimes, - TIME_FORMAT - ), + times, gStopTime.getTripIdInt(), gTrip.getWheelchairAccessible().getId() ); From 6130d6c5a415c348be83d02578b9c81cc0dad6f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathieu=20M=C3=A9a?= Date: Tue, 24 Feb 2026 15:11:53 -0500 Subject: [PATCH 2/2] PR comments --- .../parser/config/gtfs/data/RouteConfig.kt | 9 ++------- .../org/mtransit/parser/gtfs/data/GSpec.java | 2 +- .../mtransit/parser/gtfs/data/GStopTime.kt | 20 +++++++++---------- 3 files changed, 12 insertions(+), 19 deletions(-) diff --git a/src/main/java/org/mtransit/parser/config/gtfs/data/RouteConfig.kt b/src/main/java/org/mtransit/parser/config/gtfs/data/RouteConfig.kt index d2b5f0f..afd6478 100644 --- a/src/main/java/org/mtransit/parser/config/gtfs/data/RouteConfig.kt +++ b/src/main/java/org/mtransit/parser/config/gtfs/data/RouteConfig.kt @@ -220,11 +220,6 @@ data class RouteConfig( return string } - fun allowIgnoreInvalidStopTimes(todayDate: Int): Boolean { - if (allowInvalidStopTimes) return true // ALLOW - allowInvalidStopTimesUntil?.toIntOrNull()?.let { until -> - if (todayDate <= until) return true // ALLOW - } - return false // DO NOT allow - } + fun allowIgnoreInvalidStopTimes(todayDate: Int) = + allowInvalidStopTimes || (allowInvalidStopTimesUntil?.toIntOrNull()?.let { todayDate <= it } ?: false) } diff --git a/src/main/java/org/mtransit/parser/gtfs/data/GSpec.java b/src/main/java/org/mtransit/parser/gtfs/data/GSpec.java index 01822d3..a3cebeb 100644 --- a/src/main/java/org/mtransit/parser/gtfs/data/GSpec.java +++ b/src/main/java/org/mtransit/parser/gtfs/data/GSpec.java @@ -799,7 +799,7 @@ private void setDepartureTimeCal(@NotNull Calendar calendar, } else { final Pair arrivalAndDeparture = DefaultAgencyTools.extractTimeInMs(gStopTime, tripStopTimes); if (arrivalAndDeparture == null) { - throw new MTLog.Fatal("setDepartureTimeCal() > cannot extract arrival & departure time from '%s'!", gStopTime); + throw new MTLog.Fatal("setDepartureTimeCal() > cannot extract arrival & departure time from '%s'!", gStopTime.toStringPlus(true)); } final long departureTimeInMs = arrivalAndDeparture.second; calendar.setTimeInMillis(departureTimeInMs); diff --git a/src/main/java/org/mtransit/parser/gtfs/data/GStopTime.kt b/src/main/java/org/mtransit/parser/gtfs/data/GStopTime.kt index f060427..630867e 100644 --- a/src/main/java/org/mtransit/parser/gtfs/data/GStopTime.kt +++ b/src/main/java/org/mtransit/parser/gtfs/data/GStopTime.kt @@ -162,24 +162,22 @@ data class GStopTime( "+(tripId:$_tripId)" + "+(stopId:$_stopId)" } else { // shorter #CI - buildString { - append("{") - append("t:$_tripId,") - append("s:$_stopId,") - append("#:$stopSequence,") + buildList { + add("t:$_tripId") + add("s:$_stopId") + add("#:$stopSequence") if (hasDepartureTime()) { - append("d:${GTime.toString(_departureTime)},") + add("d:${GTime.toString(_departureTime)}") } else if (hasArrivalTime()) { - append("a:${GTime.toString(_arrivalTime)},") + add("a:${GTime.toString(_arrivalTime)}") } if (pickupType != GPickupType.REGULAR) { - append("$pickupType,") + add("$pickupType") } if (dropOffType != GDropOffType.REGULAR) { - append("$dropOffType,") + add("$dropOffType") } - append("}") - } + }.joinToString(separator = ",", prefix = "{", postfix = "}") } fun to() = StopTime(