- Published on
Swift Number Formatting
- Authors
- Name
- Wayne Dahlberg
- @waynedahlberg
Numbers are everywhere. They come in many different flavors. Currency, dates, scientific, etc. Computers are excellent and reading and interpreting the way numbers are expressed, humans less so. That's why we need to take the numbers that a computer understands and package them up in a way that makes sense to an average user.
This is a another post in a series intended as a personal growth exercise. As I learn and digest new things, I want to write about them to solidify my understanding.
Presention
The simplest way of representing a readable number is by using a String
.
let x = "5"
let y = "5.11"
let z = "\(5), \(5.11)"
Okay, that works but it's not very usable. We need a more full-featured approach dealing with numbers we can control. Let's define a Capacity
type that lets create a Double
with a name, which we can present as a description
.
struct Capacity: Codable {
var name: String
var value: Double
}
extension Capacity: CustomStringConvertbile {
var description: String {
"\(name): \(value)"
}
}
Now we're getting somewhere. But we still need to use the Double
in a way that is more presentable. We need the decimal for precision, but we need a cutoff. Let's use 2 decimal places.
extension Capacity: CustomStringConvertbile {
var description: String {
let formattedValue = String(format: "%.2f", value)
return "\(name): \(formattedValue)"
}
}
This will always output 2 decimal places, even when the value is 2.00. Again, we can dive a little deeper with NumberFormatter
.
NumberFormatter
Let's format our number to show decimals when it has a decimal value, and remove the "." and trailing "00" when it's a whole number equivalent. NumberFormatter
allows us to present a decimal with a max number of trailing digits. This will give us 81
, 81.8
, and 81.83
presented just like that. The precision level will included the necessary trailing digits up to a given maximum, and then simply rounded off.
extension Capacity: CustomStringConvertbile {
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)"
}
When using NumberFormatter
you can take into account the user's Locale
in order to present numbers with more context. In the US, we see the number 12345.67
while the same number viewed on a device in Europe will look like 12 345,67
. Other locales may present it as 12,345.67
. We get all that beautiful regional formatting for free when using NumberFormatter
.
Domain specificity
Depending on our needs, we'll need to deal with domain-specific that hold additional features. If we're working on a Restaurant application and dealing with Double
, we can wrap the value in a Price
struct to better describe the cost of items on the menu.
struct MenuItem: 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 mx
case cad
}
extension Price: CustomStringConvertbile {
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)!
}
}
With the above Price
type fully defined, let's look at the number 8.99
as defined by the type in different locales.
- Sweden:
8,99 usd
- Spain:
8.99 USD
- US:
USD 8.99
- France:
USD 8,99
Even though the differences are slight, it can be very important when creating a storefront that works as desired in many different locales.
Formatting numbers as human-readable strings is something we want to rely on the system for, especially when dealing with user locale. Casting a Double
as a String
may seem trivial, but formatting the value into the correct string is much more difficult than it appears.