雲端 Mac 上的 iOS 溫度換算契約:絕對溫度、溫差與顯示驗證

雲端 Mac 上的 iOS 溫度換算契約:絕對溫度、溫差與顯示驗證

雲端 Mac 上的 iOS 溫度換算契約:絕對溫度、溫差與顯示驗證

天氣或感測器應用程式若把「升溫十攝氏度」顯示成「升溫五十華氏度」,通常不是格式設定出了問題,而是把溫差當成絕對溫度來換算。在雲端 Mac 上,可以先用獨立的 Swift 測試套件確立計算契約,再接入 iOS 專案;不必連接感測器,也不必啟動模擬器。

先區分讀值與變化量

絕對溫度換算同時涉及比例與原點偏移:攝氏零度對應華氏三十二度。溫差則只有比例變化:上升十攝氏度對應上升十八華氏度。Foundation 的 Measurement<UnitTemperature> 表示溫度讀值,不會根據變數名稱判斷它是否為變化量。

建議在模型中明確區分 reading 與 delta,並在介面文件中註明單位。業務邏輯層一律保留未經格式化的數值,到顯示時才決定小數位數。不要將「二十三點五度」這類介面文字重新解析成計算輸入。

溫差可以是負值;低於絕對零度的絕對溫度則應拒絕。這兩個欄位不能共用同一套範圍驗證規則。

以下驗證針對物理溫度。若產品處理的是數學外推資料,應另外聲明適用範圍,而不是悄悄放寬輸入限制。

建立不依賴介面的測試套件

先在獨立目錄中確認工具鏈並建立套件,避免更動現有專案設定。執行這些指令前,必須已安裝可用的 Xcode 命令列工具;測試會使用目前選定的 Swift 工具鏈。

xcrun swift --version
workdir="$(mktemp -d)"
cd "$workdir"
swift package init --type library --name TemperatureChecks

保留產生的檔案,再將下一節程式碼儲存為 Tests/TemperatureChecksTests/TemperatureContractTests.swift。程式碼只依賴 Foundation 與 XCTest,不需要加入外部套件。

獨立套件驗證的是換算規則,不代表 iOS 顯示已經通過驗收。移入專案後,應讓測試呼叫正式實作,避免兩份函式各自修改,測試卻始終只檢查舊副本。

用基準點與往返換算確立規則

絕對溫度先檢查是否為有限值,再換算成克耳文,檢查是否低於物理下限。溫差則以「換算後的端點減去換算後的零點」消除偏移。這種寫法只適用於此處的溫度線性換算,不要直接套用到任意單位模型。

import Foundation
import XCTest

private enum TemperatureError: Error {
    case invalidReading
}

private func kelvin(
    _ value: Double,
    from unit: UnitTemperature
) throws -> Double {
    guard value.isFinite else {
        throw TemperatureError.invalidReading
    }
    let result = Measurement(value: value, unit: unit)
        .converted(to: .kelvin).value
    guard result.isFinite, result >= 0 else {
        throw TemperatureError.invalidReading
    }
    return result
}

private func temperatureDelta(
    _ value: Double,
    from source: UnitTemperature,
    to target: UnitTemperature
) -> Double {
    let endpoint = Measurement(value: value, unit: source)
        .converted(to: target).value
    let origin = Measurement(value: 0, unit: source)
        .converted(to: target).value
    return endpoint - origin
}

final class TemperatureContractTests: XCTestCase {
    func testReferencePoints() throws {
        let freezing = Measurement(
            value: 0,
            unit: UnitTemperature.celsius
        ).converted(to: .fahrenheit).value
        XCTAssertEqual(freezing, 32, accuracy: 1e-9)
        XCTAssertEqual(
            try kelvin(0, from: .celsius),
            273.15,
            accuracy: 1e-9
        )
        XCTAssertEqual(
            try kelvin(-273.15, from: .celsius),
            0,
            accuracy: 1e-9
        )
    }

    func testTemperatureDeltas() {
        for value in [-10.0, 0.0, 10.0] {
            XCTAssertEqual(
                temperatureDelta(
                    value,
                    from: .celsius,
                    to: .fahrenheit
                ),
                value * 1.8,
                accuracy: 1e-9
            )
        }
    }

