1

I have this Info.

let params2: [String: AnyObject] = [
    "app_token": myapptoken,
    "member_access_token": accessToken!,
    "pay_process": 0,
    "payamount_credit": 9.87 //hardcode
]

When print params2

The result is

["app_token": myapptoken, "member_access_token": accessToken, "payamount_credit": 9.869999999999999, "pay_process": 0]

The "payamount_credit": 9.87 now is "payamount_credit": 9.869999999999999

I have tried all the ways that exist round but behaves the same.

NSString(format: "%.\2f", 9.87)

Double(round(1000*9.87)/1000)

The strangest thing of all is that only happens with this specific number (9.87), is something mystical.

Playground Screen.

enter image description here

Brett
  • 4,341
  • 2
  • 19
  • 17
jose920405
  • 7,982
  • 6
  • 45
  • 71
  • 1
    That's floating point values for you. :) http://programmers.stackexchange.com/questions/101163/what-causes-floating-point-rounding-errors – Robert Gummesson Jun 13 '16 at 16:26
  • You need to generate new tokens, if they are private. You included them in the sample output. – Brett Jun 14 '16 at 04:17

3 Answers3

5

Don't use floats or doubles to represent currency, for precisely (pun) this reason. Use NSDecimalNumber instead (see Which Swift datatype do I use for currency).

NSDecimalNumber takes some effort to use, but it does perform base 10 arithmetic. To use it correctly, you have to read up a bit on the API. In particular, to do rounding you need to supply an NSDecimalNumberHandler (you can set a default one to use).

Good stuff to read:

I think that the Playground shows an unexpected result because it is coercing the internal value of the NSDecimalNumber to a Double before displaying it (I would welcome alternative or authoritative explanations, because I do find this curious). However the internal representation should be correct.

Try out this in your Playground:

let myRoundingBehavior = NSDecimalNumberHandler(roundingMode: .RoundBankers,
scale: 2, raiseOnExactness: true, raiseOnOverflow: true, raiseOnUnderflow: true, raiseOnDivideByZero: true)

NSDecimalNumber.setDefaultBehavior(myRoundingBehavior)

let integerPrice = NSDecimalNumber(mantissa: 987, exponent: -2, isNegative: false)

integerPrice             // 9.870000000000001
Float(integerPrice)      // 9.87
Double(integerPrice)     // 9.870000000000001
integerPrice.description // "9.87"

Don't assume that Float is a more accurate representation of the number, it just happens to work better here. If you keep your calculations (like taxes, discount, shipping, etc.) in the NSDecimalNumber realm, they will be accurate.


Swift 3 / Xcode beta 1 bonus:

Playground behavior is still the same, but changes the Great API Renaming Rodeo changes the above code to:

let myRoundingBehavior = NSDecimalNumberHandler(roundingMode: .roundBankers,
                                                scale: 2,
                                                raiseOnExactness: true,
                                                raiseOnOverflow: true,
                                                raiseOnUnderflow: true,
                                                raiseOnDivideByZero: true)

The change here is that the global NSRoundingMode enum goes away, and is replaced by the enum RoundingMode which is a member of NSDecimalNumber.

Community
  • 1
  • 1
Brett
  • 4,341
  • 2
  • 19
  • 17
  • You're initialising it with a `Double`, so you've already lost the precision. Initialise it with a string. – Jim Jun 13 '16 at 16:45
5

The problem lies within the numeric precission, simply put, some numbers cannot be defined as a Double without losing precision.

Instead you should use NSDecimalNumber:

let num = NSDecimalNumber(string: "9.87")
Community
  • 1
  • 1
Daniel
  • 20,420
  • 10
  • 92
  • 149
0

Try using this function:

let price: Double = 9.87

func roundDouble(num: Double) -> Float {

  //Create NSDecimalNumberHandler to specify rounding behavior
  let numHandler = NSDecimalNumberHandler(roundingMode: NSRoundingMode.RoundDown, scale: 2, raiseOnExactness: false, raiseOnOverflow: false, raiseOnUnderflow: false, raiseOnDivideByZero: false)

  let roundedNum = NSDecimalNumber(double: num).decimalNumberByRoundingAccordingToBehavior(numHandler)

  //Convert to float so you don't get the endless repeating decimal.
  return Float(roundedNum)

}

roundDouble(price) //9.87

I think the best thing to do here is to explicitly use a float instead of a double because the precision of a double is not necessary if you only want two decimal places. This function just helps round it a bit more accurately.

Ike10
  • 1,585
  • 12
  • 14