I need to convert a monetary amount into a string expressing the value in plain text (i.e. cheques, contracts and other legal documents).
This can be done with a NumberFormatter
that converts numbers in plain text when using the number style .spellOut
:
var curAmount = 10097.43
var fmtAmount : String
let formatter = NumberFormatter()
formatter.locale = Locale.init(identifier:"en_US.UTF-8") // for demo only
//formatter.locale = Locale.current // Normal case is user's preferred locale
formatter.numberStyle = .spellOut
if let fmtAmount = formatter.string(from: curAmount as NSNumber) {
print ("Amount: \(fmtAmount)")
}
It works fine for the integral part of the number, but the fractional part is unfortunately handled as a sequence of independent digits, in all the six languages I have tried. For example:
ten thousand ninety-seven point four three
But I need that the fractional part is also expressed as a number, such as
ten thousand ninety-seven point forty three
ten thousand ninety-seven dollars forty three
I could of course take the string, remove everything after the "point"
, multiply the fractional part by hundred, generate a second number string and concatenate both strings. But this would not work with locales using foreign languages, nor with currencies having zero or three digits after the point.
Is there a way to get the fractional part handled correctly as a plain text whole number, out of the box, perhaps using some subtle parameters of the formatter ? And is there a way to include also the name of the currency (i.e. like it is spelled out with number style .currencyPlural
) at the right place depending on the locale's language?
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
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)
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")
}
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)))
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) ?? "???"
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)")
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.