249

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))
Community
  • 1
  • 1
joelparkerhenderson
  • 34,808
  • 19
  • 98
  • 119
  • 7
    Note that **iOS10+ SIMPLY INCLUDES ISO 8601 BUILT-IN** .. it will just autocomplete for you. – Fattie Apr 27 '17 at 12:46
  • 3
    @Fattie And - how can it handle that last .234Z milliseconds Zulu/UTC part of the timestamp? Answer: Matt Longs @ https://stackoverflow.com/a/42101630/3078330 – smat88dd Jun 09 '17 at 12:30
  • 1
    @smat88dd -- fantastic tip, thanks. I had no clue there were "options on a formatter", weird and wild! – Fattie Jun 09 '17 at 17:03
  • I'm looking for a solution that works on linux. – neoneye Sep 20 '18 at 14:23
  • @neoneye Just use the old version (plain DateFormatter) and change the calendar iso8601 to gregorian https://stackoverflow.com/a/28016692/2303865 – Leo Dabus Oct 31 '18 at 12:38

14 Answers14

481

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

Leo Dabus
  • 229,809
  • 59
  • 489
  • 571
  • 3
    It'd be useful to add opposite conversion extension: `extension String { var dateFormattedISO8601: NSDate? {return NSDate.Date.formatterISO8601.dateFromString(self)} }` – Nat May 30 '16 at 10:50
  • 1
    Just an note that this looses a bit of precision so it's important to make sure equality of dates is compared via the generated string and not timeInterval. `let now = NSDate() let stringFromDate = now.iso8601 let dateFromString = stringFromDate.dateFromISO8601! XCTAssertEqual(now.timeIntervalSince1970, dateFromString.timeIntervalSince1970)` – pixelrevision Jun 18 '16 at 16:59
  • 1
    In [RFC3339](https://www.ietf.org/rfc/rfc3339.txt) we can find a note _"NOTE: ISO 8601 defines date and time separated by "T". Applications using this syntax may choose, for the sake of readability, to specify a full-date and full-time separated by (say) a space character."_ Does it cover as well date format without `T` eg: `2016-09-21 21:05:10+00:00` ? – manRo Sep 29 '16 at 09:00
  • @LeoDabus - thanks again. Look man, here's a puzzler for you: http://stackoverflow.com/questions/43808693/with-data-not-nsdata-in-fact-how-actually-do-you-make-a-utf8-version-of-a-jpe – Fattie May 05 '17 at 15:34
  • @LeoDabus Could you explain the reason why you extend Formatter instead of DateFormatter please? – Leon Jun 21 '17 at 12:05
  • Doesn't make any difference. You can extend DateFormatter instead if you would like to. – Leo Dabus Jun 21 '17 at 12:36
  • 1
    ***THIS DOES NOT WORK ON LINUX***. If you are targeting Linux as well, you *need* to remove the `Calendar(identifier: .iso8601)` line, or it **will** segfault and crash. – thislooksfun Jul 04 '17 at 06:33
  • 3
    @LeoDabus yes, but this is the first result for "Swift iso8601". My comment was meant to warn other developers who come across this in the future and was not directed at OP. – thislooksfun Jul 05 '17 at 01:10
  • @pixelrevision if you need to make sure the date saved into the server and the date returned are equal you need to save the date as a Double ( timeIntervalSinceReferenceDate). Check this https://stackoverflow.com/a/47502712/2303865 – Leo Dabus Dec 13 '17 at 03:58
  • @keno thanks It used to crash when setting `ISO8601DateFormatter` formatOptions to `[.withInternetDateTime, .withFractionalSeconds]` I will update the answer accordingly – Leo Dabus Jan 23 '18 at 02:58
  • iOS 11 now has support for fractional seconds with option `NSISO8601DateFormatWithFractionalSeconds` https://developer.apple.com/documentation/foundation/nsiso8601dateformatoptions?language=swift – keno Jan 23 '18 at 02:58
  • At least with Swift 4, this will not even compile (first code sample): ```static let iso8601: DateFormatter = { return ISO8601DateFormatter() }``` `ISO8601DateFormatter` subclasses `Formatter`, not `DateFormatter`. I assume, the return value should actually be the `ISO8601DateFormatter`. – NeverwinterMoon Feb 13 '18 at 10:02
  • `static let iso8601 = ISO8601DateFormatter([.withInternetDateTime, .withFractionalSeconds])` crashes for me on iOS11. .withFractionalSeconds causes the issue. – Kirill Jul 31 '19 at 18:23
  • use the plain DateFormatter version for older OSs. I've read thhat you need minimum > 11.2 – Leo Dabus Jul 31 '19 at 18:35
  • Test fails with XCTAssertEqual – Soheil Novinfard Feb 09 '20 at 12:41
  • @SoheilNovinfard the only way to preserve the date as it is is to send the timeIntervalSinceReferenceDate to your server. – Leo Dabus Feb 09 '20 at 13:05
  • https://stackoverflow.com/questions/59867206/swift-why-isnt-my-date-object-thats-equatable-equal-after-converting-it-to-a/59867254 and https://stackoverflow.com/questions/47502591/convert-a-date-absolute-time-to-be-sent-received-across-the-network-as-data-in/47502712 – Leo Dabus Feb 09 '20 at 13:07
  • Please check out this question: https://stackoverflow.com/questions/60136982/how-to-resolve-error-in-unit-testing-when-we-have-date-comparison-in-codable – Soheil Novinfard Feb 09 '20 at 13:12
  • @SoheilNovinfard Check the links I posted above. DateFormatter discards most of the fractional seconds in a date. Try with this one https://stackoverflow.com/a/47502712/2303865 – Leo Dabus Feb 09 '20 at 13:14
  • Another option is to use `dateDecodingStrategy` set to `.deferredToDate` – Leo Dabus Feb 09 '20 at 13:20
  • I can't change it to `. deferredToDate`, it comes from the remote JSON and I don't decide about the date format. None of the answers talk about decodable, please re-open my question – Soheil Novinfard Feb 09 '20 at 13:22
  • I already said what's going on there. There is no way to assert it is equal when you are discarding the FloatingPoint nanoseconds – Leo Dabus Feb 09 '20 at 13:23
  • There is nothing as a revenge. Your answer is not helpful in the situation I described, Although it is similar, but it's not the same and can't resolve it. You could let the other users check and think about the answers as well, not closing the question, it's against openness soul of the community. – Soheil Novinfard Feb 09 '20 at 13:36
  • @LeoDabus Yes I agree Leo, after rethinking about the story now i think I shouldn't have downvoted them, because they are related to different questions (that's the real reason I asked you to re-open it and I believe it should be re-opened). I will undo it after the time limit. Thanks anyway – Soheil Novinfard Feb 09 '20 at 14:11
  • https://stackoverflow.com/questions/60136982/how-to-resolve-error-in-unit-testing-when-we-have-date-comparison-in-codable#comment106365275_60136982 – Leo Dabus Feb 09 '20 at 16:49
  • I think someone said that precision is lost in decoding. But just conforming. I have a date as part of a larger json object. I set the decoding strategy as `iso8601withFractionalSeconds` and `try decoder.decode(UserInfo.self, from: data)`. In the databse, value stored is `"2021-02-19T07:23:09.799Z"` but, `logger.debug("First online: \(userInfo.activity.firstSeenDate)")` prints `First online: 2021-02-19 07:23:09 +0000`. Is this the place where precision is lost? Is +0000 supposed to be milliseconds? – Parth Feb 19 '21 at 07:34
  • @Parth sorry for the late reply but there is no way to avoid loss of precision unless you send the `timeIntervalSinceReferenceDate` to your backEnd and use the default Codable encoding/decoding strategy `deferredToDate`. Another option is to generate the ISO8601 date String, decode it and encode the resulting date again before sending it to the backend. – Leo Dabus Jun 01 '23 at 20:42
