Quantcast
Channel: NSHipster
Viewing all articles
Browse latest Browse all 382

Formatter

$
0
0

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
NumberFormatter
MeasurementFormatter
Dates, Times, and Durations
DateFormatter
ISO8601DateFormatter
DateComponentsFormatter
DateIntervalFormatter
RelativeDateTimeFormatter
People and Places
PersonNameComponentsFormatter
CNPostalAddressFormatter
Lists and Items
ListFormatter

Formatting Numbers and Quantities

ClassExample OutputAvailability
NumberFormatter“1,234.56”iOS 2.0
macOS 10.0+
MeasurementFormatter“-9.80665 m/s²”iOS 10.0+
macOS 10.12+
ByteCountFormatter“756 KB”iOS 6.0+
macOS 10.8+
EnergyFormatter“80 kcal”iOS 8.0+
macOS 10.10+
MassFormatter“175 lb”iOS 8.0+
macOS 10.10+
LengthFormatter“5 ft, 11 in”iOS 8.0+
macOS 10.10+
MKDistanceFormatter“500 miles”iOS 7.0+
macOS 10.9+

NumberFormatter

NumberFormatter 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 NumberFormatter, the first order of business is to establish what kind of number you’re working with and set the numberStyle property accordingly.

Number Styles

Number StyleExample Output
none123
decimal123.456
percent12%
scientific1.23456789E4
spellOutone hundred twenty-three
ordinal3rd
currency$1234.57
currencyAccounting($1234.57)
currencyISOCodeUSD1,234.57
currencyPlural1,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 NumberFormatter’s rounding behavior. Here, you have two options:

varformatter=NumberFormatter()formatter.usesSignificantDigits=trueformatter.minimumSignificantDigits=1// defaultformatter.maximumSignificantDigits=6// defaultformatter.string(from:1234567)// 1234570formatter.string(from:1234.567)// 1234.57formatter.string(from:100.234567)// 100.235formatter.string(from:1.23000)// 1.23formatter.string(from:0.0000123)// 0.0000123
  • Set usesSignificantDigits to 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).
varformatter=NumberFormatter()formatter.usesSignificantDigits=falseformatter.minimumIntegerDigits=0// defaultformatter.maximumIntegerDigits=42// default (seriously)formatter.minimumFractionDigits=0// defaultformatter.maximumFractionDigits=0// defaultformatter.string(from:1234567)// 1234567formatter.string(from:1234.567)// 1235formatter.string(from:100.234567)// 100formatter.string(from:1.23000)// 1formatter.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 roundingMode, roundingIncrement, and roundingBehavior 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

MeasurementFormatter 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 particular Unit
  • A UnitConverter converts quantities of one unit to a different, compatible unit
For the curious, here's the complete list of units supported by MeasurementFormatter:
MeasureUnit SubclassBase Unit
AccelerationUnitAccelerationmeters per second squared (m/s²)
Planar angle and rotationUnitAngledegrees (°)
AreaUnitAreasquare meters (m²)
Concentration of massUnitConcentrationMassmilligrams per deciliter (mg/dL)
DispersionUnitDispersionparts per million (ppm)
DurationUnitDurationseconds (sec)
Electric chargeUnitElectricChargecoulombs (C)
Electric currentUnitElectricCurrentamperes (A)
Electric potential differenceUnitElectricPotentialDifferencevolts (V)
Electric resistanceUnitElectricResistanceohms (Ω)
EnergyUnitEnergyjoules (J)
FrequencyUnitFrequencyhertz (Hz)
Fuel consumptionUnitFuelEfficiencyliters per 100 kilometers (L/100km)
IlluminanceUnitIlluminancelux (lx)
Information StorageUnitInformationStorageByte* (byte)
LengthUnitLengthmeters (m)
MassUnitMasskilograms (kg)
PowerUnitPowerwatts (W)
PressureUnitPressurenewtons per square meter (N/m²)
SpeedUnitSpeedmeters per second (m/s)
TemperatureUnitTemperaturekelvin (K)
VolumeUnitVolumeliters (L)

* Follows ISO/IEC 80000-13 standard; one byte is 8 bits, 1 kilobyte = 1000¹ bytes


MeasurementFormatter 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.

importFoundation// "The swift (Apus apus) can power itself to a speed of 111.6km/h"letspeed=Measurement<UnitSpeed>(value:111.6,unit:.kilometersPerHour)letformatter=MeasurementFormatter()formatter.string(from:speed)// 69.345 mph

Configuring the Underlying Number Formatter

By delegating much of its formatting responsibility to an underlying NumberFormatter property, MeasurementFormatter 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 usesSignificantDigits and set maximumSignificantDigits to prevent incidental changes in precision.

