Comment créer un horodatage et un format au format ISO 8601, RFC 3339, fuseau horaire UTC?

186

Comment générer un horodatage, en utilisant les normes de format ISO 8601 et RFC 3339 ?

Le but est une chaîne qui ressemble à ceci:

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

Format:

  • année, mois, jour, sous la forme "XXXX-XX-XX"
  • la lettre "T" comme séparateur
  • heure, minute, secondes, millisecondes, comme "XX: XX: XX.XXX".
  • la lettre «Z» comme désignateur de zone pour le décalage zéro, aka UTC, GMT, heure Zulu.

Meilleur cas:

  • Code source Swift simple, court et direct.
  • Pas besoin d'utiliser un framework supplémentaire, sous-projet, cocoapod, code C, etc.

J'ai recherché StackOverflow, Google, Apple, etc. et je n'ai pas trouvé de réponse Swift à cela.

Les classes qui semblent les plus prometteuses sont NSDate, NSDateFormatter, NSTimeZone.

Questions et réponses connexes: Comment obtenir la date ISO 8601 dans iOS?

Voici le meilleur que j'ai trouvé jusqu'à présent:

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
la source
6
Notez que iOS10 + COMPREND SIMPLEMENT ISO 8601 INTÉGRÉ .. il se complétera automatiquement pour vous.
Fattie
2
@Fattie Et - comment peut-il gérer cette dernière partie de .234Z millisecondes Zulu / UTC de l'horodatage? Réponse: Matt Longs @ stackoverflow.com/a/42101630/3078330
smat88dd
1
@ smat88dd - astuce fantastique, merci. Je n'avais aucune idée qu'il y avait des "options sur un formateur", bizarres et sauvages!
Fattie
Je recherche une solution qui fonctionne sous Linux.
neoneye
@neoneye Utilisez simplement l'ancienne version (DateFormatter) et changez le calendrier iso8601 en gregorian stackoverflow.com/a/28016692/2303865
Leo Dabus

Réponses:

393

Swift 4 • iOS 11.2.1 ou version ultérieure

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

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 ou version ultérieure

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

Protocole codable

Si vous avez besoin d'encoder et de décoder ce format lorsque vous travaillez avec le protocole Codable, vous pouvez créer vos propres stratégies de codage / décodage de date personnalisées:

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

et la stratégie d'encodage

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

Test de terrain de jeu

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

codage

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

décodage

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

entrez la description de l'image ici

Leo Dabus
la source
3
Il serait utile d'ajouter l'extension de conversion opposée:extension String { var dateFormattedISO8601: NSDate? {return NSDate.Date.formatterISO8601.dateFromString(self)} }
Vive
1
Notez simplement que cela perd un peu de précision, il est donc important de s'assurer que l'égalité des dates est comparée via la chaîne générée et non timeInterval. let now = NSDate() let stringFromDate = now.iso8601 let dateFromString = stringFromDate.dateFromISO8601! XCTAssertEqual(now.timeIntervalSince1970, dateFromString.timeIntervalSince1970)
pixelrevision
7
Inutile de dire que si vous n'avez pas besoin de millisecondes, le nouvel iOS 10 ISO8601DateFormattersimplifie le processus. J'ai publié un rapport de bogue (27242248) à Apple, leur demandant d'étendre ce nouveau formateur pour offrir également la possibilité de spécifier des millisecondes (car ce nouveau formateur n'est pas utile pour beaucoup d'entre nous sans les millisecondes).
Rob
1
Dans la RFC3339, nous pouvons trouver une note "REMARQUE: ISO 8601 définit la date et l'heure séparées par" T ". Les applications utilisant cette syntaxe peuvent choisir, pour des raisons de lisibilité, de spécifier une date complète et une heure complète séparées par (par exemple) un caractère d'espace. " Couvre-t-il également le format de date sans Tpar exemple 2016-09-21 21:05:10+00:00:?
manRo
3
@LeoDabus oui, mais c'est le premier résultat pour "Swift iso8601". Mon commentaire visait à avertir les autres développeurs qui rencontreraient cela à l'avenir et ne visait pas OP.
thislooksfun
39

N'oubliez pas de définir les paramètres régionaux en_US_POSIXcomme décrit dans les questions et réponses techniques1480 . Dans 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))

Le problème est que si vous êtes sur un appareil qui utilise un calendrier non grégorien, l'année ne sera pas conforme à RFC3339 / ISO8601, sauf si vous spécifiez le localeainsi que le timeZoneetdateFormat chaîne .

Ou vous pouvez utiliser ISO8601DateFormatterpour vous sortir des mauvaises herbes du décor localeet de timeZonevous - même:

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

Pour le rendu Swift 2, voir la révision précédente de cette réponse .

Rob
la source
pourquoi devrions-nous définir les paramètres régionaux sur en_US_POSIX? même si nous ne sommes pas aux États-Unis?
mnemonic23
2
Eh bien, vous avez besoin d' une locale cohérente et la convention des normes ISO 8601 / RFC 3999 est - ce le format proposé par en_US_POSIX. C'est la lingua franca pour échanger des dates sur le web. Et vous ne pouvez pas avoir une mauvaise interprétation des dates si un calendrier a été utilisé sur l'appareil lors de l'enregistrement d'une chaîne de date et un autre lorsque la chaîne est lue ultérieurement. De plus, vous avez besoin d'un format qui ne changera jamais (c'est pourquoi vous l'utilisez en_US_POSIXet non en_US). Voir Questions et réponses techniques 1480 ou ces normes RFC / ISO pour plus d'informations.
Rob
24