58

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.13
print(formatter.string(from: date))

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

Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • why we should set the locale to en_US_POSIX ? even if we are not in US ? – axunic Dec 13 '17 at 04:38
  • 2
    Well, you need _some_ consistent locale and the convention of the ISO 8601/RFC 3999 standards is that format offered by `en_US_POSIX`. It's the _lingua franca_ for exchanging dates on the web. And you can't have it misinterpreting dates if one calendar was used on device when saving a date string and another when the string is read back in later. Also, you need a format that is guaranteed to never change (which is why you use `en_US_POSIX` and not `en_US`). See [Technical Q&A 1480](https://developer.apple.com/library/ios/qa/qa1480/_index.html) or those RFC/ISO standards for more information. – Rob Dec 13 '17 at 10:23
  • `.withFractionalSeconds` - This is what I needed to support processing milliseconds. Thank you! – Rob S. Dec 26 '22 at 18:15
47

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

Matt Long
  • 24,438
  • 4
  • 73
  • 99
  • You would need to include in the options also the `.withFractionalSeconds` but I already tried that and it keeps throwing an error `libc++abi.dylib: terminating with uncaught exception of type NSException`. – Leo Dabus Oct 12 '17 at 01:36
  • @MEnnabah It works fine for me in Swift 4. Are you getting an error? – Matt Long Nov 06 '17 at 16:22
  • @LeoDabus, got the same error as yours, did you solve it? – freeman Dec 13 '17 at 03:36
  • custom JSONDecoder DateDecodingStrategy https://stackoverflow.com/a/46458771/2303865 – Leo Dabus Dec 13 '17 at 03:40
  • @freeman If you would like to preserve the Date with all its fractional seconds I suggest to use a double (time interval since reference date) when saving/receiving your date to the server. And use the default date decoding strategy `.deferredToDate` when using Codable protocol – Leo Dabus Dec 13 '17 at 03:47
  • @LeoDabus, thanks for your replies, finally I took the way to let the api return date time string without fractional section, and the reason for why not use double value is for the human readable of API request/response, I learned it from this post: http://apiux.com/2013/03/20/5-laws-api-dates-and-times/ And the fractional seconds is not so important for the APP user, so it's no harm to return without it – freeman Dec 14 '17 at 01:15
  • Works with xcode 12 but I had to include `.withSpaceBetweenDateAndTime` – Sylar Oct 30 '21 at 19:00
  • It can decode with the "Z" at the end fine but not including the Z was making my calls to an API fail. Better to use the format from @jrc – garafajon Dec 30 '22 at 21:23
  • @garafajon The Z at the end means UTC if you escape it you will be ignoring the timezone and interpret it as current timezone which is definitely wrong. – Leo Dabus Jun 01 '23 at 20:31
30

Swift 5

If you're targeting iOS 11.0+ / macOS 10.13+, you simply use ISO8601DateFormatter with the withInternetDateTime and withFractionalSeconds options, like so:

let date = Date()

let iso8601DateFormatter = ISO8601DateFormatter()
iso8601DateFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
let string = iso8601DateFormatter.string(from: date)

// string looks like "2020-03-04T21:39:02.112Z"
jrc
  • 20,354
  • 10
  • 69
  • 64
5

To further compliment Andrés Torres Marroquín and Leo Dabus, I have a version that preserves fractional seconds. I can't find it documented anywhere, but Apple truncate fractional seconds to the microsecond (3 digits of precision) on both input and output (even though specified using SSSSSSS, contrary to Unicode tr35-31).

I should stress that this is probably not necessary for most use cases. Dates online do not typically need millisecond precision, and when they do, it is often better to use a different data format. But sometimes one must interoperate with a pre-existing system in a particular way.

Xcode 8/9 and Swift 3.0-3.2

extension Date {
    struct Formatter {
        static let iso8601: DateFormatter = {
            let formatter = DateFormatter()
            formatter.calendar = Calendar(identifier: .iso8601)
            formatter.locale = Locale(identifier: "en_US_POSIX")
            formatter.timeZone = TimeZone(identifier: "UTC")
            formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSSSSXXXXX"
            return formatter
        }()
    }
    
    var iso8601: String {
        // create base Date format 
         var formatted = DateFormatter.iso8601.string(from: self)
    
        // Apple returns millisecond precision. find the range of the decimal portion
         if let fractionStart = formatted.range(of: "."),
             let fractionEnd = formatted.index(fractionStart.lowerBound, offsetBy: 7, limitedBy: formatted.endIndex) {
             let fractionRange = fractionStart.lowerBound..<fractionEnd
            // replace the decimal range with our own 6 digit fraction output
             let microseconds = self.timeIntervalSince1970 - floor(self.timeIntervalSince1970)
             var microsecondsStr = String(format: "%.06f", microseconds)
             microsecondsStr.remove(at: microsecondsStr.startIndex)
             formatted.replaceSubrange(fractionRange, with: microsecondsStr)
        }
         return formatted
    }
}

extension String {
    var dateFromISO8601: Date? {
        guard let parsedDate = Date.Formatter.iso8601.date(from: self) else {
            return nil
        }

        var preliminaryDate = Date(timeIntervalSinceReferenceDate: floor(parsedDate.timeIntervalSinceReferenceDate))

        if let fractionStart = self.range(of: "."),
            let fractionEnd = self.index(fractionStart.lowerBound, offsetBy: 7, limitedBy: self.endIndex) {
            let fractionRange = fractionStart.lowerBound..<fractionEnd
            let fractionStr = self.substring(with: fractionRange)
        
            if var fraction = Double(fractionStr) {
                fraction = Double(floor(1000000*fraction)/1000000)
                preliminaryDate.addTimeInterval(fraction)
            }
        }
        return preliminaryDate
    }
}
Eli Burke
  • 2,729
  • 27
  • 25
  • This is the best answer in my opinion in that it allows one to get to a microsecond level of precision where all the other solutions truncate at milliseconds. – Michael A. McCloskey Sep 29 '17 at 19:31
  • If you would like to preserve the Date with all its fractional seconds you should use just a double (time interval since reference date) when saving/receiving your date to the server. – Leo Dabus Dec 13 '17 at 03:45
  • @LeoDabus yes, if you control the whole system and don't need to interoperate. Like I said in the answer, this isn't necessary for most users. But we don't all always have control over the data formatting in web APIs, and as Android and Python (at least) preserve 6 digits of fractional precision, it is sometimes necessary to follow suit. – Eli Burke Dec 14 '17 at 14:57
  • This has a critical bug with microseconds almost near `1` and values with imprecise FP representation. See [the revised answer here](https://stackoverflow.com/a/75814917/5252984). – Jay Lee Mar 22 '23 at 16:40
5

Uses ISO8601DateFormatter on iOS10 or newer.

Uses DateFormatter on iOS9 or older.

Swift 4

protocol DateFormatterProtocol {
    func string(from date: Date) -> String
    func date(from string: String) -> Date?
}

extension DateFormatter: DateFormatterProtocol {}

@available(iOS 10.0, *)
extension ISO8601DateFormatter: DateFormatterProtocol {}

struct DateFormatterShared {
    static let iso8601: DateFormatterProtocol = {
        if #available(iOS 10, *) {
            return ISO8601DateFormatter()
        } else {
            // iOS 9
            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
        }
    }()
}
neoneye
  • 50,398
  • 25
  • 166
  • 151
3

In my case I have to convert the DynamoDB - lastUpdated column (Unix Timestamp) to Normal Time.

The initial value of lastUpdated was : 1460650607601 - converted down to 2016-04-14 16:16:47 +0000 via :

   if let lastUpdated : String = userObject.lastUpdated {

                let epocTime = NSTimeInterval(lastUpdated)! / 1000 // convert it from milliseconds dividing it by 1000

                let unixTimestamp = NSDate(timeIntervalSince1970: epocTime) //convert unix timestamp to Date
                let dateFormatter = NSDateFormatter()
                dateFormatter.timeZone = NSTimeZone()
                dateFormatter.locale = NSLocale.currentLocale() // NSLocale(localeIdentifier: "en_US_POSIX")
                dateFormatter.dateFormat =  "yyyy-MM-dd'T'HH:mm:ssZZZZZ"
                dateFormatter.dateFromString(String(unixTimestamp))

                let updatedTimeStamp = unixTimestamp
                print(updatedTimeStamp)

            }
ioopl
  • 1,735
  • 19
  • 19
3

In the future the format might need to be changed which could be a small head ache having date.dateFromISO8601 calls everywhere in an app. Use a class and protocol to wrap the implementation, changing the date time format call in one place will be simpler. Use RFC3339 if possible, its a more complete representation. DateFormatProtocol and DateFormat is great for dependency injection.

class AppDelegate: UIResponder, UIApplicationDelegate {

    internal static let rfc3339DateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ"
    internal static let localeEnUsPosix = "en_US_POSIX"
}

import Foundation

protocol DateFormatProtocol {

    func format(date: NSDate) -> String
    func parse(date: String) -> NSDate?

}


import Foundation

class DateFormat:  DateFormatProtocol {

    func format(date: NSDate) -> String {
        return date.rfc3339
    }

    func parse(date: String) -> NSDate? {
        return date.rfc3339
    }

}


extension NSDate {

    struct Formatter {
        static let rfc3339: NSDateFormatter = {
            let formatter = NSDateFormatter()
            formatter.calendar = NSCalendar(calendarIdentifier: NSCalendarIdentifierISO8601)
            formatter.locale = NSLocale(localeIdentifier: AppDelegate.localeEnUsPosix)
            formatter.timeZone = NSTimeZone(forSecondsFromGMT: 0)
            formatter.dateFormat = rfc3339DateFormat
            return formatter
        }()
    }

    var rfc3339: String { return Formatter.rfc3339.stringFromDate(self) }
}

extension String {
    var rfc3339: NSDate? {
        return NSDate.Formatter.rfc3339.dateFromString(self)
    }
}



class DependencyService: DependencyServiceProtocol {

    private var dateFormat: DateFormatProtocol?

    func setDateFormat(dateFormat: DateFormatProtocol) {
        self.dateFormat = dateFormat
    }

    func getDateFormat() -> DateFormatProtocol {
        if let dateFormatObject = dateFormat {

            return dateFormatObject
        } else {
            let dateFormatObject = DateFormat()
            dateFormat = dateFormatObject

            return dateFormatObject
        }
    }

}
Gary Davies
  • 920
  • 15
  • 12
3

There is a new ISO8601DateFormatter class that let's you create a string with just one line. For backwards compatibility I used an old C-library. I hope this is useful for someone.

Swift 3.0

extension Date {
    var iso8601: String {
        if #available(OSX 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) {
            return ISO8601DateFormatter.string(from: self, timeZone: TimeZone.current, formatOptions: .withInternetDateTime)
        } else {
            var buffer = [CChar](repeating: 0, count: 25)
            var time = time_t(self.timeIntervalSince1970)
            strftime_l(&buffer, buffer.count, "%FT%T%z", localtime(&time), nil)
            return String(cString: buffer)
        }
    }
}
Thomas Szabo
  • 163
  • 7
