首页 > 解决方案 > 如何在不损失小数精度的情况下格式化从字符串转换的数字

问题描述

我有以下 JSON 有效负载,我需要将其转换为数字并随后进行显示格式。

{
    "kilometers_per_second": "14.4578929636",
    "kilometers_per_hour": "52048.4146691173",
    "miles_per_hour": "32340.8607703746"
}

使用 Codable,我创建了以下结构:

struct RelativeVelocity: Codable, Equatable {
    let kilometersPerSecond: String?
    let kilometersPerHour: String?
    let milesPerHour: String?

    enum CodingKeys: String, CodingKey {
        case kilometersPerSecond = "kilometers_per_second"
        case kilometersPerHour = "kilometers_per_hour"
        case milesPerHour = "miles_per_hour"
    }
}

属性是String实例,因为这是 API 返回的内容,而且我是第一次学习使用视图模型,所以我想String在返回格式化String实例之前使用视图模型将实例转换为数字。

我的视图模型具有以下结构:

struct RelativeVelocityViewModel {
    private let relativeVelocity: RelativeVelocity

    init(relativeVelocity: RelativeVelocity) {
        self.relativeVelocity = relativeVelocity
    }
}

extension RelativeVelocityViewModel {
    var formattedKilometersPerHour: String? {
        guard
            let stringValue = relativeVelocity.kilometersPerHour,
            let decimalValue = Decimal(string: stringValue),
            let formatted = NumberFormatter.relativeVelocityFormatter.string(from: decimalValue as NSNumber)
        else { return nil }
        return formatted
    }

    var formattedKilometersPerSecond: String? {
        guard
            let stringValue = relativeVelocity.kilometersPerSecond,
            let decimalValue = Decimal(string: stringValue),
            let formatted = NumberFormatter.relativeVelocityFormatter.string(from: decimalValue as NSNumber)
        else { return nil }
        return formatted
    }

    var formattedMilesPerHour: String? {
        guard
            let stringValue = relativeVelocity.kilometersPerSecond,
            let decimalValue = Decimal(string: stringValue),
            let formatted = NumberFormatter.relativeVelocityFormatter.string(from: decimalValue as NSNumber)
        else { return nil }
        return formatted
    }
}

如您所见,它将String实例转换为Decimal实例,然后将Decimal实例按以下格式格式化NumberFormatter

extension NumberFormatter {
    static let relativeVelocityFormatter: NumberFormatter = {
        let formatter = NumberFormatter()
        formatter.maximumFractionDigits = .max
        formatter.numberStyle = .decimal
        formatter.usesGroupingSeparator = true
        return formatter
    }()
}

XCTestCase用于测试我的视图模型的子类是:

class Tests_RelativeVelocityViewModel: XCTestCase {

    let kilometersPerSecond = "14.4578929636"
    let kilometersPerHour = "52048.4146691173"
    let milesPerHour = "32340.8607703746"
    var populatedViewModel: RelativeVelocityViewModel!
    var emptyViewModel: RelativeVelocityViewModel!

    override func setUpWithError() throws {
        try super.setUpWithError()
        let populatedRelativeVelocity = RelativeVelocity(
            kilometersPerSecond: kilometersPerSecond,
            kilometersPerHour: kilometersPerHour,
            milesPerHour: milesPerHour
        )
        populatedViewModel = RelativeVelocityViewModel(relativeVelocity: populatedRelativeVelocity)
        let emptyRelativeVelocity = RelativeVelocity(
            kilometersPerSecond: nil,
            kilometersPerHour: nil,
            milesPerHour: nil
        )
        emptyViewModel = RelativeVelocityViewModel(relativeVelocity: emptyRelativeVelocity)
    }

    override func tearDownWithError() throws {
        emptyViewModel = nil
        populatedViewModel = nil
        try super.tearDownWithError()
    }

    func test_RelativeVelocityViewModel_ReturnsNilFormattedKilometersPerHour_WhenValueIsMissing() {
        XCTAssertNil(emptyViewModel.formattedKilometersPerHour)
    }

    func test_RelativeVelocityViewModel_ReturnsFormattedKilometersPerHour_WhenValueIsPresent() {
        let expected = "52,048.4146691173"
        XCTAssertEqual(populatedViewModel.formattedKilometersPerHour, expected)
    }

    func test_RelativeVelocityViewModel_ReturnsNilFormattedKilometersPerSecond_WhenValueIsMissing() {
        XCTAssertNil(emptyViewModel.formattedKilometersPerSecond)
    }

    func test_RelativeVelocityViewModel_ReturnsNilFormattedMilesPerHour_WhenValueIsMissing() {
        XCTAssertNil(emptyViewModel.formattedMilesPerHour)
    }

}

下面的测试...

func test_RelativeVelocityViewModel_ReturnsFormattedKilometersPerHour_WhenValueIsPresent() {
    let expected = "52,048.4146691173"
    XCTAssertEqual(populatedViewModel.formattedKilometersPerHour, expected)
}

...产生以下故障:

XCTAssertEqual failed: ("Optional("52,048.414669")") is not equal to ("Optional("52,048.4146691173")")

我知道我可以使用XCTAssertEqual(_:_:accuracy:_:file:line:),但我想保留所有十进制值。

我做错了什么导致格式化结果因丢失值的精度而被舍入?

标签: swift

解决方案


尝试这个:

class MyProjectTests: XCTestCase {

    func testExample() throws {
        let stringValue = "52048.12345678911111"
        let decimalValue = Decimal(string: stringValue)!
        let formatted = NumberFormatter.relativeVelocityFormatter(maxFractionDigits: decimalValue.significantFractionalDecimalDigits).string(from: decimalValue as NSNumber)
        XCTAssert(formatted == stringValue)
    }
}

extension NumberFormatter {
    static func relativeVelocityFormatter(maxFractionDigits: Int) -> NumberFormatter {
        let formatter = NumberFormatter()
        formatter.maximumFractionDigits = maxFractionDigits
        formatter.numberStyle = .none
        formatter.usesGroupingSeparator = true
        return formatter
    }
}

extension Decimal {
    var significantFractionalDecimalDigits: Int {
        return max(-exponent, 0)
    }
}

无论如何,总是有一个限制:

在此处输入图像描述

33 位十进制数字。


推荐阅读