Wie erstelle ich einen Datumsstempel und ein Format als ISO 8601, RFC 3339, UTC-Zeitzone?

186

Wie erstelle ich einen Datums- / Zeitstempel unter Verwendung der Formatstandards für ISO 8601 und RFC 3339 ?

Das Ziel ist eine Zeichenfolge, die so aussieht:

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

Format:

  • Jahr, Monat, Tag als "XXXX-XX-XX"
  • der Buchstabe "T" als Trennzeichen
  • Stunde, Minute, Sekunden, Millisekunden als "XX: XX: XX.XXX".
  • der Buchstabe "Z" als Zonenbezeichnung für Nullpunktverschiebung, auch bekannt als UTC, GMT, Zulu-Zeit.

I'm besten fall:

  • Schneller Quellcode, der einfach, kurz und unkompliziert ist.
  • Sie müssen kein zusätzliches Framework, Teilprojekt, Cocoapod, C-Code usw. verwenden.

Ich habe nach StackOverflow, Google, Apple usw. gesucht und keine schnelle Antwort darauf gefunden.

Die Klassen , die vielversprechendsten scheinen sind NSDate, NSDateFormatter, NSTimeZone.

Verwandte Fragen und Antworten: Wie erhalte ich das ISO 8601-Datum in iOS?

Hier ist das Beste, was ich mir bisher ausgedacht habe:

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))
joelparkerhenderson
quelle
5
Beachten Sie, dass iOS10 + EINFACH ISO 8601 EINGEBAUT ENTHÄLT . Es wird nur automatisch für Sie vervollständigt .
Fattie
2
@Fattie Und - wie kann es mit dem letzten Zulu / UTC-Teil des Zeitstempels von 0,234 Z Millisekunden umgehen? Antwort: Matt Longs @ stackoverflow.com/a/42101630/3078330
smat88dd
1
@ smat88dd - fantastischer Tipp, danke. Ich hatte keine Ahnung, dass es "Optionen für einen Formatierer" gibt, seltsam und wild!
Fattie
Ich suche nach einer Lösung, die unter Linux funktioniert.
Neoneye
@neoneye Verwenden Sie einfach die alte Version (einfaches DateFormatter) und ändern Sie den Kalender iso8601 in gregorian stackoverflow.com/a/28016692/2303865
Leo Dabus

Antworten:

392

Swift 4 • iOS 11.2.1 oder höher

extension ISO8601DateFormatter {
    convenience init(_ formatOptions: Options, timeZone: TimeZone = TimeZone(secondsFromGMT: 0)!) {
        self.init()
        self.formatOptions = formatOptions
        self.timeZone = timeZone
    }
}

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

Verwendung:

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 oder höher

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
    }()
}

Codierbares Protokoll

Wenn Sie dieses Format bei der Arbeit mit dem codierbaren Protokoll codieren und decodieren müssen, können Sie Ihre eigenen benutzerdefinierten Datumscodierungs- / Decodierungsstrategien erstellen:

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

und die Codierungsstrategie

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

Spielplatztests

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

Codierung

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

Dekodierung

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

Geben Sie hier die Bildbeschreibung ein

Leo Dabus
quelle
3
Es wäre nützlich, eine entgegengesetzte Konvertierungserweiterung hinzuzufügen:extension String { var dateFormattedISO8601: NSDate? {return NSDate.Date.formatterISO8601.dateFromString(self)} }
Vive
1
Nur eine Anmerkung, dass dies etwas an Genauigkeit verliert, daher ist es wichtig sicherzustellen, dass die Gleichheit der Daten über die generierte Zeichenfolge und nicht über timeInterval verglichen wird. let now = NSDate() let stringFromDate = now.iso8601 let dateFromString = stringFromDate.dateFromISO8601! XCTAssertEqual(now.timeIntervalSince1970, dateFromString.timeIntervalSince1970)
Pixelrevision
7
Wenn Sie keine Millisekunden benötigen, ISO8601DateFormattervereinfacht das neue iOS 10 den Vorgang. Ich habe Apple einen Fehlerbericht (27242248) gesendet, in dem sie aufgefordert wurden, diesen neuen Formatierer zu erweitern, um auch die Angabe von Millisekunden zu ermöglichen (da dieser neue Formatierer für viele von uns ohne Millisekunden nicht von Nutzen ist).
Rob
1
In RFC3339 finden wir einen Hinweis "HINWEIS: ISO 8601 definiert Datum und Uhrzeit, die durch" T "getrennt sind. Anwendungen, die diese Syntax verwenden, können aus Gründen der Lesbarkeit ein durch (sagen wir) getrenntes Datum und eine Vollzeitstelle angeben. ein Leerzeichen. " Deckt es auch das Datumsformat ab, ohne TzB : 2016-09-21 21:05:10+00:00?
ManRo
3
@LeoDabus ja, aber dies ist das erste Ergebnis für "Swift iso8601". Mein Kommentar sollte andere Entwickler warnen, die in Zukunft darauf stoßen und sich nicht an OP richten.
Thislooksfun
38

