From 1d2f132cb218b131469553bead4f0e2326d05e79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathieu=20M=C3=A9a?= Date: Tue, 7 Jul 2026 13:44:49 -0400 Subject: [PATCH 1/3] `JSON` > + `service_id_avoid_merge`... --- .../mtransit/parser/DefaultAgencyTools.java | 167 ++++++++++++------ src/main/java/org/mtransit/parser/Period.kt | 17 +- .../parser/config/gtfs/data/AgencyConfig.kt | 2 + .../mtransit/parser/gtfs/GAgencyTools.java | 4 +- 4 files changed, 124 insertions(+), 66 deletions(-) diff --git a/src/main/java/org/mtransit/parser/DefaultAgencyTools.java b/src/main/java/org/mtransit/parser/DefaultAgencyTools.java index 8747108b..cd82b84d 100644 --- a/src/main/java/org/mtransit/parser/DefaultAgencyTools.java +++ b/src/main/java/org/mtransit/parser/DefaultAgencyTools.java @@ -85,7 +85,7 @@ public class DefaultAgencyTools implements GAgencyTools { // 2024-04-09: 10x -> 3x because merging 2 schedule can create very bad schedule info #GRTbus private static final long MAX_CALENDAR_DATE_COVERAGE_RATIO = 3; - private static final int MIN_PREVIOUS_NEXT_ADDED_DAYS = 2; // = 1 day + private static final int MIN_PREVIOUS_NEXT_ADDED_DAYS = 1; @SuppressWarnings("WeakerAccess") public static final boolean GOOD_ENOUGH_ACCEPTED; @@ -457,6 +457,41 @@ public String cleanServiceId(@NotNull String gServiceId, boolean keep) { return cleanServiceId; } + /** + * When using only `calendar_dates.txt`, we might want to avoid adding 1 more service day if it adds a whole new "service ID period". + * For example, we can have many service IDs starting with YYMMDD-abc-123 with 2 distinct groups like 26JN29-... and 26AU03-.... + */ + private static boolean avoidServiceIdMerge(@NotNull Collection currentServiceIdInts, @NotNull Collection newServiceIdInts) { + final boolean avoidServiceIdMerge = Configs.getAgencyConfig() != null && Configs.getAgencyConfig().getServiceIdAvoidMerge(); + if (!avoidServiceIdMerge) return true; // can merge + final boolean serviceIdCleanMerged = Configs.getAgencyConfig() != null && Configs.getAgencyConfig().getServiceIdCleanMerged(); + final String serviceIdCleanupRegex = Configs.getAgencyConfig() == null ? null : Configs.getAgencyConfig().getServiceIdCleanupRegex(); + if (!serviceIdCleanMerged && (serviceIdCleanupRegex == null || serviceIdCleanupRegex.isBlank())) return true; // can merge + final Pattern serviceIdCleanupPattern = serviceIdCleanupRegex == null ? null : Pattern.compile(serviceIdCleanupRegex); + for (Integer newServiceIdInt : newServiceIdInts) { + String newServiceId = GIDs.getString(newServiceIdInt); + if (serviceIdCleanupPattern != null) { + newServiceId = GTFSCommons.cleanOriginalString(newServiceId, serviceIdCleanupPattern); + } + if (serviceIdCleanMerged) { + newServiceId = CleanUtils.cleanMergedID(newServiceId); + } + for (Integer currentServiceIdInt : currentServiceIdInts) { + String currentServiceId = GIDs.getString(currentServiceIdInt); + if (serviceIdCleanupPattern != null) { + currentServiceId = GTFSCommons.cleanOriginalString(currentServiceId, serviceIdCleanupPattern); + } + if (serviceIdCleanMerged) { + currentServiceId = CleanUtils.cleanMergedID(currentServiceId); + } + if (newServiceId.equals(currentServiceId)) { + return false; // can not merge + } + } + } + return true; // can merge + } + @Nullable @Override public String getServiceIdCleanupRegex() { @@ -964,6 +999,7 @@ public boolean removeRouteDescFromDirectionHeadsign() { directionHeadSign = ""; } } + //noinspection deprecation return cleanDirectionHeadsign(gRoute, directionId, fromStopName, directionHeadSign); } @@ -1515,9 +1551,7 @@ public int getThreadPoolSize() { @Nullable @Override - public Pair getTimes(@NotNull GStopTime gStopTime, - @NotNull List tripStopTimes, - @NotNull DateFormat timeFormat) { + public Pair getTimes(@NotNull GStopTime gStopTime, @NotNull List tripStopTimes, @NotNull DateFormat timeFormat) { if (gStopTime.hasArrivalTime() && gStopTime.hasDepartureTime()) { return new Pair<>( TimeUtils.cleanExtraSeconds(gStopTime.getArrivalTime()), @@ -1532,9 +1566,7 @@ public boolean allowIgnoreInvalidStopTimes() { } @Nullable - private static Pair extractTimes(GStopTime gStopTime, - @NotNull List tripStopTimes, - DateFormat timeFormat) { + private static Pair extractTimes(GStopTime gStopTime, @NotNull List tripStopTimes, DateFormat timeFormat) { try { Pair timesInMs = extractTimeInMs(gStopTime, tripStopTimes); if (timesInMs == null) return null; @@ -1561,8 +1593,7 @@ private static Pair extractTimes(GStopTime gStopTime, } @Nullable - public static Pair extractTimeInMs(@NotNull GStopTime gStopTime, - @NotNull List tripGStopTimes) { + public static Pair extractTimeInMs(@NotNull GStopTime gStopTime, @NotNull List tripGStopTimes) { int previousArrivalTime = -1; int previousArrivalTimeStopSequence = -1; int previousDepartureTime = -1; @@ -1815,48 +1846,52 @@ static void parseCalendarDates( boolean lookBackward, @Nullable List lastServiceDates ) { - HashSet todayServiceIds = findCalendarDatesTodayServiceIds(gCalendarDates, c, p, lookBackward, 1, true); - if (todayServiceIds.isEmpty()) { + final HashSet todayServiceIdInts = findCalendarDatesPeriodServiceIdInts(gCalendarDates, c, p, lookBackward, 1, true); + if (todayServiceIdInts.isEmpty()) { MTLog.log("NO schedule available for %s in calendar dates.", p.getTodayStringInt()); return; } - refreshStartEndDatesFromCalendarDates(p, todayServiceIds, gCalendarDates); + refreshPeriodStartEndDatesFromCalendarDates(p, todayServiceIdInts, gCalendarDates); while (true) { MTLog.log("> Schedules from %s to %s... ", p.getStartDate(), p.getEndDate()); for (GCalendarDate gCalendarDate : gCalendarDates) { if (gCalendarDate.isBetween(p.getStartDate(), p.getEndDate()) - && !gCalendarDate.isServiceIdInts(todayServiceIds)) { + && !gCalendarDate.isServiceIdInts(todayServiceIdInts)) { //noinspection DiscouragedApi MTLog.log("> new service ID from calendar date active on %s between %s and %s: '%s'", gCalendarDate.getDate(), p.getStartDate(), p.getEndDate(), gCalendarDate.getServiceId()); - todayServiceIds.add(gCalendarDate.getServiceIdInt()); + todayServiceIdInts.add(gCalendarDate.getServiceIdInt()); } } - boolean newDates = refreshStartEndDatesFromCalendarDates(p, todayServiceIds, gCalendarDates); + final boolean newDates = refreshPeriodStartEndDatesFromCalendarDates(p, todayServiceIdInts, gCalendarDates); if (newDates) { MTLog.log("> new start date '%s' & end date '%s' from calendar date active during service ID(s).", p.getStartDate(), p.getEndDate()); continue; } - Period pNext = new Period(); - pNext.setTodayStringInt(incDateDays(DATE_FORMAT, c, p.getEndDate(), 1)); - HashSet nextDayServiceIds = findCalendarDatesTodayServiceIds(gCalendarDates, c, pNext, false, 0, false); - refreshStartEndDatesFromCalendarDates(pNext, nextDayServiceIds, gCalendarDates); - if (pNext.getStartDate() != null && pNext.getEndDate() != null - && diffLowerThan(DATE_FORMAT, c, pNext.getStartDate(), pNext.getEndDate(), MIN_PREVIOUS_NEXT_ADDED_DAYS)) { - p.setEndDate(pNext.getEndDate()); - MTLog.log("> new end date '%s' because next %d day has own service ID(s)", p.getEndDate(), MIN_PREVIOUS_NEXT_ADDED_DAYS); + final Period nextDayPeriod = Period.from(incDateDays(DATE_FORMAT, c, p.getEndDate(), 1)); + final HashSet nextDayServiceIdInts = findCalendarDatesPeriodServiceIdInts(gCalendarDates, c, nextDayPeriod, false, 0, false); + refreshPeriodStartEndDatesFromCalendarDates(nextDayPeriod, nextDayServiceIdInts, gCalendarDates); + if (nextDayPeriod.getStartDate() != null && nextDayPeriod.getEndDate() != null + && diffLowerOrEqual(DATE_FORMAT, c, nextDayPeriod.getStartDate(), nextDayPeriod.getEndDate(), MIN_PREVIOUS_NEXT_ADDED_DAYS) + && avoidServiceIdMerge(todayServiceIdInts, nextDayServiceIdInts)) { + p.setEndDate(nextDayPeriod.getEndDate()); + MTLog.log("> new end date '%s' because next %d day(s) has own service ID(s)", p.getEndDate(), MIN_PREVIOUS_NEXT_ADDED_DAYS); continue; } - Period pPrevious = new Period(); - pPrevious.setTodayStringInt(incDateDays(DATE_FORMAT, c, p.getStartDate(), -1)); - HashSet previousDayServiceIds = findCalendarDatesTodayServiceIds(gCalendarDates, c, pPrevious, false, 0, false); - refreshStartEndDatesFromCalendarDates(pPrevious, previousDayServiceIds, gCalendarDates); + final Period previousDayPeriod = Period.from(incDateDays(DATE_FORMAT, c, p.getStartDate(), -1)); + final HashSet previousDayPeriodServiceIdInts = findCalendarDatesPeriodServiceIdInts(gCalendarDates, c, previousDayPeriod, false, 0, false); + refreshPeriodStartEndDatesFromCalendarDates( + previousDayPeriod, + previousDayPeriodServiceIdInts, + gCalendarDates + ); if (lookBackward // NOT next schedule, only current schedule can look behind - && pPrevious.getStartDate() != null && pPrevious.getEndDate() != null - && diffLowerThan(DATE_FORMAT, c, pPrevious.getStartDate(), pPrevious.getEndDate(), MIN_PREVIOUS_NEXT_ADDED_DAYS) - && MServiceDate.containsAllServiceIdInts(lastServiceDates, previousDayServiceIds) // 2026-01-29: only look backward if min calendar date coverage is not reached or in previous schedule + && previousDayPeriod.getStartDate() != null && previousDayPeriod.getEndDate() != null + && diffLowerOrEqual(DATE_FORMAT, c, previousDayPeriod.getStartDate(), previousDayPeriod.getEndDate(), MIN_PREVIOUS_NEXT_ADDED_DAYS) + && MServiceDate.containsAllServiceIdInts(lastServiceDates, previousDayPeriodServiceIdInts) // 2026-01-29: only look backward if min calendar date coverage is not reached or in previous schedule + && avoidServiceIdMerge(todayServiceIdInts, previousDayPeriodServiceIdInts) ) { - p.setStartDate(pPrevious.getStartDate()); - MTLog.log("> new start date '%s' because previous %d day has own service ID(s)", p.getStartDate(), MIN_PREVIOUS_NEXT_ADDED_DAYS); + p.setStartDate(previousDayPeriod.getStartDate()); + MTLog.log("> new start date '%s' because previous %d day(s) has own service ID(s)", p.getStartDate(), MIN_PREVIOUS_NEXT_ADDED_DAYS); continue; } if (diffLowerThan(DATE_FORMAT, c, p.getStartDate(), p.getEndDate(), MIN_CALENDAR_DATE_COVERAGE_TOTAL_IN_DAYS)) { @@ -1865,15 +1900,15 @@ && diffLowerThan(DATE_FORMAT, c, pPrevious.getStartDate(), pPrevious.getEndDate( p.getStartDate(), // morning p.getEndDate() + 1 // midnight ≈ tomorrow ); - long nextPeriodCoverageInMs = pNext.getStartDate() == null || pNext.getEndDate() == null ? 0L : + long nextPeriodCoverageInMs = nextDayPeriod.getStartDate() == null || nextDayPeriod.getEndDate() == null ? 0L : diffInMs(DATE_FORMAT, c, - pNext.getStartDate(), // morning - pNext.getEndDate() + 1 // midnight ≈ tomorrow + nextDayPeriod.getStartDate(), // morning + nextDayPeriod.getEndDate() + 1 // midnight ≈ tomorrow ); - long previousPeriodCoverageInMs = pPrevious.getStartDate() == null || pPrevious.getEndDate() == null ? 0L : + long previousPeriodCoverageInMs = previousDayPeriod.getStartDate() == null || previousDayPeriod.getEndDate() == null ? 0L : diffInMs(DATE_FORMAT, c, - pPrevious.getStartDate(), // morning - pPrevious.getEndDate() + 1 // midnight ≈ tomorrow + previousDayPeriod.getStartDate(), // morning + previousDayPeriod.getEndDate() + 1 // midnight ≈ tomorrow ); long previousToCurrent = previousPeriodCoverageInMs / currentPeriodCoverageInMs; long nextToCurrent = nextPeriodCoverageInMs / currentPeriodCoverageInMs; @@ -1897,7 +1932,7 @@ && diffLowerThan(DATE_FORMAT, c, pPrevious.getStartDate(), pPrevious.getEndDate( } } - private static boolean refreshStartEndDatesFromCalendarDates( + private static boolean refreshPeriodStartEndDatesFromCalendarDates( @NotNull Period p, @NotNull Collection serviceIds, @NotNull List gCalendarDates @@ -1918,12 +1953,14 @@ private static boolean refreshStartEndDatesFromCalendarDates( } @NotNull - private static HashSet findCalendarDatesTodayServiceIds(@NotNull List gCalendarDates, - Calendar c, - Period p, - boolean lookBackward, - int minTodayServiceIdsSize, - boolean canIncDays) { + private static HashSet findCalendarDatesPeriodServiceIdInts( + @NotNull List gCalendarDates, + Calendar c, + Period p, + boolean lookBackward, + int minTodayServiceIdsSize, + boolean canIncDays + ) { final HashSet todayServiceIds = new HashSet<>(); final Integer initialTodayStringInt = p.getTodayStringInt(); while (true) { @@ -1990,9 +2027,10 @@ static void parseCalendars(@NotNull List gCalendars, @Nullable List new end date '%s' because next day has own service ID(s)", p.getEndDate()); + MTLog.log("[parse-calendars] > new end date '%s' because next day(s) has own service ID(s)", p.getEndDate()); continue; } else if (pNext.getStartDate() != null && pNext.getEndDate() != null) { MTLog.logDebug("[parse-calendars] > ignore next period because coverage '%s' not < %s days: %s", @@ -2211,10 +2249,7 @@ private static Period getEntirePeriodMinMaxDate(List gCalendars, List return p; } - public static int incDateDays(@NotNull DateFormat dateFormat, - @NotNull Calendar calendar, - @Nullable Integer dateInt, - int numberOfDays) { + public static int incDateDays(@NotNull DateFormat dateFormat, @NotNull Calendar calendar, @Nullable Integer dateInt, int numberOfDays) { try { calendar.setTime(dateFormat.parse(String.valueOf(dateInt))); calendar.add(Calendar.DAY_OF_MONTH, numberOfDays); @@ -2225,11 +2260,28 @@ public static int incDateDays(@NotNull DateFormat dateFormat, } @SuppressWarnings("WeakerAccess") - public static boolean diffLowerThan(@NotNull DateFormat dateFormat, - @NotNull Calendar calendar, - @Nullable Integer startDateInt, - @Nullable Integer endDateInt, - int diffInDays) { + public static boolean diffLowerOrEqual( + @NotNull DateFormat dateFormat, + @NotNull Calendar calendar, + @Nullable Integer startDateInt, + @Nullable Integer endDateInt, + int diffInDays + ) { + try { + return diffInMs(dateFormat, calendar, startDateInt, endDateInt) <= TimeUnit.DAYS.toMillis(diffInDays); + } catch (Exception e) { + throw new MTLog.Fatal(e, "Error while checking date difference!"); + } + } + + @SuppressWarnings("WeakerAccess") + public static boolean diffLowerThan( + @NotNull DateFormat dateFormat, + @NotNull Calendar calendar, + @Nullable Integer startDateInt, + @Nullable Integer endDateInt, + int diffInDays + ) { try { return diffInMs(dateFormat, calendar, startDateInt, endDateInt) < TimeUnit.DAYS.toMillis(diffInDays); } catch (Exception e) { @@ -2289,8 +2341,7 @@ protected static boolean excludeUselessCalendarDateInt(@NotNull GCalendarDate gC } @SuppressWarnings("WeakerAccess") - protected static boolean excludeUselessTripInt(@NotNull GTrip gTrip, - @Nullable HashSet serviceIds) { + protected static boolean excludeUselessTripInt(@NotNull GTrip gTrip, @Nullable HashSet serviceIds) { if (serviceIds != null) { boolean knownServiceId = gTrip.isServiceIdInts(serviceIds); //noinspection RedundantIfStatement diff --git a/src/main/java/org/mtransit/parser/Period.kt b/src/main/java/org/mtransit/parser/Period.kt index cdeeca90..a0cfd6bb 100644 --- a/src/main/java/org/mtransit/parser/Period.kt +++ b/src/main/java/org/mtransit/parser/Period.kt @@ -1,9 +1,16 @@ package org.mtransit.parser -data class Period( - var todayStringInt: Int?, - var startDate: Int?, - var endDate: Int?, +data class Period @JvmOverloads constructor( + var todayStringInt: Int? = null, + var startDate: Int? = null, + var endDate: Int? = null, ) { - constructor() : this(null, null, null) + companion object { + @JvmStatic + fun from(today: Int) = Period( + todayStringInt = today, + startDate = null, + endDate = null + ) + } } diff --git a/src/main/java/org/mtransit/parser/config/gtfs/data/AgencyConfig.kt b/src/main/java/org/mtransit/parser/config/gtfs/data/AgencyConfig.kt index 4281a6fe..fd24bfd9 100644 --- a/src/main/java/org/mtransit/parser/config/gtfs/data/AgencyConfig.kt +++ b/src/main/java/org/mtransit/parser/config/gtfs/data/AgencyConfig.kt @@ -63,6 +63,8 @@ data class AgencyConfig( val serviceIdCleanMerged: Boolean = false, // OPT-IN feature @SerialName("service_id_not_unique_allowed") val serviceIdNotUniqueAllowed: Boolean = false, // OPT-IN feature + @SerialName("service_id_avoid_merge") + val serviceIdAvoidMerge: Boolean = false, // OPT-IN feature ) { fun getAllLanguages(supportedLanguages: List): List? { if (supportedLanguages.isEmpty()) return null diff --git a/src/main/java/org/mtransit/parser/gtfs/GAgencyTools.java b/src/main/java/org/mtransit/parser/gtfs/GAgencyTools.java index 12e39a83..a45fb2d7 100644 --- a/src/main/java/org/mtransit/parser/gtfs/GAgencyTools.java +++ b/src/main/java/org/mtransit/parser/gtfs/GAgencyTools.java @@ -353,9 +353,7 @@ public interface GAgencyTools { // SCHEDULE @Nullable - Pair getTimes(@NotNull GStopTime gStopTime, - @NotNull List tripStopTimes, - @NotNull DateFormat timeFormat); + Pair getTimes(@NotNull GStopTime gStopTime, @NotNull List tripStopTimes, @NotNull DateFormat timeFormat); boolean allowIgnoreInvalidStopTimes(); From 28cbd71a87449768214c4e80151a7c14120d88d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathieu=20M=C3=A9a?= Date: Tue, 7 Jul 2026 13:55:10 -0400 Subject: [PATCH 2/3] PR comments --- .../mtransit/parser/DefaultAgencyTools.java | 40 ++++++++++--------- 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/src/main/java/org/mtransit/parser/DefaultAgencyTools.java b/src/main/java/org/mtransit/parser/DefaultAgencyTools.java index cd82b84d..156d3d84 100644 --- a/src/main/java/org/mtransit/parser/DefaultAgencyTools.java +++ b/src/main/java/org/mtransit/parser/DefaultAgencyTools.java @@ -11,6 +11,7 @@ import org.mtransit.commons.GTFSCommons; import org.mtransit.commons.StringsCleaner; import org.mtransit.parser.config.Configs; +import org.mtransit.parser.config.gtfs.data.AgencyConfig; import org.mtransit.parser.gtfs.GAgencyTools; import org.mtransit.parser.gtfs.GReader; import org.mtransit.parser.gtfs.data.GAgency; @@ -461,13 +462,25 @@ public String cleanServiceId(@NotNull String gServiceId, boolean keep) { * When using only `calendar_dates.txt`, we might want to avoid adding 1 more service day if it adds a whole new "service ID period". * For example, we can have many service IDs starting with YYMMDD-abc-123 with 2 distinct groups like 26JN29-... and 26AU03-.... */ - private static boolean avoidServiceIdMerge(@NotNull Collection currentServiceIdInts, @NotNull Collection newServiceIdInts) { - final boolean avoidServiceIdMerge = Configs.getAgencyConfig() != null && Configs.getAgencyConfig().getServiceIdAvoidMerge(); - if (!avoidServiceIdMerge) return true; // can merge - final boolean serviceIdCleanMerged = Configs.getAgencyConfig() != null && Configs.getAgencyConfig().getServiceIdCleanMerged(); - final String serviceIdCleanupRegex = Configs.getAgencyConfig() == null ? null : Configs.getAgencyConfig().getServiceIdCleanupRegex(); + private static boolean canMergeServiceIds(@NotNull Collection currentServiceIdInts, @NotNull Collection newServiceIdInts) { + final AgencyConfig agencyConfig = Configs.getAgencyConfig(); + if (agencyConfig == null || !agencyConfig.getServiceIdAvoidMerge()) return true; // can merge + final boolean serviceIdCleanMerged = agencyConfig.getServiceIdCleanMerged(); + final String serviceIdCleanupRegex = agencyConfig.getServiceIdCleanupRegex(); if (!serviceIdCleanMerged && (serviceIdCleanupRegex == null || serviceIdCleanupRegex.isBlank())) return true; // can merge final Pattern serviceIdCleanupPattern = serviceIdCleanupRegex == null ? null : Pattern.compile(serviceIdCleanupRegex); + // make current set of cleaned service IDs + final Set cleanCurrentServiceIds = new HashSet<>(); + for (Integer currentServiceIdInt : currentServiceIdInts) { + String currentServiceId = GIDs.getString(currentServiceIdInt); + if (serviceIdCleanupPattern != null) { + currentServiceId = GTFSCommons.cleanOriginalString(currentServiceId, serviceIdCleanupPattern); + } + if (serviceIdCleanMerged) { + currentServiceId = CleanUtils.cleanMergedID(currentServiceId); + } + cleanCurrentServiceIds.add(currentServiceId); + } for (Integer newServiceIdInt : newServiceIdInts) { String newServiceId = GIDs.getString(newServiceIdInt); if (serviceIdCleanupPattern != null) { @@ -476,17 +489,8 @@ private static boolean avoidServiceIdMerge(@NotNull Collection currentS if (serviceIdCleanMerged) { newServiceId = CleanUtils.cleanMergedID(newServiceId); } - for (Integer currentServiceIdInt : currentServiceIdInts) { - String currentServiceId = GIDs.getString(currentServiceIdInt); - if (serviceIdCleanupPattern != null) { - currentServiceId = GTFSCommons.cleanOriginalString(currentServiceId, serviceIdCleanupPattern); - } - if (serviceIdCleanMerged) { - currentServiceId = CleanUtils.cleanMergedID(currentServiceId); - } - if (newServiceId.equals(currentServiceId)) { - return false; // can not merge - } + if (cleanCurrentServiceIds.contains(newServiceId)) { + return false; // can not merge } } return true; // can merge @@ -1872,7 +1876,7 @@ static void parseCalendarDates( refreshPeriodStartEndDatesFromCalendarDates(nextDayPeriod, nextDayServiceIdInts, gCalendarDates); if (nextDayPeriod.getStartDate() != null && nextDayPeriod.getEndDate() != null && diffLowerOrEqual(DATE_FORMAT, c, nextDayPeriod.getStartDate(), nextDayPeriod.getEndDate(), MIN_PREVIOUS_NEXT_ADDED_DAYS) - && avoidServiceIdMerge(todayServiceIdInts, nextDayServiceIdInts)) { + && canMergeServiceIds(todayServiceIdInts, nextDayServiceIdInts)) { p.setEndDate(nextDayPeriod.getEndDate()); MTLog.log("> new end date '%s' because next %d day(s) has own service ID(s)", p.getEndDate(), MIN_PREVIOUS_NEXT_ADDED_DAYS); continue; @@ -1888,7 +1892,7 @@ && avoidServiceIdMerge(todayServiceIdInts, nextDayServiceIdInts)) { && previousDayPeriod.getStartDate() != null && previousDayPeriod.getEndDate() != null && diffLowerOrEqual(DATE_FORMAT, c, previousDayPeriod.getStartDate(), previousDayPeriod.getEndDate(), MIN_PREVIOUS_NEXT_ADDED_DAYS) && MServiceDate.containsAllServiceIdInts(lastServiceDates, previousDayPeriodServiceIdInts) // 2026-01-29: only look backward if min calendar date coverage is not reached or in previous schedule - && avoidServiceIdMerge(todayServiceIdInts, previousDayPeriodServiceIdInts) + && canMergeServiceIds(todayServiceIdInts, previousDayPeriodServiceIdInts) ) { p.setStartDate(previousDayPeriod.getStartDate()); MTLog.log("> new start date '%s' because previous %d day(s) has own service ID(s)", p.getStartDate(), MIN_PREVIOUS_NEXT_ADDED_DAYS); From 161d6c728b705602a7efbfb566bb6bf0352d8d54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathieu=20M=C3=A9a?= Date: Tue, 7 Jul 2026 13:59:19 -0400 Subject: [PATCH 3/3] fix --- src/main/java/org/mtransit/parser/DefaultAgencyTools.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/mtransit/parser/DefaultAgencyTools.java b/src/main/java/org/mtransit/parser/DefaultAgencyTools.java index 156d3d84..36438fb5 100644 --- a/src/main/java/org/mtransit/parser/DefaultAgencyTools.java +++ b/src/main/java/org/mtransit/parser/DefaultAgencyTools.java @@ -468,7 +468,7 @@ private static boolean canMergeServiceIds(@NotNull Collection currentSe final boolean serviceIdCleanMerged = agencyConfig.getServiceIdCleanMerged(); final String serviceIdCleanupRegex = agencyConfig.getServiceIdCleanupRegex(); if (!serviceIdCleanMerged && (serviceIdCleanupRegex == null || serviceIdCleanupRegex.isBlank())) return true; // can merge - final Pattern serviceIdCleanupPattern = serviceIdCleanupRegex == null ? null : Pattern.compile(serviceIdCleanupRegex); + final Pattern serviceIdCleanupPattern = serviceIdCleanupRegex == null || serviceIdCleanupRegex.isBlank() ? null : Pattern.compile(serviceIdCleanupRegex); // make current set of cleaned service IDs final Set cleanCurrentServiceIds = new HashSet<>(); for (Integer currentServiceIdInt : currentServiceIdInts) {