Weekly Swift articles, podcasts and tips by

# Formatting numbers in Swift

Published on 01 Nov 2020

A significant part of any given app’s logic is likely going to involve working with numbers in one way or another. Whether it’s in order to perform layout calculations, to schedule events using time intervals, or by dealing with our own, custom metrics, numbers really are everywhere.

While working with numbers is one of those things that computers are inherently good at, we also occasionally need to format and present some of our numbers in a human-readable way, which can often be tricker than expected. So this week, let’s explore that topic, and how different kinds of numbers might warrant different formatting strategies.

This ad keeps all of Swift by Sundell free for everyone. If you can, please check this sponsor out, as that directly helps support this site:

Architecting SwiftUI apps with MVC and MVVM: Although you can create an app simply by throwing some code together, without best practices and a robust architecture, you’ll soon end up with unmanageable spaghetti code. Learn how to create solid and maintainable apps with fewer bugs using this free guide.

## Solving the decimal problem

At the most basic level, creating a textual representation of a given number simply involves initializing a `String` with it, which can either be done directly, or by using a string literal:

``````let a = String(42) // "42"
let b = String(3.14) // "3.14"
let c = "\(42), \(3.14)" // "42, 3.14"``````

However, while that approach might work well for generating simpler descriptions of numbers that are under our complete control, we’ll likely going to need much more robust formatting strategies when dealing with dynamic numbers.

For example, here we’ve defined a `Metric` type that lets us associate a given `Double` with a name, which we then use when generating a custom `description` for such a value:

``````struct Metric: Codable {
var name: String
var value: Double
}

extension Metric: CustomStringConvertible {
var description: String {
"\(name): \(value)"
}
}``````

Since the above `Metric` type can contain any `Double` value, we probably want to format it in a more predictable way. For example, rather than simply converting its `Double` into a `String`, we could use a custom format to round it to two decimal places, which will make our output consistent regardless of how precise each underlying `Double` value actually is:

``````extension Metric: CustomStringConvertible {
var description: String {
let formattedValue = String(format: "%.2f", value)
return "\(name): \(formattedValue)"
}
}``````

However, we’ll now always output two decimal places, even when our `Double` is a whole number, or if it just has a single decimal place — which might not be what we were looking for. Take the number `42` for example. We probably don’t want it to be formatted as `42.00`, which is what will happen with our current implementation.

An initial idea on how to solve that problem might be to go for the manual approach, and trim all trailing zeros and decimal points from our formatted string before returning it — like this:

``````extension Metric: CustomStringConvertible {
var description: String {
var formattedValue = String(format: "%.2f", value)

while formattedValue.last == "0" {
formattedValue.removeLast()
}

if formattedValue.last == "." {
formattedValue.removeLast()
}

return "\(name): \(formattedValue)"
}
}``````

The above code certainly works, but it’s arguably not very elegant, and also makes the assumption that we’ll always format each of our numbers the same way for all users — which we might not actually want to do. Because it turns out that although mathematics might be a truly universal concept, the way people expect numbers to be represented in text varies quite a lot between different countries and locales.

## Using NumberFormatter

Instead, let’s use Foundation’s `NumberFormatter` to solve our decimal problem. Just like how a `DateFormatter` can be used to format `Date` values in various ways, the `NumberFormatter` class ships with a quite comprehensive suite of formatting tools that are all specific to numbers.

For example, using `NumberFormatter`, we can specify that we want to format each number as a decimal with a maximum of two fraction digits, which will give us our desired result without having to do any manual adjustments. Numbers like `42`, `42.1` and `42.12` will now all be rendered just like that, and any number that’s more precise will still be automatically rounded to two decimal points:

``````extension Metric: CustomStringConvertible {
var description: String {
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
formatter.maximumFractionDigits = 2

let number = NSNumber(value: value)
let formattedValue = formatter.string(from: number)!
return "\(name): \(formattedValue)"
}
}``````

We can safely force-unwrap the optional that `NumberFormatter` returns from the above call, since we’re in complete control over the `NSNumber` that is being passed into it.

