The answer may be late but hopefully this helps clarify the root cause.
Problem
- Currency code does not imply locale and region
The reason why CAD becomes CA$ is probably because NSLocale
looks up the first matching currency code, and for CAD, these are the matching localeIdentifiers
in order of NSLocale.availableLocaleIdentifiers
1. Optional("CA$") Optional("CA") iu_CA
2. Optional("$") Optional("CA") fr_CA
3. Optional("$") Optional("CA") en_CA
iu_CA
is Inuktitut but I'm not sure why it's listed as CA$
, but I hope the point is clear.
Similarly in CNY (Chinese Yuan):
1. Optional("CN¥") Optional("CN") en_CN
2. Optional("¥") Optional("CN") yue_CN
3. Optional("¥") Optional("CN") bo_CN
4. Optional("¥") Optional("CN") zh_CN
5. Optional("¥") Optional("CN") ug_CN
6. Optional("¥") Optional("CN") ii_CN
The reason for showing CN¥ when en_CN is probably because JPY also uses ¥.
In CHF (Switzerland Franc), they do not have a one-letter symbol:
1. Optional("CHF") Optional("LI") gsw_LI
2. Optional("CHF") Optional("CH") de_CH
...
9. Optional("CHF") Optional("CH") en_CH
10. Optional("CHF") Optional("CH") it_CH
Solution
Many apps vary, but this is the steps I took that I am happy with for my application:
- Find matching locale candidates using currency code lookup from all locale identifiers
- Pick the shortest symbol from the candidates
- Store the symbol somewhere so that it doesn't have to be computed each time
Implementation
func getSymbolForCurrencyCode(code: String) -> String {
var candidates: [String] = []
let locales: [String] = NSLocale.availableLocaleIdentifiers
for localeID in locales {
guard let symbol = findMatchingSymbol(localeID: localeID, currencyCode: code) else {
continue
}
if symbol.count == 1 {
return symbol
}
candidates.append(symbol)
}
let sorted = sortAscByLength(list: candidates)
if sorted.count < 1 {
return ""
}
return sorted[0]
}
func findMatchingSymbol(localeID: String, currencyCode: String) -> String? {
let locale = Locale(identifier: localeID as String)
guard let code = locale.currencyCode else {
return nil
}
if code != currencyCode {
return nil
}
guard let symbol = locale.currencySymbol else {
return nil
}
return symbol
}
func sortAscByLength(list: [String]) -> [String] {
return list.sorted(by: { $0.count < $1.count })
}
Usage
let usd = getSymbolForCurrencyCode(code: "USD")
let jpy = getSymbolForCurrencyCode(code: "JPY")
let cny = getSymbolForCurrencyCode(code: "CNY")
let cad = getSymbolForCurrencyCode(code: "CAD")
let uah = getSymbolForCurrencyCode(code: "UAH")
let krw = getSymbolForCurrencyCode(code: "KRW")
let zar = getSymbolForCurrencyCode(code: "ZAR")
let chf = getSymbolForCurrencyCode(code: "CHF")
let all = [usd, jpy, cny, cad, uah, krw, zar, chf]
(lldb) po all
▿ 8 elements
- 0 : "$"
- 1 : "¥"
- 2 : "¥"
- 3 : "$"
- 4 : "₴"
- 5 : "₩"
- 6 : "R"
- 7 : "CHF"
Problems
- Instinctively, I see that the one letter symbol approach can show an incorrect symbol if there are more than one distinct symbols for currency code, but I haven't seen such case.
- Computing this each time is heavy lifting so when a user sets their currency setting, it's wise to store the computed result and use that result upon each lookup