Search code examples
ioscurrencynsnumberformatter

How do I set NSNumberFormatter to display numbers using "万" (Japanese/Chinese 10,000 marker)?


My iOS app displays different currencies (USD, JPY, AUD, EUR) in different localizations (en_US, en_AU, ja_JP, etc).

For Japanese region/language (both are set on my device), if I have:

NSNumberFormatter *fmt = [[NSNumberFormatter alloc] init];
fmt.numberStyle = NSNumberFormatterCurrencyStyle;
fmt.currencyCode = @"JPY";
NSString *labelText = [fmt stringFromNumber:@1000000];

My label text is ¥1,000,000. However, in Japanese and Chinese, numbers greater than 10,000 might be written 100万円, which is the output I want.

Any idea what code I can write to get 100万円 as the output?

I'd like to avoid logic blocks in my code checking for locale/region, but I feel like that's what I'm coming up against (for example, using the method call fmt.multipler = @(1/10000) to divide 1,000,000 by 10,000 to get the right value).


Solution

  • EDIT: Up to date gist here: https://gist.github.com/fjolnir/cd72ea39be1476023adf

    Old thread, but I came across it while looking for a solution so I figured I'd post my implementation.

    The formatter itself does not handle placement of 円, but that's easy to do outside of it. (as the example below demonstrates)

    The expected output of the below is:

    2015-03-11 18:00:13.376 LENumberFormatter[82736:3604947] 12億3,460万円
    2015-03-11 18:00:13.377 LENumberFormatter[82736:3604947] 25円
    

    -

    @import Foundation;
    @import ObjectiveC.message;
    
    typedef NS_ENUM(NSUInteger, LENumberFormatterAbbreviationStyle) {
        kLEAbbreviateShort, // 2.5m
        kLEAbbreviateNormal // 2m 5k
    };
    
    @interface LENumberFormatter : NSNumberFormatter
    @property(nonatomic) BOOL abbreviateLargeNumbers;
    @property(nonatomic) LENumberFormatterAbbreviationStyle abbreviationStyle;
    @end
    
    
    @implementation LENumberFormatter
    - (instancetype)init
    {
        if((self = [super init])) {
            self.abbreviationStyle = [self _usingKanjiNumbers]
                                   ? kLEAbbreviateNormal
                                   : kLEAbbreviateShort;
        }
        return self;
    }
    
    - (NSString *)stringForObjectValue:(id const)aObj
    {
        if(!_abbreviateLargeNumbers || ![aObj isKindOfClass:[NSNumber class]])
            return [super stringForObjectValue:aObj];
    
        // Copy ourselves to get format the partial digits using the settings on self
        LENumberFormatter * const partialFormatter = [self copy];
        partialFormatter.currencySymbol = @"";
        if(_abbreviationStyle == kLEAbbreviateNormal)
            partialFormatter.maximumFractionDigits = 0;
    
        NSString *(^partialFormat)(NSNumber*) = ^(NSNumber *num) {
            NSString *(*superImp)(struct objc_super*,SEL,NSNumber*) = (void*)&objc_msgSendSuper;
            return superImp(&(struct objc_super) { partialFormatter, self.superclass }, _cmd, num);
        };
    
        double n = [aObj doubleValue];
        BOOL const shortFormat = _abbreviationStyle == kLEAbbreviateShort;
    
        NSDictionary * const separators         = [self _localizedGroupingSeparators];
        NSArray      * const separatorExponents = [separators.allKeys sortedArrayUsingSelector:@selector(compare:)];
    
        BOOL const currencySymbolIsSuffix = [self.positiveFormat hasSuffix:@"¤"];
        NSMutableString * const result = currencySymbolIsSuffix || self.numberStyle != NSNumberFormatterCurrencyStyle
                                       ? [NSMutableString new]
                                       : [self.currencySymbol mutableCopy];
        NSUInteger significantDigits = 0;
        NSNumber *lastExp = nil;
        for(NSNumber *exp in separatorExponents.reverseObjectEnumerator) {
            double divisor = pow(10, exp.shortValue);
            if(divisor > n)
                continue;
    
            if(lastExp)
                significantDigits += lastExp.doubleValue - exp.doubleValue;
            lastExp = exp;
    
            if(self.usesSignificantDigits && significantDigits >= self.maximumSignificantDigits)
                break;
    
            double partialNum = shortFormat
                              ? n/divisor
                              : floor(n/divisor);
            NSString * const digits = [self _groupRecursively] && ![exp isEqual:@0]
                                    ? [partialFormatter stringFromNumber:@(partialNum)]
                                    : partialFormat(@(partialNum));
            [result appendFormat:@"%@%@", digits, separators[exp]];
    
            n = fmod(n, divisor);
    
            if(shortFormat)
                break; // Just use a float+first hit
    
            // If we make it here, partialNum is integral and we can use log10 to find the number of digits
            significantDigits += log10(partialNum) + 1;
            partialFormatter.maximumSignificantDigits -= digits.length;
    
        }
        if(n > 0
           && !shortFormat
           && (!self.usesSignificantDigits || significantDigits < self.maximumSignificantDigits))
        {
            partialFormatter.maximumFractionDigits = self.maximumFractionDigits;
            [result appendString:partialFormat(@(n))];
        }
        if(self.numberStyle == NSNumberFormatterCurrencyStyle && currencySymbolIsSuffix && self.currencySymbol)
            [result appendString:self.currencySymbol];
    
        return result.length > 0
             ? [result stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceAndNewlineCharacterSet]
             : [super stringForObjectValue:aObj];
    }
    
    - (BOOL)_usingKanjiNumbers
    {
        return [self.locale.localeIdentifier rangeOfString:@"^(ja|zh)_"
                                                   options:NSRegularExpressionSearch].location != NSNotFound;
    }
    - (NSDictionary *)_localizedGroupingSeparators
    {
        if(self._usingKanjiNumbers)
            return @{ @2: @"百", @3: @"千", @4: @"万", @8: @"億" };
        else {
            NSBundle * const bundle = [NSBundle bundleForClass:self.class];
            return @{ 
                @3: [bundle localizedStringForKey:@"thousandSuffix" value:@"k " table:nil],
                @6: [bundle localizedStringForKey:@"millionSuffix"  value:@"m " table:nil]
            };
        }
    }
    
    - (BOOL)_groupRecursively
    {
        // Return _usingKanjiNumbers if you want:
        // 12億3千4百56万7千8百90
        // Rather than:
        // 1億2,3456万7千8百90
        return NO;
    }
    
    - (instancetype)copyWithZone:(NSZone * const)aZone
    {
        LENumberFormatter * const copy = [super copyWithZone:aZone];
        copy.abbreviateLargeNumbers = _abbreviateLargeNumbers;
        copy.abbreviationStyle      = _abbreviationStyle;
        return copy;
    }
    @end
    
    
    int main(int argc, char *argv[]) {
        @autoreleasepool {
            LENumberFormatter * const f = [LENumberFormatter new];
            f.locale = [NSLocale localeWithLocaleIdentifier:@"ja_JP"];
    //        f.locale = [NSLocale localeWithLocaleIdentifier:@"en_US"];
            f.numberStyle = NSNumberFormatterCurrencyStyle;
            f.abbreviateLargeNumbers = YES;
            f.abbreviationStyle = kLEAbbreviateNormal; // Automatic if using system locale
            f.maximumSignificantDigits = 5;
            f.usesSignificantDigits = YES;
    //        f.currencyCode   = @"JPY";
    //        f.currencySymbol = @"¥";
    
            if([f.locale.localeIdentifier hasPrefix:@"ja"]) {
                f.positiveFormat = @"#,##0¤";
                if([f.currencyCode isEqualToString:@"JPY"])
                    // We allow ourselves this special case because *日本円 just looks dumb
                    f.currencySymbol = @"円";
                else
                    f.currencySymbol = [f.locale displayNameForKey:NSLocaleCurrencyCode
                                              value:f.currencyCode];
            }
    
            NSLog(@"%@", [f stringFromNumber:@1234567890]);
            NSLog(@"%@", [f stringFromNumber:@25]);
        }
    }