Formatter
Conversion is a tireless errand in software development. Most programs boil down to some variation of transforming data into something more useful.
In the case of user-facing software, making data human-readable is an essential task — and a complex one at that. A user’s preferred language, calendar, and currency can all factor into how information should be displayed, as can other constraints, such as a label’s dimensions.
All of this is to say:
calling description
on an object just doesn’t cut it
under most circumstances.
Indeed,
the real tool for this job is Formatter
:
an ancient, abstract class deep in the heart of the Foundation framework
that’s responsible for transforming data into textual representations.
Formatter
’s origins trace back to NSCell
,
which is used to display information and accept user input in
tables, form fields, and other views in AppKit.
Much of the API design of (NS)Formatter
reflects this.
Back then, formatters came in two flavors: dates and numbers. But these days, there are formatters for everything from physical quantities and time intervals to personal names and postal addresses. And as if that weren’t enough to keep straight, a good portion of these have been soft-deprecated, or otherwise superseded by more capable APIs (that are also formatters).
To make sense of everything, this week’s article groups each of the built-in formatters into one of four categories:
- Numbers and Quantities
Number
Formatter Measurement
Formatter - Dates, Times, and Durations
Date
Formatter ISO8601Date
Formatter Date
Components Formatter Date
Interval Formatter Relative
Date Time Formatter - People and Places
Person
Name Components Formatter CNPostal
Address Formatter - Lists and Items
List
Formatter
Formatting Numbers and Quantities
Class | Example Output | Availability |
---|---|---|
Number |
“1,234.56” | iOS 2.0 macOS 10.0+ |
Measurement |
“-9.80665 m/s²” | iOS 10.0+ macOS 10.12+ |
Byte |
“756 KB” | iOS 6.0+ macOS 10.8+ |
Energy |
“80 kcal” | iOS 8.0+ macOS 10.10+ |
Mass |
“175 lb” | iOS 8.0+ macOS 10.10+ |
Length |
“5 ft, 11 in” | iOS 8.0+ macOS 10.10+ |
MKDistance |
“500 miles” | iOS 7.0+ macOS 10.9+ |
NumberFormatter
Number
covers every aspect of number formatting imaginable.
For better or for worse
(mostly for better),
this all-in-one API handles
ordinals and cardinals,
mathematical and scientific notation,
percentages,
and monetary amounts in various flavors.
It can even write out numbers in a few different languages!
So whenever you reach for Number
,
the first order of business is to establish
what kind of number you’re working with
and set the number
property accordingly.
Number Styles
Number Style | Example Output |
---|---|
none |
123 |
decimal |
123.456 |
percent |
12% |
scientific |
1.23456789E4 |
spell |
one hundred twenty-three |
ordinal |
3rd |
currency |
$1234.57 |
currency |
($1234.57) |
currency |
USD1,234.57 |
currency |
1,234.57 US dollars |
Rounding & Significant Digits
To prevent numbers from getting annoyingly pedantic
(“thirty-two point three three — repeating, of course…”),
you’ll want to get a handle on Number
’s rounding behavior.
Here, you have two options:
- Set
uses
toSignificant Digits true
to format according to the rules of significant figures
var formatter = Number Formatter()
formatter.uses Significant Digits = true
formatter.minimum Significant Digits = 1 // default
formatter.maximum Significant Digits = 6 // default
formatter.string(from: 1234567) // 1234570
formatter.string(from: 1234.567) // 1234.57
formatter.string(from: 100.234567) // 100.235
formatter.string(from: 1.23000) // 1.23
formatter.string(from: 0.0000123) // 0.0000123
- Set
uses
toSignificant Digits false
(or keep as-is, since that’s the default) to format according to specific limits on how many decimal and fraction digits to show (the number of digits leading or trailing the decimal point, respectively).
var formatter = Number Formatter()
formatter.uses Significant Digits = false
formatter.minimum Integer Digits = 0 // default
formatter.maximum Integer Digits = 42 // default (seriously)
formatter.minimum Fraction Digits = 0 // default
formatter.maximum Fraction Digits = 0 // default
formatter.string(from: 1234567) // 1234567
formatter.string(from: 1234.567) // 1235
formatter.string(from: 100.234567) // 100
formatter.string(from: 1.23000) // 1
formatter.string(from: 0.0000123) // 0
If you need specific rounding behavior,
such as “round to the nearest integer” or “round towards zero”,
check out the
rounding
,
rounding
, and
rounding
properties.
Locale Awareness
Nearly everything about the formatter can be customized, including the grouping separator, decimal separator, negative symbol, percent symbol, infinity symbol, and how to represent zero values.
Although these settings can be overridden on an individual basis, it’s typically best to defer to the defaults provided by the user’s locale.
MeasurementFormatter
Measurement
was introduced in iOS 10 and macOS 10.12
as part of the full complement of APIs for performing
type-safe dimensional calculations:
-
Unit
subclasses represent units of measure, such as count and ratio -
Dimension
subclasses represent dimensional units of measure, such as mass and length, (which is the case for the overwhelming majority of the concrete subclasses provided, on account of them being dimensional in nature) - A
Measurement
is a quantity of a particularUnit
- A
Unit
converts quantities of one unit to a different, compatible unitConverter
For the curious, here's the complete list of units supported by Measurement Formatter
:
Measure | Unit Subclass | Base Unit |
---|---|---|
Acceleration | Unit |
meters per second squared (m/s²) |
Planar angle and rotation | Unit |
degrees (°) |
Area | Unit |
square meters (m²) |
Concentration of mass | Unit |
milligrams per deciliter (mg/dL) |
Dispersion | Unit |
parts per million (ppm) |
Duration | Unit |
seconds (sec) |
Electric charge | Unit |
coulombs (C) |
Electric current | Unit |
amperes (A) |
Electric potential difference | Unit |
volts (V) |
Electric resistance | Unit |
ohms (Ω) |
Energy | Unit |
joules (J) |
Frequency | Unit |
hertz (Hz) |
Fuel consumption | Unit |
liters per 100 kilometers (L/100km) |
Illuminance | Unit |
lux (lx) |
Information Storage | Unit |
Byte* (byte) |
Length | Unit |
meters (m) |
Mass | Unit |
kilograms (kg) |
Power | Unit |
watts (W) |
Pressure | Unit |
newtons per square meter (N/m²) |
Speed | Unit |
meters per second (m/s) |
Temperature | Unit |
kelvin (K) |
Volume | Unit |
liters (L) |
Follows ISO/IEC 80000-13 standard; one byte is 8 bits, 1 kilobyte = 1000¹ bytes
Measurement
and its associated APIs are a intuitive —
just a delight to work with, honestly.
The only potential snag for newcomers to Swift
(or Objective-C old-timers, perhaps)
are the use of generics to constrain Measurement
values
to a particular Unit
type.
import Foundation
// "The swift (Apus apus) can power itself to a speed of 111.6km/h"
let speed = Measurement<Unit Speed>(value: 111.6,
unit: .kilometers Per Hour)
let formatter = Measurement Formatter()
formatter.string(from: speed) // 69.345 mph
Configuring the Underlying Number Formatter
By delegating much of its formatting responsibility to
an underlying Number
property,
Measurement
maintains a high degree of configurability
while keeping a small API footprint.
Readers with an engineering background may have noticed that
the localized speed in the previous example
gained an extra significant figure along the way.
As discussed previously,
we can enable uses
and set maximum
to prevent incidental changes in precision.
formatter.number Formatter.uses Significant Digits = true
formatter.number Formatter.maximum Significant Digits = 4
formatter.string(from: speed) // 69.35 mph
Changing Which Unit is Displayed
A Measurement
,
by default,
will use the preferred unit for the user’s current locale (if one exists)
instead of the one provided by a Measurement
value.
Readers with a non-American background certainly noticed that
the localized speed in the original example
converted to a bizarre, archaic unit of measure known as “miles per hour”.
You can override this default unit localization behavior
by passing the provided
option.
formatter.unit Options = [.provided Unit]
formatter.string(from: speed) // 111.6 km/h
formatter.string(from: speed.converted(to: .miles Per Hour)) // 69.35 mph
Formatting Dates, Times, and Durations
Class | Example Output | Availability |
---|---|---|
Date |
“July 15, 2019” | iOS 2.0 macOS 10.0+ |
ISO8601Date |
“2019-07-15” | iOS 10.0+ macOS 10.12+ |
Date |
“10 minutes” | iOS 8.0 macOS 10.10+ |
Date |
“6/3/19 - 6/7/19” | iOS 8.0 macOS 10.10+ |
Relative |
“3 weeks ago” | iOS 13.0+ macOS 10.15 |
DateFormatter
Date
is the OG class
for representing dates and times.
And it remains your best, first choice
for the majority of date formatting tasks.
For a while,
there was a concern that it would become overburdened with responsibilities
like its sibling Number
.
But fortunately,
recent SDK releases spawned new formatters for new functionality.
We’ll talk about those in a little bit.
Date and Time Styles
The most important properties for a Date
object are its
date
and time
.
As with Number
and its number
,
these date and time styles provide preset configurations
for common formats.
Style | Date | Time |
---|---|---|
none |
“” | “” |
short |
“11/16/37” | “3:30 PM” |
medium |
“Nov 16, 1937” | “3:30:32 PM” |
long |
“November 16, 1937” | “3:30:32 PM” |
full |
“Tuesday, November 16, 1937 AD | “3:30:42 PM EST” |
let date = Date()
let formatter = Date Formatter()
formatter.date Style = .long
formatter.time Style = .long
formatter.string(from: date)
// July 15, 2019 at 9:41:00 AM PST
formatter.date Style = .short
formatter.time Style = .short
formatter.string(from: date)
// "7/16/19, 9:41:00 AM"
NSDate Formatter *formatter = [[NSDate Formatter alloc] init];
[formatter set Date Style:NSDate Formatter Long Style];
[formatter set Time Style:NSDate Formatter Long Style];
NSLog(@"%@", [formatter string From Date:[NSDate date]]);
// July 15, 2019 at 9:41:00 AM PST
[formatter set Date Style:NSDate Formatter Short Style];
[formatter set Time Style:NSDate Formatter Short Style];
NSLog(@"%@", [formatter string From Date:[NSDate date]]);
// 7/16/19, 9:41:00 AM
date
and time
are set independently.
So,
to display just the time for a particular date,
for example,
you set date
to none
:
let formatter = Date Formatter()
formatter.date Style = .none
formatter.time Style = .medium
let string = formatter.string(from: Date())
// 9:41:00 AM
NSDate Formatter *formatter = [[NSDate Formatter alloc] init];
[formatter set Date Style:NSDate Formatter No Style];
[formatter set Time Style:NSDate Formatter Medium Style];
NSLog(@"%@", [formatter string From Date:[NSDate date]]);
// 9:41:00 AM
As you might expect, each aspect of the date format can alternatively be configured individually, a la carte. For any aspiring time wizards NSDate
has a bevy of different knobs and switches to play with.
ISO8601DateFormatter
When we wrote our first article about NSFormatter
back in 2013,
we made a point to include discussion of
Peter Hosey’s ISO8601DateFormatter’s
as the essential open-source library
for parsing timestamps from external data sources.
Fortunately,
we no longer need to proffer a third-party solution,
because, as of iOS 10.0 and macOS 10.12,
ISO8601Date
is now built-in to Foundation.
let formatter = ISO8601Date Formatter()
formatter.date(from: "2019-07-15T09:41:00-07:00")
// Jul 15, 2019 at 9:41 AM
DateIntervalFormatter
Date
is like Date
,
but can handle two dates at once —
specifically, a start and end date.
let formatter = Date Interval Formatter()
formatter.date Style = .short
formatter.time Style = .none
let from Date = Date()
let to Date = Calendar.current.date(by Adding: .day, value: 7, to: from Date)!
formatter.string(from: from Date, to: to Date)
// "7/15/19 – 7/22/19"
NSDate Interval Formatter *formatter = [[NSDate Interval Formatter alloc] init];
formatter.date Style = NSDate Interval Formatter Short Style;
formatter.time Style = NSDate Interval Formatter No Style;
NSDate *from Date = [NSDate date];
NSDate *to Date = [from Date date By Adding Time Interval:86400 * 7];
NSLog(@"%@", [formatter string From Date:from Date to Date:to Date]);
// "7/15/19 – 7/22/19"
Date Interval Styles
Style | Date | Time |
---|---|---|
none |
“” | “” |
short |
“6/30/14 - 7/11/14” | “5:51 AM - 7:37 PM” |
medium |
“Jun 30, 2014 - Jul 11, 2014” | “5:51:49 AM - 7:38:29 PM” |
long |
“June 30, 2014 - July 11, 2014” | “6:02:54 AM GMT-8 - 7:49:34 PM GMT-8” |
full |
“Monday, June 30, 2014 - Friday, July 11, 2014 | “6:03:28 PM Pacific Standard Time - 7:50:08 PM Pacific Standard Time” |
DateComponentsFormatter
As the name implies,
Date
works with Date
values
(previously),
which contain a combination of discrete calendar quantities,
such as “1 day and 2 hours”.
Date
provides localized representations of date components
in several different, pre-set formats:
let formatter = Date Components Formatter()
formatter.units Style = .full
let components = Date Components(day: 1, hour: 2)
let string = formatter.string(from: components)
// 1 day, 2 hours
NSDate Components Formatter *formatter = [[NSDate Components Formatter alloc] init];
formatter.units Style = NSDate Components Formatter Units Style Full;
NSDate Components *components = [[NSDate Components alloc] init];
components.day = 1;
components.hour = 2;
NSLog(@"%@", [formatter string From Date Components:components]);
// 1 day, 2 hours
Date Components Unit Styles
Style | Example |
---|---|
positional |
“1:10” |
abbreviated |
“1h 10m” |
short |
“1hr 10min” |
full |
“1 hour, 10 minutes” |
spell |
“One hour, ten minutes” |
Formatting Context
Some years ago,
formatters introduced the concept of formatting context,
to handle situations where
the capitalization and punctuation of a localized string may depend on whether
it appears at the beginning or middle of a sentence.
A context
property is available for Date
,
as well as Date
, Number
, and others.
Formatting Context | Output |
---|---|
standalone |
“About 2 hours” |
list |
“About 2 hours” |
beginning |
“About 2 hours” |
middle |
“about 2 hours” |
dynamic |
Depends* |
*
A Dynamic
context changes capitalization automatically
depending on where it appears in the text
for locales that may position strings differently
depending on the content.
RelativeDateTimeFormatter
Relative
is a newcomer in iOS 13 —
and at the time of writing, still undocumented,
so consider this an NSHipster exclusive scoop!
Longtime readers may recall that
Date
actually gave this a try circa iOS 4
by way of the does
property.
But that hardly ever worked,
and most of us forgot about it, probably.
Fortunately,
Relative
succeeds
where does
fell short,
and offers some great new functionality to make your app
more personable and accessible.
(As far as we can tell,)
Relative
takes the most significant date component
and displays it in terms of past or future tense
(“1 day ago” / “in 1 day”).
let formatter = Relative Date Time Formatter()
formatter.localized String(from: Date Components(day: 1, hour: 1)) // "in 1 day"
formatter.localized String(from: Date Components(day: -1)) // "1 day ago"
formatter.localized String(from: Date Components(hour: 3)) // "in 3 hours"
formatter.localized String(from: Date Components(minute: 60)) // "in 60 minutes"
For the most part,
this seems to work really well.
However, its handling of nil
, zero, and net-zero values
leaves something to be desired…
formatter.localized String(from: Date Components(hour: 0)) // "in 0 hours"
formatter.localized String(from: Date Components(day: 1, hour: -24)) // "in 1 day"
formatter.localized String(from: Date Components()) // ""
Styles
Style | Example |
---|---|
abbreviated |
“1 mo. ago” * |
short |
“1 mo. ago” |
full |
“1 month ago” |
spell |
“one month ago” |
*May produce output distinct from short
for non-English locales.
Using Named Relative Date Times
By default,
Relative
adopts the formulaic convention
we’ve seen so far.
But you can set the date
property to .named
to prefer localized deictic expressions —
“tomorrow”, “yesterday”, “next week” —
whenever one exists.
import Foundation
let formatter = Relative Date Time Formatter()
formatter.localized String(from: Date Components(day: -1)) // "1 day ago"
formatter.date Time Style = .named
formatter.localized String(from: Date Components(day: -1)) // "yesterday"
This just goes to show that
beyond calendrical and temporal relativity,
Relative
is a real whiz at linguistic relativity, too!
For example,
English doesn’t have a word to describe the day before yesterday,
whereas other languages, like German, do.
formatter.localized String(from: Date Components(day: -2)) // "2 days ago"
formatter.locale = Locale(identifier: "de_DE")
formatter.localized String(from: Date Components(day: -2)) // "vorgestern"
Hervorragend!
Formatting People and Places
Class | Example Output | Availability |
---|---|---|
Person |
“J. Appleseed” | iOS 9.0+ macOS 10.11+ |
CNContact |
“Appleseed, Johnny” | iOS 9.0+ macOS 10.11+ |
CNPostal |
“1 Infinite Loop\n Cupertino CA 95014” |
iOS 9.0+ macOS 10.11+ |
PersonNameComponentsFormatter
Person
is a sort of high water mark for Foundation.
It encapsulates one of the hardest,
most personal problems in computer
in such a way to make it accessible to anyone
without requiring a degree in Ethnography.
The documentation does a wonderful job illustrating the complexities of personal names (if I might say so myself), but if you had any doubt of the utility of such an API, consider the following example:
let formatter = Person Name Components Formatter()
var name Components = Person Name Components()
name Components.given Name = "Johnny"
name Components.family Name = "Appleseed"
formatter.string(from: name Components) // "Johnny Appleseed"
Simple enough, right? We all know names are space delimited, first-last… right?
name Components.given Name = "约翰尼"
name Components.family Name = "苹果籽"
formatter.string(from: name Components) // "苹果籽约翰尼"
’nuf said.
CNPostalAddressFormatter
CNPostal
provides a convenient Formatter
-based API
to functionality dating back to the original AddressBook framework.
The following example formats a constructed CNMutable
,
but you’ll most likely use existing CNPostal
values
retrieved from the user’s address book.
let address = CNMutable Postal Address()
address.street = "One Apple Park Way"
address.city = "Cupertino"
address.state = "CA"
address.postal Code = "95014"
let address Formatter = CNPostal Address Formatter()
address Formatter.string(from: address)
/* "One Apple Park Way
Cupertino CA 95014" */
Styling Formatted Attributed Strings
When formatting compound values, it can be hard to figure out where each component went in the final, resulting string. This can be a problem when you want to, for example, call out certain parts in the UI.
Rather than hacking together an ad-hoc,
regex-based solution,
CNPostal
provides a method that vends an
NSAttributed
that lets you identify
the ranges of each component
(Person
does this too).
The NSAttributed
API is…
to put it politely,
bad.
It feels bad to use.
So for the sake of anyone hoping to take advantage of this functionality, please copy-paste and appropriate the following code sample to your heart’s content:
var attributed String = address Formatter.attributed String(
from: address,
with Default Attributes: [:]
).mutable Copy() as! NSMutable Attributed String
let string Range = NSRange(location: 0, length: attributed String.length)
attributed String.enumerate Attributes(in: string Range, options: []) { (attributes, attributes Range, _) in
let color: UIColor
switch attributes[NSAttributed String.Key(CNPostal Address Property Attribute)] as? String {
case CNPostal Address Street Key:
color = .red
case CNPostal Address City Key:
color = .orange
case CNPostal Address State Key:
color = .green
case CNPostal Address Postal Code Key:
color = .purple
default:
return
}
attributed String.add Attribute(.foreground Color,
value: color,
range: attributes Range)
}
Formatting Lists and Items
Class | Example Output | Availability |
---|---|---|
List |
“macOS, iOS, iPadOS, watchOS, and tvOS” | iOS 13.0+ macOS 10.15+ |
ListFormatter
Rounding out our survey of formatters in the Apple SDK,
it’s another new addition in iOS 13:
List
.
To be completely honest,
we didn’t know where to put this in the article,
so we just kind of stuck it on the end here.
(Though in hindsight,
this is perhaps appropriate given the subject matter).
Once again, we don’t have any official documentation to work from at the moment, but the comments in the header file give us enough to go on.
NSListFormatter provides locale-correct formatting of a list of items using the appropriate separator and conjunction. Note that the list formatter is unaware of the context where the joined string will be used, e.g., in the beginning of the sentence or used as a standalone string in the UI, so it will not provide any sort of capitalization customization on the given items, but merely join them as-is.
The string joined this way may not be grammatically correct when placed in a sentence, and it should only be used in a standalone manner.
tl;dr:
This is joined(by:)
with locale-aware serial and penultimate delimiters.
For simple lists of strings,
you don’t even need to bother with instantiating List
—
just call the localized
class method.
import Foundation
let operating Systems = ["mac OS", "i OS", "i Pad OS", "watch OS", "tv OS"]
List Formatter.localized String(by Joining: operating Systems)
// "mac OS, i OS, i Pad OS, watch OS, and tv OS"
List
works as you’d expect
for lists comprising zero, one, or two items.
List Formatter.localized String(by Joining: [])
// ""
List Formatter.localized String(by Joining: ["Apple"])
// "Apple"
List Formatter.localized String(by Joining: ["Jobs", "Woz"])
// "Jobs and Woz"
Lists of Formatted Values
List
exposes an underlying item
property,
which effectively adds a map(_:)
before calling joined(by:)
.
You use item
whenever you’d formatting a list of non-String elements.
For example,
you can set a Number
as the item
for a List
to turn an array of cardinals (Int
values)
into a localized list of ordinals.
let number Formatter = Number Formatter()
number Formatter.number Style = .ordinal
let list Formatter = List Formatter()
list Formatter.item Formatter = number Formatter
list Formatter.string(from: [1, 2, 3])
// "1st, 2nd, and 3rd"
As some of the oldest members of the Foundation framework,
NSNumber
and NSDate
are astonishingly well-suited to their respective domains,
in that way only decade-old software can.
This tradition of excellence is carried by the most recent incarnations as well.
If your app deals in numbers or dates
(or time intervals or names or lists measurements of any kind),
then NSFormatter
is indispensable.
And if your app doesn’t… then the question is, what does it do, exactly?
Invest in learning all of the secrets of Foundation formatters
to get everything exactly how you want them.
And if you find yourself with formatting logic scattered across your app,
consider creating your own Formatter
subclass
to consolidate all of that business logic in one place.