diff --git a/src/main/java/org/mtransit/commons/Letters.kt b/src/main/java/org/mtransit/commons/Letters.kt index 82a31898..0c23e734 100644 --- a/src/main/java/org/mtransit/commons/Letters.kt +++ b/src/main/java/org/mtransit/commons/Letters.kt @@ -1,34 +1,34 @@ package org.mtransit.commons object Letters { - const val NONE_: Long = 0L + const val NONE_ = 0 - const val A: Long = 1L - const val B: Long = 2L - const val C: Long = 3L - const val D: Long = 4L - const val E: Long = 5L - const val F: Long = 6L - const val G: Long = 7L - const val H: Long = 8L - const val I: Long = 9L - const val J: Long = 10L - const val K: Long = 11L - const val L: Long = 12L - const val M: Long = 13L - const val N: Long = 14L - const val O: Long = 15L - const val P: Long = 16L - const val Q: Long = 17L - const val R: Long = 18L - const val S: Long = 19L - const val T: Long = 20L - const val U: Long = 21L - const val V: Long = 22L - const val W: Long = 23L - const val X: Long = 24L - const val Y: Long = 25L - const val Z: Long = 26L + const val A = 1 + const val B = 2 + const val C = 3 + const val D = 4 + const val E = 5 + const val F = 6 + const val G = 7 + const val H = 8 + const val I = 9 + const val J = 10 + const val K = 11 + const val L = 12 + const val M = 13 + const val N = 14 + const val O = 15 + const val P = 16 + const val Q = 17 + const val R = 18 + const val S = 19 + const val T = 20 + const val U = 21 + const val V = 22 + const val W = 23 + const val X = 24 + const val Y = 25 + const val Z = 26 - const val OTHER_MIN_: Long = 27L + const val OTHER_MIN_ = 27 } \ No newline at end of file diff --git a/src/main/java/org/mtransit/parser/DefaultAgencyTools.java b/src/main/java/org/mtransit/parser/DefaultAgencyTools.java index ae78bac5..7cbce979 100644 --- a/src/main/java/org/mtransit/parser/DefaultAgencyTools.java +++ b/src/main/java/org/mtransit/parser/DefaultAgencyTools.java @@ -38,6 +38,7 @@ import org.mtransit.parser.mt.data.MServiceId; import org.mtransit.parser.mt.data.MServiceIds; import org.mtransit.parser.mt.data.MSpec; +import org.mtransit.parser.mt.data.MStopIDConverter; import org.mtransit.parser.mt.data.MString; import org.mtransit.parser.mt.data.MStrings; import org.mtransit.parser.mt.data.MTripId; @@ -1267,10 +1268,16 @@ public int getStopId(@NotNull GStop gStop) { useStopCodeForStopId() ? cleanStopOriginalId(getStopCode(gStop)) : gStopId; if (stopIdS.isBlank() || !CharUtils.isDigitsOnly(stopIdS)) { - Integer stopIdSInt = convertStopIdFromCodeNotSupported(stopIdS); + Integer stopIdSInt = convertStopIdNotSupported(stopIdS); if (stopIdSInt != null) return stopIdSInt; stopIdSInt = Configs.getRouteConfig().convertStopIdFromOriginalNotSupported(gStopId); if (stopIdSInt != null) return stopIdSInt; + return MStopIDConverter.convert( + stopIdS, + this::convertStopIdNotSupported, + this::convertStopIdNextChars, + this::convertStopIdPreviousChars + ); } return Integer.parseInt(stopIdS); } catch (Exception e) { @@ -1278,10 +1285,19 @@ public int getStopId(@NotNull GStop gStop) { } } - @Nullable @Override - public Integer convertStopIdFromCodeNotSupported(@NotNull String stopCode) { - return Configs.getRouteConfig().convertStopIdFromCodeNotSupported(stopCode); + public @Nullable Integer convertStopIdNotSupported(@NotNull String stopCode) { + return Configs.getRouteConfig().convertStopIdNotSupported(stopCode); + } + + @Override + public @Nullable Integer convertStopIdNextChars(@NotNull String nextChars) { + return Configs.getRouteConfig().convertStopIdNextChars(nextChars); + } + + @Override + public @Nullable Integer convertStopIdPreviousChars(@NotNull String previousChars) { + return Configs.getRouteConfig().convertStopIdPreviousChars(previousChars); } @Override 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 6884d7dd..341f0d8d 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 @@ -4,6 +4,7 @@ import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import org.mtransit.commons.CleanUtils import org.mtransit.commons.StringUtils.EMPTY +import org.mtransit.commons.toIntOrNull import org.mtransit.parser.gtfs.data.GRoute import org.mtransit.parser.gtfs.data.GStopTime import org.mtransit.parser.gtfs.data.GTrip @@ -31,9 +32,9 @@ data class RouteConfig( @SerialName("route_short_name_to_route_id_configs") val routeShortNameToRouteIdConfigs: List = emptyList(), @SerialName("route_id_next_char_configs") - val routeIdNextCharConfigs: List = emptyList(), + val routeIdNextCharConfigs: List = emptyList(), @SerialName("route_id_previous_char_configs") - val routeIdPreviousCharConfigs: List = emptyList(), + val routeIdPreviousCharConfigs: List = emptyList(), // short-name @SerialName("use_route_long_name_for_route_short_name") val useRouteLongNameForRouteShortName: Boolean = false, // OPT-IN feature @@ -102,8 +103,15 @@ data class RouteConfig( val useStopCodeForStopId: Boolean = false, // OPT-IN feature @SerialName("use_stop_id_for_stop_code") val useStopIdForStopCode: Boolean = false, // OPT-IN feature + @Deprecated("use stopIdNotSupportedConfigs instead") @SerialName("stop_code_to_stop_id_configs") - val stopCodeToStopIdConfigs: List = emptyList(), + val stopCodeToStopIdConfigs: List = emptyList(), + @SerialName("stop_id_not_supported_configs") + val stopIdNotSupportedConfigs: List = stopCodeToStopIdConfigs, + @SerialName("stop_id_next_char_configs") + val stopIdNextCharConfigs: List = emptyList(), + @SerialName("stop_id_previous_char_configs") + val stopIdPreviousCharConfigs: List = emptyList(), @SerialName("stop_original_id_to_stop_id_configs") val stopOriginalIdToStopIdConfigs: List = emptyList(), @SerialName("stop_code_prepend_if_missing") @@ -146,11 +154,14 @@ data class RouteConfig( ) @Serializable - data class RouteIdCharToRouteIdPartConfig( + data class IdCharToIdPartConfig( @SerialName("char") val char: String, @SerialName("route_id_part") + @Deprecated("use idPart instead") val routeIdPart: Long, + @SerialName("id_part") + val idPart: Long = routeIdPart ) @Serializable @@ -174,11 +185,17 @@ data class RouteConfig( ) @Serializable - data class StopCodeToStopIdConfig( + data class StopIdConfig( + @Deprecated("use originalStopId instead") @SerialName("stop_code") val stopCode: String, + @SerialName("original_stop_id") + val originalStopId: String = stopCode, + @Deprecated("use stopIdInt instead") @SerialName("stop_id") val stopId: Int, + @SerialName("stop_id_int") + val stopIdInt: Int = stopId ) @Serializable @@ -237,11 +254,11 @@ data class RouteConfig( fun convertRouteIdNextChars(nextChars: String) = this.routeIdNextCharConfigs - .singleOrNull { it.char == nextChars }?.routeIdPart + .singleOrNull { it.char == nextChars }?.idPart fun convertRouteIdPreviousChars(previousChars: String) = this.routeIdPreviousCharConfigs - .singleOrNull { it.char == previousChars }?.routeIdPart + .singleOrNull { it.char == previousChars }?.idPart fun getRouteShortNameFromRouteId(routeId: String) = this.routeIdToRouteShortNameConfigs @@ -342,9 +359,17 @@ data class RouteConfig( this.stopOriginalIdToStopIdConfigs .singleOrNull { it.originalStopId == originalStopId }?.stopId - fun convertStopIdFromCodeNotSupported(stopCode: String) = - this.stopCodeToStopIdConfigs - .singleOrNull { it.stopCode == stopCode }?.stopId + fun convertStopIdNotSupported(originalStopId: String) = + this.stopIdNotSupportedConfigs + .singleOrNull { it.originalStopId == originalStopId }?.stopIdInt + + fun convertStopIdNextChars(nextChars: String) = + this.stopIdNextCharConfigs + .singleOrNull { it.char == nextChars }?.idPart?.toIntOrNull() + + fun convertStopIdPreviousChars(previousChars: String) = + this.stopIdPreviousCharConfigs + .singleOrNull { it.char == previousChars }?.idPart?.toIntOrNull() private fun cleanString(lang: Locale, originalString: String, cleaners: List): String { if (cleaners.isEmpty()) return originalString diff --git a/src/main/java/org/mtransit/parser/gtfs/GAgencyTools.java b/src/main/java/org/mtransit/parser/gtfs/GAgencyTools.java index 763f3bec..4774fda8 100644 --- a/src/main/java/org/mtransit/parser/gtfs/GAgencyTools.java +++ b/src/main/java/org/mtransit/parser/gtfs/GAgencyTools.java @@ -284,8 +284,11 @@ public interface GAgencyTools { // STOP int getStopId(@NotNull GStop gStop); - @Nullable - Integer convertStopIdFromCodeNotSupported(@NotNull String stopCode); + @Nullable Integer convertStopIdNotSupported(@NotNull String stopCode); + + @Nullable Integer convertStopIdNextChars(@NotNull String nextChars); + + @Nullable Integer convertStopIdPreviousChars(@NotNull String previousChars); boolean useStopCodeForStopId(); diff --git a/src/main/java/org/mtransit/parser/mt/data/MRouteSNToIDConverter.kt b/src/main/java/org/mtransit/parser/mt/data/MRouteSNToIDConverter.kt index 2831c022..c98550f0 100644 --- a/src/main/java/org/mtransit/parser/mt/data/MRouteSNToIDConverter.kt +++ b/src/main/java/org/mtransit/parser/mt/data/MRouteSNToIDConverter.kt @@ -110,8 +110,9 @@ object MRouteSNToIDConverter { // endregion - const val PREVIOUS: Long = 1_000_000L - const val NEXT: Long = 10_000L + const val PREVIOUS = 1_000_000L + const val NEXT = 10_000L + const val MAX_DIGIT = 1000L @JvmOverloads @JvmStatic @@ -128,19 +129,19 @@ object MRouteSNToIDConverter { if (rsn.length == 1 && rsn[0].isLetter()) { endsWithLetter(rsn)?.let { return it } } - val matcher: Matcher = RSN.matcher(rsn) + val matcher = RSN.matcher(rsn) if (!matcher.find()) { return notSupportedToRouteId?.invoke(rsn) ?: throw MTLog.Fatal("Unexpected route short name '$rsn' can not be parsed by regex!") } - val previousChars: String = matcher.group(2).uppercase() - val digits: Long = matcher.group(3).toLong() - val nextChars: String = matcher.group(4).uppercase() - if (digits !in 0..1000L) { + val previousChars = matcher.group(2).uppercase() + val digits = matcher.group(3).toLong() + val nextChars = matcher.group(4).uppercase() + if (digits !in 0..MAX_DIGIT) { return notSupportedToRouteId?.invoke(rsn) ?: throw MTLog.Fatal("Unexpected route short name digits '$digits' in short name '$rsn' to convert to route ID!") } - var routeId: Long = digits + var routeId = digits routeId += endsWithLetter(nextChars) ?: run { nextCharsToLong?.invoke(nextChars) ?: notSupportedToRouteId?.invoke(rsn) @@ -155,7 +156,7 @@ object MRouteSNToIDConverter { } @JvmStatic - fun startsWithLetter(previousChars: String): Long? = when (previousChars) { + fun startsWithLetter(previousChars: String) = when (previousChars) { "" -> startsWith(Letters.NONE_) "A" -> startsWith(Letters.A) "B" -> startsWith(Letters.B) @@ -219,17 +220,11 @@ object MRouteSNToIDConverter { } @JvmStatic - fun startsWith(digit: Long): Long { - return digit * PREVIOUS - } + fun startsWith(digit: Int) = digit * PREVIOUS @JvmStatic - fun endsWith(digit: Long): Long { - return digit * NEXT - } + fun endsWith(digit: Int) = digit * NEXT @JvmStatic - fun other(digit: Long): Long { - return Letters.OTHER_MIN_ + digit - } -} \ No newline at end of file + fun other(digit: Int) = (Letters.OTHER_MIN_ + digit).toLong() +} diff --git a/src/main/java/org/mtransit/parser/mt/data/MStopIDConverter.kt b/src/main/java/org/mtransit/parser/mt/data/MStopIDConverter.kt new file mode 100644 index 00000000..d9869bce --- /dev/null +++ b/src/main/java/org/mtransit/parser/mt/data/MStopIDConverter.kt @@ -0,0 +1,137 @@ +package org.mtransit.parser.mt.data + +import org.mtransit.commons.Letters +import org.mtransit.commons.RegexUtils.BEGINNING +import org.mtransit.commons.RegexUtils.DIGIT_CAR +import org.mtransit.commons.RegexUtils.END +import org.mtransit.commons.RegexUtils.any +import org.mtransit.commons.RegexUtils.atLeastOne +import org.mtransit.commons.RegexUtils.group +import org.mtransit.parser.MTLog +import java.util.regex.Pattern + +@Suppress("MemberVisibilityCanBePrivate", "unused") +object MStopIDConverter { + + private val STOP_ID = Pattern.compile( + group(BEGINNING + group(any("[A-Z]")) + group(atLeastOne(DIGIT_CAR)) + group(any("[A-Z]")) + END), Pattern.CASE_INSENSITIVE + ) + + const val PREVIOUS = 10_000_000 + const val NEXT = 100_000 + const val MAX_DIGIT = 10_000 + + @JvmOverloads + @JvmStatic + fun convert( + stopIdS: String, + notSupported: ((stopId: String) -> Int?)? = null, + nextCharsToInt: ((nextChars: String) -> Int?)? = null, + previousCharsToInt: ((previousChars: String) -> Int?)? = null, + ): Int { + if (stopIdS.isBlank()) { + return notSupported?.invoke(stopIdS) + ?: throw MTLog.Fatal("Unexpected stop ID '$stopIdS' to convert to stop ID integer!") + } + if (stopIdS.length == 1 && stopIdS[0].isLetter()) { + endsWithLetter(stopIdS)?.let { return it } + } + val matcher = STOP_ID.matcher(stopIdS) + if (!matcher.find()) { + return notSupported?.invoke(stopIdS) + ?: throw MTLog.Fatal("Unexpected stop ID '$stopIdS' can not be parsed by regex!") + } + val previousChars = matcher.group(2).uppercase() + val digits = matcher.group(3).toInt() + val nextChars = matcher.group(4).uppercase() + if (digits !in 0..MAX_DIGIT) { + return notSupported?.invoke(stopIdS) + ?: throw MTLog.Fatal("Unexpected stop ID digits '$digits' in stop ID '$stopIdS' to convert to stop ID integer!") + } + var stopId = digits + stopId += endsWithLetter(nextChars) ?: run { + nextCharsToInt?.invoke(nextChars) + ?: notSupported?.invoke(stopIdS) + ?: throw MTLog.Fatal("Unexpected next characters '$nextChars' in stop ID '$stopIdS'!") + } + stopId += startsWithLetter(previousChars) ?: run { + previousCharsToInt?.invoke(previousChars) + ?: notSupported?.invoke(stopIdS) + ?: throw MTLog.Fatal("Unexpected previous characters '$previousChars' in stop ID '$stopIdS'!") + } + return stopId + } + + @JvmStatic + fun startsWithLetter(previousChars: String) = when (previousChars) { + "" -> startsWith(Letters.NONE_) + "A" -> startsWith(Letters.A) + "B" -> startsWith(Letters.B) + "C" -> startsWith(Letters.C) + "D" -> startsWith(Letters.D) + "E" -> startsWith(Letters.E) + "F" -> startsWith(Letters.F) + "G" -> startsWith(Letters.G) + "H" -> startsWith(Letters.H) + "I" -> startsWith(Letters.I) + "J" -> startsWith(Letters.J) + "K" -> startsWith(Letters.K) + "L" -> startsWith(Letters.L) + "M" -> startsWith(Letters.M) + "N" -> startsWith(Letters.N) + "O" -> startsWith(Letters.O) + "P" -> startsWith(Letters.P) + "Q" -> startsWith(Letters.Q) + "R" -> startsWith(Letters.R) + "S" -> startsWith(Letters.S) + "T" -> startsWith(Letters.T) + "U" -> startsWith(Letters.U) + "V" -> startsWith(Letters.V) + "W" -> startsWith(Letters.W) + "X" -> startsWith(Letters.X) + "Y" -> startsWith(Letters.Y) + "Z" -> startsWith(Letters.Z) + else -> null + } + + @JvmStatic + fun endsWithLetter(nextChars: String) = when (nextChars) { + "" -> endsWith(Letters.NONE_) + "A" -> endsWith(Letters.A) + "B" -> endsWith(Letters.B) + "C" -> endsWith(Letters.C) + "D" -> endsWith(Letters.D) + "E" -> endsWith(Letters.E) + "F" -> endsWith(Letters.F) + "G" -> endsWith(Letters.G) + "H" -> endsWith(Letters.H) + "I" -> endsWith(Letters.I) + "J" -> endsWith(Letters.J) + "K" -> endsWith(Letters.K) + "L" -> endsWith(Letters.L) + "M" -> endsWith(Letters.M) + "N" -> endsWith(Letters.N) + "O" -> endsWith(Letters.O) + "P" -> endsWith(Letters.P) + "Q" -> endsWith(Letters.Q) + "R" -> endsWith(Letters.R) + "S" -> endsWith(Letters.S) + "T" -> endsWith(Letters.T) + "U" -> endsWith(Letters.U) + "V" -> endsWith(Letters.V) + "W" -> endsWith(Letters.W) + "X" -> endsWith(Letters.X) + "Y" -> endsWith(Letters.Y) + "Z" -> endsWith(Letters.Z) + else -> null + } + + @JvmStatic + fun startsWith(digit: Int) = digit * PREVIOUS + + @JvmStatic + fun endsWith(digit: Int) = digit * NEXT + + @JvmStatic + fun other(digit: Int) = Letters.OTHER_MIN_ + digit +}