    func testRoundTrip() {
        for value in [-40.0, 0.0, 37.0, 100.0] {
            let original = Measurement(
                value: value,
                unit: UnitTemperature.celsius
            )
            let restored = original.converted(to: .fahrenheit)
                .converted(to: .celsius)
            XCTAssertEqual(
                restored.value,
                value,
                accuracy: 1e-9
            )
        }
    }

    func testInvalidReadings() {
        for value in [
            Double.nan,
            Double.infinity,
            -Double.infinity,
            -274.0
        ] {
            XCTAssertThrowsError(
                try kelvin(value, from: .celsius)
            )
        }
    }
}

在套件根目錄執行 swift test。容差是用來吸收浮點運算誤差,不是業務上可接受的誤差。感測器精度應另行定義,不能靠放寬斷言容差來掩蓋錯誤。

往返測試也不能取代基準點:如果兩個方向的錯誤恰好互相抵消,往返測試仍可能通過。至少應保留零點、負值、已知參考值與無效輸入這四類樣本。溫差輸入端也應自行拒絕非有限值;上面的輔助函式只示範換算,不負責輸入驗證。

分開驗收在地化與數值精度

將以下程式碼儲存為套件目錄中的 format-temperature.swift,並以 swift format-temperature.swift 執行。此處固定使用攝氏單位,避免地區偏好自動改變單位,讓檢查範圍限於顯示規則。

import Foundation

for identifier in ["zh_CN", "en_US", "fr_FR"] {
    let locale = Locale(identifier: identifier)
    let formatter = MeasurementFormatter()
    formatter.locale = locale
    formatter.unitOptions = .providedUnit
    formatter.unitStyle = .short
    formatter.numberFormatter.locale = locale
    formatter.numberFormatter.minimumFractionDigits = 1
    formatter.numberFormatter.maximumFractionDigits = 1

    let reading = Measurement(
        value: 23.45,
        unit: UnitTemperature.celsius
    )
    print(identifier, formatter.string(from: reading))
}

檢查小數分隔符號、單位符號與間距,但不要假設不同系統版本輸出的完整字串會逐字相同。需要使用快照斷言時,應一併記錄系統版本與地區識別碼。

顯示溫差時,不要直接交由絕對溫度格式器隱式處理。先算出目標單位的溫差,再透過數字格式器與在地化資源明確標示「溫差」。負零也應有產品規則:若四捨五入後顯示為零,是否移除負號,應在顯示層統一處理。

接入專案時守住三個邊界

第一,原始資料必須帶有單位。遇到未知單位時,不能預設為攝氏,否則錯誤資料仍可能落在合理範圍內。通訊協定中的未知列舉值應回傳明確錯誤。

第二,閾值比較應使用已統一單位、尚未四捨五入的數值。兩筆顯示為相同數字的讀值,不一定會得到相同的警報判定結果。加入一筆「數值不同但顯示相同」的測試樣本,可以防止介面重構影響業務判斷。

第三,雲端 Mac 上的 Foundation 測試通過後,仍須在目標 iOS 執行環境中檢查格式與版面配置。小工具、圖表座標軸標籤與語音朗讀應分別驗收,不能只檢查主畫面。

最後,保留測試結果、工具鏈版本與格式化輸出。發生回歸問題時,先判斷是換算、輸入契約還是顯示有所變動,再修改對應層;不要只用一項介面字串斷言,就試圖涵蓋所有正確性驗證。

常見問題

為什麼溫差不能直接使用絕對溫度的換算方法?

攝氏與華氏之間的絕對溫度換算包含偏移量,溫差只能套用比例。可分別換算溫差值與來源單位的零點,再相減;十攝氏度的溫差對應十八華氏度的溫差。

在地化溫度字串適合直接做固定文字斷言嗎?

只有同時固定系統版本、語言地區、單位選項及精度時才適合。一般門禁應先驗證數值契約,顯示層再檢查指定單位、四捨五入及版面。

將下一次建置交給獨享雲端 Mac

MacFTP 提供搭載原生 macOS 的 Apple Silicon 獨享實體主機,可選擇日租、週租、月租或季租。先確認規格,再將現有工具鏈連接至節點;實際可用狀態請以控制台即時資訊為準。

立即租用雲端 Mac

從閱讀回到專案

先確認規格,再安排交付

比較機型與租期,或回到部落格繼續閱讀技術文章。