3

It is now 2022, but I was looking for an answer to this (i.e. how to convert a Date to ISO8601 that includes fractions of seconds). It turns out the answer nowadays is a one-liner:

var somedate: Date = Date.now
var isodate = somedate.ISO8601Format(Date.ISO8601FormatStyle(includingFractionalSeconds: true))

so this will print something like 2022-08-16T17:45:08.548Z

scuac
  • 131
  • 1
  • 3
2

The code below preserves microsecond precision. This is based on this answer, but this can actually lose a microsecond precision and lead to a lossy translation between Date-String conversion, e.g., .065005 becomes .065004 with that answer.

More seriously, dates with almost-one subsecond digits "adds" a full second with that code, e.g., xx[d].999500 becomes xx[d+1].999500. (I found this out the hard way.)

The correct version (with an up-to-date String APIs) is the following:

final class PrecisionISO8601DateFormatter: DateFormatter {
    override init() {
        super.init()
        calendar = Calendar(identifier: .iso8601)
        locale = Locale(identifier: "en_US_POSIX")
        timeZone = TimeZone(identifier: "UTC")
        dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSSSSXXXXX"
    }

    @available(*, unavailable)
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func string(from date: Date) -> String {
        // Directly converting with `super` introduces errors; `.999500` adds a second.
        var string = super.string(
            from: Date(timeIntervalSince1970: floor(date.timeIntervalSince1970))
        )

        if let fractionStart = string.range(of: "."),
           let fractionEnd = string
            .index(fractionStart.lowerBound, offsetBy: 7, limitedBy: string.endIndex)
        {
            let fractionRange = fractionStart.lowerBound..<fractionEnd
            // Replace the decimal range with the six digit fraction
            let microseconds = date.timeIntervalSince1970 - floor(date.timeIntervalSince1970)
            var microsecondsString = String(format: "%.06f", microseconds)
            microsecondsString.remove(at: microsecondsString.startIndex)
            string.replaceSubrange(fractionRange, with: microsecondsString)
        }

        return string
    }

