layout | title | category | rating | description | created | updated |
---|---|---|---|---|---|---|
post |
NSFormatter |
Foundation |
8.0 |
Conversion is the tireless errand of software development. Most programming tasks boil down to some variation of transforming data into something more useful. |
2013-11-11 |
2014-06-30 |
Conversion is the tireless errand of software development. Most programming tasks boil down to some variation of transforming data into something more useful.
In the case of user-facing software, converting data into human-readable form is an essential task, and a complex one at that. A user's preferred language, locale, calendar, or 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 that sending -description
to an object just isn't going to cut it in most circumstances. Even +stringWithFormat:
is going to ultimately disappoint. No, the real tool for this job is NSFormatter
.
NSFormatter
is an abstract class for transforming data into a textual representation. It can also interpret valid textual representations back into data.
Its 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 NSFormatter
reflects this.
Foundation provides a number of concrete subclasses for NSFormatter
(in addition to a single NSFormatter
subclass provided in the MapKit framework):
Class | Availability |
---|---|
NSNumberFormatter |
iOS 2.0 / Mac OS X 10.0 |
NSDateFormatter |
iOS 2.0 / Mac OS X 10.0 |
NSByteCountFormatter |
iOS 6.0 / Mac OS X 10.8 |
NSDateComponentsFormatter |
iOS 8.0 / Mac OS X 10.10 |
NSDateIntervalFormatter |
iOS 8.0 / Mac OS X 10.10 |
NSEnergyFormatter |
iOS 8.0 / Mac OS X 10.10 |
NSMassFormatter |
iOS 8.0 / Mac OS X 10.10 |
NSLengthFormatter |
iOS 8.0 / Mac OS X 10.10 |
MKDistanceFormatter |
iOS 7.0 / Mac OS X 10.9 |
As some of the oldest members of the Foundation framework, NSNumberFormatter
and NSDateFormatter
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.
iOS 8 & Mac OS X 10.10 more than doubled the number of system-provided formatter classes, which is pretty remarkable.
NSNumberFormatter
handles every aspect of number formatting imaginable, from mathematical and scientific notation, to currencies and percentages. Nearly everything about the formatter can be customized, whether it's the currency symbol, grouping separator, number of significant digits, rounding behavior, fractions, character for infinity, string representation for 0
, or maximum / minimum values. It can even write out numbers in several languages!
When using an NSNumberFormatter
, the first order of business is to determine what kind of information you're displaying. Is it a price? Is this a whole number, or should decimal values be shown?
NSNumberFormatter
can be configured for any one of the following formats, with the numberStyle
property.
To illustrate the differences between each style, here is how the number 12345.6789
would be displayed for each:
Formatter Style | Output |
---|---|
NoStyle |
12346 |
DecimalStyle |
12345.6789 |
CurrencyStyle |
$12345.68 |
PercentStyle |
1234567% |
ScientificStyle |
1.23456789E4 |
SpellOutStyle |
twelve thousand three hundred forty-five point six seven eight nine |
By default, NSNumberFormatter
will format according to the current locale settings, which determines things like currency symbol ($, £, €, etc.) and whether to use "," or "." as the decimal separator.
let formatter = NSNumberFormatter()
formatter.numberStyle = NSNumberFormatterStyle.CurrencyStyle
for identifier in ["en_US", "fr_FR", "ja_JP"] {
formatter.locale = NSLocale(localeIdentifier: identifier)
println("\(identifier) \(formatter.stringFromNumber(1234.5678))")
}
Locale | Formatted Number |
---|---|
en_US |
$1,234.57 |
fr_FR |
1 234,57 € |
ja_JP |
¥1,235 |
All of those settings can be overridden on an individual basis, but for most apps, the best strategy would be deferring to the locale's default settings.
In order to prevent numbers from getting annoyingly pedantic ("thirty-two point three three, repeating, of course..."), make sure to get a handle on NSNumberFormatter
's rounding behavior.
The easiest way to do this, would be to set the usesSignificantDigits
property to false
, and then set minimum and maximum number of significant digits appropriately. For example, a number formatter used for approximate distances in directions, would do well with significant digits to the tenths place for miles or kilometers, but only the ones place for feet or meters.
For anything more advanced, an
NSDecimalNumberHandler
object can be passed as theroundingBehavior
property of a number formatter.
NSDateFormatter
is the be all and end all of getting textual representations of both dates and times.
The most important properties for an NSDateFormatter
object are its dateStyle
and timeStyle
. Like NSNumberFormatter numberStyle
, these styles provide common preset configurations for common formats. In this case, the various formats are distinguished by their specificity (more specific = longer).
Both properties share a single set of enum
values:
Style | Description | Examples | |
---|---|---|---|
Date | Time | ||
NoStyle | Specifies no style. | ||
ShortStyle | Specifies a short style, typically numeric only. | 11/23/37 | 3:30pm |
MediumStyle | Specifies a medium style, typically with abbreviated text. | Nov 23, 1937 | 3:30:32pm |
LongStyle | Specifies a long style, typically with full text. | November 23, 1937 | 3:30:32pm |
FullStyle | Specifies a full style with complete details. | Tuesday, April 12, 1952 AD | 3:30:42pm PST |
dateStyle
and timeStyle
are set independently. For example, to display just the time, an NSDateFormatter
would be configured with a dateStyle
of NoStyle
:
let formatter = NSDateFormatter()
formatter.dateStyle = .NoStyle
formatter.timeStyle = .MediumStyle
let string = formatter.stringFromDate(NSDate())
// 10:42:21am
Whereas setting both to LongStyle
yields the following:
let formatter = NSDateFormatter()
formatter.dateStyle = .LongStyle
formatter.timeStyle = .LongStyle
let string = formatter.stringFromDate(NSDate())
// Monday June 30, 2014 10:42:21am PST
As you might expect, each aspect of the date format can alternatively be configured individually, a la carte. For any aspiring time wizards NSDateFormatter
has a bevy of different knobs and switches to play with.
As of iOS 4 / OS X 10.6, NSDateFormatter
supports relative date formatting for certain locales with the doesRelativeDateFormatting
property. Setting this to true
would format the date of NSDate()
to "Today".
For apps that work with files, data in memory, or information downloaded from a network, NSByteCounterFormatter
is a must-have. All of the great information unit formatting behavior seen in Finder and other OS X apps is available with NSByteCounterFormatter
, without any additional configuration.
NSByteCounterFormatter
takes a raw number of bytes and formats it into more meaningful units. For example, rather than bombarding a user with a ridiculous quantity, like "8475891734 bytes", a formatter can make a more useful approximation of "8.48 GB":
let formatter = NSByteCountFormatter()
let byteCount = 8475891734
let string = formatter.stringFromByteCount(CLongLong(byteCount))
// 8.48 GB
By default, specifying a 0
byte count will yield a localized string like "Zero KB". For a more consistent format, set allowsNonnumericFormatting
to false
:
let formatter = NSByteCountFormatter()
let byteCount = 0
formatter.stringFromByteCount(CLongLong(byteCount))
// Zero KB
formatter.allowsNonnumericFormatting = false
formatter.stringFromByteCount(CLongLong(byteCount))
// 0 bytes
One might think that dealing with bytes in code (which is, you know, a medium of bytes) would be a piece of cake, but in reality, even determining how many bytes are in a kilobyte remains a contentious and confusing matter. (Obligatory XKCD link)
In SI, the "kilo" prefix multiplies the base quantity by 1000 (i.e. 1km == 1000 m). However, being based on a binary number system, a more convenient convention has been to make a kilobyte equal to 210 bytes instead (i.e. 1KB == 1024 bytes). While the 2% difference is negligible at lower quantities, this confusion has significant implications when, for example, determining how much space is available on a 1TB drive (either 1000GB or 1024 GB).
To complicate matters further, this binary prefix was codified into the Kibibyte standard by the IEC in 1998... which is summarily ignored by the JEDEC, the trade and engineering standardization organization representing the actual manufacturers of storage media. The result is that one can represent information as either 1kB
, 1KB
, or 1KiB
. (Another obligatory XKCD link)
Rather than get caught up in all of this, simply use the most appropriate count style for your particular use case:
| File
| Specifies display of file byte counts. The actual behavior for this is platform-specific; on OS X 10.8, this uses the binary style, but that may change over time. |
| Memory
| Specifies display of memory byte counts. The actual behavior for this is platform-specific; on OS X 10.8, this uses the binary style, but that may change over time. |
In most cases, it is better to use File
or Memory
, however decimal or binary byte counts can be explicitly specified with either of the following values:
| Decimal
| Causes 1000 bytes to be shown as 1 KB. |
| Binary
| Causes 1024 bytes to be shown as 1 KB. |
NSDateFormatter
is great for points in time, but when it comes to dealing with date or time ranges, Foundation was without any particularly great options. That is, until the introduction of NSDateComponentsFormatter
& NSDateIntervalFormatter
.
As the name implies, NSDateComponentsFormatter
works with NSDateComponents
, which was covered in a previous NSHipster article. An NSDateComponents
object is a container for representing a combination of discrete calendar quantities, such as "1 day and 2 hours". NSDateComponentsFormatter
provides localized representations of NSDateComponents
objects in several different formats:
let formatter = NSDateComponentsFormatter()
formatter.unitsStyle = .Full
let components = NSDateComponents()
components.day = 1
components.hour = 2
let string = formatter.stringFromDateComponents(components)
// 1 day, 2 hours
Unit Style | Example |
---|---|
Positional |
"1:10" |
Abbreviated |
"1h 10m" |
Short |
"1hr 10min" |
Full |
"1 hour, 10 minutes" |
SpellOut |
"One hour, ten minutes" |
Like NSDateComponentsFormatter
, NSDateIntervalFormatter
deals with ranges of time, but specifically for time intervals between a start and end date:
let formatter = NSDateIntervalFormatter()
formatter.timeStyle = .ShortStyle
let fromDate = NSDate()
let toDate = fromDate.dateByAddingTimeInterval(10000)
let string = formatter.stringFromDate(fromDate, toDate: toDate)
// 5:49 - 8:36 PM
Formatter Style | Time Output | Date Output |
---|---|---|
NoStyle |
||
ShortStyle |
5:51 AM - 7:37 PM | 6/30/14 - 7/11/14 |
MediumStyle |
5:51:49 AM - 7:38:29 PM | Jun 30, 2014 - Jul 11, 2014 |
LongStyle |
6:02:54 AM GMT-8 - 7:49:34 PM GMT-8 | June 30, 2014 - July 11, 2014 |
FullStyle |
6:03:28 PM Pacific Standard Time - 7:50:08 PM Pacific Standard Time | Monday, June 30, 2014 - Friday, July 11, 2014 |
NSDateIntervalFormatter
and NSDateComponentsFormatter
are useful for displaying regular and pre-defined ranges of times such as the such as the opening hours of a business, or frequency or duration of calendar events.
In the case of displaying business hours, such as "Mon – Fri: 8:00 AM – 10:00 PM", use the
weekdaySymbols
of anNSDateFormatter
to get the localized names of the days of the week.
Prior to iOS 8 / Mac OS X 10.10, working with physical quantities was left as an exercise to the developer. However, with the introduction of HealthKit, this functionality is now provided in the standard library.
Although the fundamental unit of physical existence, mass is pretty much relegated to tracking the weight of users in HealthKit.
Yes, mass and weight are different, but this is programming, not science class, so stop being pedantic.
let massFormatter = NSMassFormatter()
let kilograms = 60.0
println(massFormatter.stringFromKilograms(kilograms)) // "132 lb"
NSLengthFormatter
can be thought of as a more useful version of MKDistanceFormatter
, with more unit options and formatting options.
let lengthFormatter = NSLengthFormatter()
let meters = 5_000.0
println(lengthFormatter.stringFromMeters(meters)) // "3.107 mi"
Rounding out the new NSFormatter
subclasses added for HealthKit is NSEnergyFormatter
, which formats energy in Joules, the raw unit of work for exercises, and Calories, which is used when working with nutrition information.
let energyFormatter = NSEnergyFormatter()
energyFormatter.forFoodEnergyUse = true
let joules = 10_000.0
println(energyFormatter.stringFromJoules(joules)) // "2.39 Cal"
Perhaps the most critical detail to keep in mind when using formatters is that they are extremely expensive to create. Even just an alloc init
of an NSNumberFormatter
in a tight loop is enough to bring an app to its knees.
Therefore, it's strongly recommended that formatters be created once, and re-used as much as possible.
If it's just a single method using a particular formatter, a static instance is a good strategy:
- (void)fooWithNumber:(NSNumber *)number {
static NSNumberFormatter *_numberFormatter = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_numberFormatter = [[NSNumberFormatter alloc] init];
[_numberFormatter setNumberStyle:NSNumberFormatterDecimalStyle];
});
NSString *string = [_numberFormatter stringFromNumber:number];
// ...
}
dispatch_once
guarantees that the specified block is called only the first time it's encountered.
If the formatter is used across several methods in the same class, that static instance can be refactored into a singleton method:
+ (NSNumberFormatter *)numberFormatter {
static NSNumberFormatter *_numberFormatter = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_numberFormatter = [[NSNumberFormatter alloc] init];
[_numberFormatter setNumberStyle:NSNumberFormatterDecimalStyle];
});
return _numberFormatter;
}
If the same formatter is privately implemented across several classes, one could either expose it publicly in one of the classes, or implement the static singleton method in a category on NSNumberFormatter
.
Prior to iOS 5 and Mac OS X 10.7,
NSDateFormatter
&NSNumberFormatter
were not thread safe. Under these circumstances, the safest way to reuse formatter instances was with a thread dictionary:
NSMutableDictionary *threadDictionary = [[NSThread currentThread] threadDictionary];
NSDateFormatter *dateFormatter = threadDictionary[@"dateFormatter"];
if (!dateFormatter) {
dateFormatter = [[NSDateFormatter alloc] init];
dateFormatter.locale = [NSLocale currentLocale];
dateFormatter.dateStyle = NSDateFormatterLongStyle;
dateFormatter.timeStyle = NSDateFormatterShortStyle;
threadDictionary[@"dateFormatter"] = dateFormatter;
}
return dateFormatter;
Another addition to formatters in iOS 8 & Mac OS X 10.10 is the idea of formatter contexts. This allows the formatted output to be correctly integrated into the localized string. The most salient application of this is the letter casing of formatted output at different parts of a sentence in western locales, such as English. For example, when appearing at the beginning of a sentence or by itself, the first letter of formatted output would be capitalized, whereas it would be lowercase in the middle of a sentence.
A context
property is available for NSDateFormatter
, NSNumberFormatter
, NSDateComponentsFormatter
, and NSByteCountFormatter
, with the following values:
Formatting Context | Output |
---|---|
Standalone |
"About 2 hours" |
ListItem |
"About 2 hours" |
BeginningOfSentence |
"About 2 hours" |
MiddleOfSentence |
"about 2 hours" |
Dynamic |
(Depends) |
In cases where localizations may change the position of formatted information within a string, the Dynamic
value will automatically change depending on where it appears in the text.
An article on NSFormatter
would be remiss without mention of some of the third party subclasses that have made themselves mainstays of everyday app development: ISO8601DateFormatter & FormatterKit
Created by Peter Hosey, ISO8601DateFormatter has become the de facto way of dealing with ISO 8601 timestamps, used as the interchange format for dates by webservices (Yet another obligatory XKCD link).
Although Apple provides official recommendations on parsing internet dates, the reality of formatting quirks across makes the suggested NSDateFormatter
-with-en_US_POSIX
-locale approach untenable for real-world usage. ISO8601DateFormatter
offers a simple, robust interface for dealing with timestamps:
let formatter = ISO8601DateFormatter()
let timestamp = "2014-06-30T08:21:56+08:00"
let date = formatter.dateFromString(timestamp)
Although not an
NSFormatter
subclass, TransformerKit offers an extremely performant alternative for parsing and formatting both ISO 8601 and RFC 2822 timestamps.
FormatterKit has great examples of NSFormatter
subclasses for use cases not currently covered by built-in classes, such as localized addresses, arrays, colors, locations, and ordinal numbers, and URL requests. It also boasts localization in 23 different languages, making it well-suited to apps serving every major market.
let formatter = TTTAddressFormatter()
let formatter.locale = NSLocale(localeIdentifier: "en_GB")
let street = "221b Baker St"
let locality = "Paddington"
let region = "Greater London"
let postalCode = "NW1 6XE"
let country = "United Kingdom"
let string = formatter.stringFromAddressWithStreet(street: street, locality: locality, region: region, postalCode: postalCode, country: country)
// 221b Baker St / Paddington / Greater London / NW1 6XE / United Kingdom
let formatter = TTTArrayFormatter()
formatter.usesAbbreviatedConjunction = true // Use '&' instead of 'and'
formatter.usesSerialDelimiter = true // Omit Oxford Comma
let array = ["Russel", "Spinoza", "Rawls"]
let string = formatter.stringFromArray(array)
// "Russell, Spinoza & Rawls"
let formatter = TTTColorFormatter()
let color = UIColor.orangeColor()
let hex = formatter.hexadecimalStringFromColor(color);
// #ffa500
let formatter = TTTLocationFormatter()
formatter.numberFormatter.maximumSignificantDigits = 4
formatter.bearingStyle = TTTBearingAbbreviationWordStyle
formatter.unitSystem = TTTImperialSystem
let pittsburgh = CLLocation(latitude: 40.4405556, longitude: -79.9961111)
let austin = CLLocation(latitude: 30.2669444, longitude: -97.7427778)
let string = formatter.stringFromDistanceAndBearingFromLocation(pittsburgh, toLocation: austin)
// "1,218 miles SW"
let formatter = TTTOrdinalNumberFormatter()
formatter.locale = NSLocale(localeIdentifier: "fr_FR")
formatter.grammaticalGender = TTTOrdinalNumberFormatterMaleGender
let string = NSString(format: "You came in %@ place", [formatter.stringFromNumber(2)])
// "Vous êtes arrivé à la 2e place!"
let request = NSMutableURLRequest(URL: NSURL(string: "http://nshipster.com"))
request.HTTPMethod = "GET"
request.addValue("text/html", forHTTPHeaderField: "Accept")
let command = TTTURLRequestFormatter.cURLCommandFromURLRequest(request)
// curl -X GET "https://nshipster.com/" -H "Accept: text/html"
If your app deals in numbers or dates (or time intervals or bytes or distance or length or energy or mass), NSFormatter
is indispensable. Actually, if your app doesn't… then what does it do, exactly?
Presenting useful information to users is as much about content as presentation. Invest in learning all of the secrets of NSNumberFormatter
, NSDateFormatter
, and the rest of the Foundation formatter crew to get everything exactly how you want them.
And if you find yourself with formatting logic scattered across your app, consider creating your own NSFormatter
subclass to consolidate all of that business logic in one place.