14

I created a new Single View App project from scratch and added only the following code to the ViewController's viewDidLoad method:

let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "MM/yy"
if let date = dateFormatter.date(from: "11/20") {
    print("got the date: \(date)")
} else {
    print("failed getting the date")
}

The above code works everywhere in the world except cities that observe Brasília Summer Time (BRST).

I tested cities in all 38 time zones listed here by changing the time zone on the device in Settings > General > Date & Time. I also confirmed that BRST cities work fine on an iOS 11.0 device, but not iOS 11.2.6.

Also note that even on iOS 11.2.6, almost every other month/year combination I've tried works fine. Only "11/20" and "11/26" seem to fail.

Why is this code returning nil for cities that observe BRST on iOS 11.2.6?

6
  • Have you been able to reproduce this by setting the TimeZone on the dateFormatter rather than just using the device settings? What time zone do you select in Date & Time settings?
    – shim
    Mar 22, 2018 at 21:48
  • You're right - if I set dateFormatter.timeZone = TimeZone(secondsFromGMT: 0) it no longer returns nil for "11/20".
    – Adam Johns
    Mar 22, 2018 at 21:51
  • @shim I used São Paulo for the UTC−02:00 time zone.
    – Adam Johns
    Mar 22, 2018 at 21:55
  • But seconds from GMT: 0 is not the same time zone as UTC-2…
    – shim
    Mar 22, 2018 at 21:57
  • @shim, right - that's why it works when changing the dateFormatter's time zone to a non-broken time zone. I was just saying if I keep the time zone on the device to UTC-02:00 and manually set the time zone of the dateFormatter that is a potential fix/workaround.
    – Adam Johns
    Mar 22, 2018 at 21:58

3 Answers 3

34

shim's answer was spot on, but I'd like to provide a few more clarifying details.

On December 15, 2017, Brazilian President Michel Temer signed a decree changing the start of daylight saving time (DST) to the first Sunday of November, beginning in 2018. It looks like iOS 11.2.6 is the first version of iOS to pick up this change which is why this issue wasn't seen in previous iOS versions.

The problem with the time change moving to the first Sunday in November is that the first Sunday in November can end up being (and is in the case of 2020 and 2026) November 1st. Why does this present a problem? Well it just so happens that Brazil also has their clocks "spring forward" at midnight (going from 11:59pm to 1:00am) when DST starts.

Why does a date of November 1st at midnight present a problem? Well when we ask iOS to give us a Date object based on only month/year as the user enters (e.g. 11/20), the Date object defaults to the first day of the month at midnight. This is problematic because on years where DST starts on November 1st at midnight in Brazil, that time technically never exists. So when we ask iOS to give us a Date object for 11/20, iOS tries to return 11/01/2020 @ 00:00, but it is returning nil since that time never exists.

So this issue could technically happen in any timezone that observes DST that can start on the 1st of a month at midnight. After going through some countries and checking their DST rules, I've only found Brazil to allow DST to start on the 1st of a month at midnight.

To fix this issue I effectively did the following:

let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "MM/yy"
let dateFormatterWithTime = DateFormatter()
dateFormatterWithTime.dateFormat = "MM/yy HH:mm"
if let date = dateFormatter.date(from: "11/20") ??
    dateFormatterWithTime.date(from: "11/20 03:00") {
    print("got the date: \(date)")
} else {
    print("failed getting the date")
}

03:00 is an arbitrary amount of time in the future that should safely account for DST changes from midnight.

3
  • This is brilliant!
    – Behdad
    Apr 25, 2018 at 16:25
  • Note that you should never rely on a specific amount of daylight savings time hours considering that it might change at any time. Note that Brazil no longer has DST since the election of the actual president Bolsonaro which revoked the DST. No need to set a specific time. What you need is to set the date formatter's calendar property.
    – Leo Dabus
    May 18, 2020 at 23:40
  • And BTW now that there is no more DST in Brazil it no longer returns nil for that date.
    – Leo Dabus
    May 18, 2020 at 23:55
14

The reason it doesn't work for that one timezone (Sao Paulo) is because of considerations specific to that timezone, namely daylight saving. Daylight saving in Brazil begins on the first Sunday in November, which happens to be November 1 in 2020.

The time that it would default to does not exist, so it returns nil.

You can play around with it by changing the formatter to include time and seeing exactly when the date formatter starts returning nil.

Have run into similar confusion before; Dates and time zones are tricky.

If you're using the date formatter for displaying dates to a user, you should typically not override their device settings and you probably want to avoid using a fixed date format, and should be aware of the potential for the date formatter to return a nil value (Swift optionals to the rescue). But if you're using it for internal dates, such as for your API, you should consider setting an explicit locale/time zone on your date formatter so these kinds of things don't happen, for example:

dateFormatter.locale = Locale(identifier: "en_US_POSIX")
dateFormatter.timeZone = TimeZone(abbreviation: "UTC")
1

Another way to make it work is to set the DateFormatter time zone to UTC. So your code would look like:

let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "MM/yy"
dateFormatter.timeZone = TimeZone(abbreviation: "UTC")

if let date = dateFormatter.date(from: "11/20") {
    print("got the date: \(date)")
} else {
    print("failed getting the date")
}

By doing this, the code will print the following:

got the date: 2020-11-01 00:00:00 +0000

0

Not the answer you're looking for? Browse other questions tagged or ask your own question.