Parsing and Formatting Dates in Swift

Episode #367 | 18 minutes | published on December 13, 2018 | Uses swift-4.2, Xcode-10.1
Free Video
Working with dates is a task that is universally applicable to Swift developers. Particularly when dealing with an API, dates can arrive in all shapes and sizes. We‘ll examine some of the common ones such as ISO 8601, show how to parse these formats into Date instances, and how to use DateFormatter to display them back again as a string. We‘ll also cover the importance of using en_US_POSIX and honoring the user‘s Locale when displaying dates.

Episode Links

Related Screencasts

  • Episode 171 - NSDateComponents - If you need to extract the day, year, month, etc from a Date instance, you use DateComponents.
  • Episode 219 - NSCalendar - Use NSCalendar if you ever need to do date math, like Add 1 day to this Date. Doing this without NSCalendar is probably going to bite you in the long run.

Parsing Dates

Parsing dates is what is referred to when we take a raw value (say a String or Int) and convert it into a Date instance we can work with in our application. This is because most of the time we have to transfer these values to and from databases, files on disk, JSON representations from APIs and so on.

To parse a date, we'll use a DateFormatter:

let input = "12/10/2018"
let formatter = DateFormatter()
formatter.dateFormat = "MM/dd/yyyy"
if let date = formatter.date(from: input) {
  print(date)  // Prints:  2018-12-10 06:00:00 +0000
}

How did I know what date format to use? Check the table above or go to NSDateFormatter.com.

A more common date format we'll see are those that come from APIs in a format like this:

let input = "2018-12-10T17:29:50Z"

This is in ISO 8601 format, which is the most common standard that we'll run into.

For the format string, we'll use:

formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ"

The 'Z' here refers to the time zone, and if the date

Setting the locale to en_US_POSIX is strongly recommended when parsing dates from a server to avoid issues with the device's local settings. For some background on why, read this Tech note from Apple.

let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ"
if let date = formatter.date(from: input) {
    print(date)    
}

Displaying dates to the user

Once you have a Date, you might be tempted to do something like this:

let formatter = DateFormatter()
formatter.dateFormat = "MM/dd/yyyy"
dateLabel.text = formatter.string(from: date)

This will produce a result that might look okay in the US, but international users will be annoyed (and worse, this date could be ambiguous... is 12/10/2018 December 10th, or October 12th?).

Instead, utilize the built-in formats for displaying that will honor the user's locale:

formatter.locale = Locale(identifier: "en_US")
formatter.dateStyle = .short

formatter.string(from: date) // returns 12/10/2018

But if we're in the UK:

formatter.locale = Locale(identifier: "en_GB")
formatter.string(from: date) // returns 10/12/18

Note the 2 digit year.

And if we're in Germany?

formatter.locale = Locale(identifier: "de_DE")
formatter.string(from: date) // returns 10.12.18

Turns out the periods are the accepted standard. In addition, DateFormatter has already translated names for you, so you don't have to worry about it:

formatter.locale = Locale(identifier: "es_ES") // Spanish from Spain
formatter.dateStyle = .long
formatter.string(from: date) // returns "10 de diciembre, 2018"

Here not only are the words translated, but the month isn't capitalized.

There are dozens of difference in how users expect their dates to be displayed. Let DateFormatter do the hard work and don't use a custom format string for displaying dates.

Codable

Let's say we have a model locally and we need to produce JSON:

struct Issue : Encodable {
    let title: String
    let createdAt: Date
}

let issue = Issue(title: "My issue", createdAt: Date())
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted

let data = try! encoder.encode(issue)
print(String(data: data, encoding: .utf8)!)

This prints:

{
  "title" : "My issue",
  "createdAt" : 566414494.96444798
}

That date looks weird... How can we support ISO 8601? Turns out that JSONEncoder has a built-in way to customize the date formats:

encoder.dateEncodingStrategy = .iso

Now we get:

{
  "title" : "My issue",
  "createdAt" : "2018-12-13T17:22:46Z"
}

Perfect!

ISO 8601 With Milliseconds

I have run into a variant of ISO 8601 that includes milliseconds. How would we produce this?

Do do this we have to provide our own date formatter and include this in the format string:

let fullISO8610Formatter = DateFormatter()
fullISO8610Formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"
encoder.dateEncodingStrategy = .formatted(fullISO8610Formatter)

Here we added .SSS to our format string to include 3 decimal places for fractional seconds.

{
  "title" : "My issue",
  "createdAt" : "2018-12-13T11:23:44.575-0600"
}

There's also a lesser known type called ISO8601DateFormatter. This looks handy! Use it like this:

let f = ISO8601DateFormatter()
f.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
f.string(from: Date())

There are some other interesting options for formatOptions so take a look at how you can customize it further.

Unfortunately, ISO8601DateFormatter doesn't inherit from DateFormatter, so we can't use this as a formatter like this:

encoder.dateEncodingStrategy = .formatted(f) // ISO8601DateFormatter can't be used like this

Instead, we'd have to use the .custom((Date, Encoder) -> Void) and encode the date inside the block.

Common Date Formats

The following table's sample column are mostly based on the time December 14th, 2008 4:35 PM UTC.

Character Example Description
Year
yy 08 2-digit year
yyyy 2008 4-digit year
Quarter
Q 4 The quarter of the year. Use QQ if you want zero padding.
QQQ Q4 Quarter including "Q"
QQQQ 4th quarter Quarter spelled out
Month
M 12 The numeric month of the year. A single M will use '1' for January.
MM 12 The numeric month of the year. A double M will use '01' for January.
MMM Dec The shorthand name of the month
MMMM December Full name of the month
MMMMM D Narrow name of the month
Day
d 14 The day of the month. A single d will use 1 for January 1st.
dd 14 The day of the month. A double d will use 01 for January 1st.
F 3rd Tuesday in December The day of week in the month
E Tues The day of week in the month
EEEE Tuesday The full name of the day
EEEEE T The narrow day of week
Hour
h 4 The 12-hour hour.
hh 04 The 12-hour hour padding with a zero if there is only 1 digit
H 16 The 24-hour hour.
HH 16 The 24-hour hour padding with a zero if there is only 1 digit.
a PM AM / PM for 12-hour time formats
Minute
m 35 The minute, with no padding for zeroes.
mm 35 The minute with zero padding.
Second
s 8 The seconds, with no padding for zeroes.
ss 08 The seconds with zero padding.
Time Zone
zzz CST The 3 letter name of the time zone. Falls back to GMT-08:00 (hour offset) if the name is not known.
zzzz Central Standard Time The expanded time zone name, falls back to GMT-08:00 (hour offset) if name is not known.
zzzz CST-06:00 Time zone with abbreviation and offset
Z -0600 RFC 822 GMT format. Can also match a literal Z for Zulu (UTC) time.
ZZZZZ -06:00 ISO 8601 time zone format
blog comments powered by Disqus