This seems like an oversight by Apple since there are tons of relative times, metrics, dates, list, person, bytes, etc, etc, etc formatters but this is a pretty common case especially with social media, graphs, and others. Ok end rant..
Here's my version below wrapping the NumberFormatter
and handles all Int
values including negatives, as well as locale aware:
public struct AbbreviatedNumberFormatter {
private let formatter: NumberFormatter
public init(locale: Locale? = nil) {
let formatter = NumberFormatter()
formatter.allowsFloats = true
formatter.minimumIntegerDigits = 1
formatter.minimumFractionDigits = 0
formatter.maximumFractionDigits = 1
formatter.numberStyle = .decimal
if let locale = locale {
formatter.locale = locale
}
self.formatter = formatter
}
}
public extension AbbreviatedNumberFormatter {
/// Returns a string containing the formatted value of the provided `Int` value.
func string(from value: Int) -> String {
let divisor: Double
let suffix: String
switch abs(value) {
case ..<1000:
return "\(value)"
case ..<1_000_000:
divisor = 1000
suffix = "K"
case ..<1_000_000_000:
divisor = 1_000_000
suffix = "M"
case ..<1_000_000_000_000:
divisor = 1_000_000_000
suffix = "B"
default:
divisor = 1_000_000_000_000
suffix = "T"
}
let number = NSNumber(value: Double(value) / divisor)
guard let formatted = formatter.string(from: number) else {
return "\(value)"
}
return formatted + suffix
}
}
And the test cases:
final class AbbreviatedNumberFormatterTests: XCTestCase {}
extension AbbreviatedNumberFormatterTests {
func testFormatted() {
let formatter = AbbreviatedNumberFormatter()
XCTAssertEqual(formatter.string(from: 0), "0")
XCTAssertEqual(formatter.string(from: -10), "-10")
XCTAssertEqual(formatter.string(from: 500), "500")
XCTAssertEqual(formatter.string(from: 999), "999")
XCTAssertEqual(formatter.string(from: 1000), "1K")
XCTAssertEqual(formatter.string(from: 1234), "1.2K")
XCTAssertEqual(formatter.string(from: 9000), "9K")
XCTAssertEqual(formatter.string(from: 10_000), "10K")
XCTAssertEqual(formatter.string(from: -10_000), "-10K")
XCTAssertEqual(formatter.string(from: 15_235), "15.2K")
XCTAssertEqual(formatter.string(from: -15_235), "-15.2K")
XCTAssertEqual(formatter.string(from: 99_500), "99.5K")
XCTAssertEqual(formatter.string(from: -99_500), "-99.5K")
XCTAssertEqual(formatter.string(from: 100_500), "100.5K")
XCTAssertEqual(formatter.string(from: -100_500), "-100.5K")
XCTAssertEqual(formatter.string(from: 105_000_000), "105M")
XCTAssertEqual(formatter.string(from: -105_000_000), "-105M")
XCTAssertEqual(formatter.string(from: 140_800_200_000), "140.8B")
XCTAssertEqual(formatter.string(from: 170_400_800_000_000), "170.4T")
XCTAssertEqual(formatter.string(from: -170_400_800_000_000), "-170.4T")
XCTAssertEqual(formatter.string(from: -9_223_372_036_854_775_807), "-9,223,372T")
XCTAssertEqual(formatter.string(from: Int.max), "9,223,372T")
}
}
extension AbbreviatedNumberFormatterTests {
func testFormattedLocale() {
let formatter = AbbreviatedNumberFormatter(locale: Locale(identifier: "fr"))
XCTAssertEqual(formatter.string(from: 0), "0")
XCTAssertEqual(formatter.string(from: -10), "-10")
XCTAssertEqual(formatter.string(from: 500), "500")
XCTAssertEqual(formatter.string(from: 999), "999")
XCTAssertEqual(formatter.string(from: 1000), "1K")
XCTAssertEqual(formatter.string(from: 1234), "1,2K")
XCTAssertEqual(formatter.string(from: 9000), "9K")
XCTAssertEqual(formatter.string(from: 10_000), "10K")
XCTAssertEqual(formatter.string(from: -10_000), "-10K")
XCTAssertEqual(formatter.string(from: 15_235), "15,2K")
XCTAssertEqual(formatter.string(from: -15_235), "-15,2K")
XCTAssertEqual(formatter.string(from: 99_500), "99,5K")
XCTAssertEqual(formatter.string(from: -99_500), "-99,5K")
XCTAssertEqual(formatter.string(from: 100_500), "100,5K")
XCTAssertEqual(formatter.string(from: -100_500), "-100,5K")
XCTAssertEqual(formatter.string(from: 105_000_000), "105M")
XCTAssertEqual(formatter.string(from: -105_000_000), "-105M")
XCTAssertEqual(formatter.string(from: 140_800_200_000), "140,8B")
XCTAssertEqual(formatter.string(from: -170_400_800_000_000), "-170,4T")
XCTAssertEqual(formatter.string(from: -9_223_372_036_854_775_807), "-9 223 372T")
XCTAssertEqual(formatter.string(from: Int.max), "9 223 372T")
}
}
Only thing I don't like about it is that it's not localized as to what K
, M
, B
, or T
means in other languages. Much appreciated to everyone's inspiration.