You’re reading Ry’s Objective-C Tutorial → Data Types |
NSDecimalNumber
The NSDecimalNumber class provides fixed-point arithmetic
capabilities to Objective-C programs. They’re designed to perform base-10
calculations without loss of precision and with predictable rounding behavior.
This makes it a better choice for representing currency than floating-point
data types like double. However, the trade-off is that they are
more complicated to work with.
Internally, a fixed-point number is expressed as sign mantissa x
10^exponent. The sign defines whether it’s positive or negative,
the mantissa is an unsigned integer representing the significant digits, and
the exponent determines where the decimal point falls in the mantissa.
It’s possible to manually assemble an NSDecimalNumber
from a mantissa, exponent, and sign, but it’s often easier to convert it
from a string representation. The following snippet creates the value
15.99 using both methods.
NSDecimalNumber*price;price=[NSDecimalNumberdecimalNumberWithMantissa:1599exponent:-2isNegative:NO];price=[NSDecimalNumberdecimalNumberWithString:@"15.99"];
Like NSNumber, all NSDecimalNumber objects are
immutable, which means you cannot change their value after they’ve been
created.
Arithmetic
The main job of NSDecimalNumber is to provide fixed-point
alternatives to C’s native arithmetic operations. All five of
NSDecimalNumber’s arithmetic methods are demonstrated
below.
NSDecimalNumber*price1=[NSDecimalNumberdecimalNumberWithString:@"15.99"];NSDecimalNumber*price2=[NSDecimalNumberdecimalNumberWithString:@"29.99"];NSDecimalNumber*coupon=[NSDecimalNumberdecimalNumberWithString:@"5.00"];NSDecimalNumber*discount=[NSDecimalNumberdecimalNumberWithString:@".90"];NSDecimalNumber*numProducts=[NSDecimalNumberdecimalNumberWithString:@"2.0"];NSDecimalNumber*subtotal=[price1decimalNumberByAdding:price2];NSDecimalNumber*afterCoupon=[subtotaldecimalNumberBySubtracting:coupon];NSDecimalNumber*afterDiscount=[afterCoupondecimalNumberByMultiplyingBy:discount];NSDecimalNumber*average=[afterDiscountdecimalNumberByDividingBy:numProducts];NSDecimalNumber*averageSquared=[averagedecimalNumberByRaisingToPower:2];NSLog(@"Subtotal: %@",subtotal);// 45.98NSLog(@"After coupon: %@",afterCoupon);// 40.98NSLog((@"After discount: %@"),afterDiscount);// 36.882NSLog(@"Average price per product: %@",average);// 18.441NSLog(@"Average price squared: %@",averageSquared);// 340.070481
Unlike their floating-point counterparts, these operations are guaranteed to be accurate. However, you’ll notice that many of the above calculations result in extra decimal places. Depending on the application, this may or may not be desirable (e.g., you might want to constrain currency values to 2 decimal places). This is where custom rounding behavior comes in.
Rounding Behavior
Each of the above arithmetic methods have an alternate
withBehavior: form that let you define how the operation rounds
the resulting value. The NSDecimalNumberHandler class encapsulates
a particular rounding behavior and can be instantiated as follows:
NSDecimalNumberHandler*roundUp=[NSDecimalNumberHandlerdecimalNumberHandlerWithRoundingMode:NSRoundUpscale:2raiseOnExactness:NOraiseOnOverflow:NOraiseOnUnderflow:NOraiseOnDivideByZero:YES];
The NSRoundUp argument makes all operations round up to the
nearest place. Other rounding options are NSRoundPlain,
NSRoundDown, and NSRoundBankers, all of which are
defined by NSRoundingMode.
The scale: parameter defines the number of decimal places the
resulting value should have, and the rest of the parameters define the
exception-handling behavior of any operations. In this case,
NSDecimalNumber will only raise an exception if you try to divide
by zero.
This rounding behavior can then be passed to the
decimalNumberByMultiplyingBy:withBehavior: method (or any of the
other arithmetic methods), as shown below.
NSDecimalNumber*subtotal=[NSDecimalNumberdecimalNumberWithString:@"40.98"];NSDecimalNumber*discount=[NSDecimalNumberdecimalNumberWithString:@".90"];NSDecimalNumber*total=[subtotaldecimalNumberByMultiplyingBy:discountwithBehavior:roundUp];NSLog(@"Rounded total: %@",total);
Now, instead of 36.882, the total gets rounded up
to two decimal points, resulting in 36.89.
Comparing NSDecimalNumbers
Like NSNumber, NSDecimalNumber objects should use
the compare: method instead of the native inequality operators.
Again, this ensures that values are compared, even if they are stored
in different instances. For example:
NSDecimalNumber*discount1=[NSDecimalNumberdecimalNumberWithString:@".85"];NSDecimalNumber*discount2=[NSDecimalNumberdecimalNumberWithString:@".9"];NSComparisonResultresult=[discount1compare:discount2];if(result==NSOrderedAscending){NSLog(@"85%% < 90%%");}elseif(result==NSOrderedSame){NSLog(@"85%% == 90%%");}elseif(result==NSOrderedDescending){NSLog(@"85%% > 90%%");}
NSDecimalNumber also inherits the isEqualToNumber:
method from NSNumber.
Decimal Numbers in C
For most practical purposes, the NSDecimalNumber class should
satisfy your fixed-point needs; however, it’s worth noting that there is
also a function-based alternative available in pure C. This provides increased
efficiency over the OOP interface discussed above and is thus preferred for
high-performance applications dealing with a large number of calculations.
NSDecimal
Instead of an NSDecimalNumber object, the C interface is built
around the NSDecimal
struct. Unfortunately, the Foundation Framework doesn’t make
it easy to create an NSDecimal from scratch. You need to generate
one from a full-fledged NSDecimalNumber using its
decimalValue method. There is a corresponding factory method, also
shown below.
NSDecimalNumber*price=[NSDecimalNumberdecimalNumberWithString:@"15.99"];NSDecimalasStruct=[pricedecimalValue];NSDecimalNumber*asNewObject=[NSDecimalNumberdecimalNumberWithDecimal:asStruct];
This isn’t exactly an ideal way to create
NSDecimal’s, but once you have a struct
representation of your initial values, you can stick to the functional API
presented below. All of these functions use struct’s as
inputs and outputs.
Arithmetic Functions
In lieu of the arithmetic methods of NSDecimalNumber, the C
interface uses functions like NSDecimalAdd(),
NSDecimalSubtract(), etc. Instead of returning the result, these
functions populate the first argument with the calculated value. This makes it
possible to reuse an existing NSDecimal in several
operations and avoid allocating unnecessary structs just to hold intermediary
values.
For example, the following snippet uses a single result
variable across 5 function calls. Compare this to the Arithmetic section, which created a new
NSDecimalNumber object for each calculation.
NSDecimalprice1=[[NSDecimalNumberdecimalNumberWithString:@"15.99"]decimalValue];NSDecimalprice2=[[NSDecimalNumberdecimalNumberWithString:@"29.99"]decimalValue];NSDecimalcoupon=[[NSDecimalNumberdecimalNumberWithString:@"5.00"]decimalValue];NSDecimaldiscount=[[NSDecimalNumberdecimalNumberWithString:@".90"]decimalValue];NSDecimalnumProducts=[[NSDecimalNumberdecimalNumberWithString:@"2.0"]decimalValue];NSLocale*locale=[NSLocalecurrentLocale];NSDecimalresult;NSDecimalAdd(&result,&price1,&price2,NSRoundUp);NSLog(@"Subtotal: %@",NSDecimalString(&result,locale));NSDecimalSubtract(&result,&result,&coupon,NSRoundUp);NSLog(@"After coupon: %@",NSDecimalString(&result,locale));NSDecimalMultiply(&result,&result,&discount,NSRoundUp);NSLog(@"After discount: %@",NSDecimalString(&result,locale));NSDecimalDivide(&result,&result,&numProducts,NSRoundUp);NSLog(@"Average price per product: %@",NSDecimalString(&result,locale));NSDecimalPower(&result,&result,2,NSRoundUp);NSLog(@"Average price squared: %@",NSDecimalString(&result,locale));
Notice that these functions accept references to
NSDecimal structs, which is why we need to use the reference
operator (&) instead of passing them directly. Also note that
rounding is an inherent part of each operation—it’s not
encapsulated in a separate entity like NSDecimalNumberHandler.
The NSLocale instance defines the formatting of
NSDecimalString(), and is discussed more thoroughly in the
Dates module.
Error Checking
Unlike their OOP counterparts, the arithmetic functions don’t raise
exceptions when a calculation error occurs. Instead, they follow the common C
pattern of using the return value to indicate success or failure. All of the
above functions return an NSCalculationError,
which defines what kind of error occurred. The potential scenarios are
demonstrated below.
NSDecimala=[[NSDecimalNumberdecimalNumberWithString:@"1.0"]decimalValue];NSDecimalb=[[NSDecimalNumberdecimalNumberWithString:@"0.0"]decimalValue];NSDecimalresult;NSCalculationErrorsuccess=NSDecimalDivide(&result,&a,&b,NSRoundPlain);switch(success){caseNSCalculationNoError:NSLog(@"Operation successful");break;caseNSCalculationLossOfPrecision:NSLog(@"Error: Operation resulted in loss of precision");break;caseNSCalculationUnderflow:NSLog(@"Error: Operation resulted in underflow");break;caseNSCalculationOverflow:NSLog(@"Error: Operation resulted in overflow");break;caseNSCalculationDivideByZero:NSLog(@"Error: Tried to divide by zero");break;default:break;}
Comparing NSDecimals
Comparing NSDecimal’s works exactly like the OOP interface,
except you use the NSDecimalCompare() function:
NSDecimaldiscount1=[[NSDecimalNumberdecimalNumberWithString:@".85"]decimalValue];NSDecimaldiscount2=[[NSDecimalNumberdecimalNumberWithString:@".9"]decimalValue];NSComparisonResultresult=NSDecimalCompare(&discount1,&discount2);if(result==NSOrderedAscending){NSLog(@"85%% < 90%%");}elseif(result==NSOrderedSame){NSLog(@"85%% == 90%%");}elseif(result==NSOrderedDescending){NSLog(@"85%% > 90%%");}
![]() |
Be sure to check out Ry’s Cocoa Tutorial. This brand new guide is a complete walkthrough of Mac App development, and it leverages all of the Objective-C skills that we just discussed. Learn more › |
Mailing List
Sign up for my low-volume mailing list to find out when new content is released. Next up is a comprehensive Swift tutorial planned for late January.
You’ll only receive emails when new tutorials are released, and your contact information will never be shared with third parties. Click here to unsubscribe.
