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".
CodePudding user response:
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())
CodePudding user response:
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.