Search code examples
swiftstringinternationalizationcurrencyformatter

Spelling out the fractional part of a monetary amount using a NumberFormatter in Swift


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?


Solution

  • 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ölf­tausend­drei­hundert­fünf­und­vierzig Euro sieben­und­sechzig
    nl, EUR: twaalf­duizend­drie­honderd­vijf­en­veertig euro zeven­en­zestig
    it, EUR: dodici­mila­tre­cento­quaranta­cinque euro sessanta­sette
    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.