    override func date(from string: String) -> Date? {
        guard var date = super.date(from: string) else { return nil }

        date = Date(timeIntervalSinceReferenceDate: floor(date.timeIntervalSinceReferenceDate))
        if let fractionStart = string.range(of: "."),
           let fractionEnd = string
            .index(fractionStart.lowerBound, offsetBy: 7, limitedBy: string.endIndex)
        {
            // fractionString is a string containing six decimal digits.
            let fractionString = string[fractionStart.lowerBound..<fractionEnd].trimmingPrefix(".")
            // Directly converting with `Double` introduces errors; `.065005` becomes `.065004`.
            if let fraction = Int(fractionString) {
                date.addTimeInterval(Double(fraction) / 1E6)
            }
        }

        return date
    }
}

This is tested with all microseconds in range of 000000 to 999999.

Jay Lee
  • 1,684
  • 1
  • 15
  • 27
  • 1
    Sorry about your _the hard way_ bug Jay. Thanks for posting a fixed version (and unit testing!) – Eli Burke Mar 23 '23 at 18:18
  • Note that Date stores the number of seconds since reference date (00:00:00 UTC on 1 January 2001) not since 1970 (00:00:00 UTC on 1 January 1970). If you are worried about precision you should work with `timeIntervalSinceReferenceDate`. If you need to compare two dates or send the date without any precision loss to your back end check this [post](https://stackoverflow.com/a/47502712/2303865) – Leo Dabus Jun 01 '23 at 20:25
1

To complement the version of Leo Dabus, I added support for projects written Swift and Objective-C, also added support for the optional milliseconds, probably isn't the best but you would get the point:

Xcode 8 and Swift 3

extension Date {
    struct Formatter {
        static let iso8601: 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
        }()
    }

    var iso8601: String {
        return Formatter.iso8601.string(from: self)
    }
}


