Preliminary remark: The main problem here, is that I want an internationalized solutino that relies on the build-in iOS/macOS features. I can therefore not use any solution based on a given decimal separator, as it would change dependeing on the user's locale, nor use hard-coded strings. This is why I can only upvote Roman's solution for its very elegant code, but not accept it, since it doesn't fully meet the requirements.
let value = 12345.678 // value to spell
currency: String? = nil // ISO currency if you want one specific
1.Initialize the formatter:
let formatter = NumberFormatter() // formatter with user's default locale
If I want to use a different language, or a different country formalling, I have to explicitly overwrite the locale:
formatter.locale = Locale.init(identifier: "en_US.UTF-8") // for demo only
This works fine, if I use the currency of the locale country. But if I use another currency, the formatter behavior might e wrong for some formatting styles. So, the trick is to use a currency-specific locale:
if currency != nil {
let newLocale = "\(formatter.locale.identifier)@currency=\(currency!)"
formatter.locale = Locale(identifier:newLocale)
}
The interesting Foundation feature is that when I change the currency, the currency specific locale settings, i.e. the currency code, currency symbol, and number of digits automatically change:
formatter.numberStyle = .currency
let decimals = max(formatter.minimumFractionDigits, formatter.maximumFractionDigits)
2.Get the full currency name in the right grammatical form
Now I have the decimals, but I would like the full name spelledCurrency of the currency. The problem is that some languages have it neutral. For some languages you have to write it singular for 1 unit, but plural from 2 onwards. For some languages, the decimals matter in the choice between singular and plural. Fortunately, NumberFormatter allows to tackle this with the style .currencyPlural. This weird style write in the numbers in digits, but writes the plain currency according to language specific rules. It's magic:
The problem is that I want only the name of the currency: no space, no separator, no digits. And I want it to work for Japanese (locale "ja_JP.UTF-8") as well. Fortunately, we can filter very easily the string:
formatter.numberStyle = .currencyPlural
var spelledCurrency : String
if let plural = formatter.string(from: value as NSNumber) {
// Keep only the currency spelling: trim spaces, punctuation and digits
// ASSUMPTION: decimal and thousand separators belong to punctuation
var filter = CharacterSet()
filter.formUnion(.punctuationCharacters)
filter.formUnion(.decimalDigits)
filter.formUnion(.whitespaces)
spelledCurrency = plural.trimmingCharacters(in: filter)
}
else {
// Fall back - worst case: the ISO code XXX for no currency is used
spelledCurrency = formatter.currencyCode ?? (formatter.internationalCurrencySymbol ?? "XXX")
}
3.Separate the numeric components
I used a value defined as Double. But you could easily adapt for the Decimal type, whcih is more robust for currencies. The trick is to have the wholePart in currency units, and the wholeDecimal, i.e. the part expressed in an integer number corresponding to the number of subunits of the currency (which is known, thanks to the number of currency's number of decimals):
let decimalPart = value.truncatingRemainder(dividingBy: 1)
let wholePart = value - decimalPart
let wholeDecimals = floor(decimalPart * pow(10.0, Double(decimals)))
4. Spell out the components
Here we use the native .spellOut feature to spell numbers. That's fantasic if you try to learn foreign languages ;-)
formatter.numberStyle = .spellOut
let wholeString = formatter.string(from: wholePart as NSNumber) ?? "???"
let decimalString = formatter.string(from: wholeDecimals as NSNumber) ?? "???"
5. Final assembly
We now have to assemble the components. But instead of saying twenty dollars point five seven, we should say officially twenty dollars and fifty seven cents. What I've decided is to replace "and" with the localised name of the currency and followed by the number of sub-units, without expressing the subunit itself. It works in the couple of languages I know:
// ASSUMPTION: currency is always between whole and fractional
// TO BE VERIFIED: Ok for Right to left writing?
var spelled : String
if (decimals>0 && wholeDecimals != 0) {
spelled = "\(wholeString) \(spelledCurrency) \(decimalString)"
} else {
spelled = "\(wholeString) \(spelledCurrency)"
}
Just need to print (or return) the final value:
print (">>>>> \(spelled)")
6. Tests
Here the test results in some langauges:
en, USD: twelve thousand three hundred forty-five US dollars sixty-seven
fr, EUR: douze mille trois cent quarante-cinq euros soixante-sept
de, EUR: zwölftausenddreihundertfünfundvierzig Euro siebenundsechzig
nl, EUR: twaalfduizenddriehonderdvijfenveertig euro zevenenzestig
it, EUR: dodicimilatrecentoquarantacinque euro sessantasette
es, EUR: doce mil trescientos cuarenta y cinco euros sesenta y siete
ja, JPY: 一万二千三百四十五 円
fr, TND: douze mille trois cent quarante-cinq dinars tunisiens six cent soixante-dix-sept
The last test shows a rounding error for Double, so it's better to use Decimal.
I also used with amounts between 1 and 2 euros with and witout subunits, and integer numbers.