Refactor OSM opening hour parsing

Fix #1368
This commit is contained in:
MM20 2025-04-18 22:10:32 +02:00
parent 8ba988c929
commit 30f11565cf
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
2 changed files with 172 additions and 101 deletions

View File

@ -9,6 +9,28 @@ fun <T,V> Iterable<T>.flatMapNotNull(transform: (T) -> Iterable<V>?) : List<V> {
return destination
}
/**
* Converts an Iterable of pairs into a Map where the keys are the first elements of the pairs
* and the values are lists of the second elements of the pairs.
*
* For example, given the input:
* ```
* val pairs = listOf(
* "a" to 1,
* "b" to 2,
* "a" to 3,
* "c" to 4,
* )
* ```
* the output will be:
* ```
* mapOf(
* "a" to listOf(1, 3),
* "b" to listOf(2),
* "c" to listOf(4),
* )
* ```
*/
fun <T,V> Iterable<Pair<T, V>>.toMultiMap() : Map<T, List<V>> {
val destination = mutableMapOf<T, MutableList<V>>()
for ((k, v) in this) {

View File

@ -337,30 +337,48 @@ internal fun parseOpeningSchedule(
): OpeningSchedule? {
val parsed = it?.toOpeningHoursOrNull(lenient = true) ?: return null
if (parsed.rules.singleOrNull()?.selector is TwentyFourSeven)
if (parsed.rules.singleOrNull()?.selector is TwentyFourSeven) {
return OpeningSchedule.TwentyFourSeven
}
val rangeRules = parsed.rules.mapNotNull { it.selector as? Range }
val applicableRules = rangeRules
.flatMapNotNull { range ->
with(range) {
when {
weekdays != null -> weekdays!!.flatMap {
when (it) {
is Weekday -> listOf(it to range)
is WeekdayRange -> (it.start.ordinal..it.end.ordinal).map { Weekday.entries[it] to range }
is SpecificWeekdays -> listOf(it.weekday to range)
// Group rules by the weekdays they apply to. Rules can apply to multiple weekdays.
val rulesMap = mutableMapOf<Weekday, MutableList<Range>>(
Weekday.Monday to mutableListOf(),
Weekday.Tuesday to mutableListOf(),
Weekday.Wednesday to mutableListOf(),
Weekday.Thursday to mutableListOf(),
Weekday.Friday to mutableListOf(),
Weekday.Saturday to mutableListOf(),
Weekday.Sunday to mutableListOf()
)
for (rule in rangeRules) {
if (rule.weekdays != null) {
for (selector in rule.weekdays!!) {
when (selector) {
is Weekday -> rulesMap[selector]!!.add(rule)
is WeekdayRange -> {
for (weekday in selector.start.ordinal..selector.end.ordinal) {
rulesMap[Weekday.entries[weekday]]!!.add(rule)
}
}
is SpecificWeekdays -> {
rulesMap[selector.weekday]!!.add(rule)
}
}
}
} else if (!rule.times.isNullOrEmpty() || !rule.months.isNullOrEmpty()) {
rulesMap.forEach { _, it ->
it.add(rule)
}
}
}
times.isNullOrEmpty().not() || months.isNullOrEmpty().not() ->
Weekday.entries.map { it to range }
else -> null
}
}
}.toMultiMap().mapNotNull { (day, rules) ->
// Filter out rules that are not valid for the current year, month, and week. Hopefully,
// there is only one rule left per weekday. If not, skip that weekday.
val applicableRules = rulesMap.mapNotNull { (day, rules) ->
rules.filterYears(localTime)
.filterMonths(localTime)
.filterNthDays(localTime)
@ -373,7 +391,7 @@ internal fun parseOpeningSchedule(
for (range in applicableRules) {
val localTimesWithDuration =
range.times?.flatMap { it.toLocalTimeWithDuration() } ?: continue
range.times?.mapNotNull { it.toLocalTimeWithDuration() } ?: continue
val daysOfWeek = range.weekdays
.ifNullOrEmpty { Weekday.entries.toList() }
.flatMap { it.toDaysOfWeek() }
@ -389,27 +407,35 @@ internal fun parseOpeningSchedule(
return OpeningSchedule.Hours(hours)
}
private fun List<Range>.filterYears(localTime: LocalDateTime): List<Range> = when {
none { it.years.isNullOrEmpty().not() } -> this
else -> filter {
(it.years ?: return@filter false).any {
/**
* Returns only the rules that are valid for the given year.
*/
private fun List<Range>.filterYears(localTime: LocalDateTime): List<Range> {
if (all { it.years.isNullOrEmpty() }) return this
val thisYear = filter {
it.years?.any {
when (it) {
is Year -> it.year == localTime.year
is StartingAtYear -> it.start <= localTime.year
is YearRange -> localTime.year in it.start..it.end step (it.step ?: 1)
}
} == true
}
}.ifEmpty {
filter {
it.years.isNullOrEmpty()
}
}
if (!thisYear.isEmpty()) return thisYear
return filter { it.years.isNullOrEmpty() }
}
private fun List<Range>.filterMonths(localTime: LocalDateTime): List<Range> = when {
none { it.months.isNullOrEmpty().not() } -> this
else -> filter {
(it.months ?: return@filter false).any {
/**
* Returns only the rules that are valid for the given month.
*/
private fun List<Range>.filterMonths(localTime: LocalDateTime): List<Range> {
if (all { it.months.isNullOrEmpty() }) return this
val thisMonth = filter {
it.months?.any {
when (it) {
is MonthRange -> (it.year?.let { it == localTime.year } != false) && localTime.month.ordinal in it.start.ordinal..it.end.ordinal
@ -417,26 +443,31 @@ private fun List<Range>.filterMonths(localTime: LocalDateTime): List<Range> = wh
else -> false
}
} == true
}
}.ifEmpty {
filter {
it.months.isNullOrEmpty()
}
}
if (!thisMonth.isEmpty()) return thisMonth
return filter { it.months.isNullOrEmpty() }
}
private fun List<Range>.filterNthDays(localTime: LocalDateTime): List<Range> = when {
none { it.weekdays?.any { it is SpecificWeekdays } == true } -> this
else -> localTime.getNthWeekdaysOfCurrentWeek().let { currentWeek ->
/**
* Returns only the rules that are valid for the given week.
* (i.e. if the given week is the 2nd week of the month, it will return only the rules that are
* valid for the 2nd week of the month)
*/
private fun List<Range>.filterNthDays(localTime: LocalDateTime): List<Range> {
if (none { it.weekdays?.any { it is SpecificWeekdays } == true }) return this
val currentWeek = localTime.getNthWeekdaysOfCurrentWeek()
val specific = mapNotNull { range ->
(range.weekdays?.singleOrNull() as? SpecificWeekdays)?.let {
range to it
}
}
val unspecific = single { it.weekdays?.singleOrNull() !is SpecificWeekdays }
specific.firstOrNull { (_, specific) ->
val rule = specific.firstOrNull { (_, specific) ->
currentWeek.any { (dow, nthFwd, nthBwd) ->
specific.weekday.ordinal == dow.ordinal && specific.nths.any {
when (it) {
@ -446,13 +477,17 @@ private fun List<Range>.filterNthDays(localTime: LocalDateTime): List<Range> = w
}
}
}
}?.let {
(rule, _) -> listOf(rule)
} ?: listOf(unspecific)
}
if (rule != null) return listOf(rule.first)
return listOfNotNull(
singleOrNull { it.weekdays?.singleOrNull() !is SpecificWeekdays }
)
}
private fun WeekdaysSelector.toDaysOfWeek(): List<DayOfWeek> = when (this) {
private fun WeekdaysSelector.toDaysOfWeek(): List<DayOfWeek> {
return when (this) {
is Weekday -> listOf(
when (this) {
Weekday.Monday -> DayOfWeek.MONDAY
@ -469,27 +504,41 @@ private fun WeekdaysSelector.toDaysOfWeek(): List<DayOfWeek> = when (this) {
.into { start, end -> (start..end).map { DayOfWeek.of(it) } }
is SpecificWeekdays -> weekday.toDaysOfWeek()
}
}
private fun TimesSelector.toLocalTimeWithDuration(): List<Pair<LocalTime, Duration>> {
return when (this) {
is TimeSpan -> {
val start = start as? ClockTime ?: return emptyList()
val end = end as? ExtendedClockTime ?: return emptyList()
private fun TimesSelector.toLocalTimeWithDuration(): Pair<LocalTime, Duration>? {
if (this !is TimeSpan) return null
listOf(
LocalTime.of(
val start = start as? ClockTime ?: return null
val end = end as? ExtendedClockTime ?: return null
return LocalTime.of(
start.hour,
start.minutes
) to Duration.ofMinutes((Math.floorMod(end.hour - start.hour, 24) * 60 + end.minutes - start.minutes).toLong())
) to Duration.ofMinutes(
(Math.floorMod(
end.hour - start.hour,
24
) * 60 + end.minutes - start.minutes).toLong()
)
}
else -> return emptyList()
}
}
/**
* Calculates the ordinal position of each day of the current week (Monday to Sunday) within the month.
*
* @receiver LocalDateTime The current date and time.
* @return A list of triples, where each triple contains:
* - The `DayOfWeek` (e.g., Monday, Tuesday, etc.).
* - The forward ordinal position of the day in the month (e.g., 1st Monday, 2nd Tuesday, etc.).
* - The backward ordinal position of the day in the month (e.g., last Monday, 2nd last Tuesday, etc.).
*
* Example:
* If today is the 15th of a month (Wednesday), the function will return:
* - For Monday: `(DayOfWeek.MONDAY, 3, 2)` (3rd Monday of the month, 2nd last Monday of the month).
* - For Wednesday: `(DayOfWeek.WEDNESDAY, 3, 2)` (3rd Wednesday of the month, 2nd last Wednesday of the month).
*/
private fun LocalDateTime.getNthWeekdaysOfCurrentWeek(): List<Triple<DayOfWeek, Int, Int>> {
val monday = with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY))
return (0 until 7).map { i ->