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?
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.
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")
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With