Format currency using a compact notation

Is there a way to format currency using a compact version, for example instead of $1,500,000 show $1.5M (and similar to thousands, billions and trillions)?

For instance, numbers can be set in a compact form using number.notation:

let price = Decimal(1_500_000.59)
price.formatted(.number.notation(.compactName)) 
// "1.5M"

Prices can be formatted using the currency style:

price.formatted(.currency(code: "USD").presentation(.narrow).rounded())
// $1,500,000.59

However, I am not sure if it is possible to easily use both formats to get $1.5M.

I don't see ready APIs that can do this for you unfortunately. You should be cautious about using your own logic to combine these parsers if your app needs to be highly localized. Note that if you dump the formatted currencies for different Locales, you will sometimes have leading symbols like "$" and other times letters. Here's a few samples:

Locale: af_NA
currencySymbol: $
numberFormatted: 1,5 m
currencyFormatted: $1,500,000.59


Locale: af_ZA
currencySymbol: R
numberFormatted: 1,5 m
currencyFormatted: R 1,500,000.59


Locale: agq_CM
currencySymbol: FCFA
numberFormatted: 1,5M
currencyFormatted: FCFA 1,500,001

I wouldn't be sure that mixing their compact number notation with the leading currency symbol would appear proper to native speakers. For instance,

$1,5 m
R 1,5 m
FCFA 1,5M

So this is not answer - just a note this problem may be more complicated than it seems.

1 Like

Thanks gestrich for the example. This is exactly why I wanted to see if this is already handled by existing APIs.

I wouldn't be sure that mixing their compact number notation with the
leading currency symbol

It’s worse than that. Different locales use different orders for their currency symbols, even for the same currency symbol. For example:

let n = 1234.56
var fs = FloatingPointFormatStyle<Double>.Currency(code: "EUR")
fs.locale = Locale(identifier: "en_IE")
print(n.formatted(fs))
// €1,234.56
fs.locale = Locale(identifier: "fr_FR")
print(n.formatted(fs))
// 1 234,56 €

Note that this output contains two different things that look like normal spaces but aren’t (-:


enobat, I think you might be able to make this work by using the currency formatter to render to an attributed string, using the attributes to find the numeric component of the output, and replacing that with the number from your number formatter. That’ll probably be wrong somewhere — one thing I’ve learnt about internationalise is that there are no universal truths — but I think it’ll work in most cases.

Here’s the first part of that process:

let n = 1234.56
var fs = FloatingPointFormatStyle<Double>.Currency(code: "EUR")
fs.locale = Locale(identifier: "en_IE")
let fsa = fs.attributed
let s = n.formatted(fsa)
print(String(s.characters))
// €1,234.56
guard
    let start = s.runs[\.numberPart].first(where: { $0.0 == .integer})?.1.lowerBound,
    let end = s.runs[\.numberPart].first(where: { $0.0 == .fraction})?.1.upperBound
else {
    fatalError()
}
let numberRange = s[start..<end]
print(String(numberRange.characters))
// 1,234.56

Share and Enjoy

Quinn “The Eskimo!” @ DTS @ Apple

2 Likes

How about something like this?

extension Decimal.FormatStyle {

    struct CompactCurrency: FormatStyle {
        let code: String
        var locale: Locale = .autoupdatingCurrent

        func format(_ value: Decimal) -> String {
            let currencyFormatStyle = Decimal.FormatStyle.Currency(code: code, locale: locale).presentation(.narrow).rounded()
            let numberFormatStyle = Decimal.FormatStyle(locale: locale).rounded()
            let currencyFormatted = value.formatted(currencyFormatStyle)
            let fullNumber = value.formatted(numberFormatStyle)
            let compactNumber = value.formatted(numberFormatStyle.notation(.compactName))

            return currencyFormatted.replacing(fullNumber, with: compactNumber)
        }

        func locale(_ locale: Locale) -> Decimal.FormatStyle.CompactCurrency {
            var formatStyle = self
            formatStyle.locale = locale
            return formatStyle
        }
    }

}

price.formatted(Decimal.FormatStyle.CompactCurrency(code: "USD", locale: .init(identifier: "fr_FR")))

1 Like

There's currently no way to do it. I think @eskimo 's answer is the most correct. It wouldn't work, however, if there are more than one number parts in the string, such as "123$.456". I don't think there is currently any locale that does that, but just something to watch out.

I went ahead and created an issue for swift-foundation.

2 Likes

Different locales also use different definitions of “billion”. And Indian English uses lakh and crore. So if you roll your own, make sure you handle these conventions correctly.

Thank you eskimo and hello_im_szymon for your detailed attempts.

Thanks for creating an issue, I’ll keep an eye on it.