Fractional Part NSDecimalNumber

I use NSDecimalNumber to store the value for the currency. I am trying to write a method called "cents" that returns the decimal part of a number as an NSString with a leading 0 if the number is <10. So, basically

NSDecimalNumber *n = [[NSDecimalNumber alloc] initWithString:@"1234.55"]; NSString *s = [object cents:n]; 

And I'm trying to create a method that will return 01, 02, etc .... to 99 as a string. I cannot figure out how to return the mantissa as a string in this format. I feel like Iโ€™m missing the convenience method, but I looked at the documentation again and donโ€™t see anything popping up on me.

+7
source share
5 answers

Update: This is a relatively old answer, but it looks like people are still finding it. I want to update this for the sake of correctness - I initially answered the question, because I just demonstrated how you can get certain decimal places from double , but I do not protect this as a way of representing currency information.

Never use floating point numbers to represent currency information. As soon as you start dealing with decimal numbers (as with dollars with cents), you enter possible floating point errors into your code. The computer cannot accurately represent all decimal values โ€‹โ€‹( 1/100.0 , for example, 1 cent, is represented as 0.01000000000000000020816681711721685132943093776702880859375 on my machine). Depending on what currencies you plan to represent, it is always more correct to store the quantity in terms of its base quantity (in this case, a cent).

If you keep your dollar values โ€‹โ€‹in terms of whole cents, you will never encounter floating point errors for most operations, and it is trivially easy to convert cents to dollars for formatting. If you need to apply a tax, for example, or multiply the value of cents by double , do this to get a double value, apply the banker's rounding to round to the nearest cent and convert back to an integer.

This gets complicated if you are trying to support several different currencies, but there are ways to handle this.

tl; dr Do not use floating point numbers to represent the currency, and you will be much happier and more correct. NSDecimalNumber can accurately (and accurately) represent decimal values, but as soon as you convert to double / float , you run the risk of introducing floating point errors.


This can be done relatively easily:

  • Get the double value of a decimal number (this will work if the number is not too large to store in double size).
  • In a separate variable, cast double to an integer.
  • Multiply both numbers by 100 to take into account the loss of accuracy (in fact, convert to cents) and subtract dollars from the original to get the number of cents.
  • Return to string.

(This is a general formula for working with all decimal numbers - only a little glue code is needed to work with NSDecimalNumber to make it work).

In practice, it will look like this (in the NSDecimalNumber category):

 - (NSString *)cents { double value = [self doubleValue]; unsigned dollars = (unsigned)value; unsigned cents = (value * 100) - (dollars * 100); return [NSString stringWithFormat:@"%02u", cents]; } 
+11
source

This is a safer implementation that will work with values โ€‹โ€‹that do not fit into double :

  @implementation NSDecimalNumber (centsStringAddition)

 - (NSString *) centsString;
 {
     NSDecimal value = [self decimalValue];

     NSDecimal dollars;
     NSDecimalRound (& dollars, & value, 0, NSRoundPlain);

     NSDecimal cents;
     NSDecimalSubtract (& ampcents, & value, & dollars, NSRoundPlain);

     double dv = [[[NSDecimalNumber decimalNumberWithDecimal: cents] decimalNumberByMultiplyingByPowerOf10: 2] doubleValue];
     return [NSString stringWithFormat: @ "% 02d", (int) dv];
 }

 @end

Prints 01 :

  NSDecimalNumber * dn = [[NSDecimalNumber alloc] initWithString: @ "123456789012345678901234567890.0108"];
     NSLog (@ "% @", [dn centsString]);
+11
source

I do not agree with the Itai Ferber method, because using the doubleValue NSNumber method can lead to loss of accuracy, for example 1.60 โ†’ 1.5999999999, so the value of your cents will be "59". Instead, I cut out the dollars / cent from the string obtained using NSNumberFormatter :

 +(NSString*)getSeparatedTextForAmount:(NSNumber*)amount centsPart:(BOOL)cents { static NSNumberFormatter* fmt2f = nil; if(!fmt2f){ fmt2f = [[NSNumberFormatter alloc] init]; [fmt2f setNumberStyle:NSNumberFormatterDecimalStyle]; [fmt2f setMinimumFractionDigits:2]; // | [fmt2f setMaximumFractionDigits:2]; // | - exactly two digits for "money" value } NSString str2f = [fmt2f stringFromNumber:amount]; NSRange r = [str2f rangeOfString:fmt2f.decimalSeparator]; return cents ? [str2f substringFromIndex:r.location + 1] : [str2f substringToIndex:r.location]; } 
+2
source

I have a different approach. This works for me, but I'm not sure if this can cause problems? I let NSDecimalNumber return a value directly, returning an integer value ...

eg:.

 // n is the NSDecimalNumber from above NSInteger value = [n integerValue]; NSInteger cents = ([n doubleValue] *100) - (value *100); 

Would this approach be more expensive since I convert NSDecimalNumber two times?

+1
source

Fast version with bonus feature to receive only the amount in dollars.

 extension NSDecimalNumber { /// Returns the dollars only, suitable for printing. Chops the cents. func dollarsOnlyString() -> String { let behaviour = NSDecimalNumberHandler(roundingMode:.RoundDown, scale: 0, raiseOnExactness: false, raiseOnOverflow: false, raiseOnUnderflow: false, raiseOnDivideByZero: false) let rounded = decimalNumberByRoundingAccordingToBehavior(behaviour) let formatter = NSNumberFormatter() formatter.numberStyle = .NoStyle let str = formatter.stringFromNumber(rounded) return str ?? "0" } /// Returns the cents only, eg "00" for no cents. func centsOnlyString() -> String { let behaviour = NSDecimalNumberHandler(roundingMode:.RoundDown, scale: 0, raiseOnExactness: false, raiseOnOverflow: false, raiseOnUnderflow: false, raiseOnDivideByZero: false) let rounded = decimalNumberByRoundingAccordingToBehavior(behaviour) let centsOnly = decimalNumberBySubtracting(rounded) let centsAsWholeNumber = centsOnly.decimalNumberByMultiplyingByPowerOf10(2) let formatter = NSNumberFormatter() formatter.numberStyle = .NoStyle formatter.formatWidth = 2 formatter.paddingCharacter = "0" let str = formatter.stringFromNumber(centsAsWholeNumber) return str ?? "00" } } 

Tests:

 class NSDecimalNumberTests: XCTestCase { func testDollarsBreakdown() { var amount = NSDecimalNumber(mantissa: 12345, exponent: -2, isNegative: false) XCTAssertEqual(amount.dollarsOnlyString(), "123") XCTAssertEqual(amount.centsOnlyString(), "45") // Check doesn't round dollars up. amount = NSDecimalNumber(mantissa: 12365, exponent: -2, isNegative: false) XCTAssertEqual(amount.dollarsOnlyString(), "123") XCTAssertEqual(amount.centsOnlyString(), "65") // Check zeros amount = NSDecimalNumber(mantissa: 0, exponent: 0, isNegative: false) XCTAssertEqual(amount.dollarsOnlyString(), "0") XCTAssertEqual(amount.centsOnlyString(), "00") // Check padding amount = NSDecimalNumber(mantissa: 102, exponent: -2, isNegative: false) XCTAssertEqual(amount.dollarsOnlyString(), "1") XCTAssertEqual(amount.centsOnlyString(), "02") } } 
+1
source

All Articles