Proper way to instantiate an NSDecimalNumber from float or double

cocoa, iphone, objective-c

Solution

You were on the right track (with caveats, see below). You should be able to just use the initialisers from NSNumber as they are inherited by NSDecimalNumber.

NSDecimalNumber *floatDecimal = [[[NSDecimalNumber alloc] initWithFloat:42.13f] autorelease];
NSDecimalNumber *doubleDecimal = [[[NSDecimalNumber alloc] initWithDouble:53.1234] autorelease];
NSDecimalNumber *intDecimal = [[[NSDecimalNumber alloc] initWithInt:53] autorelease];

NSLog(@"floatDecimal floatValue=%6.3f", [floatDecimal floatValue]);
NSLog(@"doubleDecimal doubleValue=%6.3f", [doubleDecimal doubleValue]); 
NSLog(@"intDecimal intValue=%d", [intDecimal intValue]);

More info on this subject can be found here.

However, I've seen a lot of discussion on Stack Overflow and around the web about issues with initialising NSDecimalNumber. There seems to be some issues relating to precision and conversion to/from doubles with `NSDecimalNumber`. Especially when you use the initialisation members inherited from NSNumber.

I knocked up the test below:

double dbl = 36.76662445068359375000;
id xx1 = [NSDecimalNumber numberWithDouble: dbl]; // Don't do this
id xx2 = [[[NSDecimalNumber alloc] initWithDouble: dbl] autorelease];
id xx3 = [NSDecimalNumber decimalNumberWithString:@"36.76662445068359375000"];
id xx4 = [NSDecimalNumber decimalNumberWithMantissa:3676662445068359375L exponent:-17 isNegative:NO];

NSLog(@"raw doubleValue: %.20f,              %.17f", dbl, dbl);

NSLog(@"xx1 doubleValue: %.20f, description: %@", [xx1 doubleValue], xx1);   
NSLog(@"xx2 doubleValue: %.20f, description: %@", [xx2 doubleValue], xx2);   
NSLog(@"xx3 doubleValue: %.20f, description: %@", [xx3 doubleValue], xx3);   
NSLog(@"xx4 doubleValue: %.20f, description: %@", [xx4 doubleValue], xx4);   

The output is:

raw doubleValue: 36.76662445068359375000               36.76662445068359375
xx1 doubleValue: 36.76662445068357953915, description: 36.76662445068359168
xx2 doubleValue: 36.76662445068357953915, description: 36.76662445068359168
xx3 doubleValue: 36.76662445068357953915, description: 36.76662445068359375
xx4 doubleValue: 36.76662445068357953915, description: 36.76662445068359375

So you can see that when using the `numberWithDouble` convenience method on `NSNumber` (that you shouldn't really use due to it returning the wrong pointer type) and even the initialiser `initWithDouble` (that IMO "should" be OK to call) you get a NSDecimalNumber with an internal representation (as returned by calling `description` on the object) that is not as accurate as the one you get when you invoke `decimalNumberWithString:` or `decimalNumberWithMantissa:exponent:isNegative:`.

Also, note that converting back to a double by calling `doubleValue` on the `NSDecimalNumber` instance is losing precision but is, interestingly, the same no matter what initialiser you call.

So in the end, I think it is recommend that you use one of the `decimalNumberWith*` methods declared at the `NSDecimalNumber` class level to create your NSDecimalNumber instances when dealing with high precision floating point numbers.

So how do you call these initialisers easily when you have a double, float or other NSNumber?

Two methods described here "work" but still have precision issues.

If you already have the number stored as an NSNumber then this should work:

id n = [NSNumber numberWithDouble:dbl];
id dn1 = [NSDecimalNumber decimalNumberWithDecimal:[n decimalValue]];

NSLog(@"  n doubleValue: %.20f, description: %@", [n doubleValue], n);   
NSLog(@"dn1 doubleValue: %.20f, description: %@", [dn1 doubleValue], dn1);  

But as you can see from the output below it lops off some of the less significant digits:

  n doubleValue: 36.76662445068359375000, description: 36.76662445068359
dn1 doubleValue: 36.76662445068357953915, description: 36.76662445068359

If the number is a primitive (float or double) then this should work:

id dn2 = [NSDecimalNumber decimalNumberWithMantissa:(dbl * pow(10, 17))
                                          exponent:-17 
                                        isNegative:(dbl < 0 ? YES : NO)];

NSLog(@"dn2 doubleValue: %.20f, description: %@", [dn2 doubleValue], dn2);

But you will get precision errors again. As you can see in the output below:

dn2 doubleValue: 36.76662445068357953915, description: 36.76662445068359168

The reason I think for this precision loss is due to the involvement of the floating point multiply, because the following code works fine:

id dn3 = [NSDecimalNumber decimalNumberWithMantissa:3676662445068359375L 
                                           exponent:-17 
                                         isNegative:NO];

NSLog(@"dn3 doubleValue: %.20f, description: %@", [dn3 doubleValue], dn3);   

Output:

dn3 doubleValue: 36.76662445068357953915, description: 36.76662445068359375

So the most consistently accurate conversion/initialisation from a `double` or `float` to NSDecimalNumber is using `decimalNumberWithString:`. But, as Brad Larson has pointed out in his answer, this might be a little slow. His technique for conversion using NSScanner might be better if performance becomes an issue.

Problem

I'm looking for the best way to instantiate an NSDecimalNumber from a double or short. There are the following NSNumber class and instance methods... ``` +NSNumber numberWithFloat +NSNumber numberWithDouble -NSNumber initWithFloat -NSNumber initWithDouble ``` but these appear to return NSNumber. On the other side, NSDecimalNumber defines the following: ``` +NSDecimalNumber decimalNumberWithMantissa:exponent:isNegative: +NSDecimalNumber decimalNumberWithDecimal: ``` There are a few possibilities here on which way to go. Xcode generates a warning if you have a NSDecimalNumber set to the return value of the NSNumber convenience methods above. Would appreciate input on the cleanest and correct way to go...

Original source

Related problems