Using MeasurementFormatter in Swift
Understanding MeasurementFormatter
The MeasurementFormatter
class provides formatted and localized representations of units and measurements. When catering to a global audience, you should present this data in local units. Suppose your app shows the end user a distance between two points. Assume that the distance is in imperial units: feet, yards and miles. When catering to a global audience, you should present this data in local units. Consider the following:
The distance between New York and Paris is 3,626.81 miles
In French, you would want to not only translate the string’s text, but also the measurement units therein contained:
La distance entre New York et Paris est de 5 836,78 km
Instead of attempting to write your own utility classes to perform these conversions, you should leverage the power of Apple’s Foundation.
Up and Running
When dealing with distance measurements, MeasurementFormatter
does great work with zero configuration. By the time the user installs your app, the user’s device has a default locale. Working with the distance measurement above, we can convert to the user’s localized standard:
// distance in user's default locale
// this would print with different formats in various locales:
// 3626.81 miles for en_US
// 5.836,77 km for en_IT
// 5 836,77 km for en_FR
// 5,836.77 km for en_JP
// To find out your locale: NSLocale.current
import Foundation
let distanceInMiles = Measurement(value: 3626.81, unit: UnitLength.miles)
MeasurementFormatter().string(from: distanceInMiles)
Given the simplicity, it becomes clear that using MeasurementFormatter
is the way to go. As a developer, you should get into a habit of always using Foundation formatters to present data. This will lead to future-proof code and less refactors.
Understanding Locales
Locales in programming describe linguistic, cultural, and technological conventions and standards. Most units of measure fall into one of three systems: British imperial, US customary, or Metric (aka: SI system). Examples of information encapsulated by a locale may include:
- Symbols used in numbers and currency, for example:
- 1,200.99 and $1,200.99 in US
- 1 200,99 and €1.234.567,89 in FR
- Dates formatting, for example:
- Order of date: MM/DD/YYYY vs DD/MM/YYYY
- Presentation of date: November 23, 2016 or 11.23.2016
- Naming of international holidays
- Units of measure, for example:
- Imperial vs Metric measurements
- The spelling of such (meters, metres, etc)
- Abbreviations like “cal” vs “C” for calories
While we may be used to our own set of measure and symbols, many regions around the world are accustomed to their own. Spelling, abbreviations and symbols vary by country and language. This makes for a very complex set of rules and conversions. Here is just a quick sample of some of the variations in units which Foundation typically handles:
- Naming and spelling of units
- e.g.: meters, metros, mètres, metri, etc
- Size of units
- e.g.: US gallon (.26 liters), Imperial Gallon (.22 liters)
- Abbreviations of units
- e.g.: cal, kcal, Cal or C
- Natural sizes
- e.g.: feet vs miles, meters vs kilometers
You can also set locale and store the user’s preference. Here is an example of how you might use this in the real world, building on the code from above:
import Foundation
// Create a local variable instance of MeasurementFormatter which we can reuse and configure
let formatter = MeasurementFormatter()
let distanceInMiles = Measurement(value: 3626.81, unit: UnitLength.miles)
// You should collect this for UIPickerView or similar input
// and replace "en_FR" with the result of the picker
// This will save the string to the "locale" key in device storage
UserDefaults.standard.set("en_FR", forKey: "locale")
// From this point on, we want to refer to the saved preference
// If the User default is nil, we failover to "en_FR"
let localeIdentifier = UserDefaults.standard.object(forKey: "locale") ?? "en_FR"
// Last we initiate our Locale class with this identifier
let locale = Locale(identifier: localeIdentifier as! String)
// Optionally set the `locale` property of our instance.
// If we do not set this, it will default to the user's device locale
formatter.locale = locale
formatter.string(from: distanceInMiles) // prints "5 836,77 km"
Your application should allow the user to globally set their localization preferences. This especially important when working with data. Developers frequently rely on the user’s device setting but this can cause problems. For example: imagine an US expat living in France. Their device may be set to a French locale as they may know the language. This can still be problematic, as they may not have a grasp on metric measurements over imperial. It is a welcome feature to have localization preferences on a per-application basis.
Unit Style
The unitStyle
property of MeasurementFormatter
is the most basic of them all. This property expects a case from the UnitStyle
enum which provides the following:
.short
- Formats the unit of measure as the shortest abbreviated version or symbol.
- e.g.: 10 feet would be formatted as 10’
.medium
- This is the default value if none is specified.
- Formats the unit of measure as an abbreviation
- e.g.: 10 feet would be formatted as 10 ft
.long
- Formats the unit of measure in a fully written form.
- e.g.: 10 feet would be formatted as 10 feet
import Foundation
// You can optionally set the `unitStyle` property
// Expects enum case from `MeasurementFormatter.UnitStyle`
// Defaults to `.medium`
// You can simply pass the case value: `.long`
// or you can write it out as shown below
let formatter = MeasurementFormatter()
let distanceInMiles = Measurement(value: 3626.81, unit: UnitLength.miles)
formatter.unitStyle = MeasurementFormatter.UnitStyle.long
formatter.string(from: distanceInMiles) // 3,626.81 miles
Unit Options
The unitOptions
property is like the unitStyle
property. It also expects an enum case and the values are:
.providedUnit
– displays the unit of measure provided and does not perform conversions..naturalScale
– does some magic for us. Apple mapped out the typical units of measure to more human readable values. For example, a person would not describe 10 feet as 0.00189394 miles. Thus, the naturalScale property is usually appropriate to convert this measurement..temperatureWithoutUnit
– is pretty self-explanatory. At times you may want to use or display a temperature without a unit of measure. This is set apart from the others as conversions of temperature tend to remain the same or be custom.
let formatter = MeasurementFormatter()
// Working with meters and evaluating the results
let distanceInMeters = Measurement(value: 2, unit: UnitLength.meters)
formatter.string(from: distanceInMeters) // prints "0,002 kilometres"
// However, we can see the class is aware of the original unit of measure
formatter.string(from: UnitLength.meters) // prints "metres"
// Optionally set the `unitOptions` property
// This can be: .providedUnit, .naturalScale, or .temperatureWithoutUnit
// Options provided by MeasurementFormatter.UnitOptions
// This defaults to a privately defined format if unset
formatter.unitOptions = .naturalScale
formatter.string(from: distanceInMeters) // prints "2 metres"
Formatting Numbers
When working with measure, it is not uncommon to change the formatting of the numbers. The numberFormatter property exists for this exact reason. A similar class to MeasurementFormatter
is the NumberFormatter
class. They work side-by-side to give you more granular control of string output. We can instantiate and configure the NumberFormatter
and then set our MeasuremFormatter.numberFormatter
to our NumberFormatter
instance:
import Foundation
let formatter = MeasurementFormatter()
formatter.unitOptions = .naturalScale
formatter.unitStyle = .long
formatter.locale = Locale(identifier: "en_FR")
let distanceInMeters = Measurement(value: 2, unit: UnitLength.meters) // 2.0 m
// Optionally set the `numberFormatter` property
// Expects an instance of `NumberFormatter`
// You could set the `NumberFormatter.numberStyle` property to:
// .none
// .decimal
// .currency
// .percent
// .scientific
// .spellOut
// Defaults to .decimal if unset
let numberFormatter = NumberFormatter()
numberFormatter.numberStyle = .spellOut
formatter.numberFormatter = numberFormatter
formatter.string(from: distanceInMeters) // two meters
This gives us a granular control for a wide variety of applications.
Nuances and Gotchya’s
Measurement conversion is simple arithmetic, but presentation is not. The core libraries will not always follow conventions that you may expect. The underlying documentation and Swift source is written by developers in many different countries. As such, many of the comments and outputs seem to have small inconsistencies.
Inconsistency in unit names
You may find “metres” written as “meters” despite the locale remaining the same. I believe this is simply oversight, and some community contributors have opened radars to fix these issues in future versions of Swift.
Natural Scale is not perfect
You may find the naturalScale
property converting outside of ranges you would expect. In my own development, I have found inches converting to feet vice versa at times I did not expect. However, despite these nuances, I would trust Apple 99% of the time rather than dealing with edge cases. These libraries continue to evolve and improve with each release.
Wrapping Up
The MeasurementFormatter
class is one of the more straightforward utility classes in the Foundation framework. Despite being a more basic class, it is a powerful one and very simple one to learn.