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.
alloc
that determines the type of an object not the init* methods. initWithFloat does not return an NSNumber. You are calling initWithFloat on the NSDecimalNumber instance returned by alloc. – Culpa