parent
8ba988c929
commit
30f11565cf
@ -9,6 +9,28 @@ fun <T,V> Iterable<T>.flatMapNotNull(transform: (T) -> Iterable<V>?) : List<V> {
|
|||||||
return destination
|
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>> {
|
fun <T,V> Iterable<Pair<T, V>>.toMultiMap() : Map<T, List<V>> {
|
||||||
val destination = mutableMapOf<T, MutableList<V>>()
|
val destination = mutableMapOf<T, MutableList<V>>()
|
||||||
for ((k, v) in this) {
|
for ((k, v) in this) {
|
||||||
|
|||||||
@ -337,43 +337,61 @@ internal fun parseOpeningSchedule(
|
|||||||
): OpeningSchedule? {
|
): OpeningSchedule? {
|
||||||
val parsed = it?.toOpeningHoursOrNull(lenient = true) ?: return null
|
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
|
return OpeningSchedule.TwentyFourSeven
|
||||||
|
}
|
||||||
|
|
||||||
val rangeRules = parsed.rules.mapNotNull { it.selector as? Range }
|
val rangeRules = parsed.rules.mapNotNull { it.selector as? Range }
|
||||||
|
|
||||||
val applicableRules = rangeRules
|
// Group rules by the weekdays they apply to. Rules can apply to multiple weekdays.
|
||||||
.flatMapNotNull { range ->
|
val rulesMap = mutableMapOf<Weekday, MutableList<Range>>(
|
||||||
with(range) {
|
Weekday.Monday to mutableListOf(),
|
||||||
when {
|
Weekday.Tuesday to mutableListOf(),
|
||||||
weekdays != null -> weekdays!!.flatMap {
|
Weekday.Wednesday to mutableListOf(),
|
||||||
when (it) {
|
Weekday.Thursday to mutableListOf(),
|
||||||
is Weekday -> listOf(it to range)
|
Weekday.Friday to mutableListOf(),
|
||||||
is WeekdayRange -> (it.start.ordinal..it.end.ordinal).map { Weekday.entries[it] to range }
|
Weekday.Saturday to mutableListOf(),
|
||||||
is SpecificWeekdays -> listOf(it.weekday to range)
|
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 -> {
|
||||||
times.isNullOrEmpty().not() || months.isNullOrEmpty().not() ->
|
rulesMap[selector.weekday]!!.add(rule)
|
||||||
Weekday.entries.map { it to range }
|
}
|
||||||
|
|
||||||
else -> null
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.toMultiMap().mapNotNull { (day, rules) ->
|
} else if (!rule.times.isNullOrEmpty() || !rule.months.isNullOrEmpty()) {
|
||||||
rules.filterYears(localTime)
|
rulesMap.forEach { _, it ->
|
||||||
.filterMonths(localTime)
|
it.add(rule)
|
||||||
.filterNthDays(localTime)
|
}
|
||||||
.map { it.copy(weekdays = listOf(day)) }
|
|
||||||
.singleOrNull()
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
.map { it.copy(weekdays = listOf(day)) }
|
||||||
|
.singleOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
val hours = mutableListOf<OpeningHours>()
|
val hours = mutableListOf<OpeningHours>()
|
||||||
|
|
||||||
for (range in applicableRules) {
|
for (range in applicableRules) {
|
||||||
|
|
||||||
val localTimesWithDuration =
|
val localTimesWithDuration =
|
||||||
range.times?.flatMap { it.toLocalTimeWithDuration() } ?: continue
|
range.times?.mapNotNull { it.toLocalTimeWithDuration() } ?: continue
|
||||||
val daysOfWeek = range.weekdays
|
val daysOfWeek = range.weekdays
|
||||||
.ifNullOrEmpty { Weekday.entries.toList() }
|
.ifNullOrEmpty { Weekday.entries.toList() }
|
||||||
.flatMap { it.toDaysOfWeek() }
|
.flatMap { it.toDaysOfWeek() }
|
||||||
@ -389,27 +407,35 @@ internal fun parseOpeningSchedule(
|
|||||||
return OpeningSchedule.Hours(hours)
|
return OpeningSchedule.Hours(hours)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun List<Range>.filterYears(localTime: LocalDateTime): List<Range> = when {
|
/**
|
||||||
none { it.years.isNullOrEmpty().not() } -> this
|
* Returns only the rules that are valid for the given year.
|
||||||
else -> filter {
|
*/
|
||||||
(it.years ?: return@filter false).any {
|
private fun List<Range>.filterYears(localTime: LocalDateTime): List<Range> {
|
||||||
|
if (all { it.years.isNullOrEmpty() }) return this
|
||||||
|
|
||||||
|
val thisYear = filter {
|
||||||
|
it.years?.any {
|
||||||
when (it) {
|
when (it) {
|
||||||
is Year -> it.year == localTime.year
|
is Year -> it.year == localTime.year
|
||||||
is StartingAtYear -> it.start <= localTime.year
|
is StartingAtYear -> it.start <= localTime.year
|
||||||
is YearRange -> localTime.year in it.start..it.end step (it.step ?: 1)
|
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
|
* Returns only the rules that are valid for the given month.
|
||||||
else -> filter {
|
*/
|
||||||
(it.months ?: return@filter false).any {
|
private fun List<Range>.filterMonths(localTime: LocalDateTime): List<Range> {
|
||||||
|
if (all { it.months.isNullOrEmpty() }) return this
|
||||||
|
|
||||||
|
val thisMonth = filter {
|
||||||
|
it.months?.any {
|
||||||
when (it) {
|
when (it) {
|
||||||
is MonthRange -> (it.year?.let { it == localTime.year } != false) && localTime.month.ordinal in it.start.ordinal..it.end.ordinal
|
is MonthRange -> (it.year?.let { it == localTime.year } != false) && localTime.month.ordinal in it.start.ordinal..it.end.ordinal
|
||||||
|
|
||||||
@ -417,95 +443,118 @@ private fun List<Range>.filterMonths(localTime: LocalDateTime): List<Range> = wh
|
|||||||
|
|
||||||
else -> false
|
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
|
* Returns only the rules that are valid for the given week.
|
||||||
else -> localTime.getNthWeekdaysOfCurrentWeek().let { currentWeek ->
|
* (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 specific = mapNotNull { range ->
|
val currentWeek = localTime.getNthWeekdaysOfCurrentWeek()
|
||||||
(range.weekdays?.singleOrNull() as? SpecificWeekdays)?.let {
|
|
||||||
range to it
|
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) ->
|
currentWeek.any { (dow, nthFwd, nthBwd) ->
|
||||||
specific.weekday.ordinal == dow.ordinal && specific.nths.any {
|
specific.weekday.ordinal == dow.ordinal && specific.nths.any {
|
||||||
when (it) {
|
when (it) {
|
||||||
is Nth -> it.nth == nthFwd
|
is Nth -> it.nth == nthFwd
|
||||||
is NthRange -> nthFwd in it.start..it.end
|
is NthRange -> nthFwd in it.start..it.end
|
||||||
is LastNth -> it.nth == nthBwd
|
is LastNth -> it.nth == nthBwd
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}?.let {
|
|
||||||
(rule, _) -> listOf(rule)
|
|
||||||
} ?: listOf(unspecific)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun WeekdaysSelector.toDaysOfWeek(): List<DayOfWeek> = when (this) {
|
|
||||||
is Weekday -> listOf(
|
|
||||||
when (this) {
|
|
||||||
Weekday.Monday -> DayOfWeek.MONDAY
|
|
||||||
Weekday.Tuesday -> DayOfWeek.TUESDAY
|
|
||||||
Weekday.Wednesday -> DayOfWeek.WEDNESDAY
|
|
||||||
Weekday.Thursday -> DayOfWeek.THURSDAY
|
|
||||||
Weekday.Friday -> DayOfWeek.FRIDAY
|
|
||||||
Weekday.Saturday -> DayOfWeek.SATURDAY
|
|
||||||
Weekday.Sunday -> DayOfWeek.SUNDAY
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rule != null) return listOf(rule.first)
|
||||||
|
|
||||||
|
return listOfNotNull(
|
||||||
|
singleOrNull { it.weekdays?.singleOrNull() !is SpecificWeekdays }
|
||||||
)
|
)
|
||||||
|
|
||||||
is WeekdayRange -> (start to end).map { it.toDaysOfWeek().single().value }
|
|
||||||
.into { start, end -> (start..end).map { DayOfWeek.of(it) } }
|
|
||||||
|
|
||||||
is SpecificWeekdays -> weekday.toDaysOfWeek()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun WeekdaysSelector.toDaysOfWeek(): List<DayOfWeek> {
|
||||||
private fun TimesSelector.toLocalTimeWithDuration(): List<Pair<LocalTime, Duration>> {
|
|
||||||
return when (this) {
|
return when (this) {
|
||||||
is TimeSpan -> {
|
is Weekday -> listOf(
|
||||||
val start = start as? ClockTime ?: return emptyList()
|
when (this) {
|
||||||
val end = end as? ExtendedClockTime ?: return emptyList()
|
Weekday.Monday -> DayOfWeek.MONDAY
|
||||||
|
Weekday.Tuesday -> DayOfWeek.TUESDAY
|
||||||
|
Weekday.Wednesday -> DayOfWeek.WEDNESDAY
|
||||||
|
Weekday.Thursday -> DayOfWeek.THURSDAY
|
||||||
|
Weekday.Friday -> DayOfWeek.FRIDAY
|
||||||
|
Weekday.Saturday -> DayOfWeek.SATURDAY
|
||||||
|
Weekday.Sunday -> DayOfWeek.SUNDAY
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
listOf(
|
is WeekdayRange -> (start to end).map { it.toDaysOfWeek().single().value }
|
||||||
LocalTime.of(
|
.into { start, end -> (start..end).map { DayOfWeek.of(it) } }
|
||||||
start.hour,
|
|
||||||
start.minutes
|
|
||||||
) to Duration.ofMinutes((Math.floorMod(end.hour - start.hour, 24) * 60 + end.minutes - start.minutes).toLong())
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> return emptyList()
|
is SpecificWeekdays -> weekday.toDaysOfWeek()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun TimesSelector.toLocalTimeWithDuration(): Pair<LocalTime, Duration>? {
|
||||||
|
if (this !is TimeSpan) return null
|
||||||
|
|
||||||
|
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()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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>> {
|
private fun LocalDateTime.getNthWeekdaysOfCurrentWeek(): List<Triple<DayOfWeek, Int, Int>> {
|
||||||
val monday = with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY))
|
val monday = with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY))
|
||||||
return (0 until 7).map { i ->
|
return (0 until 7).map { i ->
|
||||||
val (nth, nthLast) = monday.plusDays(i.toLong()).let { weekday ->
|
val (nth, nthLast) = monday.plusDays(i.toLong()).let { weekday ->
|
||||||
(
|
(
|
||||||
ChronoUnit.WEEKS.between(
|
ChronoUnit.WEEKS.between(
|
||||||
with(TemporalAdjusters.firstInMonth(weekday.dayOfWeek)),
|
with(TemporalAdjusters.firstInMonth(weekday.dayOfWeek)),
|
||||||
weekday
|
weekday
|
||||||
).toInt() + 1
|
).toInt() + 1
|
||||||
) to (
|
) to (
|
||||||
ChronoUnit.WEEKS.between(
|
ChronoUnit.WEEKS.between(
|
||||||
weekday,
|
weekday,
|
||||||
with(TemporalAdjusters.lastInMonth(weekday.dayOfWeek))
|
with(TemporalAdjusters.lastInMonth(weekday.dayOfWeek))
|
||||||
).toInt() + 1
|
).toInt() + 1
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Triple(DayOfWeek.entries[i], nth, nthLast)
|
Triple(DayOfWeek.entries[i], nth, nthLast)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user