Proper way to instantiate an NSDecimalNumber from float or double
Asked Answered
A

5

41

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...

Armstead answered 14/3, 2011 at 21:34 Comment(2)
Remember that it is the class method 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
@Culpa - Yes, but as I comment on your answer, this effectively is typecasting an NSNumber as an NSDecimalNumber, which is not recommended: cocoabuilder.com/archive/cocoa/… . I point out a case where this fails in my comments below.Sore
C
72

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.

Culpa answered 14/3, 2011 at 21:48 Comment(6)
Note that using NSNumber's initializers with NSDecimalNumber is not recommended. It can lead to bad conversions of floating point numbers, as seen in the example of this question: #2479600 . Also, see Marcus Zarra's comments after his article here: cimgf.com/2008/04/23/… . This was also brought up at some point in the cocoa-dev mailing list, but I can't find the reference right now.Sore
Ah, here's that response by Bill Bumgarner on the cocoa-dev mailing list: cocoabuilder.com/archive/cocoa/… .Sore
Nice follow-on analysis. It's good to see some hard results for what happens with the various initializers.Sore
Good detailed info. Thanks for sharing. I'm writing an app that receives stock quotes in floating point and just displays them without having to do any math with them. In this case, is it better for performance to use NSNumber? I could always use floats on their own, but I figure its better to use NSNumbers since this allow instant use of the NSNumberFormatter class.Armstead
The purists might say that anything financial should use a decimal type (like NSDecimalNumber) but in a practical sense given that it is likely that the stock quotes are not going to be highly precise using just NSNumber will be fine. Hell even standard C float or double will probably be fine.Culpa
@Armstead - NSNumberFormatter works just fine with NSDecimalNumber: #2940939 . However, if you're just doing display of values gathered by a service, floating point numbers should be fine. NSDecimalNumber is for when you are doing any manipulation of these numbers, because the last thing you want when handling currency is to have 43.1 - 43.2 + 1 = 0.89999999999999857891 (an actual case). It's not just about precision, but problems with IEEE 754 floating point representation of decimals.Sore
S
15

There are some problems that can arise if you use NSNumber's initializers when creating an NSDecimalNumber. See the example posted in this question for a great case of that. Also, I trust Bill Bumgarner's word on this from his response in the cocoa-dev mailing list. See also Ashley's answer here.

Because I'm paranoid about these conversion issues, I've used the following code in the past to create NSDecimal structs:

NSDecimal decimalValueForDouble(double doubleValue)
{
    NSDecimal result;
    NSString *stringRepresentationOfDouble = [[NSString alloc] initWithFormat:@"%f", doubleValue];
    NSScanner *theScanner = [[NSScanner alloc] initWithString:stringRepresentationOfDouble];
    [stringRepresentationOfDouble release];

    [theScanner scanDecimal:&result];
    [theScanner release];

    return result;
}

The reason for the complex code here is that I've found NSScanner to be about 90% faster than initializing an NSDecimalNumber from a string. I use NSDecimal structs because they can yield significant performance advantages over their NSDecimalNumber brothers.

For a simpler but slightly slower NSDecimalNumber approach, you could use something like

NSDecimalNumber *decimalNumberForDouble(double doubleValue)
{
    NSString *stringRepresentationOfDouble = [[NSString alloc] initWithFormat:@"%f", doubleValue];
    NSDecimalNumber *stringProcessor = [[NSDecimalNumber alloc] initWithString:stringRepresentationOfDouble];
    [stringRepresentationOfDouble release];
    return [stringProcessor autorelease];
}

However, if you're working with NSDecimalNumbers, you'll want to do what you can to avoid having a value become cast to a floating point number at any point. Read back string values from your text fields and convert them directly to NSDecimalNumbers, do your math with NSDecimalNumbers, and write them out to disk as NSStrings or decimal values in Core Data. You start introducing floating point representation errors as soon as your values are cast to floating point numbers at any point.

Sore answered 14/3, 2011 at 23:27 Comment(2)
Brad, Thanks for sharing. How do I go about creating the NSDecimal C-style structs? I've researched them and didn't much in Apple's documentationArmstead
@Armstead - I descibe the internal format of the NSDecimal struct in my answer here, which was obtained by examining the appropriate header file. However, this format is considered private and is undocumented outside of the header. Generally, the safest way to create them is either by grabbing the value from an NSDecimalNumber or by using an NSScanner on a string like I do above. Once you have a value in NSDecimal format, it's generally easy to work with.Sore
A
1

You could try:

float myFloat = 42.0;
NSDecimal myFloatDecimal = [[NSNumber numberWithFloat:myFloat] decimalValue];
NSDecimalNumber* num = [NSDecimalNumber decimalNumberWithDecimal:myFloatDecimal];

Apparently there are faster ways of doing this, but it may not be worth it if the code is not a bottleneck.

Al answered 14/3, 2011 at 21:39 Comment(0)
M
0

-[NSNumber initWithFloat:] and -[NSNumber initWithDouble:] actually don’t return NSNumber * objects, they return id, and NSDecimalNumber inherits from NSNumber, so you can use:

NSDecimalNumber *decimal = [[[NSDecimalNumber alloc] initWithFloat:1.1618] autorelease];

And duh, I just realized that the same goes for +numberWithFloat:, so you can just use that instead.

Morty answered 14/3, 2011 at 22:5 Comment(0)
E
-1

Or may be you could define a macro to shorten code

#define DECIMAL(x) [[NSDecimalNumber alloc]initWithDouble:x]

budget.amount = DECIMAL(5000.00);
Electrocautery answered 13/11, 2017 at 13:8 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.