ISO8601DateFormatter doesn't parse ISO date string

19,605

Solution 1

Prior to macOS 10.13 / iOS 11 ISO8601DateFormatter does not support date strings including milliseconds.

A workaround is to remove the millisecond part with regular expression.

let isoDateString = "2017-01-23T10:12:31.484Z"
let trimmedIsoString = isoDateString.replacingOccurrences(of: "\\.\\d+", with: "", options: .regularExpression)
let formatter = ISO8601DateFormatter()
let date = formatter.date(from: trimmedIsoString)

In macOS 10.13+ / iOS 11+ a new option is added to support fractional seconds:

static var withFractionalSeconds: ISO8601DateFormatter.Options { get }

let isoDateString = "2017-01-23T10:12:31.484Z"
let formatter = ISO8601DateFormatter()
formatter.formatOptions =  [.withInternetDateTime, .withFractionalSeconds]
let date = formatter.date(from: isoDateString)

Solution 2

For people that are not ready to go to iOS 11 yet, you can always create your own formatter to handle milliseconds, e.g.:

extension DateFormatter {
    static var iSO8601DateWithMillisec: DateFormatter {
        let dateFormatter = DateFormatter()
        dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"
        return dateFormatter
    }
}

Usage:

let formater = DateFormatter.iSO8601DateWithMillisec
let date = formater.date(from: "2017-01-23T10:12:31.484Z")!
print(date) // output: 2017-01-23 10:12:31 +0000

It is slightly more elegant than writing a regex to strip out the milliseconds from the input string.

Solution 3

Maybe this will help to decode slightly different formats:

extension JSONDecoder {
    enum DateDecodeError: String, Error {
        case invalidDate
    }

    static var bestDateAttemptDecoder: JSONDecoder {
        let decoder = JSONDecoder()
        decoder.dateDecodingStrategy = .custom({ (decoder) -> Date in
            let container = try decoder.singleValueContainer()
            if let dateSecs = try? container.decode(Double.self) {
                return Date(timeIntervalSince1970: dateSecs)
            }

            if let dateSecs = try? container.decode(UInt.self) {
                return Date(timeIntervalSince1970: TimeInterval(dateSecs))
            }

            let dateStr = try container.decode(String.self)
            let isoFormatter = ISO8601DateFormatter()
            isoFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
            if let date = isoFormatter.date(from: dateStr) {
                return date
            }

            isoFormatter.formatOptions = [.withInternetDateTime ]
            if let date = isoFormatter.date(from: dateStr) {
                return date
            }

            log.warning("Cannot decode date");
            throw DateDecodeError.invalidDate
        })

        return decoder
    }
}

From: https://gist.github.com/th3m477/442a0d1da6354dd3b84e3b71df5dca6a

Solution 4

I encountered same issue some months ago. And here's my solution for reference:

// *****************************************
// MARK: - Formatter extension
// *****************************************
extension Formatter {
    static let iso8601: ISO8601DateFormatter = {
        let formatter = ISO8601DateFormatter()
        formatter.timeZone = TimeZone.current 
        formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
        return formatter
    }()
    static let iso8601NoSecond: ISO8601DateFormatter = {
        let formatter = ISO8601DateFormatter()
        formatter.timeZone = TimeZone.current 
        formatter.formatOptions = [.withInternetDateTime]
        return formatter
    }()
}

// *****************************************
// MARK: - ISO8601 helper
// *****************************************
    func getDateFrom(DateString8601 dateString:String) -> Date?
    {
        if let date = Formatter.iso8601.date(from: dateString)  {
            return date
        }
        if let date = Formatter.iso8601NoSecond.date(from: dateString)  {
            return date
        }
        return nil
    }

// *****************************************
// usage
// *****************************************
    let d = getDateFrom(DateString8601: "2017-01-23T10:12:31.484Z")
    print("2017-01-23T10:12:31.484Z millis= ", d?.timeIntervalSinceReferenceDate)

    let d2 = getDateFrom(DateString8601: "2017-01-23T10:12:31Z")
    print("2017-01-23T10:12:31Z millis= ", d2?.timeIntervalSinceReferenceDate)


// *****************************************
// result
// *****************************************
2017-01-23T10:12:31.484Z millis=  Optional(506859151.48399997)
2017-01-23T10:12:31Z millis=  Optional(506859151.0)
Share:
19,605
mhergon
Author by

mhergon

Updated on June 25, 2022

Comments

  • mhergon
    mhergon almost 2 years

    I'm trying to parse this

    2017-01-23T10:12:31.484Z

    using native ISO8601DateFormatter class provided by iOS 10 but always fails. If the string not contains milliseconds, the Date object is created without problems.

    I'm tried this and many options combination but always fails...

    let formatter = ISO8601DateFormatter()
    formatter.timeZone = TimeZone(secondsFromGMT: 0)
    formatter.formatOptions = [.withInternetDateTime, .withDashSeparatorInDate, .withColonSeparatorInTime, .withColonSeparatorInTimeZone, .withFullTime]
    

    Any idea? Thanks!

  • Fogmeister
    Fogmeister over 6 years
    When was that added? I filed a radar about 3 weeks ago because it didn't have fractional seconds. It still doesn't seem to allow for selecting the number of decimal places though :( openradar.appspot.com/radar?id=6095965782016000
  • vadian
    vadian over 6 years
    It was added with the macOS 10.13 / iOS 11 SDK
  • Fogmeister
    Fogmeister over 6 years
    No, it really wasn't. That wasn't there 3 weeks ago. Must have been added very recently. It may support back to 11.0 but it must only have been added in the past couple days. I even showed a colleague that it wasn't there just last week. :)
  • vadian
    vadian over 6 years
    Could be. I read about it in the release notes / API changes when Xcode 9 was released but I didn't prove that the API was really available.
  • 4b0
    4b0 almost 6 years
    Mm282, while this link may answer the question, it is better to include the essential parts of the answer here and provide the link for reference. Answers that are little more than a link may be deleted.
  • craft
    craft over 5 years
    stackoverflow.com/questions/28016578/… also provides some additional ideas and context re: pre 10.13 and post 10.13 approaches
  • hash3r
    hash3r about 5 years
    Be careful withFractionalSeconds crashes up to 11.2. it is fixed in 11.2+
  • zero3nna
    zero3nna about 5 years
    thx @hash3r i was wondering if I'm the only one that run into this problem
  • vauxhall
    vauxhall about 4 years
    I was crashing for us also, documentation is misleading, thanks @hash3r
  • Leo Dabus
    Leo Dabus almost 4 years
    formatter.timeZone = TimeZone.current is wrong. Why do you think Apple made ISO8601DateFormatter timezone defaults to zero seconds from GMT?