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:
Meilleur cas:
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))
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"]
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 .
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:
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
}
}
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)
}
}
}
À 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
}
}
}
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)
}
Utilise ISO8601DateFormatter
sur iOS10 ou plus récent.
Utilise DateFormatter
sur iOS9 ou une version antérieure.
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
}
}()
}
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