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.

1 Like

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

3 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.

1 Like

Thank you eskimo and hello_im_szymon for your detailed attempts.

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

Hi enobat, here's how I'd do it. I'm using the copyright symbol to signify constant or unit of measurement, and k# to signify multiplication by powers of thousand. We could also use m# for million, g# for billion, t# for trillion, and so on. But it's probably more sensible to restrict ourselves to k# for thousand.

Shorthand notation Exponential equivalent Longform notation
1_500_000©USD One million five hundred thousand dollars
1.5kp2©USD 1.5 x (1_000^+2) One point five kilo-power-positive-two dola
1.5k2©USD 1.5 x (1_000^2) One point five kilo-two dola
1.5e6©USD 1.5 x (10^6) One point five deca-power-six dola
1.5m©USD 1.5 x 1_000_000 One point five mega dola
Shorthand notation Exponential equivalent Longform notation
1_500©USD One thousand five hundred dollars
1.5kp1©USD 1.5 x (1_000^+1) One point five kilo-power-positive-one dola
1.5k1©USD 1.5 x (1_000^1) One point five kilo-one dola
1.5e3©USD 1.5 x (10^3) One point five deca-power-three dola
1.5k©USD 1.5 x 1_000 One point five kilo dola