开发者

Proper way to instantiate an NSDecimalNumber from float or double

开发者 https://www.devze.com 2023-02-16 16:01 出处:网络
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...

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

+开发者_如何学CNSNumber 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...


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.


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.


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.


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


Or may be you could define a macro to shorten code

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

budget.amount = DECIMAL(5000.00);
0

精彩评论

暂无评论...
验证码 换一张
取 消

关注公众号