Denken Sie daran, das Gebietsschema en_US_POSIXwie in Technische Fragen und Antworten A1480 beschrieben einzustellen . 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))

Das Problem ist, dass wenn Sie sich auf einem Gerät befinden, das einen nicht-gregorianischen Kalender verwendet, das Jahr nicht RFC3339 / ISO8601 entspricht, es sei denn, Sie geben sowohl die Zeichenfolge localeals auch die Zeichenfolge timeZoneund an dateFormat.

Oder Sie können verwenden ISO8601DateFormatter, um Sie aus dem Unkraut der Umgebung localeund sich timeZoneselbst herauszuholen :

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

Informationen zur Swift 2-Wiedergabe finden Sie in der vorherigen Überarbeitung dieser Antwort .

rauben
quelle
Warum sollten wir das Gebietsschema auf en_US_POSIX setzen? auch wenn wir nicht in den USA sind?
Mnemonik23
2
Nun, Sie benötigen ein einheitliches Gebietsschema, und die Konvention der ISO 8601 / RFC 3999-Standards ist das Format, das von angeboten wird en_US_POSIX. Es ist die Verkehrssprache für den Austausch von Daten im Internet. Und Sie können Datumsangaben nicht falsch interpretieren, wenn beim Speichern einer Datumszeichenfolge ein Kalender auf dem Gerät verwendet wurde und ein anderer, wenn die Zeichenfolge später wieder eingelesen wird. Außerdem benötigen Sie ein Format, das sich garantiert nie ändert (weshalb Sie es verwenden en_US_POSIXund nicht en_US). Weitere Informationen finden Sie in den technischen Fragen und Antworten 1480 oder in diesen RFC / ISO-Standards.
Rob
24

Wenn Sie den ISO8601DateFormatter()mit einem Datum aus einem Rails 4+ JSON-Feed verwenden möchten (und natürlich keine Millis benötigen), müssen Sie einige Optionen für den Formatierer festlegen, damit er richtig date(from: string)funktioniert. Andernfalls gibt die Funktion null zurück. Folgendes verwende ich:

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
    }()
}

Hier ist das Ergebnis der Verwendung der Optionsverse, die nicht in einem Spielplatz-Screenshot enthalten sind:

Geben Sie hier die Bildbeschreibung ein

Matt Long
quelle
Sie müssten in die Optionen auch die aufnehmen, .withFractionalSecondsaber ich habe das bereits versucht und es wird immer wieder ein Fehler ausgegeben libc++abi.dylib: terminating with uncaught exception of type NSException.
Leo Dabus
@MEnnabah Es funktioniert gut für mich in Swift 4. Erhalten Sie eine Fehlermeldung?
Matt Long
@LeoDabus, habe den gleichen Fehler wie du, hast du ihn gelöst?
Freeman
benutzerdefinierte JSONDecoder DateDecodingStrategy stackoverflow.com/a/46458771/2303865
Leo Dabus
@freeman Wenn Sie das Datum mit all seinen Sekundenbruchteilen beibehalten möchten, empfehle ich, beim Speichern / Empfangen Ihres Datums auf dem Server ein doppeltes (Zeitintervall seit Referenzdatum) zu verwenden. Und verwenden Sie die Standard- .deferredToDateDatumsdecodierungsstrategie, wenn Sie das codierbare Protokoll verwenden
Leo Dabus,
6