formatter.numberFormatter.usesSignificantDigits=trueformatter.numberFormatter.maximumSignificantDigits=4formatter.string(from:speed)// 69.35 mph

Changing Which Unit is Displayed

A MeasurementFormatter, 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 providedUnit option.

formatter.unitOptions=[.providedUnit]formatter.string(from:speed)// 111.6 km/hformatter.string(from:speed.converted(to:.milesPerHour))// 69.35 mph

Formatting Dates, Times, and Durations

ClassExample OutputAvailability
DateFormatter“July 15, 2019”iOS 2.0
macOS 10.0+
ISO8601DateFormatter“2019-07-15”iOS 10.0+
macOS 10.12+
DateComponentsFormatter“10 minutes”iOS 8.0
macOS 10.10+
DateIntervalFormatter“6/3/19 - 6/7/19”iOS 8.0
macOS 10.10+
RelativeDateTimeFormatter“3 weeks ago”iOS 13.0+
macOS 10.15

DateFormatter

DateFormatter 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 NumberFormatter. 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 DateFormatter object are its dateStyle and timeStyle. As with NumberFormatter and its numberStyle, these date and time styles provide preset configurations for common formats.

StyleExample Output
DateTime
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”
letdate=Date()letformatter=DateFormatter()formatter.dateStyle=.longformatter.timeStyle=.longformatter.string(from:date)// July 15, 2019 at 9:41:00 AM PSTformatter.dateStyle=.shortformatter.timeStyle=.shortformatter.string(from:date)// "7/16/19, 9:41:00 AM"

dateStyle and timeStyle are set independently. So, to display just the time for a particular date, for example, you set dateStyle to none:

