6
votes

I am running into unexpected behaviour formatting very large numbers in ObjC using the NSNumberFormatter.

It seems that the number formatter rounds decimals (NSDecimalNumber) after the fifteenth digit regardless of fraction digits.

The below test fails on values 1,3 and 5.

Two requests:

  • Any suggestions on alternative code would be greatly appreciated?
  • I assume the issue is happening due to the usage of a hard-coded digit limit in NSNumberFormatter?

The post here lists a workaround without sufficient description if the problem. Also our application (banking sector) runs across multiple countries and we link the formatting to the user's locale as configured in the backend. This workaround would imply that we write our own number formatter to handle the requirement. Something I do not want to do.

- (void)testFormatterUsingOnlySDK {
    NSDecimalNumber *value1 = [NSDecimalNumber decimalNumberWithMantissa: 9423372036854775808u exponent:-3 isNegative:YES];
    NSDecimalNumber *value2 = [NSDecimalNumber decimalNumberWithMantissa: 9999999999999990u exponent:-3 isNegative:YES];
    NSDecimalNumber *value3 = [NSDecimalNumber decimalNumberWithMantissa: 9999999999999991u exponent:-3 isNegative:YES];
    NSDecimalNumber *value4 = [NSDecimalNumber decimalNumberWithMantissa: 99999999999999900u exponent:-4 isNegative:YES];
    NSDecimalNumber *value5 = [NSDecimalNumber decimalNumberWithMantissa: 11111111111111110u exponent:-4 isNegative:YES];

    NSNumberFormatter *formatter = [[[NSNumberFormatter alloc] init] autorelease];
    formatter.allowsFloats = YES;
    formatter.maximumFractionDigits = 3;

    [self assertStringAreEqualWithActual:[formatter stringFromNumber:value1] andExpeted: @"-9423372036854775.808"];
    [self assertStringAreEqualWithActual:[formatter stringFromNumber:value2] andExpeted: @"-9999999999999.99"];
    [self assertStringAreEqualWithActual:[formatter stringFromNumber:value3] andExpeted: @"-9999999999999.991"];
    [self assertStringAreEqualWithActual:[formatter stringFromNumber:value4] andExpeted: @"-9999999999999.99"];
    [self assertStringAreEqualWithActual:[formatter stringFromNumber:value5] andExpeted: @"-1111111111111.111"];
}

- (void)assertStringAreEqualWithActual:(NSString *)actual andExpeted:(NSString *)expected {
    STAssertTrue([expected isEqualToString:actual], @"Expected %@ but got %@", expected, actual);
}
3

3 Answers

4
votes

Unfortunately, NSNumberFormatter doesn't work correctly with NSDecimalNumber. The problem (very probably) is that the first thing it does is calling doubleValue on the number it wants to format.

See also NSDecimalNumber round long numbers

After many tries with NSNumberFormatter, I have created my own formatter, it's actually very easy:

  1. Handle NaN.
  2. Round using roundToScale:
  3. Get stringValue
  4. Check if negative, remove leading -
  5. Find decimal point (.)
  6. Localize decimal point ([locale objectForKey:NSLocaleDecimalSeparator])
  7. Add grouping separators ([locale objectForKey:NSLocaleGroupingSeparator])
  8. If negative, add leading - or put the number into parenthesis if you are formatting currency.
  9. Done.
1
votes

You should compile your own NSNumberFormatter from this open source code, changing the prefix. This should allow you to debug into the formatting and to understand why this is happening. Worst case you can submit a patch to Apple.

http://code.google.com/p/cocotron/source/browse/Foundation/NSNumberFormatter.m?r=7542c3a7ef0ef75479e6154a75d304113f5a9738

1
votes

You've set maximumFractionDigits to two. All of the failing tests have three fraction digits in the expected value. Either the expectation or the code needs to change to match. If I make this change:

formatter.maximumFractionDigits = 3;

then all of your test cases are met.