I'm building a app that needs to perform calculations on money.
I wonder how to properly use NSDecimalNumber, especially how to initialize it from integers, floats & doubles?
I only found it easy to use the -decimalNumberWithString:
method. The -initWith...
methods are discouraged so that only left the ones with mantissa, but never in any of 7 languages I used before did I need that so I don't know what put there...
Do NOT use NSNumber
's +numberWith...
methods to create NSDecimalNumber
objects. They are declared to return NSNumber
objects and are not guaranteed to function as NSDecimalNumber
instances.
This is explained in this thread by Bill Bumgarner, a developer at Apple. I would encourage you to file a bug against this behavior, referencing bug rdar://6487304.
As an alternative these are all of the appropriate methods to use to create an NSDecimalNumber
:
+ (NSDecimalNumber *)decimalNumberWithMantissa:(unsigned long long)mantissa
exponent:(short)exponent isNegative:(BOOL)flag;
+ (NSDecimalNumber *)decimalNumberWithDecimal:(NSDecimal)dcm;
+ (NSDecimalNumber *)decimalNumberWithString:(NSString *)numberValue;
+ (NSDecimalNumber *)decimalNumberWithString:(NSString *)numberValue locale:(id)locale;
+ (NSDecimalNumber *)zero;
+ (NSDecimalNumber *)one;
+ (NSDecimalNumber *)minimumDecimalNumber;
+ (NSDecimalNumber *)maximumDecimalNumber;
+ (NSDecimalNumber *)notANumber;
If you simply want an NSDecimalNumber
from a float
or int
constant try something like this:
NSDecimalNumber *dn = [NSDecimalNumber decimalNumberWithDecimal:
[[NSNumber numberWithFloat:2.75f] decimalValue];
The correct way is actually to do this:
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]);
See more info here.
Design-wise, you should try to avoid converting NSDecimalNumber or NSDecimals to and from int, float, and double values for the same reasons it's recommended you use NSDecimalNumbers: loss of precision and binary floating point representation issues. I know, sometimes it's unavoidable (taking input from a slider, doing trigonometric calculations, etc.), but you should try to take input from users as NSStrings and then use initWithString:locale: or decimalNumberWithString:locale: to generate the NSDecimalNumbers. Do all your math with the NSDecimalNumbers and return their representation to users or save them to SQLite (or wherever) as their string description using descriptionWithLocale:.
If you have to input from an int, float, or double, you could do something like the following:
int myInt = 3;
NSDecimalNumber *newDecimal = [NSDecimalNumber decimalNumberWithString:[NSString stringWithFormat:@"%d", myInt]];
or you could follow Ashley's suggestion to make sure you're safe in the decimal construction.
One small addition: if you init NSDecimalNumber
from a string, it might be useful to set a locale also. For example, if your string contains comma as a decimal separator
.
self.order.amount = [NSDecimalNumber decimalNumberWithString:self.amountText locale:[NSLocale currentLocale]];