23
Apr
2008
 

Cocoa Tutorial: Don’t Be Lazy With NSDecimalNumber (Like Me)

by Marcus Zarra

NSDecimalNumber is Objective-C’s solution to numbers that need to be very precise. The documentation defines it as:

NSDecimalNumber, an immutable subclass of NSNumber, provides an object-oriented wrapper for doing base-10 arithmetic. An instance can represent any number that can be expressed as mantissa x 10^exponent where mantissa is a decimal integer up to 38 digits long, and exponent is an integer from –128 through 127.

NSDecimalNumber

If you are dealing with currency at all, then you should be using NSDecimalNumber. However, since it is immutable and definitely not a primitive then it is difficult to use right? Well — yes — a bit. But if you do not want to see your $9.50 item displayed as $9.49999994 or something then you are better off using NSDecimalNumber right from the beginning. Otherwise you are going to be converting to it later and that is a LOT more painful.

To create an NSDecimalNumber there are a few helper class methods:

  • +decimalNumberWithDecimal:

  • +decimalNumberWithMantissa:exponent:isNegative:

  • +decimalNumberWithString:

  • +deicmalNumberWithString:locale:

  • +one

  • +zero

  • +notANumber

The two methods that I ended up using the most are +decimalNumberWithDecimal: and +decimalNumberWithMantissa:exponent:isNegative: If you already have the number stored as an NSNumber then
[NSDecimalNumber decimalNumberWithDecima:[yourNumber decimalValue]]
is the easiest way to convert it. If the number is a primitive then
[NSDecimalNumber decimalNumberWithMantissa:(yourPrimitive * precision) exponent:-(precision) isNegative:(yourPrimitive < 0 ? YES : NO)] will convert it.

Math functions

So now you have your number as an NSDecimalNumber. How do you do anything with it? Specifically how do you add, divide, multiple and subtract an immutable number? Fortunately the class has methods to handle all of these:

  • -decimalNumberByAdding:

  • -decimalNumberBySubtracting:

  • -decimalNumberByMultiplingBy:

  • -decimalNumberByDividingBy:

  • -decimalNumberByRaisingToPower:

  • -decimalNumberByMultiplyingByPowerOf10:

With these methods you can do all of the math functions and create a new NSDecimalNumber with each call. Naturally these can even be chained together to make a deliciously convoluted method call.

Rounding

So what happens when you need to control the decimal precision of an NSDecimalNumber? Specifically what happens when you multiply 9.49 * 10% and what only two decimal points left over? That is where the NSDecimalNumberBehaviors come in. The NSDecimalNumberBehaviors protocol is defined as:

The NSDecimalBehaviors protocol declares three methods that control the discretionary aspects of working with NSDecimalNumber objects.

The scale and roundingMode methods determine the precision of NSDecimalNumber’s return values and the way in which those values should be rounded to fit that precision. The exceptionDuringOperation:error:leftOperand:rightOperand: method determines the way in which an NSDecimalNumber object should handle different calculation errors.

For an example of a class that adopts the NSDecimalBehaviors protocol, see the specification for NSDecimalNumberHandler.

This protocol is implemented in the NSDecimalNumberHandler class. By constructing an NSDecimalNumberHandler and passing it in as part of the math call you can control the rounding applied to the math function. Even better, you can reuse the handler class as often as you want. Therefore to perform the calculation above:

NSDecimalNumber *price = [NSDecimalNumber decimalNumberWithMantissa:949
                                                           exponent:-2
                                                         isNegative:NO];

NSDecimalNumber *percent = [NSDecimalNumber decimalNumberWithMatissa:10
                                                            exponent:-2
                                                          isNegative:NO];

NSDecimalNumberHandler *handler = [NSDecimalNumberHandler decimalNumberHandlerWithRoundingMode:NSRoundPlain
                                                                                         scale:-2
                                                                              raiseOnExactness:NO
                                                                               raiseOnOverflow:NO
                                                                              raiseOnUnderflow:NO
                                                                           raiseOnDivideByZero:NO];
