Parsing and Formatting Dates in Swift
Want more? Subscribers can view all 473 episodes. New episodes are released regularly.
Episode Links
- NSDateFormatter.com - A useful website that uses
DateFormatter
live on the server. Use it to play around with different formats and locales to test your format strings. - Apple Date Formatting Guide
- Date Format Patterns - Unicode Technical Standard #35
- Working with Date and Time - a fantastic blog post covering tons of useful (and esoteric) concepts relating to dates, times, and calendars. Read it.
Related Screencasts
- Episode 171 - NSDateComponents - If you need to extract the day, year, month, etc from a
Date
instance, you useDateComponents
. - Episode 219 - NSCalendar - Use
NSCalendar
if you ever need to do date math, like Add 1 day to this Date. Doing this withoutNSCalendar
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 |