Come creare un timestamp e un formato di data come ISO 8601, RFC 3339, fuso orario UTC?


186

Come generare un timestamp di data, utilizzando gli standard di formato per ISO 8601 e RFC 3339 ?

L'obiettivo è una stringa simile a questa:

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

Formato:

  • anno, mese, giorno, come "XXXX-XX-XX"
  • la lettera "T" come separatore
  • ora, minuti, secondi, millisecondi, come "XX: XX: XX.XXX".
  • la lettera "Z" come designatore di zona per offset zero, ovvero UTC, GMT, ora Zulu.

Caso migliore:

  • Codice sorgente rapido che è semplice, breve e diretto.
  • Non è necessario utilizzare alcun framework aggiuntivo, sottoprogetto, cocoapod, codice C, ecc.

Ho cercato StackOverflow, Google, Apple, ecc. E non ho trovato una risposta rapida a questo.

Le classi che sembrano più promettenti sono NSDate, NSDateFormatter, NSTimeZone.

Domande e risposte correlate: Come posso ottenere la data ISO 8601 in iOS?

Ecco il meglio che ho escogitato finora:

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

6
Nota che iOS10 + INCLUDE SEMPLICEMENTE ISO 8601 INTEGRATO .. si completerà automaticamente per te.
Fattie,

2
@Fattie E - come può gestire l'ultima parte .234Z millisecondi Zulu / UTC del timestamp? Risposta: Matt Longs @ stackoverflow.com/a/42101630/3078330
smat88dd

1
@ smat88dd - fantastico suggerimento, grazie. Non avevo idea che esistessero "opzioni su un formatter", strano e selvaggio!
Fattie,

Sto cercando una soluzione che funzioni su Linux.
neoneye,

@neoneye Basta usare la vecchia versione (DateFormatter pianura) e cambiare l'ISO8601 calendario gregoriano stackoverflow.com/a/28016692/2303865
Leo Dabus

Risposte:


393

Swift 4 • iOS 11.2.1 o successivo

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

Uso:

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

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

Protocollo codificabile

Se è necessario codificare e decodificare questo formato quando si lavora con il protocollo Codable, è possibile creare strategie personalizzate di codifica / decodifica della data:

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

e la strategia di codifica

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

Test del parco giochi

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

codifica

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

decodifica

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

inserisci qui la descrizione dell'immagine


3
Sarebbe utile aggiungere l'estensione di conversione opposta:extension String { var dateFormattedISO8601: NSDate? {return NSDate.Date.formatterISO8601.dateFromString(self)} }
Vive

1
Solo una nota che questo perde un po 'di precisione, quindi è importante assicurarsi che l'uguaglianza delle date venga confrontata tramite la stringa generata e non timeInterval. let now = NSDate() let stringFromDate = now.iso8601 let dateFromString = stringFromDate.dateFromISO8601! XCTAssertEqual(now.timeIntervalSince1970, dateFromString.timeIntervalSince1970)
pixelrevision,

7
Inutile dire che, se non hai bisogno di millisecondi, il nuovo iOS 10 ISO8601DateFormattersemplifica il processo. Ho inviato una segnalazione di bug (27242248) ad Apple, chiedendo loro di espandere questo nuovo formattatore per offrire anche la possibilità di specificare millisecondi (poiché questo nuovo formatter non è utile per molti di noi senza i millisecondi).
Rob,

1
In RFC3339 possiamo trovare una nota "NOTA: ISO 8601 definisce la data e l'ora separate da" T ". Le applicazioni che usano questa sintassi possono scegliere, per motivi di leggibilità, di specificare una data intera e una a tempo pieno separate da (diciamo) un personaggio spaziale ". Copre anche il formato della data senza Tad esempio 2016-09-21 21:05:10+00:00:?
manRo,

3
@LeoDabus sì, ma questo è il primo risultato per "Swift iso8601". Il mio commento aveva lo scopo di avvertire gli altri sviluppatori che si imbatteranno in questo in futuro e non era diretto a OP.
thislooksfun

39

Ricordare di impostare le impostazioni internazionali en_US_POSIXcome descritto in Domande e risposte tecniche 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))

Il problema è che se si utilizza un dispositivo che utilizza un calendario non gregoriano, l'anno non sarà conforme a RFC3339 / ISO8601 a meno che non si specifichino localesia la stringa timeZonesia la dateFormatstringa.

Oppure puoi usare ISO8601DateFormatterper farti uscire dalle erbacce di ambientazione localee timeZonete stesso:

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

Per la rappresentazione di Swift 2, vedere la revisione precedente di questa risposta .


perché dovremmo impostare la locale su en_US_POSIX? anche se non siamo negli Stati Uniti?
mnemonic23,

2
Bene, hai bisogno di alcune impostazioni internazionali coerenti e la convenzione degli standard ISO 8601 / RFC 3999 è quel formato offerto da en_US_POSIX. È la lingua francese per lo scambio di date sul web. E non puoi avere interpretazioni errate delle date se un calendario è stato utilizzato sul dispositivo durante il salvataggio di una stringa di date e un altro quando la stringa viene letta in un secondo momento. Inoltre, hai bisogno di un formato che non cambierà mai (motivo per cui lo usi en_US_POSIXe non en_US). Vedere Domande e risposte tecniche 1480 o tali standard RFC / ISO per ulteriori informazioni.
Rob,

