Keeping dates local

December 11th, 2018

In our day-to-day life, dates are pretty straight forward - we read them all the time, make plans around them, and share them with other people. Apart from the occasional missed birthday party all of these date-based tasks go surprisingly smoothly. Which is remarkable when you stop to think how complex our date systems are. It works this smoothly because everyone is making some pretty large, unspoken assumptions around the dates that they see - what calendar is used, what timezone is used, the ordering of date elements, etc. While for most of us these assumptions rarely create issues in our day-to-day lives, if we want to build a system that uses dates we need to discover what assumptions we are making and remove them. Take for example the following date:

02/12/06

If you are from the UK then you would read this as:

2nd of December 2006

whereas if you are from the US then you would read this as:

February 12th, 2006

Both of these interpretations are valid however only one is correct. If the developer is from the UK then the former interpretation is correct and any US users are going to be either confused when they start seeing dates like 14/12/06 😕 or angry when they show up to an appointment on the wrong day 🤬. It's easy to fall into this date ordering trap especially if everyone in the development and testing teams is working off the same set of date assumptions/conventions - this is the real danger with assumptions, often we don't know that we are making them.

Getting to know what's local

When it comes to formatting dates we can choose to take the user's conventions into account or not, so for example, with the above date example 02/12/06 if we want to confuse our US users we could hardcode the date element order to match UK conventions by:

let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "dd/MM/yy"
let formattedDate = dateFormatter.string(from: date)

It's not uncommon to see this approach to configuring a DateFormatter and while there are valid scenarios for hardcoding the dateFormat value (we will see an example of that later) I can't think of any valid scenarios for doing so when displaying a date to a user. By hardcoding the dateFormat value we are instructing the DateFormatter to ignore the date element ordering for the user's conventions and instead use the same element ordering for all users. By not meeting our user's date element ordering expectations we degrade that users experience by making something that should be as simple as reading a date into a surprise maths puzzle 📖. I've seen a number of novel but naive approaches on how to solve the expectation mismatch a hardcoded dateFormat produces. These solutions tend to centre on two approaches:

  1. Add conditional logic and set a dateFormat value that's unique for each user's conventions.
  2. Move the dateFormat value into the localisation .strings file.

While it's possible to meet our user's expectations with either of these solutions both have two major drawbacks:

  1. The developer needs to actively decide which conventions each DateFormatter will support and.
  2. The developer needs to take on the responsibility of ensuring that any date and time formatting rules are correct for each supported set of conventions.

In order to define the formatting rules for each dateFormat value, the developer would need to answer questions such as:

  • Which calendar to use?
  • Which clock to use: 24-hour or 12-hour?
  • What is the ordering of date elements?
  • What is the date separator character(s)?

And the list goes on.

Answering all these questions correctly is a massive task and as it turns out, a totally unnecessary one as iOS already answers them (and many more besides) for us. In iOS, the linguistic, cultural and technological conventions are collected within the Locale class (each locale's conventions are provided by the Unicode Common Locale Data Repository (CLDR) project). The conventions defined within each locale should match our user's expectations for how their world is measured and represented; as such Locale plays a vital role in making our users comfortable when using our apps to complete their tasks. By working with the user's locale conventions, it's possible to produce formatted dates that match our user's expectations. Even better than just improving our user's in-app experience, is that this can all be achieved without actually having to know or care what those locale conventions actually are.

While each locale comes with default conventions, some of them can be customised by the user e.g. switching from 12-hour to 24-hour clock representation. So even if two users have the same locale, it would be a mistake to assume these locales where identical.

Photo of clocks set to different timezones

For the rest of this post I'm going to assume your locale is the default US locale. To keep the examples comparable, each will use a date based off of the Unix epoch timestamp: 1165071389 which equates to 2nd of December, 2006 at 14:56:29. You can see the completed playground with all of the below examples by following this link.

Staying local

When it comes to presenting date information to the user, it's best to leave all locale concerns to DateFormatter - there are two ways to do this:

  1. Using DateFormatter.Style
  2. Using a template

Let's explore both these approaches in more detail.

Using DateFormatter.Style

One way to ensure that a Date is always shown to the user using a formatting style that they are expecting is to use the DateFormatter.Style enum. The DateFormatter.Style enum is a set of predefined values that correspond to a date format, at the time of writing (iOS 12) this enum has 5 possible cases to choose from:

  1. none
  2. short
  3. medium
  4. long
  5. full

Each case will take into account the user's locale settings when formatting dates. DateFormatter has two DateFormatter.Style properties: dateStyle and timeStyle. Each property works semi-independently of the other and as such each can take a different value - this flexibility allows for a wide range of formatting options (25 possible permutations):

let dateFormatter = DateFormatter()
dateFormatter.dateStyle = .full
dateFormatter.timeStyle = .medium
let formattedDate = dateFormatter.string(from: date)

formattedDate would be set to Saturday, December 2, 2006 at 2:56:29 PM.

In the above example, we can see the user's locale influencing the formattedDate value in a number of ways:

  1. The date format ordering - EEEE, MMMM d, y 'at' h:m:s a.
  2. The names of the months and days.
  3. The separator between the various date elements - / for calendar date elements and : for time date elements.

It's also interesting to note that DateFormatter is handling combining the calendar date and time elements together into a sentence structure that is appropriate for both DateFormatter.Style values. So in the above example at is being used as a connector between those two elements however if dateStyle was changed to .short then at becomes , and we get 12/2/06, 2:56:29 PM as the formatted date should be shown in a more compacted form - pretty powerful stuff 🤯.

To demonstrate the power of this further, if we set the locale to Locale(identifier: "de_DE") the above .full example becomes Samstag, 2. Dezember 2006 um 14:56:29 and the .short example becomes 02.12.06, 14:56:29 - all this localisation without me having to know anything about German date conventions or even anything about the German language - 🤯 in 🇩🇪.

Another way to use the DateFormatter.Style approach is by using the available class method:

let formattedDate = DateFormatter.localizedString(from: date, dateStyle: .short, timeStyle: .medium)

formattedDate has the same value as the property based approach: 12/2/06, 2:56:29 PM.

When deciding which option to use I tend to favour the property(s) approach when I either need to cache the DateFormatter instance being used (see "Sneaky date formatters exposing more than you think" for more details on how to cache date formatters) or further customise it (outside of just the dateStyle and/or timeStyle properties). For all other scenarios, I favour the class method approach as I think it's easier to reason about and reads better (from the method name I know that I'm getting a localised string value back).