letformatter=DateFormatter()formatter.dateStyle=.noneformatter.timeStyle=.mediumletstring=formatter.string(from: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 NSDateFormatter 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, ISO8601DateFormatter is now built-in to Foundation.

letformatter=ISO8601DateFormatter()formatter.date(from:"2019-07-15T09:41:00-07:00")// Jul 15, 2019 at 9:41 AM

DateIntervalFormatter

DateIntervalFormatter is like DateFormatter, but can handle two dates at once — specifically, a start and end date.

letformatter=DateIntervalFormatter()formatter.dateStyle=.shortformatter.timeStyle=.noneletfromDate=Date()lettoDate=Calendar.current.date(byAdding:.day,value:7,to:fromDate)!formatter.string(from:fromDate,to:toDate)// "7/15/19 – 7/22/19"

Date Interval Styles

StyleExample Output
DateTime
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, DateComponentsFormatter works with DateComponents values (previously), which contain a combination of discrete calendar quantities, such as “1 day and 2 hours”.

DateComponentsFormatter provides localized representations of date components in several different, pre-set formats:

letformatter=DateComponentsFormatter()formatter.unitsStyle=.fullletcomponents=DateComponents(day:1,hour:2)letstring=formatter.string(from:components)// 1 day, 2 hours

Date Components Unit Styles

StyleExample
positional“1:10”
abbreviated“1h 10m”
short“1hr 10min”
full“1 hour, 10 minutes”
spellOut“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 DateComponentsFormatter, as well as DateFormatter, NumberFormatter, and others.

Formatting ContextOutput
standalone“About 2 hours”
listItem“About 2 hours”
beginningOfSentence“About 2 hours”
middleOfSentence“about 2 hours”
dynamicDepends*

* 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

RelativeDateTimeFormatter 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 DateFormatter actually gave this a try circa iOS 4 by way of the doesRelativeDateFormatting property. But that hardly ever worked, and most of us forgot about it, probably. Fortunately, RelativeDateTimeFormatter succeeds where doesRelativeDateFormatting fell short, and offers some great new functionality to make your app more personable and accessible.

(As far as we can tell,) RelativeDatetimeFormatter takes the most significant date component and displays it in terms of past or future tense (“1 day ago” / “in 1 day”).

letformatter=RelativeDateTimeFormatter()formatter.localizedString(from:DateComponents(day:1,hour:1))// "in 1 day"formatter.localizedString(from:DateComponents(day:-1))// "1 day ago"formatter.localizedString(from:DateComponents(hour:3))// "in 3 hours"formatter.localizedString(from:DateComponents(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.localizedString(from:DateComponents(hour:0))// "in 0 hours"formatter.localizedString(from:DateComponents(day:1,hour:-24))// "in 1 day"formatter.localizedString(from:DateComponents())// ""

Styles

StyleExample
abbreviated“1 mo. ago” *
short“1 mo. ago”
full“1 month ago”
spellOut“one month ago”

*May produce output distinct from short for non-English locales.

Using Named Relative Date Times

By default, RelativeDateTimeFormatter adopts the formulaic convention we’ve seen so far. But you can set the dateTimeStyle property to .named to prefer localized deictic expressions— “tomorrow”, “yesterday”, “next week” — whenever one exists.

importFoundationletformatter=RelativeDateTimeFormatter()formatter.localizedString(from:DateComponents(day:-1))// "1 day ago"formatter.dateTimeStyle=.namedformatter.localizedString(from:DateComponents(day:-1))// "yesterday"

This just goes to show that beyond calendrical and temporal relativity, RelativeDateTimeFormatter 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.localizedString(from:DateComponents(day:-2))// "2 days ago"formatter.locale=Locale(identifier:"de_DE")formatter.localizedString(from:DateComponents(day:-2))// "vorgestern"

Hervorragend!


Formatting People and Places

ClassExample OutputAvailability
PersonNameComponentsFormatter“J. Appleseed”iOS 9.0+
macOS 10.11+
CNContactFormatter“Applessed, Johnny”iOS 13.0+
macOS 10.15+
CNPostalAddressFormatter“1 Infinite Loop\n
Cupertino CA 95014”
iOS 13.0+
macOS 10.15+

PersonNameComponentsFormatter

PersonNameComponentsFormatter 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:

letformatter=PersonNameComponentsFormatter()varnameComponents=PersonNameComponents()nameComponents.givenName="Johnny"nameComponents.familyName="Appleseed"formatter.string(from:nameComponents)// "Johnny Appleseed"

Simple enough, right? We all know names are space delimited, first-last… right?

nameComponents.givenName="约翰尼"nameComponents.familyName="苹果籽"formatter.string(from:nameComponents)// "苹果籽约翰尼"

‘nuf said.

CNPostalAddressFormatter

CNPostalAddressFormatter provides a convenient Formatter-based API to functionality dating back to the original AddressBook framework.

The following example formats a constructed CNMutablePostalAddress, but you’ll most likely use existing CNPostalAddress values retrieved from the user’s address book.

letaddress=CNMutablePostalAddress()address.street="One Apple Park Way"address.city="Cupertino"address.state="CA"address.postalCode="95014"letaddressFormatter=CNPostalAddressFormatter()addressFormatter.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, CNPostalAddressFormatter provides a method that vends an NSAttributedString that lets you identify the ranges of each component (PersonNameComponentsFormatter does this too).

The NSAttributedString 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:

varattributedString=addressFormatter.attributedString(from:address,withDefaultAttributes:[:]).mutableCopy()as!NSMutableAttributedStringletstringRange=NSRange(location:0,length:attributedString.length)attributedString.enumerateAttributes(in:stringRange,options:[]){(attributes,attributesRange,_)inletcolor:UIColorswitchattributes[NSAttributedString.Key(CNPostalAddressPropertyAttribute)]as?String{case CNPostalAddressStreetKey:
        color=.redcase CNPostalAddressCityKey:
        color=.orangecase CNPostalAddressStateKey:
        color=.greencase CNPostalAddressPostalCodeKey:
        color=.purpledefault:return}attributedString.addAttribute(.foregroundColor,value:color,range:attributesRange)}

One Apple Park Way
CupertinoCA95014


Formatting Lists and Items

ClassExample OutputAvailability
ListFormatter“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: ListFormatter. 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 ListFormatter— just call the localizedString(byJoining:) class method.

importFoundationletoperatingSystems=["macOS","iOS","iPadOS","watchOS","tvOS"]ListFormatter.localizedString(byJoining:operatingSystems)// "macOS, iOS, iPadOS, watchOS, and tvOS"

ListFormatter works as you’d expect for lists comprising zero, one, or two items.

ListFormatter.localizedString(byJoining:[])// ""ListFormatter.localizedString(byJoining:["Apple"])// "Apple"ListFormatter.localizedString(byJoining:["Jobs","Woz"])// "Jobs and Woz"

Lists of Formatted Values

ListFormatter exposes an underlying itemFormatter property, which effectively adds a map(_:) before calling joined(by:). You use itemFormatter whenever you’d formatting a list of non-String elements. For example, you can set a NumberFormatter as the itemFormatter for a ListFormatter to turn an array of cardinals (Int values) into a localized list of ordinals.

letnumberFormatter=NumberFormatter()numberFormatter.numberStyle=.ordinalletlistFormatter=ListFormatter()listFormatter.itemFormatter=numberFormatterlistFormatter.string(from:[1,2,3])// "1st, 2nd, and 3rd"

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.

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.


Viewing all articles
Browse latest Browse all 382

Trending Articles