Search code examples
iosswiftfoundation

DateFormatter returns nil with specific combination of TimeZone and Locale


In our app there is the issue with creating a date from string but is only reproducible with a very specific combination. Unfortunately, there is no way of getting it from the user that experienced the issue, so I decided to just go for it and try every possible combination:

import Foundation

var dateOnlyDateFormatter: (String, String) -> DateFormatter = { timeZoneS, localeS in
    let formatter = DateFormatter()
    formatter.dateFormat = "yyyy-MM-dd"
    formatter.timeZone = TimeZone(identifier: timeZoneS)
    formatter.locale = Locale(identifier: localeS)
    return formatter
}

let date = "2022-05-27"
let time = "06:15"

for timeZone in TimeZone.knownTimeZoneIdentifiers {
    for locale in Locale.availableIdentifiers {
        let dateFormatter = dateOnlyDateFormatter(timeZone, locale)
        let printDate = dateFormatter.date(from: date)
        if printDate == nil {
            print("TimeZone: \(timeZone), Locale: \(locale)")
        }
    }
}

The result:

TimeZone: America/Asuncion, Locale: ar_SA
TimeZone: America/Asuncion, Locale: en_SA

I am not too sure what is the best way to handle this issue. Obviously our BE could return date using one specific Locale, like en_US_POSIX, but I have very little control over that, being a part of a much bigger older system. Has anybody experienced an issue like that?


Solution

  • If you read the "Working With Fixed Format Date Representations" section of the DateFormatter docs, you'll find:

    For most fixed formats, you should also set the locale property to a POSIX locale ("en_US_POSIX"), and set the timeZone property to UTC.

    You should probably just follow the advice here... But here's possibly why SA and the Paraguay timezone produces nil.

    Further down that section, there is a link to a technical Q&A where this is explained in more detail. The part that is most related to your problem is:

    A user can change their calendar (using System Preferences > Language & Region > Calendar on OS X, or Settings > General > International > Calendar on iOS). In that case NSDateFormatter will treat the numbers in the string you parse as if they were in the user's chosen calendar. For example, if the user selects the Buddhist calendar, parsing the year "2010" will yield an NSDate in 1467, because the year 2010 on the Buddhist calendar was the year 1467 on the (Gregorian) calendar that we use day-to-day.

    In the locale SA, the numbers of your date string seem to be interpreted using the Islamic Calendar. Take a look at today's date when formatted with en_SA and America/New_York.

    let dateFormatter = dateOnlyDateFormatter("America/New_York", "en_SA")
    let printDate = dateFormatter.string(from: .init())
    print(printDate)
    // 1443-10-26
    

    Also take a look at the non-nil dates that is parsed by en_SA and America/New_York

    let dateFormatter = dateOnlyDateFormatter("America/New_York", "en_SA")
    let printDate = dateFormatter.date(from: date)
    print(printDate)
    // 2583-10-05 04:00:00 +0000
    

    Notice that 10-05 is the first Sunday of the year 2583 (See this calendar). If Paraguay still uses the same DST rules as it does now in 2583, then it would mean that there is a DST gap transition at 2583-10-05 00:00:00, starting the DST period. The hour starting from 00:00:00 would be skipped, so 00:00:00 would not exist.

    When parsing a date only, DateFormatter would try to set the time components to be 00:00:00 in the timezone of the formatter, but 00:00:00 does not exist, so the parsing fails.


    In any case, just set locale to posix and timeZone to UTC when you have set dateFormat.