This is a helpful cheatsheet showing the output for each DateFormatter.Style case. To see the conventions used in different locales set the locale property for the DateFormatter instance to the locale you are interested in e.g. dateFormatter.locale = Locale(identifier: "en_CA").

Using DateFormatter.Style works really well if you to want to display a date in one of the available formats but if you want to display a custom format you need to go down a different path.

Using a template

A template is a customised instruction to DateFormatter of the date elements that the formatted date should contain - these date elements are specified using the same symbols as used with a fixed string date format approach: d, MM, y etc. The beauty of the template approach is that it will take this customised instruction and produce a formatted date that takes into account the user's locale when displaying those elements. For example to produce a formatted date only containing the day and month, the template could look like:

let dateFormatter = DateFormatter()
dateFormatter.setLocalizedDateFormatFromTemplate("d MM")
let formattedDate = dateFormatter.string(from: date)

formattedDate would be set to 12/2. The more eagle-eyed among you will have spotted that two transformations have occurred here, the first is that a locale-specific separator was added between the date elements and that the date elements have switched positions from the ordering in the template i.e. from d MM to MM d 🚀.

It's important to note that when defining the above template I added a space between the different date elements, strictly speaking this space wasn't needed - dMM would have resulted in the same formattedDate value. I included the space here only to make the template easier to read.

Just like with the DateFormatter.Style, the template approach has both an instance and class interface:

let localisedDateFormat = DateFormatter.dateFormat(fromTemplate: "d MM", options: 0, locale: Locale.current)
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = localisedDateFormat
let formattedDate = dateFormatter.string(from: date)

Again, formattedDate would be set to 12/2. The class approach is little more verbose than the instance approach but could be more useful if you wanted to pass the localised formatting string around rather a DateFormatter instance.

Going remote

You may be thinking at this point:

"Is using dateFormat with a fixed string ever the correct approach?"

The simple answer:

Yes.

The date formatting examples shown so far have been concerned with presenting a formatted date to the user. However as iOS developers we (often) have another consumer of our date data: the backend. Typically the backend will be expecting all date values to be sent using a fixed, locale-neutral format. In order to ensure that the user's locale does not affect these dates when converting from a Date instance to the formatted date value we need to take full control of our DateFormatter instances and hardcode the locale, timeZone and dateFormat properties to match the backend's expectations:

let dateFormatter = DateFormatter()
dateFormatter.locale = Locale(identifier: "en_US_POSIX")
dateFormatter.timeZone = TimeZone(identifier: "UTC")
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss'Z'"
let formattedDate = dateFormatter.string(from: date)

formattedDate would be set to 2006-12-02T14:56:29Z regardless of the user's locale settings. An interesting side effect of setting the locale property is that the formatter's calendar property is always set to the default calendar for that locale (which for this local is the Gregorian calendar). It's important to note that en_US_POSIX is not the same locale as en_US - en_US_POSIX while based off of en_US is a special locale that isn't tied to any country/region so shouldn't change even if en_US does change.

You may have noticed that the dateFormatter is using the ISO 8601 string format, in this case (and if your project has a base iOS version of at least iOS 10) I would recommend using ISO8601DateFormatter instead of the more generic DateFormatter class.

Letting go of (some) control

When displaying a date in our apps, it makes sense to do so in a format that the user is expecting and can easily understand- this isn't as simple as it first seems. Thankfully, DateFormatter has two very straight forward ways to achieve this:

  1. Using DateFormatter.Style
  2. Using a template

Both approaches work well and require very little effort to solve what is a real iceberg of a problem. The only thing it costs us is that we need to give up a little bit of control on exactly how the date is formatted - that's a price I'm happy to pay to ensure that my users are shown dates in the most convenient format for them and I don't need to think about which calendar a certain locale uses or if month comes before day, etc.

Now I just need to convince those pixel-perfect designers on my team that giving up some control is actually a good thing... 😅

If you are interested, there is an accompanying playground that contains all of the above code snippets.

What do you think? Let me know by getting in touch on Twitter - @wibosco