云端 Mac 上的 iOS 日程冲突检测:半开区间与扫描线验收

云端 Mac 上的 iOS 日程冲突检测:半开区间与扫描线验收

预约列表显示两场会议首尾相接,提交时却提示冲突;另一条记录完整包住三场短预约,统计页又只显示一场并发。这两种故障都不需要先查界面。在云端 Mac 上抽出纯 Swift 区间函数,固定边界与参考答案,就能把错误缩到可重复的输入。下面处理的是单个资源的占用时间,不涉及日期显示或网络请求。

先固定半开区间契约

把每次预约表示为 [start, end):包含开始,不包含结束。因此 [0, 10) 与 [10, 20) 不冲突,而 [0, 10) 与 [9, 20) 冲突。刻度可以是秒或分钟,但一次计算必须统一单位,不能把两种精度直接混用。

入口应拒绝结束不晚于开始的输入。零时长提醒不是占用预约,应走另一种数据模型;不要为了让它参与排序,偷偷补上一秒。

“占用范围合并”与“最大同时占用数”是两个问题。合并会丢失重叠层数,不能先合并再计算并发。

如果业务要求两场之间留出准备时间,应先将准备时间转换为占用区间,再应用同一套规则。不要只在界面层额外判断,否则列表与提交接口会给出不同答案。

写出可独立执行的核心实现

将以下内容保存为 Intervals.swift。代码只使用标准库,合并时仅连接真正重叠的区间,不把首尾相接当作重叠。这里使用整数刻度,是为了先隔离算法与日期转换。

typealias Slot = Range<Int>

func merged(_ input: [Slot]) -> [Slot] {
    precondition(input.allSatisfy { !$0.isEmpty })
    let sorted = input.sorted {
        if $0.lowerBound == $1.lowerBound {
            return $0.upperBound < $1.upperBound
        }
        return $0.lowerBound < $1.lowerBound
    }
    var result: [Slot] = []
    for slot in sorted {
        if let last = result.last,
           slot.lowerBound < last.upperBound {
            result[result.count - 1] =
                last.lowerBound..<max(last.upperBound, slot.upperBound)
        } else {
            result.append(slot)
        }
    }
    return result
}

func peak(_ input: [Slot]) -> Int {
    precondition(input.allSatisfy { !$0.isEmpty })
    let events = input.flatMap {
        [(time: $0.lowerBound, delta: 1),
         (time: $0.upperBound, delta: -1)]
    }.sorted {
        if $0.time == $1.time {
            return $0.delta < $1.delta
        }
        return $0.time < $1.time
    }
    var active = 0
    var maximum = 0
    for event in events {
        active += event.delta
        maximum = max(maximum, active)
    }
    return maximum
}

precondition(merged([]).isEmpty)
precondition(peak([]) == 0)
precondition(merged([0..<10, 10..<20]) == [0..<10, 10..<20])
precondition(peak([0..<10, 10..<20]) == 1)
precondition(merged([0..<10, 5..<15, 10..<20]) == [0..<20])
precondition(peak([0..<10, 5..<15, 10..<20]) == 2)
precondition(merged([0..<30, 5..<10, 8..<12]) == [0..<30])
precondition(peak([0..<30, 5..<10, 8..<12]) == 3)
precondition(peak([0..<10, 0..<10]) == 2)

在终端执行:

xcrun swift Intervals.swift

通过时没有输出且退出码为零。使用 precondition 而非仅用 assert,可以避免普通优化构建关闭断言后测试形同虚设;验收脚本也不要采用禁用安全检查的编译选项。

验收同刻事件与嵌套区间

扫描线把开始记为加一、结束记为减一。同一时刻必须先减后加,否则两场相邻预约会短暂被算成两场。只按时间排序、没有明确次序,是常见的边界漏洞。

至少保留五类固定样本:空列表、首尾相接、部分重叠、完整包含、完全重复。重复区间是否代表两笔独立预约,由业务输入决定;本文将每个元素视为一笔占用,所以完全重复的两笔并发数是二。

合并算法还有一个容易漏掉的细节:更新右端点要取较大值。若直接覆盖为当前区间的结束时间,长预约包含短预约时,合并结果反而会缩短。输入乱序也应得到相同结果,不能依赖服务端碰巧排序。

用另一条计算路径交叉检查

固定案例容易跟实现一起“写错答案”。对小范围整数样本,可以逐刻度数包含关系,作为独立参考。把下面代码追加到同一文件即可运行;它用于测试,不替代生产算法。

func referencePeak(_ input: [Slot]) -> Int {
    guard let start = input.map(\.lowerBound).min(),
          let end = input.map(\.upperBound).max() else {
        return 0
    }
    return (start..<end).map { tick in
        input.filter { $0.contains(tick) }.count
    }.max() ?? 0
}

let candidates: [Slot] = (0..<5).flatMap { start in
    ((start + 1)...5).map { start..<$0 }
}

for first in candidates {
    for second in candidates {
        let sample = [first, second]
        precondition(peak(sample) == referencePeak(sample))
        precondition(merged(sample) ==
                     merged(Array(sample.reversed())))
        precondition(merged(merged(sample)) == merged(sample))
    }
}

这些检查分别约束并发正确性、输入顺序无关性与合并幂等性。离散参考算法只适用于这里的整数刻度契约,不能据此声称已覆盖任意连续时间。若加入随机测试,应保存失败输入,而不只是记录随机种子,方便工具链变更后复现。

接入工程前收紧输入与失败证据

正式接入时,把解析、规范化和区间计算分成三层。先得到同一时间基准上的起止值,再统一精度,最后检查结束严格晚于开始。精度转换后原本很短的预约可能变成零时长,因此验证不能只发生在转换前。

若多个房间共用预约列表,先按资源标识分组,再分别计算;不同房间同时开会不应互相冲突。需要人员也互斥时,为人员建立另一组资源约束,不要把所有预约无条件混进一条扫描线。

失败记录保留资源标识、规范化后的区间、期望值、实际值及工具链版本即可,不必附带参会者信息。修复前先把该输入加入固定样本,再运行完整脚本。最终通过条件应是边界样本、参考计算和业务分组全部一致,而不只是某个界面不再弹出冲突提示。

常见问题

上一场结束时下一场开始,应该算冲突吗?

本文采用半开区间 [start, end),因此不算冲突。同一时刻先处理结束事件,再处理开始事件;如果业务需要准备时间,应先扩展占用区间再检测。

合并后的区间数量能代表最大并发数吗?

不能。合并结果表示被占用的时间范围,会丢失重叠层数。最大并发数应对原始区间生成开始与结束事件,再用扫描线计算。

为什么先使用整数时间而不是直接解析日期?

整数刻度可以隔离区间算法与日期解析问题。生产入口仍需把时间统一成相同基准与精度,并在转换完成后拒绝结束不晚于开始的输入。

在独享物理 Mac 上运行你的工程

使用云端 Mac 完成构建、测试与远程桌面操作。按工程需要选择配置和计费周期,实际可用状态以控制台实时返回为准。

下一步,确认你的运行环境

先比较配置,再选择接入方式。SSH 命令行与 VNC 桌面可分别用于构建和交互调试。