44

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!

mhergon
  • 1,688
  • 1
  • 18
  • 39
  • 1
    see this once http://stackoverflow.com/questions/28016578/swift-how-to-create-a-date-time-stamp-and-format-as-iso-8601-rfc-3339-utc-tim/28016619 – Anbu.Karthik Jan 25 '17 at 09:29
  • Yes, I know that using `dateFormat` is the way, but I want to use native class of iOS 10... :( – mhergon Jan 25 '17 at 09:37

4 Answers4

85

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)
vadian
  • 274,689
  • 30
  • 353
  • 361
  • 1
    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 :( http://openradar.appspot.com/radar?id=6095965782016000 – Fogmeister Oct 23 '17 at 08:46
  • 2
    It was added with the [macOS 10.13 / iOS 11 SDK](https://developer.apple.com/documentation/foundation/iso8601dateformatter.options/2923300-withfractionalseconds) – vadian Oct 23 '17 at 08:48
  • 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. :) – Fogmeister Oct 23 '17 at 08:49
  • 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. – vadian Oct 23 '17 at 08:52
  • https://stackoverflow.com/questions/28016578/swift-how-to-create-a-date-time-stamp-and-format-as-iso-8601-rfc-3339-utc-tim also provides some additional ideas and context re: pre 10.13 and post 10.13 approaches – craft Nov 19 '18 at 00:56
  • 11
    Be careful withFractionalSeconds crashes up to 11.2. it is fixed in 11.2+ – hash3r Apr 11 '19 at 09:41
  • thx @hash3r i was wondering if I'm the only one that run into this problem – zero3nna Apr 15 '19 at 15:42
  • 1
    I was crashing for us also, documentation is misleading, thanks @hash3r – vicegax Apr 01 '20 at 15:05
9

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.

Yuchen
  • 30,852
  • 26
  • 164
  • 234
4

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

Yule
  • 9,668
  • 3
  • 51
  • 72
mm282
  • 453
  • 3
  • 7
  • 1
    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.](//stackoverflow.com/help/deleted-answers) – 4b0 Jul 26 '18 at 08:44
3

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)
Orange
  • 349
  • 3
  • 13
  • `formatter.timeZone = TimeZone.current` is wrong. Why do you think Apple made ISO8601DateFormatter timezone defaults to zero seconds from GMT? – Leo Dabus Jul 16 '20 at 17:02