Another major benefit of using `NumberFormatter` is that it’ll automatically take the user’s current `Locale` into account when formatting our numbers. For instance, in some countries the number `50932.52` is expected to be formatted as `50 932,52`, while other locales prefer `50,932.52` instead. All of those complexities are now handled for us completely automatically, which is most likely what we want when formatting user-facing numbers.

However, when that’s not the case, and we’re instead looking for consistency across all locales, then we could either assign a specific `Locale` to our `NumberFormatter`, or we could configure it to use specific characters as its `decimalSeparator` and `groupingSeparator` — like this:

``````extension Metric: CustomStringConvertible {
var description: String {
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
formatter.maximumFractionDigits = 2
formatter.decimalSeparator = "."
formatter.groupingSeparator = ""

...
}
}``````

Worth noting is that we can also produce localized numbers when using `String` directly, by passing a specific `locale` (or `.current`) to the `format`-based initializer that we used earlier.

In this case, let’s say that we do want our formatting to be localized. To finish our implementation, let’s move the creation of our `NumberFormatter` to a static property (which will let us reuse the same instance across all `Metric` values), and let’s also introduce a dedicated API for retrieving each formatted value by itself — like this:

``````extension Metric: CustomStringConvertible {
private static var valueFormatter: NumberFormatter = {
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
formatter.maximumFractionDigits = 2
return formatter
}()

var formattedValue: String {
let number = NSNumber(value: value)
return Self.valueFormatter.string(from: number)!
}

var description: String {
"\(name): \(formattedValue)"
}
}``````

So `NumberFormatter` is incredibly useful when we wish to format a raw numeric value into a human-readable description, but it can also do much more than just that. Let’s continue exploring!

## Domain-specific numbers

Depending on what kind of app that we’re working on, chances are that we’ll also have to deal with numbers that are domain-specific. That is, they represent something more than just a raw numeric value.

For example, let’s say that we’re working on a shopping app, and that we’re using a `Double` wrapped in a custom `Price` struct to describe a given product’s price:

``````struct Product: Codable {
var name: String
var price: Price
...
}

struct Price: Codable {
var amount: Double
var currency: Currency
}

enum Currency: String, Codable {
case eur
case usd
case sek
case pln
...
}``````

Now the question is, how to format such a `Price` instance in a way that makes sense to each of our users, regardless of which country that they’re in and what locale that they’re using?

This is another type of situation in which `NumberFormatter` can be incredibly useful, as it also includes full support for localized currency formatting. All that we have to do is to set its `numberStyle` to `currency` and give it the code of the currency that we’re using — like this:

``````extension Price: CustomStringConvertible {
var description: String {
let formatter = NumberFormatter()
formatter.numberStyle = .currency
formatter.currencyCode = currency.rawValue
formatter.maximumFractionDigits = 2

let number = NSNumber(value: amount)
return formatter.string(from: number)!
}
}``````

For example, here’s how a price of `3.14` in the Swedish currency SEK will be displayed in a few different locales when using the above approach:

• Sweden: `3,14 kr`
• Spain: `3.14 SEK`
• US: `SEK 3.14`
• France: `SEK 3,14`

Those might seem like minor differences in the grand scheme of things, but making the way we format prices and other numbers seem completely natural to each user can really make an app feel much more polished. Of course, the next step would be to also automatically convert each price into the current user’s own currency, but that’s definitely out of scope for this particular article.

Besides prices, another common category of numeric values that we likely want to localize is measurements. For example, let’s say that the imaginary shopping app that we were just working on has now pivoted into being solely focused on selling vehicles, and that we’ve converted our `Product` type into a more specific `Vehicle` model, which includes properties such as `topSpeed`:

``````struct Vehicle {
var name: String
var price: Price
var topSpeed: Double
...
}``````

Currently, our `topSpeed` property is once again a `Double`, and while that’s certainly a great choice for most raw numbers that should have floating-point precision, it’s actually not a great fit in this case — since our current implementation doesn’t tell us anything about what unit of measurement that our value is using. It could be kilometers per hour, miles per hour, meters per second, and so on.

