You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 13:33:09