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...
Related problems
- What is the right choice between NSDecimal, NSDecimalNumber, CFNumber?
- How to use NSDecimalNumber?
- Is there a way to create an NSDecimal without using NSNumber and creating autoreleased objects?
- Is NSNumberFormatter the only way to format an NSDecimalNumber?
- cocoa - I've discovered what I think is a bug with NSDecimalNumber