web-dev-qa-db-fra.com

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

Comment générer un horodatage à l'aide des normes de format pour 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, seconde, millisecondes, sous la forme "XX: XX: XX.XXX".
  • la lettre "Z" comme désignateur de zone pour le décalage d'origine, a.k.a. UTC, GMT, heure zoulou.

Meilleur cas:

  • Un code source rapide, simple, court et simple.
  • Pas besoin d'utiliser de framework, sous-projet, cocoapod, code C, etc. supplémentaire.

J'ai cherché StackOverflow, Google, Apple, etc. et je n'ai pas trouvé de réponse rapide à cette question. 

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

Questions/Réponses connexes: Comment puis-je obtenir la date ISO 8601 sur iOS?

Voici le meilleur que j'ai créé 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))
145
joelparkerhenderson

Xcode 9 • Swift 4 • iOS 11 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 iso8601 = ISO8601DateFormatter([.withInternetDateTime, .withFractionalSeconds])
}

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

extension String {
    var iso8601: Date? {
        return Formatter.iso8601.date(from: self)
    }
}

Usage:

Date().description(with: .current)  //  Tuesday, February 5, 2019 at 10:35:01 PM Brasilia Summer Time"
let dateString = Date().iso8601   //  "2019-02-06T00:35:01.746Z"

if let date = dateString.iso8601 {
    date.description(with: .current) // "Tuesday, February 5, 2019 at 10:35:01 PM Brasilia Summer Time"
    print(date.iso8601)           //  "2019-02-06T00:35:01.746Z\n"
}

iOS 9 • Swift 3 ou ultérieur

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

Protocole codable

Si vous devez encoder et décoder ce format lorsque vous utilisez Codable protocole, 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.iso8601.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.iso8601.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)
String(data: data, encoding: .utf8)! // ["2019-02-08T23:46:12.985Z"]\n"

décodage

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

315
Leo Dabus

N'oubliez pas de définir les paramètres régionaux sur en_US_POSIX comme décrit dans Q & A1480 . Dans Swift 3:

let date = Date()
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ"
formatter.timeZone = TimeZone(secondsFromGMT: 0)
formatter.locale = Locale(identifier: "en_US_POSIX")
print(formatter.string(from: date))

Le problème est que si vous utilisez un appareil utilisant un calendrier non grégorien, l'année ne sera pas conforme à RFC3339/ISO8601 à moins que vous ne spécifiiez la chaîne locale ainsi que la chaîne timeZone et dateFormat.

Ou vous pouvez utiliser ISO8601DateFormatter pour vous sortir des mauvaises herbes de régler vous-même locale et timeZone:

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 de Swift 2, voir révision précédente de cette réponse .

24
Rob

Si vous souhaitez utiliser la fonction ISO8601DateFormatter() avec la date d'un flux JSON Rails 4+ (et n'avez pas besoin de plusieurs millis, bien sûr), vous devez définir quelques options sur le formateur pour qu'il fonctionne correctement, sinon la fonction date(from: string) retournera néant. 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 des options pas dans une capture d'écran d'un terrain de jeu:

 enter image description here

19
Matt Long

Pour complimenter davantage Andrés Torres Marroquín et Leo Dabus, j'ai une version qui conserve une fraction de seconde. Je ne le trouve pas documenté nulle part, mais Apple tronque les secondes fractionnelles à la microseconde (précision à 3 chiffres) en entrée et en sortie (même si spécifié avec SSSSSSS, contrairement à Unicode tr35-31 ).

Je tiens à souligner que ce n'est probablement pas nécessaire dans la plupart des cas d'utilisation}. Les dates en ligne n'ont généralement pas besoin d'une précision à la milliseconde. Dans ce 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
    }
}
4
Eli Burke

Il existe une nouvelle classe ISO8601DateFormatter 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 cela 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)
        }
    }
}
3
Thomas Szabo

À l'avenir, il faudra peut-être changer le format, ce qui pourrait être un peu mal à la tête et avoir des appels date.dateFromISO8601 partout dans une application. Utilisez une classe et un protocole pour envelopper l’implémentation. Il sera plus simple de modifier l’appel au format date-heure à un endroit. Utilisez RFC3339 si possible, c'est une représentation plus complète. DateFormatProtocol et DateFormat sont parfaits pour l'injection de dépendance.

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

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

La valeur initiale de lastUpdated était: 1460650607601 - convertie jusqu'au 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)

            }
3
ioopl

Utilise ISO8601DateFormatter sur iOS10 ou plus récent.

Utilise DateFormatter sur iOS9 ou une 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
        }
    }()
}
2
neoneye

Pour compléter la version de Leo Dabus, j'ai ajouté un support pour les projets écrits en Swift et Objective-C, ainsi qu'un support pour les 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
    }
}

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
0
Dmitrii Z