Skip to content

Instantly share code, notes, and snippets.

@fjolnir
Last active April 18, 2019 02:52
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save fjolnir/cd72ea39be1476023adf to your computer and use it in GitHub Desktop.
Save fjolnir/cd72ea39be1476023adf to your computer and use it in GitHub Desktop.
Abbreviates numbers like: 1234567 to 1m 234k 567, 1.23m, 123万4千5百67, 123.4万 etc..
@import Foundation;
typedef NS_ENUM(NSUInteger, LENumberFormatterAbbreviationStyle) {
kLEAbbreviateShort, // 2.5m
kLEAbbreviateNormal // 2m 5k
};
@interface LENumberFormatter : NSNumberFormatter
@property(nonatomic) BOOL abbreviateLargeNumbers;
@property(nonatomic) LENumberFormatterAbbreviationStyle abbreviationStyle;
@end
@import ObjectiveC.message;
#import "LENumberFormatter.h"
@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 format the partial digits using the settings on self (minus the currency symbol)
LENumberFormatter * const partialFormatter = [self copy];
partialFormatter.currencySymbol = @"";
if(_abbreviationStyle == kLEAbbreviateNormal)
partialFormatter.maximumFractionDigits = 0;
NSString *(^const partialFormat)(NSNumber*) = ^(NSNumber *num) {
NSString *(*superImp)(struct objc_super*,SEL,NSNumber*) = (void*)&objc_msgSendSuper;
return superImp(&(struct objc_super) { partialFormatter, self.superclass }, _cmd, num);
};
BOOL const usingShortFormat = _abbreviationStyle == kLEAbbreviateShort;
double n = fabs([aObj doubleValue]);
NSDictionary * const separators = [self _localizedGroupingSeparators];
NSArray * const separatorExponents = [separators.allKeys sortedArrayUsingSelector:@selector(compare:)];
NSMutableString * const result = [NSMutableString new];
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 const partialNum = usingShortFormat
? 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(usingShortFormat)
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
&& !usingShortFormat
&& (!self.usesSignificantDigits || significantDigits < self.maximumSignificantDigits))
{
partialFormatter.maximumFractionDigits = self.maximumFractionDigits;
[result appendString:partialFormat(@(n))];
}
if(result.length > 0) {
// If our grouping separators contain spaces, we need to trim any trailing ones
[result replaceOccurrencesOfString:@"\\s+$" withString:@""
options:NSRegularExpressionSearch
range:(NSRange) { 0, result.length }];
if([aObj compare:@0] == NSOrderedAscending) {
if(self.negativePrefix) [result insertString:self.negativePrefix atIndex:0];
if(self.negativeSuffix) [result appendString:self.negativeSuffix];
} else {
if(self.positivePrefix) [result insertString:self.positivePrefix atIndex:0];
if(self.positiveSuffix) [result appendString:self.positiveSuffix];
}
return result;
} else
return [super stringForObjectValue:aObj];
}
- (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)_usingKanjiNumbers
{
return [self.locale.localeIdentifier rangeOfString:@"^(ja|zh)_"
options:NSRegularExpressionSearch].location != NSNotFound;
}
- (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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment