Reputation: 299265
Using the FormatStyle APIs, is there a way to format large numbers with trailing SI units like "20M" or "10k"? In particular I'm looking for a way to format large currency values like "$20M" with proper localization and currency symbols.
I currently have a currency formatter:
extension FormatStyle where Self == FloatingPointFormatStyle<Double>.Currency {
public static var dollars: FloatingPointFormatStyle<Double>.Currency {
.currency(code: "usd").precision(.significantDigits(2))
}
}
I'd like to extend this to format Double(20_000_000)
as "$20M".
Upvotes: 2
Views: 883
Reputation: 299265
After digging more deeply into this, I've developed a solution that get the best of Joakim Danielson and lorem ipsum's answers, using .compactName
to avoid reimplementing unit scaling, while still getting a currency symbol.
It's important that this is not a fully localized solution, which is quite difficult. In American English, 20 million euros would commonly be written "€20M", but in French there's no common practice I can find. Something like "20 M€" seems more likely from my research. But how would "millions of Canadian dollars" be written in French? Where does the space go? "20 MCA$"? "20 M CAD"? It's a mess just looking at American English and French, let alone every other supported Locale and currency.
But that's not the problem I'm solving. I'm the only user of this program and I just want to display quantities of US Dollars. How do I write a formatter that does just what I want?
Just like the Percent formatter and the Currency formatter, create a ShortDollars formatter nested inside of FloatingPointFormatStyle:
extension FloatingPointFormatStyle {
public struct ShortDollars: FormatStyle {
public func format(_ value: Value) -> String {
let s = FloatingPointFormatStyle()
.precision(.significantDigits(2))
.notation(.compactName)
.format(value)
return "$\(s)"
}
}
}
This uses the standard FloatingPointFormatStyle for all the heavy lifting, and then slaps a $
on the front.
And to give it the standard syntax, add static properties (you need this for each specific type of BinaryFloatingPoint this can format):
extension FormatStyle where Self == FloatingPointFormatStyle<Double>.ShortDollars {
public static var shortDollars: Self { .init() }
}
extension FormatStyle where Self == FloatingPointFormatStyle<Float>.ShortDollars {
public static var shortDollars: Self { .init() }
}
Upvotes: 1
Reputation: 51821
You can format ordinary numbers this way using the notation
modifier with compactName
as argument
Double(20_000_000).formatted(.number.notation(.compactName))
Unfortunately this modifier doesn't exist for Currency
although it also exists for Percent
so hopefully this is something we will see implemented in the future.
So the question is if this is good enough or if it is worth it to implement a custom solution.
Upvotes: 1
Reputation: 29242
You can create a custom struct
that conforms to FormatStyle
public struct ShortCurrency<Value>: FormatStyle, Equatable, Hashable, Codable where Value : BinaryFloatingPoint{
let locale: Locale
enum Options: Int{
case million = 2
case billion = 3
case trillion = 4
func short(locale: Locale) -> String{
switch self {
case .million:
return millionAbbr[locale, default: "M"]
case .billion:
return billionAbbr[locale, default: "B"]
case .trillion:
return trillionAbbr[locale, default: "T"]
}
}
///Add other supported locales
var millionAbbr: [Locale: String] { [Locale(identifier: "en_US") : "M"]}
var billionAbbr: [Locale: String] { [Locale(identifier: "en_US") : "B"]}
var trillionAbbr: [Locale: String] { [Locale(identifier: "en_US") : "T"]}
}
public func format(_ value: Value) -> String {
let f = NumberFormatter()
f.locale = locale
f.numberStyle = .currency
f.usesSignificantDigits = true
let basic = f.string(for: value) ?? "0"
let count = basic.count(of: ".000")
//Checks for million value
if let abbr = Options(rawValue: count)?.short(locale: f.locale){
//Get the symbol and the most significant numbers
var short = String(basic.prefix(basic.count - (4*count)))
//Append from the dictionary based on locale
short.append(abbr)
//return modified string
return short
}else{
//return the basic string
return basic
}
}
}
extension String {
func count(of string: String) -> Int {
guard !string.isEmpty else{
return 0
}
var count = 0
var searchRange: Range<String.Index>?
while let foundRange = range(of: string, options: .regularExpression, range: searchRange) {
count += 1
searchRange = Range(uncheckedBounds: (lower: foundRange.upperBound, upper: endIndex))
}
return count
}
}
Then extend FormatStyle
@available(iOS 15.0, *)
extension FormatStyle where Self == FloatingPointFormatStyle<Double>.Currency {
public static func shortCurrency (locale: Locale? = nil) -> ShortCurrency<Double> {
return ShortCurrency(locale: locale ?? .current)
}
}
It will be available for usage just as any other FormatStyle
Text(Double(20_000_000), format: .shortCurrency())
Upvotes: 3