首页 > 解决方案 > 字符串格式说明符:用于十进制值的舍入规则

问题描述

String(format:)用来转换一个Float. 我以为这个数字会四舍五入。

有时是的。

String(format: "%.02f", 1.455)     //"1.46"

有时不是。

String(format: "%.02f", 1.555)     //"1.55"
String(round(1.555 * 100) / 100.0) //"1.56"

在此处输入图像描述

我猜 1.55 不能完全表示为二进制。它变成了 1.549999XXXX

NumberFormatter似乎不会导致同样的问题......为什么?应该优先于它String(format:)吗?

let formatter = NumberFormatter()
formatter.maximumFractionDigits = 2
formatter.minimumFractionDigits = 2

if let string = formatter.string(for: 1.555) {
    print(string)  // 1.56
}

标签: swiftnumbersroundingnumber-formattingswift-string

解决方案


String (format :)可以在这些问题的答案(或更常见的评论)中找到对问题的参考(用于四舍五入):将双精度值快速舍入到 x 小数位数以及如何将双精度值格式化为货币 -斯威夫特 3。但是它涵盖的问题(使用浮点数的数学)已经在 SO(所有语言)上处理了很多次。

String(format:)没有对十进制数进行四舍五入的功能(即使不幸的是在某些答案中提出了它),而是对其进行格式化(顾名思义)。这种格式有时会导致舍入。那是真实的。但是我们必须记住一个问题,数字 1.555 是……不值 1.555。

在 SwiftDoubleFloat中,符合FloatingPoint 协议的 IEEE 754 规范。但是,有些值不能用 IEEE 754 标准准确表示。

就像你不能在(有限)十进制扩展中精确地表示三分之一一样,有很多数字在十进制中看起来很简单,但在二进制扩展中却有很长或无限的扩展。”(来源

为了确信这一点,我们可以使用浮点转换器在数字的十进制表示(如“1.02”)和所有现代 CPU 使用的二进制格式(IEEE 754 浮点)之间进行转换。对于1.555,实际存储在 float 中的值是1.55499994754791259765625

所以问题不来自String (format :)。例如,我们可以尝试另一种方法四舍五入到千分之一,我们发现同样的问题。:

round (8.45 * pow (10.0, 3.0)) / pow (10.0, 3.0)
// 8.449999999999999

就是这样:“二进制浮点运算很好,只要您知道发生了什么并且不要期望值与您放入程序中的十进制值完全相同”。

所以真正的问题是:这对你来说真的是个问题吗?这取决于应用程序。一般来说,如果我们通过限制精度(通过四舍五入)将数字转换为字符串,那是因为我们认为这种精度对用户没有用处。如果这是我们正在谈论的那种数据,那么可以使用FloatingPoint.

但是,要对其进行格式化使用NumberFormatter. 不一定是因为它的舍入算法,而是因为它允许您定位格式:

let formatter = NumberFormatter()
formatter.maximumFractionDigits = 2
formatter.minimumFractionDigits = 2
formatter.locale = Locale(identifier: "fr_FR")
formatter.string(for: 1.55)!
// 1,55
formatter.locale = Locale(identifier: "en_US")
formatter.string(for: 1.55)!
// 1.55

相反,如果我们在精度很重要的情况下,我们必须放弃Double/Float并使用Decimal. 仍然保留我们的舍入示例,我们可以使用这个扩展(这可能是问题“将双精度值快速舍入到 x 小数位数”的最佳答案):

extension Double {
    func roundedDecimal(to scale: Int = 0, mode: NSDecimalNumber.RoundingMode = .plain) -> Decimal {
        var decimalValue = Decimal(self)
        var result = Decimal()
        NSDecimalRound(&result, &decimalValue, scale, mode)
        return result
    }
}

1.555.roundedDecimal(to: 2)
// 1.56

推荐阅读