Положительный NSDecimalNumber возвращает неожиданные 64-битные целочисленные значения

Я наткнулся на нечетное поведение NSDecimalNumber: для некоторых значений вызовы integerValue, longValue, longLongValue и т.д. возвращают неожиданное значение. Пример:

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.

Ответ 1

Я бы написал ошибку с Apple, если бы был вами. docs говорят, что NSDecimalNumber может представлять любое значение длиной до 38 цифр. NSDecimalNumber наследует эти свойства из NSNumber, и в документах явно не указано, какое преобразование задействовано в этой точке, но единственная разумная интерпретация заключается в том, что если число округлено и представимо как Int, тогда вы получите правильный ответ.

Он выглядит как ошибка при обработке расширений знака во время преобразования где-то, поскольку intValue является 32-битным, а integerValue - 64-битным (в Swift).

Ответ 2

Поскольку вы уже знаете, что проблема связана с "слишком высокой точностью", вы можете обходить ее путем округления десятичного числа:

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 внутренне представлен как Десятичная структура:

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-разрядного номера. Поэтому я подозреваю, что ошибка связана с тем, что Foundation реализует int64Value как (1) преобразовать мантисс непосредственно в Int64, а затем (2) делить на 10 | экспонент |.

На самом деле, если вы разобьете Foundation.framework, вы обнаружите, что в основном реализовано int64Value (это не зависит от ширины указателя платформы).

Но почему int32Value не влияет? Потому что внутренне он реализован как Int32(self.doubleValue), поэтому проблема с переполнением не возникнет. К сожалению, двойной имеет только 53 бит точности, поэтому у Apple нет выбора, кроме как реализовать int64Value (требующий 64 бита точности) без арифметики с плавающей запятой.