NSDecimalNumber(x).intValue returns -2, 0, 15 and 199, depending on the amount of decimals in x (x = 199.999...5)
Asked Answered
M

1

5

We found an interesting case in our business logic that totally breaks our logic and we don't understand why NSDecimalNumber and Decimal behaves the way it does.

My playground for the cases is as follows:

import Foundation

let pQuantity = Decimal(string: "0.2857142857142857")!
let pPrice = Decimal(string: "7.00000000000000035")!

let calced = NSDecimalNumber(decimal: pQuantity * pPrice * Decimal(integerLiteral: 100))   // 200
let decimal = calced.decimalValue                                                          // 199.9999999999999999999999999999995
let integer = calced.intValue                                                              // 0

NSDecimalNumber(decimal: Decimal(string: "199.9999999999999999999999999999995")!).intValue // 0
NSDecimalNumber(decimal: Decimal(string: "199.9999999999999995")!).intValue                // 199
NSDecimalNumber(decimal: Decimal(string: "199.99999999999999995")!).intValue               // 15
NSDecimalNumber(decimal: Decimal(string: "199.999999999999999995")!).intValue              // -2

In the playground code above you can see the return value if you scroll to the right, if you don't want to run it yourselves.

We need to convert our raw decimal values, quantity and price, to ints temporarily when calculating how much to evenly split these quantities to produce nice-looking prices. We can't however for some reason in this case as the initial step of conversion fails, producing a 0 instead of 200 (and yes, the current code would produce 199 which is a bug).

Why does the NSDecimalNumber return these weird values depending on the amount of decimals, ranging from -2 to 199?

Our solution would be to round the inner calculation before putting it into NSDecimalNumber, but we'd like to know the cause of this to begin with. Is it a bug or is it expected and one should be aware that it might happen?

Mound answered 8/5, 2019 at 8:34 Comment(5)
Most probably related to this bug bugs.swift.org/browse/SR-2980.Concubine
@MartinR Seems like the obvious candidate here. I guess there's nothing I can do other than rounding it before then as we wait for the bug to be resolved?Mound
Similar bizarre behavior of NSDecimalNumber was observed here https://mcmap.net/q/375930/-nsdecimalnumber-integervalue-behaving-strangely-in-ios8.Concubine
@MartinR so according to that thread it is something in super class and not overriding an accessor? Interesting that it does not do that though, in that case.Mound
I am not sure if the accepted answer of that Q&A explains it correctly, but I did not spend much time reading it (therefore I won't close this as a duplicate for now). I just wanted to provide another example of such behavior. – NSDecimalNumber is part of the (closed source) Foundation framework on Apple platforms, therefore I can only suggest to file a bug at the Apple Bugreporter.Concubine
B
7

It's clearly a foundation bug, probably the one mentioned by Martin R in the comments.

I experimented in Playground (Swift 5) and found that the comment on that bug that int32Value works correctly is true.

import Foundation

let pQuantity = Decimal(string: "0.2857142857142857")!
let pPrice = Decimal(string: "7.00000000000000035")!

let calced = NSDecimalNumber(decimal: pQuantity * pPrice * Decimal(integerLiteral: 100))   // 200
let decimal = calced.decimalValue                                                          // 199.9999999999999999999999999999995
let integer = calced.int32Value                                                              // 200

NSDecimalNumber(decimal: Decimal(string: "199.9999999999999999999999999999995")!).uint32Value // 200
NSDecimalNumber(decimal: Decimal(string: "199.9999999999999995")!).int32Value                // 200
NSDecimalNumber(decimal: Decimal(string: "199.99999999999999995")!).int32Value               // 200
NSDecimalNumber(decimal: Decimal(string: "199.999999999999999995")!).int32Value              // 200

Also, as you can see uint32Value also works correctly. However, none of the 64 bit variants work.

Provided you are sure that your result will fit into an Int32 you can use that as a work around until they fix it, which is probably never, given that the bug has been outstanding for a while.

Bregenz answered 8/5, 2019 at 9:52 Comment(2)
For our purposes I think we will round it before shoving it into the NSDecimalNumber. But that's an interesting catch there. I'll accept this answer though as it is clearly a bug and the workaround will probably depend on the team and what they need.Mound
@Mound thanks. Yes, I think it is better to do the rounding before converting to an integer, because you have more control over how the rounding occurs and also, the rounding behaviour of intxxValue doesn't seem to be very well documented.Bregenz

© 2022 - 2024 — McMap. All rights reserved.