Как настроить NSNumberFormatter для отображения чисел с помощью 万 (японский/китайский маркер 10 000)?

В моем приложении для iOS отображаются разные валюты (USD, JPY, AUD, EUR) в разных локализациях (en_US, en_AU, ja_JP и т. д.).

Для японского региона/языка (оба установлены на моем устройстве), если у меня есть:

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

Текст моей метки ¥1,000,000. Однако в японском и китайском языках числа больше 10 000 могут быть записаны как 100万円, что мне и нужно.

Есть идеи, какой код я могу написать, чтобы получить 100万円 в качестве вывода?

Я хотел бы избежать логических блоков в моем коде, проверяющем локаль/регион, но мне кажется, что я сталкиваюсь с этим (например, используя вызов метода fmt.multipler = @(1/10000) для деления 1 000 000 на 10 000, чтобы получить правильное значение).


person makdad    schedule 23.01.2013    source источник
comment
Вы всегда ожидаете этот формат? (например, сто иен, 100 円?) Или вы хотите, чтобы маленькие значения выглядели иначе, чем большие?   -  person StilesCrisis    schedule 23.01.2013
comment
В идеале, я бы хотел, чтобы 100 円 отображались как 100 円, но если это больше 10 000 иен, это будет означать 万円.   -  person makdad    schedule 23.01.2013


Ответы (3)


EDIT: актуальная информация здесь: https://gist.github.com/fjolnir/cd72ea39be1476023adf

Старый поток, но я наткнулся на него, ища решение, поэтому решил опубликовать свою реализацию.

Форматтер сам по себе не обрабатывает размещение 円, но это легко сделать вне его. (как показано в примере ниже)

Ожидаемый результат ниже:

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]);
    }
}
person Fjölnir    schedule 11.03.2015

В конце концов, я создал подкласс NSNumberFormatter и заменил stringWithNumber:.

Вот соответствующий код, который я использовал для перенастройки NSNumberFormatter, когда код валюты JPY.

  NSString *localeString = [self.locale localeIdentifier];
  if ([localeString isEqualToString:@"ja_JP"])
  {
    // 1-oku
    if (num >= 100000000)
    {
      self.negativeFormat = @"-#,###0億円";
      self.positiveFormat = @"#,###0億円";
      self.multiplier = @(1.0f/100000000.0f);
    }
    // 1-man
    else if (num >= 10000)
    {
      self.negativeFormat = @"-#,###0万円";
      self.positiveFormat = @"#,###0万円";
      self.multiplier = @(1.0f/10000.0f);
    }
    // Less than 10,000
    else
    {
      self.negativeFormat = @"-#,###0円";
      self.positiveFormat = @"#,###0円";
    }
  }
  // This could be en_AU, en_UK, en_US -- but all use "million yen"
  else if ([localeString hasPrefix:@"en"])
  {
    // We only care about 1M JPY+
    if (num >= 1000000)
    {
      self.negativeFormat = @"-¥#,###0M";
      self.positiveFormat = @"¥#,###0M";
      self.multiplier = @(1.0f/1000000.0f);
    }
  }
person makdad    schedule 23.01.2013
comment
Эта реализация не идеальна, поскольку такие значения, как 12億3460万円, невозможны. Проверьте реализацию @Fjölnir. - person Mokkun; 13.03.2015
comment
Да, я просто выбрал его как правильный ответ - я давно не смотрел на этот код, но я рад, что этот вопрос стал де-факто местом назначения Google для людей, пытающихся решить эту проблему :) - person makdad; 14.03.2015

Отличный вопрос.

Я предполагаю, что обозначение цены «мужской» похоже на европейское обозначение «К» (что, однако, не так распространено). В этом смысле, я думаю, не существует стандарта для некоторых обозначений «короткая цена/число», поэтому они не включены в стандартные средства форматирования и спецификаторы формата. Я предполагаю, что стандарты ориентированы на какой-то разумный общий знаменатель, и 万円 туда не подходит. Также довольно странно, что стандартный символ ¥ используется впереди, а - после значения цены.

Стандарты довольно жесткие, поэтому я бы не рассматривал поддержку поддержки валюты в стиле 万円 в ближайшем будущем. Итак, я думаю, что на данный момент есть только «ручное» решение.

P.S. Я предполагаю, что для этого должна быть какая-то сторонняя библиотека, потому что это должно быть довольно распространенной задачей.

person Petr Abdulin    schedule 23.01.2013
comment
Спасибо за ответ. Возможно, мне нужен подкласс NSNumberFormatter, который имеет особое поведение на китайском/японском языке/регионах. - person makdad; 23.01.2013
comment
В Японии [почти] всегда цены, равные или превышающие 10000, выражены в 万. Японцы думают даже не 10 тысячами, а 万. При этом я бы рассмотрел обязательный требование выражать количества в 万 , когда используется локализация ja_JP. - person user454322; 23.01.2013
comment
Я знаю о японской специфике, думаю, стандарты ориентированы на какой-то разумный общий знаменатель, и 万円 туда не подходит. - person Petr Abdulin; 23.01.2013
comment
Ага, меня вообще разочаровывает, что я могу задать количество знаков после запятой в группировке, но что в локализации ja_JP нет поддержки этого. Время подкласса?? - person makdad; 23.01.2013