24

Se si desidera utilizzare il ISO8601DateFormatter()con una data da un feed JSON di Rails 4+ (e ovviamente non sono necessari millis), è necessario impostare alcune opzioni sul formatter affinché funzioni correttamente, altrimenti la date(from: string)funzione restituirà zero. Ecco cosa sto usando:

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

Ecco il risultato dell'utilizzo delle opzioni versi non in uno screenshot del parco giochi:

inserisci qui la descrizione dell'immagine


Dovresti includere nelle opzioni anche il .withFractionalSecondsma l'ho già provato e continua a generare un errore libc++abi.dylib: terminating with uncaught exception of type NSException.
Leo Dabus,

@MEnnabah Funziona bene per me in Swift 4. Stai ricevendo un errore?
Matt Long,

@LeoDabus, hai avuto lo stesso errore del tuo, l'hai risolto?
Freeman il

personalizzato JSONDecoder DateDecodingStrategy stackoverflow.com/a/46458771/2303865~~V~~singular~~3rd
Leo Dabus

@freeman Se si desidera conservare la data con tutti i suoi secondi frazionari, si consiglia di utilizzare un doppio (intervallo di tempo dalla data di riferimento) quando si salva / si riceve la data sul server. E usa la strategia di decodifica della data predefinita .deferredToDatequando usi il protocollo Codable
Leo Dabus il

6

Swift 5

Se scegli come target iOS 11.0+ / macOS 10.13+, devi semplicemente utilizzare ISO8601DateFormatterle opzioni withInternetDateTimee withFractionalSeconds, in questo modo:

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"

5

Utilizza ISO8601DateFormattersu iOS10 o successivi.

Utilizza DateFormattersu iOS9 o precedente.

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

5

Per complimentarmi ulteriormente con Andrés Torres Marroquín e Leo Dabus, ho una versione che conserva secondi frazionari. Non riesco a trovarlo documentato da nessuna parte, ma Apple tronca i secondi frazionari al microsecondo (3 cifre di precisione) sia in input che in output (anche se specificato usando SSSSSSS, contrariamente a Unicode tr35-31 ).

Dovrei sottolineare che questo probabilmente non è necessario per la maggior parte dei casi d'uso . Le date online in genere non richiedono una precisione di millisecondi e, quando lo fanno, è spesso meglio utilizzare un formato di dati diverso. Ma a volte bisogna interagire con un sistema preesistente in un modo particolare.

Xcode 8/9 e 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
    }
}

Questa è la migliore risposta secondo me in quanto consente di raggiungere un livello di precisione in microsecondi in cui tutte le altre soluzioni si troncano a millisecondi.
Michael A. McCloskey,

Se desideri conservare la Data con tutti i suoi secondi frazionari, devi utilizzare solo una doppia (intervallo di tempo dalla data di riferimento) quando salvi / ricevi la data sul server.
Leo Dabus,

@LeoDabus sì, se controlli l'intero sistema e non hai bisogno di interagire. Come ho detto nella risposta, questo non è necessario per la maggior parte degli utenti. Ma non tutti abbiamo sempre il controllo sulla formattazione dei dati nelle API Web e poiché Android e Python (almeno) conservano 6 cifre di precisione frazionata, a volte è necessario seguire l'esempio.
Eli Burke,

3

Nel mio caso, devo convertire la colonna DynamoDB - lastUpdated (Timestamp Unix) in Tempo normale.

Il valore iniziale di lastUpdated era: 1460650607601 - convertito in 14-04-2016 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)

            }

3

In futuro potrebbe essere necessario modificare il formato, che potrebbe essere un piccolo mal di testa con date.dateFromISO8601 chiamate ovunque in un'app. Utilizzare una classe e un protocollo per completare l'implementazione, cambiando il formato della data e dell'ora in una posizione sarà più semplice. Utilizzare RFC3339 se possibile, è una rappresentazione più completa. DateFormatProtocol e DateFormat è ottimo per l'iniezione delle dipendenze.

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

}

3

C'è una nuova ISO8601DateFormatterclasse che ti consente di creare una stringa con una sola riga. Per compatibilità con le versioni precedenti ho usato una vecchia libreria C. Spero che questo sia utile per qualcuno.

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

1

Per integrare la versione di Leo Dabus, ho aggiunto il supporto per i progetti scritti Swift e Objective-C, ho anche aggiunto il supporto per i millisecondi opzionali, probabilmente non è il massimo ma otterresti il ​​punto:

Xcode 8 e 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

Senza alcune maschere String o TimeFormatters manuali

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

Questa è una buona risposta, ma l'utilizzo .iso8601non includerà i millisecondi.
Stefan Arentz,

0

Basato sulla risposta accettabile in un paradigma di oggetto

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())")
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.