extension String {
    var dateFromISO8601: Date? {
        var data = self
        if self.range(of: ".") == nil {
            // Case where the string doesn't contain the optional milliseconds
            data = data.replacingOccurrences(of: "Z", with: ".000000Z")
        }
        return Date.Formatter.iso8601.date(from: data)
    }
}


extension NSString {
    var dateFromISO8601: Date? {
        return (self as String).dateFromISO8601
    }
}
0

Without some manual String masks or TimeFormatters

import Foundation

struct DateISO: Codable {
    var date: Date
}

extension Date{
    var isoString: String {
        let encoder = JSONEncoder()
        encoder.dateEncodingStrategy = .iso8601
        guard let data = try? encoder.encode(DateISO(date: self)),
        let json = try? JSONSerialization.jsonObject(with: data, options: .allowFragments) as?  [String: String]
            else { return "" }
        return json?.first?.value ?? ""
    }
}

let dateString = Date().isoString
Dmitrii Z
  • 145
  • 1
  • 5
0

Based on the acceptable answer in an object paradigm

class ISO8601Format
{
    let format: ISO8601DateFormatter

    init() {
        let format = ISO8601DateFormatter()
        format.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
        format.timeZone = TimeZone(secondsFromGMT: 0)!
        self.format = format
    }

    func date(from string: String) -> Date {
        guard let date = format.date(from: string) else { fatalError() }
        return date
    }

    func string(from date: Date) -> String { return format.string(from: date) }
}


class ISO8601Time
{
    let date: Date
    let format = ISO8601Format() //FIXME: Duplication

    required init(date: Date) { self.date = date }

    convenience init(string: String) {
        let format = ISO8601Format() //FIXME: Duplication
        let date = format.date(from: string)
        self.init(date: date)
    }

    func concise() -> String { return format.string(from: date) }

    func description() -> String { return date.description(with: .current) }
}

callsite

let now = Date()
let time1 = ISO8601Time(date: now)
print("time1.concise(): \(time1.concise())")
print("time1: \(time1.description())")


let time2 = ISO8601Time(string: "2020-03-24T23:16:17.661Z")
print("time2.concise(): \(time2.concise())")
print("time2: \(time2.description())")
aaronium112
  • 2,992
  • 4
  • 35
  • 50