Swift 5

Wenn Sie iOS ab Version 11.0 / macOS 10.13+ Targeting sind, verwenden Sie einfach ISO8601DateFormattermit den withInternetDateTimeund withFractionalSecondsOptionen, etwa 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
quelle
5

Um Andrés Torres Marroquín und Leo Dabus weiter zu beglückwünschen, habe ich eine Version, die Sekundenbruchteile bewahrt. Ich kann es nirgendwo dokumentiert finden, aber Apple schneidet sowohl bei der Eingabe als auch bei der Ausgabe Sekundenbruchteile auf die Mikrosekunde (3 Stellen Genauigkeit) ab (obwohl dies im Gegensatz zu Unicode tr35-31 mit SSSSSSS angegeben wurde ).

Ich sollte betonen, dass dies für die meisten Anwendungsfälle wahrscheinlich nicht notwendig ist . Online-Daten erfordern normalerweise keine Millisekundengenauigkeit. In diesem Fall ist es häufig besser, ein anderes Datenformat zu verwenden. Manchmal muss man jedoch auf bestimmte Weise mit einem bereits vorhandenen System zusammenarbeiten.

Xcode 8/9 und 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
quelle
Dies ist meiner Meinung nach die beste Antwort, da man so eine Mikrosekundengenauigkeit erreichen kann, bei der alle anderen Lösungen nach Millisekunden abgeschnitten werden.
Michael A. McCloskey
Wenn Sie das Datum mit all seinen Sekundenbruchteilen beibehalten möchten, sollten Sie beim Speichern / Empfangen Ihres Datums auf dem Server nur ein Doppel (Zeitintervall seit dem Referenzdatum) verwenden.
Leo Dabus
@LeoDabus ja, wenn Sie das gesamte System steuern und nicht zusammenarbeiten müssen. Wie ich in der Antwort sagte, ist dies für die meisten Benutzer nicht erforderlich. Wir haben jedoch nicht immer alle Kontrolle über die Datenformatierung in Web-APIs. Da Android und Python (mindestens) 6 Stellen mit gebrochener Genauigkeit beibehalten, ist es manchmal erforderlich, diesem Beispiel zu folgen.
Eli Burke
4

Verwendet ISO8601DateFormatterunter iOS10 oder neuer.

Verwendet DateFormatterunter iOS9 oder älter.

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

In meinem Fall muss ich die Spalte DynamoDB - lastUpdated (Unix Timestamp) in Normal Time konvertieren.

Der Anfangswert von lastUpdated war: 1460650607601 - konvertiert auf 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
quelle
3

In Zukunft muss das Format möglicherweise geändert werden. Dies kann ein kleiner Kopfschmerz sein, wenn date.dateFromISO8601 überall in einer App aufgerufen wird. Verwenden Sie eine Klasse und ein Protokoll, um die Implementierung zu verpacken. Das Ändern des Datums-Zeit-Format-Aufrufs an einer Stelle ist einfacher. Verwenden Sie nach Möglichkeit RFC3339, um eine vollständigere Darstellung zu erhalten. DateFormatProtocol und DateFormat eignen sich hervorragend für die Abhängigkeitsinjektion.

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

Es gibt eine neue ISO8601DateFormatterKlasse, mit der Sie eine Zeichenfolge mit nur einer Zeile erstellen können. Aus Gründen der Abwärtskompatibilität habe ich eine alte C-Bibliothek verwendet. Ich hoffe das ist nützlich für jemanden.

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

Um die Version von Leo Dabus zu ergänzen, habe ich Unterstützung für Projekte hinzugefügt, die Swift und Objective-C geschrieben haben, und Unterstützung für die optionalen Millisekunden hinzugefügt. Dies ist wahrscheinlich nicht die beste, aber Sie würden den Punkt verstehen:

Xcode 8 und 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
    }
}
Andrés Torres Marroquín
quelle
0

Ohne manuelle String-Masken oder 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.
quelle
Dies ist eine gute Antwort, aber die Verwendung umfasst .iso8601keine Millisekunden.
Stefan Arentz
0

Basierend auf der akzeptablen Antwort in einem Objektparadigma

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
quelle