Home > Net >  Formatting large currency numbers
Formatting large currency numbers

Time:10-31

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.

  • Related