Currency / Text

Currency Curiosities

Formatting currency values can be tricky because to do it correctly requires knowledge of each individual currency and how it should be rendered. Fortunately the java.text package contains some classes which have that local knowledge which means that us developers do not need it, except in really rare circumstances. However, there are some subtleties to this which may, at first, seem quite surprising yet make perfect sense we understand them better through a little digging. In this article we’ll take a look in to these subtleties.

By far the easiest way to format currency values is to call NumberFormat.getCurrencyInstance() to obtain a NumberFormat instance which will format a number in to a currency string. It also supports parsing of currency strings back to numbers, but for this article we’ll focus on numbers to strings. What this will do internally is obtain a java.util.Currency instance from the default Locale of the device and use this to determine the currency format. That’s all well and good if we’re happy to display a value in the default currency for the Locale, but there may be times when we know that a given currency value is actually in a specific currency which may not be the same as the default currency for the device. For example, if our app is an online store which only accepts payment in one specific currency, we should not depend upon using the device default locale to determine the currency to use. Even if we limit our app which only accepts payment in US dollars (USD) to be available on the US Play Store, the user can still change the locale of the device by changing the default language in device settings.

One apparently easy but totally flawed way we can avoid this is to override the default locale when our app starts:

Locale.setDefault(Locale.US)

While this will certainly work it actually has some quite nasty knock on effect. It feels like we’re ignoring the user’s preferences by overriding the entire locale, and there may be other reasons besides currency that the user has chosen an alternative locale. Moreover, if we actually want to provide language support for different countries then this will break localisation because it may force all resources to be loaded as US English. The problems with this approach are actually even more subtle than this as we shall see shortly. While we could override the locale while formatting the currency string and then change it back again, that feels pretty hacky.

There is a way in which we can, quite cleanly, obtain a text formatter for a given currency whilst honouring the locale, and that is by requesting the NumberFormat instance from the locale as before, and then overriding the currency:

NumberFormat.getCurrencyInstance().apply {
    currency = Currency.getInstance("USD")
}

Whenever we use this NumberFormatter to format a currency value it will always display the value in US dollars while honouring the default locale settings for the device.

This is quite a useful little trick, but it can reveals some really interesting hidden subtleties. While it would seem perfectly reasonable to display one dollar as $1, there are more that twenty countries which have a currency named the dollar. So if we display this to someone in the US, it would be a reasonable assumption that the value is in US dollars, but if we displayed the same value to someone in Canada, Australia, New Zealand, or any other countries which have a currency named the dollar, then it would be a reasonable assumption by those users that the amount was being displayed in the currency for that locale. If we were to display the currency value in an ambiguous form such as this, it could lead to misunderstandings and the user feeling that they have been incorrectly charged.

This further highlights the issues with just changing the default locale while the app is running – there could be precisely these kinds of ambiguities if we take that approach. Particularly as the user may not be aware that we are displaying currencies using a different locale to the one they have set for their device.

But all is not doom and gloom, the approach we have already looked at of getting a NumberFormat instance and then changing the Currency actually deals with this in quite a neat way. The NumberFormat itself is obtained from the default locale for the device. When we change the currency of the NumberFormat instance it does not change the locale, but when we come to format a currency using that configuration it is context aware enough to format the given currency in a way that it is given any necessary context for the locale from which the NumberFormat instance was obtained. In other words, If we obtain a NumberFormat from a Canadian locale, set the currency to US dollars, and then format a currency value. Then the formatted value will have qualifiers added to make it clear that the value is in US dollars and not Canadian dollars.

An example will demonstrate this much better than lots of dry explanation:

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        textView1.text = formatCurrency(1f, CURRENCY_US_DOLLARS, LANGUAGE_ENGLISH, COUNTRY_US)
        textView2.text = formatCurrency(1f, CURRENCY_US_DOLLARS, LANGUAGE_ENGLISH, COUNTRY_CANADA)
        textView3.text = formatCurrency(1f, CURRENCY_US_DOLLARS, LANGUAGE_ENGLISH, COUNTRY_AUSTRALIA)
    }

    private fun formatCurrency(amount: Float, currency: String, language: String, country: String) =
            currencyInLocale(currency, language, country).format(amount)

    private fun currencyInLocale(
            currencyCode: String,
            language: String,
            country: String = "",
            variant: String = ""): NumberFormat =
            Locale(language, country, variant).let {
                NumberFormat.getCurrencyInstance(it).apply {
                    currency = Currency.getInstance(currencyCode)
                }
            }

    companion object {
        private const val CURRENCY_US_DOLLARS: String = "USD"
        private const val LANGUAGE_ENGLISH: String = "EN"
        private const val COUNTRY_US: String = "US"
        private const val COUNTRY_CANADA: String = "CA"
        private const val COUNTRY_AUSTRALIA: String = "AU"
    }
}

Here we display a value if 1 USD as it would appear to users with a default locale of US, Canada, and Australia. When we run this we see the following:

The first line is how this value is displayed for a US locale, the second line is for Canada, and the third for Australia. Qualifiers are added to both Canada & Australia to make it clear that the value is US dollars and not Canadian or Australian dollars. But also there is a subtle difference between the Canadian & Australian formatting – Canada uses US$ and Australia USD. Although both are understandable, the formatting is still tailored to the specifics of the locale.

All of this behaviour comes because of the International Components for Unicode which is included in Android through the ICU4J library. This has been developed since 1999, initially by IBM, but since 2016 it has been under the stewardship of the Unicode consortium. ICU4J not only covers currency formatting, but there are many other components as well.

Although we’ve only covered one quite simple use-case here, it highlights how tricky localisation can quickly become if we fail to properly utilise the default locale of the device.

The source code for this article is available here.

© 2017, Mark Allison. All rights reserved.

Copyright © 2017 Styling Android. All Rights Reserved.
Information about how to reuse or republish this work may be available at http://blog.stylingandroid.com/license-information.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.