正NSDecimalNumber返回意外的64位整数值

时间:2016-03-31 00:49:45

标签: ios swift cocoa foundation

我偶然发现了一个奇怪的NSDecimalNumber行为:对于某些值,integerValuelongValuelongLongValue等的调用会返回一个意外的值。例如:

let v = NSDecimalNumber(string: "9.821426272392280061")
v                  // evaluates to 9.821426272392278
v.intValue         // evaluates to 9
v.integerValue     // evaluates to -8
v.longValue        // evaluates to -8
v.longLongValue    // evaluates to -8

let v2 = NSDecimalNumber(string: "9.821426272392280060")
v2                  // evaluates to 9.821426272392278
v2.intValue         // evaluates to 9
v2.integerValue     // evaluates to 9
v2.longValue        // evaluates to 9
v2.longLongValue    // evaluates to 9

这是使用XCode 7.3;我没有使用早期版本的框架进行测试。

我看过很多关于使用NSDecimalNumber的意外舍入行为的讨论,以及不使用继承的NSNumber初始化程序初始化它的警告,但我还没有看到任何有关此特定内容的信息。行为。然而,关于内部表示和舍入的一些相当详细的讨论,其中可能包含我寻求的金块,所以如果我错过它,请提前道歉。

编辑:它已被隐藏在评论中,但我已将此问题作为问题#25465729提交给Apple。 OpenRadar:http://www.openradar.me/radar?id=5007005597040640

编辑2: Apple已将此标记为#19812966的副本。

2 个答案:

答案 0 :(得分:0)

如果我是你,我会向Apple提交一个错误。 docs表示NSDecimalNumber可以表示长达38位的任何值。 NSDecimalNumber从NSNumber继承这些属性,并且文档没有明确说明在那一点涉及什么转换,但唯一合理的解释是如果数字可以圆整并可表示为Int,那么你得到正确的答案。 / p>

在我看来,在转换过程中处理符号扩展的错误,因为intValue是32位而integerValue是64位(在Swift中)。

答案 1 :(得分:0)

由于您已经知道问题是由于“精度太高”,您可以先通过舍入十进制数来解决问题:

let b = NSDecimalNumber(string: "9.999999999999999999")
print(b, "->", b.int64Value)
// 9.999999999999999999 -> -8

let truncateBehavior = NSDecimalNumberHandler(roundingMode: .down,
                                              scale: 0,
                                              raiseOnExactness: true,
                                              raiseOnOverflow: true,
                                              raiseOnUnderflow: true,
                                              raiseOnDivideByZero: true)
let c = b.rounding(accordingToBehavior: truncateBehavior)
print(c, "->", c.int64Value)
// 9 -> 9

如果您想使用int64Value(即-longLongValue),请避免使用超过62位精度的数字,即避免完全超过18位数。解释原因如下。

NSDecimalNumber在内部表示为Decimal structure

typedef struct {
      signed int _exponent:8;
      unsigned int _length:4;
      unsigned int _isNegative:1;
      unsigned int _isCompact:1;
      unsigned int _reserved:18;
      unsigned short _mantissa[NSDecimalMaxSize];  // NSDecimalMaxSize = 8
} NSDecimal;

这可以使用.decimalValue获得,例如

let v2 = NSDecimalNumber(string: "9.821426272392280061")
let d = v2.decimalValue
print(d._exponent, d._mantissa, d._length)
// -18 (30717, 39329, 46888, 34892, 0, 0, 0, 0) 4

这意味着9.821426272392280061内部存储为9821426272392280061×10 -18 - 请注意9821426272392280061 = 34892 ×65536 3 + 46888 ×65536 2 + 39329 ×65536 + 30717

现在与9.821426272392280060进行比较:

let v2 = NSDecimalNumber(string: "9.821426272392280060")
let d = v2.decimalValue
print(d._exponent, d._mantissa, d._length)
// -17 (62054, 3932, 17796, 3489, 0, 0, 0, 0) 4

请注意,指数减少到-17,这意味着Foundation会忽略尾随零。

了解内部结构,我现在提出索赔:错误是因为34892≥32768。观察:

let a = NSDecimalNumber(decimal: Decimal(
    _exponent: -18, _length: 4, _isNegative: 0, _isCompact: 1, _reserved: 0,
    _mantissa: (65535, 65535, 65535, 32767, 0, 0, 0, 0)))
let b = NSDecimalNumber(decimal: Decimal(
    _exponent: -18, _length: 4, _isNegative: 0, _isCompact: 1, _reserved: 0,
    _mantissa: (0, 0, 0, 32768, 0, 0, 0, 0)))
print(a, "->", a.int64Value)
print(b, "->", b.int64Value)
// 9.223372036854775807 -> 9
// 9.223372036854775808 -> -9

请注意,32768×65536 3 = 2 63 的值足以溢出带符号的64位数字。因此,我怀疑这个错误是由于基金会实施int64Value因为(1)将尾数直接转换为Int64,然后(2)除以10 | exponent |

事实上,如果你反汇编Foundation.framework,你会发现它基本上是int64Value的实现方式(这与平台的指针宽度无关)。

但为什么int32Value不受影响?因为内部它只是实现为Int32(self.doubleValue),所以不会发生溢出问题。不幸的是,双精度只有53位的精度,所以Apple别无选择,只能实现int64Value(需要64位精度),而不需要浮点运算。