One day, I was walking to the office and came across an “Office Space For Rent” sign. “It’s either a really tiny office or maybe something else is going on,” I thought, trying to decipher what “1.000” represented.
It can get confusing. For example, in the US, UK and Australia, the dot in the number represents a decimal separator. However, in Germany, the Netherlands and Turkey, the dot could serve as a thousands or grouping separator. Of course, the context helped decipher this mystery. Since this happened in Amsterdam, “1.000” meant one thousand.
When we write apps, we need to take this context into account, because people who use our app may live in other countries and speak other languages. Fortunately, we don’t have to remember the details about each region in the world ourselves. Most modern operating systems encapsulate such knowledge in a “locale,” which knows how to print numbers and dates, among other things.
Locales and Playgrounds
Internationalization and localization tools have seen major improvements in Xcode 6, but perhaps the unsung hero is the Playground. As you type code, you can see its results immediately, which makes it much easier to experiment and learn.
First, let’s determine the locale of our Playground:
NSLocale.currentLocale().localeIdentifier
On my laptop, the results sidebar shows "en_US"
. This is not the only locale I have at my disposal, however. Let’s see what else is available:
NSLocale.availableLocaleIdentifiers()
This is what I see in the results sidebar:
Since the list is very large and doesn’t fit, we can click the Quick Look (“eye”) icon in the sidebar.
If I want to play with particular locales, I can reference them as follows:
let unitedStatesLocale = NSLocale(localeIdentifier: "en_US")
let chinaLocale = NSLocale(localeIdentifier: "zh_Hans")
let germanyLocale = NSLocale(localeIdentifier: "de_DE")
let indiaLocale = NSLocale(localeIdentifier: "en_IN")
Let’s see what they are called in English:
unitedStatesLocale.displayNameForKey(NSLocaleIdentifier, value: unitedStatesLocale.localeIdentifier)!
unitedStatesLocale.displayNameForKey(NSLocaleIdentifier, value: chinaLocale.localeIdentifier)!
unitedStatesLocale.displayNameForKey(NSLocaleIdentifier, value: germanyLocale.localeIdentifier)!
unitedStatesLocale.displayNameForKey(NSLocaleIdentifier, value: indiaLocale.localeIdentifier)!
The results sidebar will show:
"English (United States)"
"Chinese (Simplified)"
"German (Germany)"
"English (India)"
If we wanted to learn what a particular locale is called somewhere else, we would write:
germanyLocale.displayNameForKey(NSLocaleIdentifier, value: unitedStatesLocale.localeIdentifier)!
The “en_US” locale happens to be called "Englisch (Vereinigte Staaten)"
in German.
Representing Numbers
Locales know more than just what to call themselves and others.
When it comes to numbers, a lot of English-speaking locales follow similar rules. Many people know the differences between number formatting rules in the US and UK and continental Europe. In the US or UK, 42 million would be printed as 42,000,000.00
. In some parts of Europe, the same number would be printed as 42.000.000,00
. But in other parts of Europe, it would be printed as 42 000 000,00
. We haven’t even started scratching the surface, so how are we supposed to keep track of all these differences? Number formatters to the rescue!
Let’s pick a large number to identify differences in formatting in these various locales:
let largeNumber = 360451996007.42
var numberFormatter = NSNumberFormatter()
numberFormatter.numberStyle = NSNumberFormatterStyle.DecimalStyle
numberFormatter.locale = unitedStatesLocale
numberFormatter.stringFromNumber(largeNumber)!
numberFormatter.locale = chinaLocale
numberFormatter.stringFromNumber(largeNumber)!
numberFormatter.locale = germanyLocale
numberFormatter.stringFromNumber(largeNumber)!
numberFormatter.locale = indiaLocale
numberFormatter.stringFromNumber(largeNumber)!
Not surprisingly, we find the US and UK number formatting identical, but there are several variations for other locales:
"360,451,996,007.42"
"360,451,996,007.42"
"360.451.996.007,42"
"3,60,45,19,96,007.42"
A few interesting observations:
- The Indian numbering system groups digits differently, using the comma to denote the lowest three digits (thousand) and then grouping every 2 digits:
3,60,45,19,96,007.42
.
- While both of them are in Europe, France uses spaces to group thousands, whereas Germany uses periods. So you can’t really generalize things about numbers in Europe.
- In some locales, even digits may appear differently.
Next time somebody sends you an email saying that their app was downloaded 1,500
times in the first hour, make sure you know where they are coming from. It could mean a major hit (one and a half thousand) or it could mean only one customer managed to download the app fully, while the second customer only managed to get half the app (one and a half).
Side Note: Numbers in Code
Sometimes it is useful to group digits in numbers in a certain way while we are writing code. This does not affect the actual value of those numbers. For example:
let worldPopulation = 7_288_000_000
// or 7_28_80_00_000 in Indian Numbering System
let phoneNumber = 1_800_555_1234
let socialSecurityNumber = 123_45_6789
Dollars and Cents (Er, Pounds and Pence)
Sometimes numbers mean more than just the digits and symbols that comprise them. Sometimes numbers represent money. If it doesn’t make any difference to you, I would like to give you 1.50
dollars in exchange for 1,500
euros. You can already spot that something doesn’t look right. Jeremy Sherman wrote a post about this. What can we learn about currency formatting with Swift?
var currencyFormatter = NSNumberFormatter()
currencyFormatter.numberStyle = NSNumberFormatterStyle.CurrencyStyle
currencyFormatter.locale = unitedStatesLocale
currencyFormatter.stringFromNumber(largeNumber)!
currencyFormatter.locale = chinaLocale
currencyFormatter.stringFromNumber(largeNumber)!
currencyFormatter.locale = germanyLocale
currencyFormatter.stringFromNumber(largeNumber)!
currencyFormatter.locale = indiaLocale
currencyFormatter.stringFromNumber(largeNumber)!
This shows us how the same amount is formatted using the local currency.
"$360,451,996,007.42"
"¤ 360,451,996,007.42"
"360.451.996.007,42 €"
"₹ 3,60,45,19,96,007.42"
It’s clear that different locales have different currency symbols and place them differently. Depending on the application, this may or may not be what we intended. So let’s specify the currency explicitly, to avoid any loss and to potentially forego any gains from such careless currency “exchange”:
var dollarFormatter = NSNumberFormatter()
dollarFormatter.numberStyle = NSNumberFormatterStyle.CurrencyStyle
dollarFormatter.currencyCode = "USD"
dollarFormatter.locale = unitedStatesLocale
dollarFormatter.stringFromNumber(largeNumber)!
dollarFormatter.locale = chinaLocale
dollarFormatter.stringFromNumber(largeNumber)!
dollarFormatter.locale = germanyLocale
dollarFormatter.stringFromNumber(largeNumber)!
dollarFormatter.locale = indiaLocale
dollarFormatter.stringFromNumber(largeNumber)!
This amount represents a very large sum. Just don’t try to cash it.
"$360,451,996,007.42"
"US$ 360,451,996,007.42"
"360.451.996.007,42 $"
"US$ 3,60,45,19,96,007.42"
In some cases we see the currency symbol $
, sometimes we see US$
. That’s because the dollar sign is used for some other currencies and may be ambiguous.
Even within the EU, formatting amounts with Euro will yield many different forms:
let euroLocaleIdentifiers = ["bg_BG", "hr_HR", "cs_CS", "da_DK", "nl_NL", "en_UK", "et_EE", "fi_FI", "fr_FR", "de_DE", "el_GR", "hu_HU", "ga_IE", "it_IT", "lv_LV", "lt_LT", "mt_MT", "pl_PL", "pt_PT", "ro_RO", "sk_SK", "sl_SI", "es_ES", "sv_SE"]
let EuroLocales = euroLocaleIdentifiers.map({s in NSLocale(localeIdentifier: s)})
var euroFormatter = NSNumberFormatter()
euroFormatter.numberStyle = NSNumberFormatterStyle.CurrencyStyle
euroFormatter.currencyCode = "EUR"
for l in EuroLocales {
euroFormatter.locale = l
euroFormatter.stringFromNumber(largeNumber)
}
Here are all the variations. As you can see with Dutch and German, the currency may be formatted differently, depending on the country it’s being used in.
Formatted euro amount |
Languages |
360 451 996 007,42 € |
Bulgarian, Czech, Estonian, Finnish, French, Italian, Lithuanian, Polish, Portuguese, Slovak, Swedish |
360.451.996.007,42 € |
Croatian, Danish, Dutch (Belgium), German (Germany), Greek, Romanian, Slovenian, Spanish |
€ 360.451.996.007,42 |
Dutch (Netherlands), German (Austria) |
€360,451,996,007.42 |
English (UK), Irish, Maltese |
360 451 996 007,42 EUR |
Hungarian |
€360 451 996 007,42 |
Latvian |
Units, or How to Prevent Crash Landings
Currency is not the only numerical value with units. iOS 8 added HealthKit, with support for quantities representing things like weight, height, caloric intake, etc. For example, our program would manipulate weight in kilograms internally, but print out results in whatever unit is appropriate for the current locale:
let massFormatter = NSMassFormatter()
massFormatter.stringFromKilograms(5) // 11.023 lb
massFormatter.numberFormatter.locale = NSLocale(localeIdentifier: "zh_Hans")
massFormatter.stringFromKilograms(5) // 5千克
In addition to formatting the numbers, HealthKit makes it much easier to manipulate these quantities. For example, we can convert between pounds and kilograms:
let pounds = HKUnit.poundUnit()
let kilograms = HKUnit.gramUnitWithMetricPrefix(HKMetricPrefix.Kilo)
var personWeight = HKQuantity(unit: pounds, doubleValue: 180)
personWeight.isCompatibleWithUnit(kilograms) // true
personWeight.doubleValueForUnit(kilograms) // 81.6466266
Lessons Learned
Any time you want to display numbers to humans, you are likely to need the number formatter. Putting dots, commas, spaces or other marks in the number to separate thousands, groups or decimals is hard work, so let the number formatter do it for you.
First, understand the context in which the numbers are used. Do they represent quantities with units, such as money, weight or height? Or are they unit-less quantities? This will dictate how you handle the numbers. You might need to use special classes that wrap those quantities.
Second, choose the right number formatter to display the number. Dealing with money? Make sure you use the currency style number formatter AND specify the correct currency. Dealing with physical units, like mass? Take advantage of the formatters that came with HealthKit and use the right units (e.g., pounds vs kilograms). These little steps will help you deliver a more delightful experience.