NSNumberFormatter maximum fractional digits issue only on iOS 10 (unit test fail: £50 expected, £50.00 returned)

Hello everyone,


Some unit tests have started failing only when ran on iOS 10, iOS Simulator, while they still succeed for iOS 8 and iOS 9 (same device type, but different OS).

The way to make the number formatter work is quite surreal though (I essentially have to format an amount into a string, set a property on the formatter [it does not matter if I set it before or not], and then formatting the same number into a string).


Unit test:

- (void)testLegacyFormatCurrencyInGame_demoModeNo_forceDecimalsYes_splitThousandsNo_hideCurrencySymbolNo_addTrailingZerosNo_GreaterThanOne
{
    NSNumber *amount = @(50);
    NSString *expectedFormattedString = [NSString stringWithFormat:@"%@50", self.largeCurrencySymbol];

    BOOL demoMode = NO;
    BOOL forceDecimals = YES;
    BOOL splitThousands = NO;
    BOOL hideCurrencySymbol = NO;
    BOOL addTrailingZeros = NO;

    CurrencyModel *configParameters = [self currencyModelWithAmount:amount
                                                     addTrailingZeros:addTrailingZeros
                                                        forceDecimals:forceDecimals
                                                      splitThrousands:splitThousands
                                                   hideCurrencySymbol:hideCurrencySymbol
                                                             demoMode:demoMode];

    NSString *convertedString = [self.accountLogic formatCurrencyInGame:amount
                                                     withLegacySettings:configParameters];

    XCTAssertTrue([expectedFormattedString isEqualToString:convertedString],
                  @"expected %@, received %@", expectedFormattedString, convertedString);
}


Actual implementation (version that passes the test):

// It is important to note that this one handles in game currency code a little bit differently.
// This code handles the Lua specification as specified in host events v01
- (NSString *)formatCurrencyInGame:(NSNumber *)amount
                withLegacySettings:(CurrencyModel *)settings
{
    if (amount == nil) {
        return  kEmptyString;
    }

    NSNumberFormatter *formatter = [[NSNumberFormatter alloc] init];
    [formatter setNumberStyle:NSNumberFormatterCurrencyStyle];
    [formatter setLocale:[self memberLocale]];

    BOOL forceDecimals = NO;
    BOOL addTrailingZeros = NO;
    BOOL hideCurrencySymbol = NO;
    BOOL splitThousands = NO;

    if (settings != nil) {
        forceDecimals = settings.forceDecimals;
        addTrailingZeros = settings.addTrailingZeros;
        hideCurrencySymbol = settings.hideCurrencySymbols;
        splitThousands = settings.splitThousands;
    }

    if (hideCurrencySymbol == YES) {
        [formatter setCurrencySymbol:kEmptyString];
    }

    if (splitThousands == NO) {
        [formatter setCurrencyGroupingSeparator:kEmptyString];
    }

    if ([amount doubleValue] < 1.0 && forceDecimals == YES) {
        // Ensure that if we are using NO currency symbol (i.e. demo mode) we don't do any of this.
        if (hideCurrencySymbol == YES) {
            return [formatter stringFromNumber:amount];
        }
    
        [formatter setCurrencySymbol:kEmptyString];
        [formatter setMultiplier:@100.0];
        [formatter setMaximumFractionDigits:0];
    
        return [NSString stringWithFormat:@"%@%@", [formatter stringFromNumber:amount], [self smallCurrencySymbol]];
    }

    if ([amount doubleValue] >= 1.0) {
        if ( addTrailingZeros == YES ) {
            [formatter setMinimumFractionDigits:2];
        } else {
            // Truncate all decimal places unless we intend on printing a cent value.
            double value = [amount doubleValue];
            if (((value * 100.0) - (floor(value) * 100.0)) < 0.5) {
                [formatter setMaximumFractionDigits:0];
            }
        }
    }

    NSString *formattedString = [formatter stringFromNumber:amount];
  
    if (SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(@"10.0") &&
        formatter.maximumFractionDigits < 1) {
        formatter.minimumFractionDigits = 0;
        formattedString = [formatter stringFromNumber:amount];
    }
    return formattedString;
}


Actual implementation (version that fails the test):

// It is important to note that this one handles in game currency code a little bit differently.
// This code handles the Lua specification as specified in host events v01
- (NSString *)formatCurrencyInGame:(NSNumber *)amount
                withLegacySettings:(CurrencyModel *)settings
{
    if (amount == nil) {
        return  kEmptyString;
    }

    NSNumberFormatter *formatter = [[NSNumberFormatter alloc] init];
    [formatter setNumberStyle:NSNumberFormatterCurrencyStyle];
    [formatter setLocale:[self memberLocale]];

    BOOL forceDecimals = NO;
    BOOL addTrailingZeros = NO;
    BOOL hideCurrencySymbol = NO;
    BOOL splitThousands = NO;

    if (settings != nil) {
        forceDecimals = settings.forceDecimals;
        addTrailingZeros = settings.addTrailingZeros;
        hideCurrencySymbol = settings.hideCurrencySymbols;
        splitThousands = settings.splitThousands;
    }

    if (hideCurrencySymbol == YES) {
        [formatter setCurrencySymbol:kEmptyString];
    }

    if (splitThousands == NO) {
        [formatter setCurrencyGroupingSeparator:kEmptyString];
    }

    if ([amount doubleValue] < 1.0 && forceDecimals == YES) {
        // Ensure that if we are using NO currency symbol (i.e. demo mode) we don't do any of this.
        if (hideCurrencySymbol == YES) {
            return [formatter stringFromNumber:amount];
        }
     
        [formatter setCurrencySymbol:kEmptyString];
        [formatter setMultiplier:@100.0];
        [formatter setMaximumFractionDigits:0];
     
        return [NSString stringWithFormat:@"%@%@", [formatter stringFromNumber:amount], [self smallCurrencySymbol]];
    }

    if ([amount doubleValue] >= 1.0) {
        if ( addTrailingZeros == YES ) {
            [formatter setMinimumFractionDigits:2];
        } else {
            // Truncate all decimal places unless we intend on printing a cent value.
            double value = [amount doubleValue];
            if (((value * 100.0) - (floor(value) * 100.0)) < 0.5) {
                [formatter setMaximumFractionDigits:0];
            }
        }
    }

    NSString *formattedString = [formatter stringFromNumber:amount];
    return formattedString;
}


This is quite odd... A bug? I have not seen anything in the docs to suggest there was such a change required for NSNumberFormatter...

Opened bug report #28356798


This could possibly be a duplicate of this other rdar: https://openradar.appspot.com/18034852

I can confirm that the fix mentioned in that open rdar works as well (the open radar bug is from 2014 and applied to iOS 8 although it is still open, but my code was fine on iOS 8 and iOS 9... strangely enough):

If we set currencyFormatter.internationalCurrencySymbol as well,
the maximumFractionDigits performs as expected

Hello? I see no activity on the open radar, on the bug report, or on here... is there something trivial about this I am missing?

NSNumberFormatter maximum fractional digits issue only on iOS 10 (unit test fail: £50 expected, £50.00 returned)
 
 
Q