Osama Naeem
Osama Naeem

Reputation: 1952

Formatting a currency depending upon the currency code selected regardless of device's locale (Swift)

I am trying to format currencies depending on the currency selected by the user. If no currency is selected, then device's current locale is used for formatting. however, I am having issues:

I am using a number formatter to format the double to currency string.

let formatter = NumberFormatter()
    formatter.numberStyle = .currency
    formatter.currencySymbol = ""
        
    if currencyCode != nil {
        formatter.currencyCode = currencyCode
    }
        
    let amount = Double(amt/100) + Double(amt%100)/100
    return formatter.string(from: NSNumber(value: amount))
}

The currencyCode is basically the currency the user has selected. However, if say the user selects EURO, the formatting is pretty much the same as for USD, meaning that it is not respecting the currency selected. I know that we cannot possibly create a Locale out of currencyCode since EUR is used in 26 different countries so it's impossible to derive the correct locale.

Also since I am using a format which basically fills the decimals position, then ONES, Tenths and so on and some currencies don't support decimal positions for example, PKR (Pakistani Ruppee) so how can I cater for that?

So my question is how can I format a currency correctly regardless of which device locale is selected. If my device locale is say USD and I create a EUR list, I would like all payments inside the list to be in EUR format. So if in USD a price is $3,403.23 in EUR it should be € 3 403,23.

Any advice on how I should go about formatting? Thanks!

Upvotes: 4

Views: 4050

Answers (2)

David Pasztor
David Pasztor

Reputation: 54716

You can dynamically match Locales to currency codes, since you can create all supported Locales from the availableIdentifiers property of Locale and then you can check their currencyCode property to match the currency code your user input.

extension Locale: CaseIterable {
    public static let allCases: [Locale] = availableIdentifiers.map(Locale.init(identifier:))
}

public extension Locale { 
    init?(currencyCode: String) { 
        guard let locale = Self.allCases.first(where: { $0.currencyCode == currencyCode }) else { return nil } 
        self = locale 
     } 
}

Locale(currencyCode: "EUR") // es_EA
Locale(currencyCode:"GBP") // kw_GB

However, as you can see, this can return exotic locales, which might not necessarily give your desired formatting.

I would rather suggest hardcoding the desired Locale for each currency code that your app supports, that way you can be 100% sure the formatting always matches your requirements. You can also mix the two approaches and have hardcoded Locales for the well-known currency codes, but use the dynamic approach for more exotic currency codes that you have no hard requirement over how they should be formatted.

Upvotes: 2

Christophe
Christophe

Reputation: 73376

In short

The locale settings related to currency are of two kinds:

  • Currency dependent: these are related to the monetary value and depend only on the currency and remain valid wherever you use that currency. This is only the international ISO code and the number of decimals, as defined by ISO 4217.
  • Cultural settings: these depend on the usages and practices related to the language and the country of the users and not directly to the currency. Typically, it's the position of the currency code or symbol relatively to the value, as well as the decimal and thousand separators.

Fortunately, Swift makes very well the difference. Here some code, that allows you to adapt the currency dependent settings, without ever touching to the cultural settings that important for the user. I'll also explain why you shouldn't change all the local settings.

The code

Here the demo code, with a couple of representative currencies:

let value: Double = 1345.23
for mycur in ["USD", "TND", "EUR", "JPY" ] {
    let myformatter = NumberFormatter()
    myformatter.numberStyle = .currencyISOCode
    let newLocale = "\(Locale.current.identifier)@currency=\(mycur)" // this is it!
    myformatter.locale = Locale(identifier:newLocale)
    print ("currency:\(mycur): min:\(myformatter.minimumFractionDigits) max:\(myformatter.maximumFractionDigits)" 
    print ("result: \(myformatter.string(from: value as NSNumber) ?? "xxx")")
}

For a representative demo, I've used :

  • the USD and the EUR, which, like most currencies can be divided in 100 sub-units (the cents),
  • the TND (Tunesian Dinar), which, like a handfull of other dinar-currencies, can be divided in 1000 sub-units (the millims),
  • the JPY(Japanese Yen), which could in the past be divided into sub-units (the sens) of such a small value that the Japanese government decided not to use them anymore. This is why there are no decimals anymore for JPY amounts.

The results

For the user, will benefit from the principle of least astonishment, and see the decimal and thousand separators and the positioning he/she is used-to.

in my current locale (in my language currency code is to the right, decimals are separated by a comma, and thousands with a hard space) the result will be:

cur:USD: min:2 max:2 result: 1  345,23 USD
cur:TND: min:3 max:3 result: 1  345,230 TND
cur:EUR: min:2 max:2 result: 1  345,23 EUR
cur:JPY: min:0 max:0 result: 1  345 JPY

But if you'd usually work in an English speaking environment, for example in a US culture, you'd get:

cur:USD: min:2 max:2 result: USD 1,345.23
cur:TND: min:3 max:3 result: TND 1,345.230
cur:EUR: min:2 max:2 result: EUR 1,345.23
cur:JPY: min:0 max:0 result: JPY 1,345

How it works:

The trick of the code is to create a new locale, by just changing the currency settings, but leaving intact all other country and language dependent parameters.

    let newLocale = "\(Locale.current.identifier)@currency=\(mycur)" // this is it!
    myformatter.locale = Locale(identifier:newLocale)

Why you should not fully implement what you wanted

If you would start to adapt the positioning to take the practice of the language of the country the currency is originating from, you might irritate the users who no longer see the currency code where they expect them. Fortunately, it will not create a real confusion.

Example: the EUR is the currency of countries with very different cultures. The rule about positioning of the currency or the currency symbol was therefore defined to be dependent on the language of the text in which the amount appears. Official reference

Now, if you would start to adopt thousand and decimal separators of another language or country because it's the currency's home country, this would create a real confusion, especially for smaller amounts. Moreover, it's not always possible.

Example: In Canada the same currency amount is written with comma decimal separator by French-speaking Canadians, but dot decimal separator by english-speaking Canadians. This clearly shows it's not the currency that determines the separators to use, but the language of the user.

You should therefore be respectful of the user's settings in this regard, and only adapt the currency specific settings.

Upvotes: 8

Related Questions