Si vous souhaitez utiliser le ISO8601DateFormatter()avec une date d'un flux JSON Rails 4+ (et n'avez pas besoin de millis bien sûr), vous devez définir quelques options sur le formateur pour que cela fonctionne correctement, sinon la date(from: string)fonction retournera nil. Voici ce que j'utilise:

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

Voici le résultat de l'utilisation des versets d'options pas dans une capture d'écran de terrain de jeu:

entrez la description de l'image ici

Matt Long
la source
Vous devrez également inclure dans les options le .withFractionalSecondsmais j'ai déjà essayé cela et cela continue de générer une erreur libc++abi.dylib: terminating with uncaught exception of type NSException.
Leo Dabus
@MEnnabah Cela fonctionne très bien pour moi dans Swift 4. Vous obtenez une erreur?
Matt Long
@LeoDabus, vous avez la même erreur que la vôtre, l'avez-vous résolue?
freeman
JSONDecoder personnalisé DateDecodingStrategy stackoverflow.com/a/46458771/2303865
Leo Dabus
@freeman Si vous souhaitez conserver la date avec toutes ses fractions de seconde, je suggère d'utiliser un double (intervalle de temps depuis la date de référence) lors de l'enregistrement / réception de votre date sur le serveur. Et utilisez la stratégie de décodage de date par défaut .deferredToDatelors de l'utilisation du protocole Codable
Leo Dabus
6

Swift 5

Si vous ciblez iOS 11.0+ / macOS 10.13+, vous utilisez simplement ISO8601DateFormatteravec les options withInternetDateTimeet withFractionalSeconds, comme ceci:

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
la source
5

Utilise ISO8601DateFormattersur iOS10 ou plus récent.

Utilise DateFormattersur iOS9 ou version antérieure.

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
la source
5

Pour complimenter encore Andrés Torres Marroquín et Leo Dabus, j'ai une version qui préserve les fractions de seconde. Je ne peux le trouver documenté nulle part, mais Apple tronque les fractions de seconde à la microseconde (3 chiffres de précision) à la fois en entrée et en sortie (même si spécifié à l'aide de SSSSSSS, contrairement à Unicode tr35-31 ).

Je dois souligner que ce n'est probablement pas nécessaire pour la plupart des cas d'utilisation . Les dates en ligne n'ont généralement pas besoin d'une précision à la milliseconde, et quand c'est le cas, il est souvent préférable d'utiliser un format de données différent. Mais parfois, il faut interagir avec un système préexistant d'une manière particulière.

Xcode 8/9 et 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
la source
C'est la meilleure réponse à mon avis en ce sens qu'elle permet d'atteindre un niveau de précision microseconde où toutes les autres solutions tronquent en millisecondes.
Michael A. McCloskey
Si vous souhaitez conserver la date avec toutes ses fractions de seconde, vous devez utiliser juste un double (intervalle de temps depuis la date de référence) lors de l'enregistrement / réception de votre date sur le serveur.
Leo Dabus
@LeoDabus oui, si vous contrôlez tout le système et n'avez pas besoin d'interopérer. Comme je l'ai dit dans la réponse, ce n'est pas nécessaire pour la plupart des utilisateurs. Mais nous n'avons pas tous toujours le contrôle sur le formatage des données dans les API Web, et comme Android et Python conservent (au moins) 6 chiffres de précision fractionnaire, il est parfois nécessaire de faire de même.
Eli Burke
3

Dans mon cas, je dois convertir la colonne DynamoDB - lastUpdated (horodatage Unix) en heure normale.

La valeur initiale de lastUpdated était: 1460650607601 - convertie en 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
la source
3

À l'avenir, le format devra peut-être être modifié, ce qui pourrait être un petit mal de tête avec des appels date.dateFromISO8601 partout dans une application. Utilisez une classe et un protocole pour encapsuler l'implémentation, changer l'appel de format de date et d'heure en un seul endroit sera plus simple. Utilisez RFC3339 si possible, c'est une représentation plus complète. DateFormatProtocol et DateFormat sont parfaits pour l'injection de dépendances.

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
la source
3

Il y a un nouveau ISO8601DateFormatter classe qui vous permet de créer une chaîne avec une seule ligne. Pour la compatibilité ascendante, j'ai utilisé une ancienne bibliothèque C. J'espère que c'est utile pour quelqu'un.

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
la source
1

Pour compléter la version de Leo Dabus, j'ai ajouté la prise en charge des projets écrits Swift et Objective-C, ainsi que la prise en charge des millisecondes optionnelles, ce n'est probablement pas le meilleur, mais vous obtiendrez le point:

Xcode 8 et 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
la source
0

Sans certains masques de chaîne manuels ou 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
la source
C'est une bonne réponse, mais l'utilisation .iso8601n'inclura pas les millisecondes.
Stefan Arentz
0

Basé sur la réponse acceptable dans un paradigme d'objet

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

site d'appel

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