NSDecimalNumber *result = [price decimalNumberByMultiplyingBy:percent
                                                 withBehavior:handler];

Note that instead of using -decimalNumberByMultiplyingBy: I used a similar method which adds withBehavior: on the end of it. Each of the math functions listed above includes a companion method which allows you to pass in the behavior. Also note that the NSDecimalNumberHandler class allows you to raise exceptions in several circumstances. You can review Apple’s documentation on each of these exceptions if you find a need for them.

Core Data

So how does Core Data handle NSDecimalNumber(s)? In a word — perfectly. In your Core Data model simply define the attribute as “decimal” instead of double or another primitive and you can store the NSDecimalNumber directly in the repository and retrieve it as an NSDecimalNumber. You can also retrieve it as an NSNumber if needed since NSDecimalNumber is a subclass of NSNumber.

Formatting

All of the number formatters will handle NSDecimalNumber perfectly with no loss of precision. If you pass it to a currency formatter you will get back the number you expect unlike passing a double and hoping for the best.

Conclusion

So is NSDecimalNumber harder to use than primitives? Absolutely. Compared to primitives it is a lot harder to code, maintain and read. Is it worth it? Depends.

If you are dealing with currency then there is no question — use NSDecimalNumber and avoid doubles like the plague. Spend the time, learn the API. Otherwise you will end up having to migrate over to them later when you discover that $9.49 * 10% may not equal $0.95.

If you are not dealing with currency or another number that requires absolute precision (such as screen drawing, et al) then you probably do not need to deal with the pain.

So why did I title this article with the words “Like Me”? Take a good guess…

Comments

Sebastian Ahlman says:

Thanks for the tip!

Great blog BTW. I recently started programming with Cocoa and I have found CIMGF to be a valuable resource. Keep it up and I will keep reading!

jediknil says:

NSDecimalNumber also supports the various +numberWith(Primitive): methods that come with NSNumber. And yes, they correctly return an NSDecimalNumber.

Marcus Zarra says:

jediknil,

Since NSDecimalNumber *test = [NSDecimalNumber numberWithDouble:9.5]; will produce a warning it is not recommended. When dealing with NSDecimalNumber objects it is much safer to use the recognized initializers and avoid the ones from its NSNumber parent class.

jediknil says:

Ah, OK. In that case -initWithDouble: and friends are still valid initializers (with no warnings), and they of course give you NSDecimalNumbers.

On the other hand, for doing base-10 arithmetic, you’re probably right not to use -initWithDouble:, since NSDecimalNumber’s internal representation uses base-10 exponents, while primitive floats and doubles of course use base-2 exponents, and there could be some (slight) loss of precision.

For integers, though, I’d still prefer [[NSDecimalNumber alloc] initWithInteger:100] to [[NSDecimalNumber alloc] initWithMantissa:1 exponent:2 isNegative:NO].

Marcus Zarra says:

I would not recommend even doing that. There is a reason that the NSDecimalNumber is configured differently and has an unusual constructor. Ignoring that seems foolish just to save a couple of characters typing.

Definitely sounds like the wrong kind of lazy to me :)

dpenny says:

I cam across this article just as I was writing a small app the deals with currency as well. It seems that historically wisdom has said to always convert any currency value to a whole integer before doing any calculations with, does NSDecimalNumber alleviate the need for doing this?

Marcus Zarra says:

dpenny,

Yes they are designed for currency use and the proper rounding of decimal points. They solve the issues with floating point math.

mj1531 says:

In converting primitives to the mantissa/exponent format, you mention precision. Is this the same value as the scale in NSDecimalNumberBehaviors, or is it a printf-style float-precision specifier (like 4.3 for 4 places before the decimal point and 3 after)?

mj1531 says:

Nevermind, I figured it out. The precision refers to the number of digits after the decimal point and the calculation of the mantissa should read:

mantissa = (yourPrimitive * 10^precision)

Thanks for this informative post.