如何用Kotlin优雅合并两个列表中的重叠营业时间?
用Kotlin简洁实现营业时间列表的重叠合并
问题背景
给定两个营业时间列表,需要计算两者的重叠营业时段,并按天整理结果(包含所有七天,无重叠的天留空)。
输入列表1
val list1 = mapOf( "monday" to listOf( BusinessEvent.Open("08:00"), BusinessEvent.Close("20:00") ), "wednesday" to listOf( BusinessEvent.Open("05:00"), BusinessEvent.Close("10:00"), BusinessEvent.Open("20:00") ), "thursday" to listOf( BusinessEvent.Close("03:00") ) )
输入列表2
val list2 = mapOf( "monday" to listOf( BusinessEvent.Open("09:00"), BusinessEvent.Close("19:00") ), "tuesday" to listOf( BusinessEvent.Open("08:00"), BusinessEvent.Close("12:00") ), "wednesday" to listOf( BusinessEvent.Open("06:00"), BusinessEvent.Close("08:00"), BusinessEvent.Open("22:00") ), "thursday" to listOf( BusinessEvent.Close("01:00") ) )
期望输出
mapOf( "monday" to listOf( BusinessEvent.Open("09:00"), BusinessEvent.Close("19:00") ), "tuesday" to emptyList(), "wednesday" to listOf( BusinessEvent.Open("06:00"), BusinessEvent.Close("08:00"), BusinessEvent.Open("22:00"), BusinessEvent.Close("01:00") ), "thursday" to emptyList(), "friday" to emptyList(), "saturday" to emptyList(), "sunday" to emptyList() )
Kotlin实现方案
核心思路:先将两个列表的营业时间转换为全局时间轴上的连续区间(处理跨天情况),再计算区间交集,最后将交集转换回按天分组的格式。全程用Kotlin函数式编程特性简化逻辑,避免大量分支判断。
1. 定义核心数据结构
// 表示营业事件:开门/关门 sealed class BusinessEvent(val time: String) { class Open(time: String) : BusinessEvent(time) class Close(time: String) : BusinessEvent(time) override fun toString(): String = "${this::class.simpleName.lowercase()}: { $time }" } // 表示一个连续的营业区间(跨天用起始/结束日区分) data class BusinessInterval( val startDay: String, val startTime: String, val endDay: String, val endTime: String )
2. 工具函数与常量
// 一周天数的顺序(用于计算跨天的先后) private val DAYS_OF_WEEK = listOf("monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday") private val DAY_INDEX = DAYS_OF_WEEK.withIndex().associate { it.value to it.index } // 将时间字符串转换为分钟数(方便比较) private fun String.toMinutes(): Int { val (hour, minute) = split(":").map { it.toInt() } return hour * 60 + minute } // 将每日事件列表转换为全局营业区间 private fun Map<String, List<BusinessEvent>>.toGlobalIntervals(): List<BusinessInterval> { val intervals = mutableListOf<BusinessInterval>() var currentOpenDay: String? = null var currentOpenTime: String? = null // 按周顺序遍历每一天 DAYS_OF_WEEK.forEach { day -> val events = this[day].orEmpty() events.forEach { event -> when (event) { is BusinessEvent.Open -> { currentOpenDay = day currentOpenTime = event.time } is BusinessEvent.Close -> { currentOpenDay?.let { openDay -> currentOpenTime?.let { openTime -> intervals.add(BusinessInterval(openDay, openTime, day, event.time)) } } currentOpenDay = null currentOpenTime = null } } } } // 处理跨周的未关闭区间(可选,根据业务需求调整) currentOpenDay?.let { openDay -> currentOpenTime?.let { openTime -> intervals.add(BusinessInterval(openDay, openTime, DAYS_OF_WEEK.last(), "23:59")) } } return intervals }
3. 计算区间交集
// 判断两个区间是否存在重叠,并返回重叠区间 private fun BusinessInterval.intersect(other: BusinessInterval): BusinessInterval? { // 计算起始/结束的全局分钟数(将天数转换为分钟偏移) fun getGlobalMinutes(day: String, time: String): Int { val dayOffset = DAY_INDEX[day]!! * 24 * 60 return dayOffset + time.toMinutes() } val thisStart = getGlobalMinutes(startDay, startTime) val thisEnd = getGlobalMinutes(endDay, endTime) val otherStart = getGlobalMinutes(other.startDay, other.startTime) val otherEnd = getGlobalMinutes(other.endDay, other.endTime) // 计算交集的起始和结束全局分钟数 val overlapStart = maxOf(thisStart, otherStart) val overlapEnd = minOf(thisEnd, otherEnd) if (overlapStart >= overlapEnd) return null // 将全局分钟数转换回天和时间 fun globalMinutesToDayTime(minutes: Int): Pair<String, String> { val totalDays = minutes / (24 * 60) val day = DAYS_OF_WEEK[totalDays % 7] val remainingMinutes = minutes % (24 * 60) val hour = remainingMinutes / 60 val minute = remainingMinutes % 60 return day to "%02d:%02d".format(hour, minute) } val (startDay, startTime) = globalMinutesToDayTime(overlapStart) val (endDay, endTime) = globalMinutesToDayTime(overlapEnd) return BusinessInterval(startDay, startTime, endDay, endTime) } // 计算两个区间列表的所有交集 private fun List<BusinessInterval>.findOverlaps(other: List<BusinessInterval>): List<BusinessInterval> { return flatMap { interval -> other.mapNotNull { interval.intersect(it) } }.distinct() // 去重重复的重叠区间 }
4. 将交集转换为按天分组的结果
// 将全局区间转换为按天分组的事件列表 private fun List<BusinessInterval>.toDailyEvents(): Map<String, List<BusinessEvent>> { val dailyEvents = DAYS_OF_WEEK.associateWith { mutableListOf<BusinessEvent>() }.toMutableMap() forEach { interval -> val startDayIndex = DAY_INDEX[interval.startDay]!! val endDayIndex = DAY_INDEX[interval.endDay]!! // 区间在同一天内 if (startDayIndex == endDayIndex) { dailyEvents[interval.startDay]!!.add(BusinessEvent.Open(interval.startTime)) dailyEvents[interval.startDay]!!.add(BusinessEvent.Close(interval.endTime)) } else { // 跨多天的区间,拆分到每一天 dailyEvents[interval.startDay]!!.add(BusinessEvent.Open(interval.startTime)) dailyEvents[interval.startDay]!!.add(BusinessEvent.Close("23:59")) // 中间的每一天添加全天营业事件 for (dayIndex in startDayIndex + 1 until endDayIndex) { val day = DAYS_OF_WEEK[dayIndex] dailyEvents[day]!!.add(BusinessEvent.Open("00:00")) dailyEvents[day]!!.add(BusinessEvent.Close("23:59")) } dailyEvents[interval.endDay]!!.add(BusinessEvent.Open("00:00")) dailyEvents[interval.endDay]!!.add(BusinessEvent.Close(interval.endTime)) } } // 优化:将跨天的关门事件合并到起始日列表,符合示例格式 val optimized = dailyEvents.toMutableMap() DAYS_OF_WEEK.forEachIndexed { index, day -> val nextDay = DAYS_OF_WEEK[(index + 1) % 7] val nextDayEvents = optimized[nextDay] ?: emptyList() val hasMidnightOpen = nextDayEvents.firstOrNull() is BusinessEvent.Open && (nextDayEvents.first() as BusinessEvent.Open).time == "00:00" val closeEvent = nextDayEvents.firstOrNull { it is BusinessEvent.Close } as? BusinessEvent.Close if (hasMidnightOpen && closeEvent != null) { val dayEvents = optimized[day]!! val lastCloseIndex = dayEvents.indexOfLast { it is BusinessEvent.Close } if (lastCloseIndex != -1 && (dayEvents[lastCloseIndex] as BusinessEvent.Close).time == "23:59") { dayEvents[lastCloseIndex] = BusinessEvent.Close(closeEvent.time) // 清空次日的对应事件 optimized[nextDay] = nextDayEvents.drop(2) } } } return optimized.mapValues { it.value.distinct() } }
5. 合并入口函数
fun mergeBusinessHours(list1: Map<String, List<BusinessEvent>>, list2: Map<String, List<BusinessEvent>>): Map<String, List<BusinessEvent>> { val intervals1 = list1.toGlobalIntervals() val intervals2 = list2.toGlobalIntervals() val overlaps = intervals1.findOverlaps(intervals2) return overlaps.toDailyEvents() }
测试调用
fun main() { val result = mergeBusinessHours(list1, list2) result.forEach { (day, events) -> println("$day: [") events.forEach { println(" $it,") } println("],") } }
实现说明
- 用密封类清晰区分开门/关门事件,避免Map存储的混乱
- 用全局分钟数统一处理跨天时间比较,简化重叠判断逻辑
- 全程使用Kotlin函数式API(
flatMap、mapNotNull、associateWith等)替代大量if-else分支 - 通过优化步骤将跨天区间转换为示例要求的格式(把次日关门事件合并到前一天列表)
内容的提问来源于stack exchange,提问作者Dashing Boy
相关产品推荐
相关产品推荐

