雲端 Mac 上的 iOS 百分比分配驗收:最大餘數法

雲端 Mac 上的 iOS 百分比分配驗收:最大餘數法

雲端 Mac 上的 iOS 百分比分配驗收:最大餘數法

iOS 統計頁將三個相同權重的項目各自顯示為 33%,合計卻只有 99%。若把差額全補到最後一項,又可能讓權重為零的項目也分到百分比。在雲端 Mac 的建置流程中,這類問題不必啟動模擬器就能驗證:將分配演算法抽成純 Swift 函式,即可檢查總和、排序與異常輸入,再讓介面使用同一份結果。

先定義顯示單位與輸入契約

這裡處理的是非負整數計數,例如三個分類各自完成的任務數,而非金額、浮點量測值或已經四捨五入的百分比。將顯示總單位數設為 100,每個單位代表一個百分點;若需要顯示到小數點後一位,就改為 1000,每個單位代表零點一個百分點。

契約包含四項:輸出長度與輸入相同;輸出皆為非負數,且合計等於總單位數;零權重分配到零單位;相同輸入始終回傳相同結果。遇到空陣列、負權重或總權重為零時,直接回報失敗,不自行產生平均分配。

「沒有資料」與「每項占比相同」是兩種不同的業務狀態。輸入全為零時,頁面應顯示空白狀態,不能透過分配演算法將它偽裝成有效統計。

餘數相同時,以輸入索引決定先後,因此呼叫端必須先依穩定的分類識別碼排列資料。不要把字典的走訪順序當成產品規則,否則重新整理頁面後,獲得補足單位的項目可能會改變。

用整數最大餘數法補足差額

先將各項權重乘以總單位數,再除以總權重。商數就是基礎配額,餘數則用來決定補足的順序。基礎配額合計不足的部分,依餘數由大到小,逐項增加一個單位。

由於每一項都無條件捨去小數,待補足的單位數必定小於項目數。不需要反覆迴圈直到數值收斂,也不需要設定浮點容許誤差。將以下程式碼儲存為 percentage-check.swift,其中同時包含實作與最小檢查。

enum AllocationError: Error {
    case invalidInput
    case overflow
}

func allocate(_ weights: [Int], units: Int = 100) throws -> [Int] {
    guard !weights.isEmpty,
          (1...10_000).contains(units),
          weights.allSatisfy({ $0 >= 0 }) else {
        throw AllocationError.invalidInput
    }

    var total = 0
    for weight in weights {
        let sum = total.addingReportingOverflow(weight)
        guard !sum.overflow else {
            throw AllocationError.overflow
        }
        total = sum.partialValue
    }
    guard total > 0 else {
        throw AllocationError.invalidInput
    }

    var result = [Int]()
    var remainders = [Int]()
    for weight in weights {
        let product = weight.multipliedReportingOverflow(by: units)
        guard !product.overflow else {
            throw AllocationError.overflow
        }
        result.append(product.partialValue / total)
        remainders.append(product.partialValue % total)
    }

    let remaining = units - result.reduce(0, +)
    let order = weights.indices.sorted {
        if remainders[$0] == remainders[$1] {
            return $0 < $1
        }
        return remainders[$0] > remainders[$1]
    }
    for index in order.prefix(remaining) {
        result[index] += 1
    }
    return result
}

func expectFailure(_ weights: [Int], units: Int = 100) {
    do {
        _ = try allocate(weights, units: units)
    } catch {
        return
    }
    preconditionFailure("Expected allocation to fail")
}

let equal = try allocate([1, 1, 1])
precondition(equal == [34, 33, 33])

let withZero = try allocate([0, 1, 1])
precondition(withZero == [0, 50, 50])

let tenths = try allocate([1, 1, 1], units: 1000)
precondition(tenths == [334, 333, 333])

for a in 0...12 {
    for b in 0...12 {
        for c in 0...12 {
            let weights = [a, b, c]
            let total = weights.reduce(0, +)
            if total == 0 { continue }

            let output = try allocate(weights)
            precondition(output.count == weights.count)
            precondition(output.reduce(0, +) == 100)
            precondition(output.allSatisfy { $0 >= 0 })

            for index in weights.indices {
                let floorQuota = weights[index] * 100 / total
                precondition(
                    output[index] == floorQuota ||
                    output[index] == floorQuota + 1
                )
                if weights[index] == 0 {
                    precondition(output[index] == 0)
                }
            }
        }
    }
}

