Search code examples
dateswifttimeiso8601rfc3339

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


How to generate a date time stamp, using the format standards for ISO 8601 and RFC 3339?

The goal is a string that looks like this:

"2015-01-01T00:00:00.000Z"

Format:

  • year, month, day, as "XXXX-XX-XX"
  • the letter "T" as a separator
  • hour, minute, seconds, milliseconds, as "XX:XX:XX.XXX".
  • the letter "Z" as a zone designator for zero offset, a.k.a. UTC, GMT, Zulu time.

Best case:

  • Swift source code that is simple, short, and straightforward.
  • No need to use any additional framework, subproject, cocoapod, C code, etc.

I've searched StackOverflow, Google, Apple, etc. and haven't found a Swift answer to this.

The classes that seem most promising are NSDate, NSDateFormatter, NSTimeZone.

Related Q&A: How do I get an ISO 8601 date on iOS?

Here's the best I've come up with so far:

var now = NSDate()
var formatter = NSDateFormatter()
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"
formatter.timeZone = NSTimeZone(forSecondsFromGMT: 0)
println(formatter.stringFromDate(now))

Solution

  • Swift 5.5 • iOS 15 • Xcode 13 or later

    extension Date.ISO8601FormatStyle {
        static let iso8601withFractionalSeconds: Self = .init(includingFractionalSeconds: true)
    }
    

    extension ParseStrategy where Self == Date.ISO8601FormatStyle {
        static var iso8601withFractionalSeconds: Date.ISO8601FormatStyle { .iso8601withFractionalSeconds }
    }
    

    extension FormatStyle where Self == Date.ISO8601FormatStyle {
        static var iso8601withFractionalSeconds: Date.ISO8601FormatStyle { .iso8601withFractionalSeconds }
    }
    

    extension Date {
    
        init(iso8601withFractionalSeconds parseInput: ParseStrategy.ParseInput) throws {
            try self.init(parseInput, strategy: .iso8601withFractionalSeconds)
        }
    
        var iso8601withFractionalSeconds: String {
            formatted(.iso8601withFractionalSeconds)
        }
    }
    

    extension String {
        func iso8601withFractionalSeconds() throws -> Date {
            try .init(iso8601withFractionalSeconds: self)
        }
    }
    

    extension JSONDecoder.DateDecodingStrategy {
        static let iso8601withFractionalSeconds = custom {
            try .init(iso8601withFractionalSeconds: $0.singleValueContainer().decode(String.self))
        }
    }
    

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

    Usage:

    let date: Date = .now  // "19 Nov 2023 at 11:29 PM"
    date.description(with: .current)  // "Sunday, 19 November 2023 at 11:29:40 PM Brasilia Standard Time"
    let dateString = date.iso8601withFractionalSeconds  // "2023-11-20T02:29:40.920Z"
    
    if let date = try? dateString.iso8601withFractionalSeconds() {
        date.description(with: .current) // "Sunday, 19 November 2023 at 11:29:40 PM Brasilia Standard Time"
        print(date.iso8601withFractionalSeconds)  // "2023-11-20T02:29:40.920Z\n"
    }
    


    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"
    }
    

    let dates: [Date] = [.now]
    
    let encoder = JSONEncoder()
    encoder.dateEncodingStrategy = .iso8601withFractionalSeconds
    let data = try! encoder.encode(dates)
    print(String(data: data, encoding: .utf8)!)  // "["2023-11-20T02:11:29.158Z"]\n"
    
    let decoder = JSONDecoder()
    decoder.dateDecodingStrategy = .iso8601withFractionalSeconds
    let decodedDates = try! decoder.decode([Date].self, from: data)
    print(decodedDates)   // "[2023-11-20 02:11:29 +0000]\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 Codable protocol 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 = .iso8601withFractionalSeconds
    let data = try! encoder.encode(dates)
    print(String(data: data, encoding: .utf8)!)
    

    decoding

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

    enter image description here