雲端 Mac 上的 iOS 預約衝突檢測:半開區間與掃描線驗收

雲端 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 桌面可分別用於建置和互動式除錯。