expectFailure([])
expectFailure([0, 0])
expectFailure([-1, 2])
expectFailure([1], units: 0)
expectFailure([Int.max, 1])
expectFailure([Int.max])

程式碼刻意在加總與乘法兩處檢查溢位。即使最後的百分比很小,計算過程中的權重乘積仍可能超出整數範圍。這裡選擇拒絕輸入,不以轉換成浮點數的方式掩蓋精度變化;若要接受更大的計數,應另外設計更寬位元的整數運算。

在雲端 Mac 執行獨立驗收

先以 xcrun swift --version 確認目前的工具鏈,再執行以下命令。檢查成功時不會有任何輸出;斷言失敗或未處理的錯誤都會使程序失敗,建置流程必須保留該結束狀態。

set -eu
xcrun swift percentage-check.swift

作為正式合併門檻時,可以先用 xcrun swiftc -Onone 編譯再執行,但不能使用會停用這些斷言的最佳化模式。更穩妥的長期做法,是將函式放進業務模組,把斷言移入測試目標,再透過既有的測試命令執行,避免指令碼與產品各自維護一份演算法。

這裡列舉的是三個權重在小範圍內的組合,並非效能評測。涵蓋的情境包括接近全零、單一項目占全部權重、餘數相同,以及小權重被捨去為零等結構。新增測試時應擴充輸入型態,而非單純重複同一個均勻樣本。

區分捨入誤差與業務規則

最容易忽略的問題,是排序後遺失與原始項目的對應關係。這份實作只排序索引,最終陣列仍對應原始項目;介面不能直接將餘數排名當成分類的顯示順序。

其次,不要要求「每個正權重都至少顯示一個單位」。當分類數超過顯示總單位數時,這項要求便無法與總和守恆同時成立。若需要呈現極小的占比,可以提高顯示精度,或另外提供「低於顯示門檻」的文字,但不能暗中修改配額。

最後,不要將此方法視為跨資料集的單調性保證。調整其他分類的權重或顯示精度後,餘數排名可能改變。測試應驗證目前資料的配額範圍,而不是斷言某個項目的數值在每次重新整理後都絕不減少。

小數格式化只負責顯示

總單位數為 1000 時,334 代表 33.4%。如果格式化介面接收的是零到一之間的比例,應傳入 334 / 1000 所對應的數值,而不是再乘以一百。格式化階段不能逐項以原始權重重新計算,也不能再次捨入成另一套精度。

將失敗整理成可檢查的證據

合併前應保留輸入權重、總單位數、基礎配額、餘數排名與最終輸出。若正式環境的統計可能包含敏感分類,請使用固定的測試資料重現,不要將真實業務明細寫入建置產物。

審查重點有三個邊界:無效輸入能否明確回報失敗;餘數相同時能否穩定決定先後;介面格式化是否維持既定精度。驗收通過後,資料層只提供權重,分配層只提供整數單位,顯示層只負責本地化格式。將三層職責分開,百分比合計異常時就能定位到具體環節,而不必靠最後一項補差額來維持表面正確。

常見問題

為什麼不能將每項百分比各自四捨五入?

逐項取整無法保證總和守恆。三個相同權重若各顯示百分之三十三,合計只有百分之九十九。最大餘數法先分配整數配額,再依餘數補足剩餘單位。

權重增加時,顯示百分比一定會增加嗎?

不能視為跨資料集的單調性保證。其他權重或顯示單位數改變時,餘數排名可能不同。本實作保證當次輸入的總和、配額範圍與確定性,不保證跨輸入的顯示結果單調。

用自己的 CI/CD 流程驗證雲端 Mac

ArmRDP 提供專屬實體 Mac mini,並非虛擬機。可按天租用,先確認工具鏈與建置結果,再選擇適合團隊的租期。

立即租用雲端 Mac

設定有據可查,建置結果可重現

確認機型、租期與連線步驟,再將工作交給專屬實體節點。