How to Create an Nsdecimal Without Using Nsnumber and Creating Autoreleased Objects

Is there a way to create an NSDecimal without using NSNumber and creating autoreleased objects?

Unfortunately, Apple does not provide any easy ways of putting values into an NSDecimal struct. The struct definition itself can be found in the NSDecimal.h header:

typedef struct {
signed int _exponent:8;
unsigned int _length:4; // length == 0 && isNegative -> NaN
unsigned int _isNegative:1;
unsigned int _isCompact:1;
unsigned int _reserved:18;
unsigned short _mantissa[NSDecimalMaxSize];
} NSDecimal;

but I don't know that I would go around trying to reverse-engineer how the structs hold values. The underscores on the fields in the struct indicate that these are private and subject to change. I don't imagine that a lot of changes occur in the low-level NSDecimal functions, but I'd be nervous about things breaking at some point.

Given that, initializing an NSDecimal from a floating point number is best done in the way that you describe. However, be aware that any time you use a floating point value you are losing the precision you've gained by using an NSDecimal, and will be subjecting yourself to floating point errors.

I always work only with NSDecimals within my high-precision calculations, and take in and export NSStrings for exchanging these values with the outside world. To create an NSDecimal based on an NSString, you can use the approach we take in the Core Plot framework:

NSDecimal CPDecimalFromString(NSString *stringRepresentation)
{
NSDecimal result;
NSScanner *theScanner = [[NSScanner alloc] initWithString:stringRepresentation];
[theScanner scanDecimal:&result];
[theScanner release];

return result;
}

Using an NSScanner instead of NSDecimalNumber -initWithString:locale: is about 90% faster in my benchmarks.

HOW to declare NSDecimal and how to use NSDecimalAdd

NSDecimal is a C-struct with private members, you would usually create one from the Objective-C class NSDecimalNumber (decimalValue).

However, using NSDecimal directly is rarely needed, dealing with NSDecimalNumber is much easier. You can use the decimalNumberByAdding: method instead of the NSDecimalAdd function.

Set NSDecimal to zero

I have never noticed that NSDecimal doesn't have a nice Create function.

I can't imagine another method than the 3 you list.

NSDecimal zero = {0};

You are right, this is not obvious, this is actually very far from obvious. Usual programmers won't look up the documentation to see what value the struct will represent with all attributes set to zero.

NSDecimal zero = @(0).decimalValue

I don't like it because it uses NSNumber and needlessly creates an autoreleased object.

NSDecimal zero = [NSDecimalNumber zero].decimalValue

This is my preferred solution. Using NSDecimal structs directly is done mostly for performance boosts (without creating objects on the heap). This method matches the intent because it uses a singleton constant.

However, if you have a lot of code involving NSDecimal and performance is really important for you, create your own function/macro to initialize the struct and use this function everywhere. Then this problem won't matter to you because the function name will make the code obvious.

Proper way to instantiate an NSDecimalNumber from float or double

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.

What is the right choice between NSDecimal, NSDecimalNumber, CFNumber?

If you are dealing with financial computations, you really should use base-10 arithmetic to avoid the rounding errors that can occur with the standard base-2 floating point types. So it's either NSDecimal or NSDecimalNumber. And since you're writing object-oriented code, NSDecimalNumber is the right choice for you.

To answer your questions: only testing of your code can reveal whether the memory overhead and performance loss are acceptable to you. I haven't really worked much with NSDecimalNumber but I'd wager that Apple's implementation is quite efficient and will be more than adequate for most people's needs.

Unfortunately, you won't be able to avoid the likes of decimalNumberByAdding: since Objective-C does not support operator overloading like C++ does. I agree that it makes your code somewhat less elegant.

One comment on the code you posted: r = [obj performSelector:NSSelectorFromString(@"capitalizedAmount")]; is rather unelegant. Either

r = [obj performSelector:@selector(capitalizedAmount)];

or even the simple

r = [obj capitalizedAmount];

would be better unless you require the NSSelectorFromString syntax for some other reason.

NSDecimal requires a bridge cast

Apple is not providing an easy way putting values into an NSDecimal, so you have to do this yourself. This thread will help you: Is there a way to create an NSDecimal without using NSNumber and creating autoreleased objects?

On the other hand you can just use a NSNumber or a regular float:

NSString *inputString = self.Value.text;
float inputFloat = [inputString floatValue];
NSNumber *mynumber = [NSNumber numberWithFloat:inputFloat]

NSDecimalNumber compare fails

2.6 cannot be represented exactly as a floating-point number, but NSDecimal can represent this value exactly (that's why we need NSDecimals in the first place).

According to IEEE 754 Calculator, 2.6 gets converted to 2.5999999046325684, which is less than 2.6, and therefore compares as NSOrderedDescending.

How to use NSDecimalNumber?

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];

NSDecimalNumber question for core data and iPhone

Try this:

NSDecimalNumber *budgetNumber = [NSDecimalNumber decimalNumberWithString:@"1.00"];

PS: Adding solution for the second issue stated in comments

(void)configureCell:(UITableViewCell *)cell withEnvelope:(NSManagedObject *)model{ 
UILabel *envelopeNameLabel = (UILabel *) [cell viewWithTag:1];

envelopeNameLabel.text = [model valueForKey:@"name"];

UILabel *envelopeBudgetLabel = (UILabel *) [cell viewWithTag:2];
envelopeBudgetLabel.text = [model valueForKey:@"budget"];
}

'NSInvalidArgumentException', reason:
'-[NSDecimalNumber isEqualToString:]:
unrecognized selector sent to instance
0x653fc80' .

This issue is caused by assigning a decimal number to a string. You have to get the string representation before assigning it to the label. Something like this:

envelopeBudgetLabel.text = [[model valueForKey:@"budget"] description];

Memory Management with NSDecimalNumber: How to avoid memory problems in tight loops?

Yes. You should create your own autorelease pool inside the loop. This is exactly the situation that nested autorelease pools are for.



Related Topics



Leave a reply



Submit