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

14

252

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))
Telegraphic answered 19/1, 2015 at 0:58 Comment(5)
Note that iOS10+ SIMPLY INCLUDES ISO 8601 BUILT-IN .. it will just autocomplete for you.Mystify
@Mystify And - how can it handle that last .234Z milliseconds Zulu/UTC part of the timestamp? Answer: Matt Longs @ https://mcmap.net/q/115683/-how-can-i-parse-create-a-date-time-stamp-formatted-with-fractional-seconds-utc-timezone-iso-8601-rfc-3339-in-swiftIonogen
@Ionogen -- fantastic tip, thanks. I had no clue there were "options on a formatter", weird and wild!Mystify
I'm looking for a solution that works on linux.Silda
@Silda Just use the old version (plain DateFormatter) and change the calendar iso8601 to gregorian https://mcmap.net/q/115683/-how-can-i-parse-create-a-date-time-stamp-formatted-with-fractional-seconds-utc-timezone-iso-8601-rfc-3339-in-swiftBromal
B
487

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

Bromal answered 19/1, 2015 at 1:18 Comment(27)
It'd be useful to add opposite conversion extension: extension String { var dateFormattedISO8601: NSDate? {return NSDate.Date.formatterISO8601.dateFromString(self)} }Drambuie
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)Timoteo
In RFC3339 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 ?Amidships
@LeoDabus - thanks again. Look man, here's a puzzler for you: #43809193Mystify
@LeoDabus Could you explain the reason why you extend Formatter instead of DateFormatter please?Shulman
Doesn't make any difference. You can extend DateFormatter instead if you would like to.Bromal
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.Willms
@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.Willms
@Timoteo 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://mcmap.net/q/118633/-convert-a-date-absolute-time-to-be-sent-received-across-the-network-as-data-in-swiftBromal
@keno thanks It used to crash when setting ISO8601DateFormatter formatOptions to [.withInternetDateTime, .withFractionalSeconds] I will update the answer accordinglyBromal
iOS 11 now has support for fractional seconds with option NSISO8601DateFormatWithFractionalSeconds developer.apple.com/documentation/foundation/…Semifinal
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.Pilkington
static let iso8601 = ISO8601DateFormatter([.withInternetDateTime, .withFractionalSeconds]) crashes for me on iOS11. .withFractionalSeconds causes the issue.Wadley
use the plain DateFormatter version for older OSs. I've read thhat you need minimum > 11.2Bromal
Test fails with XCTAssertEqualPre
@SoheilNovinfard the only way to preserve the date as it is is to send the timeIntervalSinceReferenceDate to your server.Bromal
#59867706 and #47503091Bromal
Please check out this question: #60137482Pre
@SoheilNovinfard Check the links I posted above. DateFormatter discards most of the fractional seconds in a date. Try with this one https://mcmap.net/q/118633/-convert-a-date-absolute-time-to-be-sent-received-across-the-network-as-data-in-swiftBromal
Another option is to use dateDecodingStrategy set to .deferredToDateBromal
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 questionPre
I already said what's going on there. There is no way to assert it is equal when you are discarding the FloatingPoint nanosecondsBromal
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.Pre
@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 anywayPre
#60137482Bromal
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?Gem
@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.Bromal
A
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.

Apocrine answered 19/1, 2015 at 1:4 Comment(3)
why we should set the locale to en_US_POSIX ? even if we are not in US ?Organizer
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 or those RFC/ISO standards for more information.Apocrine
.withFractionalSeconds - This is what I needed to support processing milliseconds. Thank you!Quinnquinol
C
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

Coelho answered 7/2, 2017 at 23:0 Comment(9)
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.Bromal
@MEnnabah It works fine for me in Swift 4. Are you getting an error?Coelho
@LeoDabus, got the same error as yours, did you solve it?Fiann
custom JSONDecoder DateDecodingStrategy https://mcmap.net/q/118636/-how-to-convert-a-date-string-with-optional-fractional-seconds-using-codable-in-swiftBromal
@Fiann 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 protocolBromal
@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: 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 itFiann
Works with xcode 12 but I had to include .withSpaceBetweenDateAndTimeTagmeme
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 @jrcPredicate
@Predicate 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.Bromal
N
32

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"
Novokuznetsk answered 4/3, 2020 at 21:40 Comment(0)
U
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
    }
}
Unruffled answered 14/4, 2017 at 13:30 Comment(4)
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.Kaohsiung
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.Bromal
@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.Unruffled
This has a critical bug with microseconds almost near 1 and values with imprecise FP representation. See the revised answer here.Cushat
S
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
        }
    }()
}
Silda answered 8/3, 2018 at 13:14 Comment(0)
N
4

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

Nadianadine answered 16/8, 2022 at 17:48 Comment(2)
Nice! I didn't know about ISOFormatStyle(includingFractionalSeconds:). (Voted)Vertumnus
Thanks for fractionSec and I Voted. Locale(identifier: "en_US_POSIX") is available in "ISO8601Format" ?Sexlimited
P
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)

            }
Prewar answered 15/4, 2016 at 11:9 Comment(0)
H
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
        }
    }

}
Hairsplitter answered 5/12, 2016 at 3:13 Comment(0)
N
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)
        }
    }
}
Na answered 28/6, 2017 at 15:35 Comment(0)
C
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.

Cushat answered 22/3, 2023 at 16:33 Comment(2)
Sorry about your the hard way bug Jay. Thanks for posting a fixed version (and unit testing!)Unruffled
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 postBromal
P
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
    }
}
Pentlandite answered 3/11, 2016 at 16:22 Comment(0)
S
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())")
Spinose answered 24/3, 2020 at 23:48 Comment(0)
C
-1

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
Chequered answered 4/12, 2018 at 5:33 Comment(1)
This is a good answer, but using .iso8601 will not include milliseconds.Alehouse

© 2022 - 2024 — McMap. All rights reserved.