Swift Tip: Unexpected Results from a Date Formatter
We have almost finished rewriting our Swift Talk backend in Swift. While testing it against the production database, we stumbled over a crashing issue with our use of a date formatter. Today's lesson: be careful when force unwrapping the result of string to date conversions, and always read the documentation carefully! ๐ฌ
Here's how we set up the formatter:
fileprivate let formatter: DateFormatter = {
let dateFormatter = DateFormatter()
dateFormatter.locale = Locale(identifier: "en_US_POSIX")
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss"
return dateFormatter
}()
On macOS 10.12+ and iOS 10+ you could use an ISO8601DateFormatter
for this, which makes the setup easier and would have avoided the problem we encountered. However, we needed this to work on Linux.
Using this date formatter, we ran into a strange issue with a seemingly well-formed date string:
formatter.date(from: "2017-03-26T00:53:31")! // this works
formatter.date(from: "2017-03-26T02:53:31")! // this crashes!
This puzzled us for a while. As a first thought, we might be dealing with a leap year issue, but the crashing example has a date in March. We asked ourselves, what else happens in March? Then it dawned: the beginning of daylight saving time! Sure enough, in 2017 German daylight saving started on March 26th, at 2am.
The date and time in the string we were trying to parse didn't actually exist. At the beginning of daylight saving the clock skipped the hour from 2am to 3am, and our date formatter seemed to know about this โย as would we if we'd read the documentation more thoroughly!
We checked the time zone of our date formatter:
print(formatter.timeZone!)
// Europe/Berlin (current)
Since we didn't specify otherwise, the date formatter uses the system time zone, which is set to our location. Therefore, it's daylight saving time aware. The fix is easy, we just have to specify a time zone with a fixed temporal offset instead of a location.
We want our date strings to be interpreted as coordinated universal time (UTC):
fileprivate let formatter: DateFormatter = {
let dateFormatter = DateFormatter()
dateFormatter.timeZone = TimeZone(secondsFromGMT: 0)
// ...
return dateFormatter
}()
formatter.date(from: "2017-03-26T02:53:31")! // now it works!
Of course, you should still be careful about force-unwrapping the result since any non-existing date/time value will return nil
, though happily, the string in question now parses correctly. ๐
We'll have more to say about our backend rewrite soon. In the meantime, you can enjoy Swift Talk in it's original Ruby version.