云端 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

从阅读回到项目

先核对配置,再安排交付

比较机型与租期,或返回博客继续查阅技术记录。