Note: see caveats at the bottom of this post. Some of my conclusions in the body of this post are wrong and motivated by a misunderstanding of NSDateFormatter’s documented behavior. I’m leaving the post here for reference because I still think it could help somebody trying to understand similar behavior in their own app, but don’t take the griping too seriously…
Apple’s NSDateFormatter class supports a method of converting a date to a string by use of a date format string. For example, the date format string I use in MarsEdit to supply dates in ISO8601 format to blog servers:
@"yyyyMMdd'T'HH:mm:ss'Z'"
That “HH” is supposed to reflect the hour as a zero-padded number between 00 and 23. And it does, or at least it has, ever since I started using this formatting string in MarsEdit eight years ago.
Starting very recently, I think with 10.10.3 (Edit: nope, not new after all, see end of post), NSDateFormatter may return a string formatted for the user’s 12-hour clock preference, and with a troubling “am” or “pm” component embedded within. So instead of a bona fide standard ISO 8601 date for the above format, MarsEdit is now prone to receive something like this:
20150526T1:58:42 pmZ
Oops! And Ugh! The whole point of using NSDateFormatter’s dateFormat string, I thought, was to specifically generate strings that defy the user’s preferences, but that comply with a very specific set of rules. In fact, Apple encourages using date format strings in their documentation:
There are broadly speaking two situations in which you need to use custom formats: 1. For fixed format strings, like Internet dates. 2. For user-visible elements that don’t match any of the existing styles
Yes, internet dates! Thank you. Well, no thanks, I guess. The current documentation also goes on to offer some caveats, particularly with respect to iOS, where I guess users have been empowered to override the 12/24-hour clock setting for longer than they have on the Mac. And in general, they warn:
Although in principle a format string specifies a fixed format, by default NSDateFormatter still takes the user’s preferences (including the locale setting) into account.
The specific scenario where this crops up for me is if the user has set their Mac’s region to one that defaults to 24-hour time, but has then specifically chosen to uncheck the 24-hour time option:
The behavior doesn’t occur, for example, if the user’s region defaults to 12-hour time as it does in the United States. It only occurs when a region’s defaults have been specifically overridden.
If you want predictable behavior from NSDateFormatter, you must set an explicit NSLocale on the formatter before requesting any string generation. I’m not sure it matters which locale you set, the key seems to be setting it to anything but the default to avoid this strange deference to the user’s default settings.
I’ll be fixing this by setting the locale on the NSDateFormatter to “en_US” because, being the very locale that my Mac is most often configured to use, I’ll be more likely to notice if the workaround stops working at some point in the future. I reported a bug (Radar 21105874) because it seems to be there should be a more straight-forward means of expressing to NSDateFormatter that you want to perform a very literal conversion, one that is guaranteed to not take into consideration any user-provided customizations of date and time formatting.
Hopefully this post will help other developers notice and repair the faulty handling of date strings in their apps, before too many of your customers run into the problem first!
Update: Many thanks to several people on Twitter noting that Apple specifically recommends using the “en_US_POSIX” locale for this purpose. I am still a bit annoyed that the behavior changed out from under me, but it sounds like setting the locale explicitly to this computer-y locale is the right solution for the long term.
Update 2: Well I made a few wrong assumptions before writing this post. After further testing I’ve confirmed the problematic “new” behavior is at least the case in 10.9.4 and possibly earlier as well. I’m now inclined to think this has been my bug all along, but I still think I’ll file a bug with Apple encouraging them to update the documentation to stress that setting a locale on the formatter is important.
Update 3: It turns out the documentation goes into some detail about the need to specify a locale, but I overlooked it because it was in a section about “parsing date strings” (not what I’m doing here). I filed Radar 21115452, requesting better documentation about the need to set a locale in the section pertinent to either parsing strings or generating them.