How can I parse / create a date time stamp formatted with fractional seconds UTC timezone (ISO 8601, RFC 3339) in Swift? How can I parse / create a date time stamp formatted with fractional seconds UTC timezone (ISO 8601, RFC 3339) in Swift? swift swift

How can I parse / create a date time stamp formatted with fractional seconds UTC timezone (ISO 8601, RFC 3339) in Swift?


Swift 4 • iOS 11.2.1 or later

extension ISO8601DateFormatter {    convenience init(_ formatOptions: Options) {        self.init()        self.formatOptions = formatOptions    }}

extension Formatter {    static let iso8601withFractionalSeconds = ISO8601DateFormatter([.withInternetDateTime, .withFractionalSeconds])}

extension Date {    var iso8601withFractionalSeconds: String { return Formatter.iso8601withFractionalSeconds.string(from: self) }}

extension String {    var iso8601withFractionalSeconds: Date? { return Formatter.iso8601withFractionalSeconds.date(from: self) }}

Usage:

Date().description(with: .current)  //  Tuesday, February 5, 2019 at 10:35:01 PM Brasilia Summer Time"let dateString = Date().iso8601withFractionalSeconds   //  "2019-02-06T00:35:01.746Z"if let date = dateString.iso8601withFractionalSeconds {    date.description(with: .current) // "Tuesday, February 5, 2019 at 10:35:01 PM Brasilia Summer Time"    print(date.iso8601withFractionalSeconds)           //  "2019-02-06T00:35:01.746Z\n"}

iOS 9 • Swift 3 or later

extension Formatter {    static let iso8601withFractionalSeconds: DateFormatter = {        let formatter = DateFormatter()        formatter.calendar = Calendar(identifier: .iso8601)        formatter.locale = Locale(identifier: "en_US_POSIX")        formatter.timeZone = TimeZone(secondsFromGMT: 0)        formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSXXXXX"        return formatter    }()}

Codable Protocol

If you need to encode and decode this format when working with Codableprotocol you can create your own custom date encoding/decoding strategies:

extension JSONDecoder.DateDecodingStrategy {    static let iso8601withFractionalSeconds = custom {        let container = try $0.singleValueContainer()        let string = try container.decode(String.self)        guard let date = Formatter.iso8601withFractionalSeconds.date(from: string) else {            throw DecodingError.dataCorruptedError(in: container,                  debugDescription: "Invalid date: " + string)        }        return date    }}

and the encoding strategy

extension JSONEncoder.DateEncodingStrategy {    static let iso8601withFractionalSeconds = custom {        var container = $1.singleValueContainer()        try container.encode(Formatter.iso8601withFractionalSeconds.string(from: $0))    }}

Playground Testing

let dates = [Date()]   // ["Feb 8, 2019 at 9:48 PM"]

encoding

let encoder = JSONEncoder()encoder.dateEncodingStrategy = .iso8601withFractionalSecondslet data = try! encoder.encode(dates)print(String(data: data, encoding: .utf8)!)

decoding

let decoder = JSONDecoder()decoder.dateDecodingStrategy = .iso8601withFractionalSecondslet decodedDates = try! decoder.decode([Date].self, from: data)  // ["Feb 8, 2019 at 9:48 PM"]

enter image description here


Remember to set the locale to en_US_POSIX as described in Technical Q&A1480. In Swift 3:

let date = Date()let formatter = DateFormatter()formatter.locale = Locale(identifier: "en_US_POSIX")formatter.timeZone = TimeZone(secondsFromGMT: 0)formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ"print(formatter.string(from: date))

The issue is that if you're on a device which is using a non-Gregorian calendar, the year will not conform to RFC3339/ISO8601 unless you specify the locale as well as the timeZone and dateFormat string.

Or you can use ISO8601DateFormatter to get you out of the weeds of setting locale and timeZone yourself:

let date = Date()let formatter = ISO8601DateFormatter()formatter.formatOptions.insert(.withFractionalSeconds)  // this is only available effective iOS 11 and macOS 10.13print(formatter.string(from: date))

For Swift 2 rendition, see previous revision of this answer.


If you want to use the ISO8601DateFormatter() with a date from a Rails 4+ JSON feed (and don't need millis of course), you need to set a few options on the formatter for it to work right otherwise the the date(from: string) function will return nil. Here's what I'm using:

extension Date {    init(dateString:String) {        self = Date.iso8601Formatter.date(from: dateString)!    }    static let iso8601Formatter: ISO8601DateFormatter = {        let formatter = ISO8601DateFormatter()        formatter.formatOptions = [.withFullDate,                                          .withTime,                                          .withDashSeparatorInDate,                                          .withColonSeparatorInTime]        return formatter    }()}

Here's the result of using the options versus not in a playground screenshot:

enter image description here