Expressing that kind of unit-based numeric values is exactly what the built-in `Measurement` type is for, so let’s use that instead. In this case, we’ll specialize it with the phantom type `UnitSpeed`, which makes it crystal clear that our `topSpeed` value represents a measurement of speed:

``````struct Vehicle {
var name: String
var price: Price
var topSpeed: Measurement<UnitSpeed>
...
}``````

When creating instances of the above `Vehicle` type, we’ll now be required to always specify the underlying unit of measurement for our `topSpeed` property, which is a great thing, as that significantly educes the ambiguity of those values. But that’s just the beginning, because `Measurement` also has its very own formatter, which we can now use to easily generate formatted descriptions of each vehicle’s top speed:

``````extension Vehicle {
var formattedTopSpeed: String {
let formatter = MeasurementFormatter()
return formatter.string(from: topSpeed)
}
}``````

What’s really great is that not only will the above description be localized, `MeasurementFormatter` will also automatically convert each value into the unit preferred by the current user’s locale — which will be either `km/h` or `mph` in this case. Really cool!

However, there’s one thing that we need to keep in mind when using `Measurement` values, and that’s how they’re encoded and decoded by default. When using compiler-generated `Codable` conformances, each `Measurement` value is expected to be decoded from a dictionary containing several metadata properties that are probably not included in any JSON that we’re downloading from our app’s server. Instead, we most likely have an agreed-upon unit of measurement that our server is using, meaning that we’d have have to perform our decoding manually in this case — for example like this:

``````extension Vehicle: Codable {
private enum CodingKeys: CodingKey {
case name, price, topSpeed, ...
}

init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)

// Decoding all other properties
...

topSpeed = try Measurement(
value: container.decode(Double.self, forKey: .topSpeed),
unit: .kilometersPerHour
)
}

// Encoding implementation
...
}``````

Since custom `Codable` implementations are often quite cumbersome to maintain, let’s also explore an alternative approach. Here’s how we could create a dedicated property wrapper that lets us encapsulate the conversions between `Double` and `Measurement` within a single type:

``````@propertyWrapper
struct KilometersPerHour {
var wrappedValue: Measurement<UnitSpeed>
}

extension KilometersPerHour: Codable {
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let rawValue = try container.decode(Double.self)

wrappedValue = Measurement(
value: rawValue,
unit: .kilometersPerHour
)
}

func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(wrappedValue.value)
}
}``````

A major benefit of the above approach is that, unless we really need `Vehicle` to use a custom `Codable` implementation, we can now simply mark our `topSpeed` property with `@KilometersPerHour` and we’ll once again be able to let the compiler generate all of that code for us:

``````struct Vehicle: Codable {
var name: String
var price: Price
@KilometersPerHour var topSpeed: Measurement<UnitSpeed>
...
}``````

For more on using property wrappers to customize `Codable` on a per-property basis, check out “Annotating properties with default decoding values”.

With the above in place, we’ll now get all of the advantages of using `Measurement` — from the additional type safety, to the built-in formatting and conversion features — while still being able to use `Double` values when encoding and decoding our models.

Support Swift by Sundell by checking out this sponsor:

Architecting SwiftUI apps with MVC and MVVM: Although you can create an app simply by throwing some code together, without best practices and a robust architecture, you’ll soon end up with unmanageable spaghetti code. Learn how to create solid and maintainable apps with fewer bugs using this free guide.

## Conclusion

The task of formatting numbers into human-readable strings is most likely something that we want to delegate to the system as much as possible, especially when we wish to produce descriptions that are localized and otherwise adapted to the current user’s locale. Because simply turning a `Double` into a `String` might be a trivial task, but actually formatting each value into a correct string is often much more difficult than what it initially might seem like.

Got questions, comments or feedback? I’d love to hear from you, either via Twitter or email. And if you enjoyed this article, please take a moment to check out the above sponsor, as that really helps support my work financially.