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]);
}
}