¿Cómo crear un sello de fecha y hora y formato como ISO 8601, RFC 3339, zona horaria UTC?


186

¿Cómo generar una marca de fecha y hora, utilizando los estándares de formato para ISO 8601 y RFC 3339 ?

El objetivo es una cadena que se ve así:

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

Formato:

  • año, mes, día, como "XXXX-XX-XX"
  • la letra "T" como separador
  • hora, minuto, segundos, milisegundos, como "XX: XX: XX.XXX".
  • la letra "Z" como un designador de zona para desplazamiento cero, también conocido como UTC, GMT, hora zulú.

Mejor caso:

  • Código fuente rápido que es simple, corto y directo.
  • No es necesario utilizar ningún marco adicional, subproyecto, cocoapod, código C, etc.

He buscado en StackOverflow, Google, Apple, etc. y no he encontrado una respuesta rápida a esto.

Las clases que parecen más prometedores son NSDate, NSDateFormatter, NSTimeZone.

Preguntas y respuestas relacionadas: ¿Cómo obtengo la fecha ISO 8601 en iOS?

Aquí está lo mejor que he encontrado hasta ahora:

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

66
Tenga en cuenta que iOS10 + SIMPLEMENTE INCLUYE ISO 8601 INTEGRADO ... simplemente se completará automáticamente para usted.
Fattie

2
@Fattie Y, ¿cómo puede manejar esa última parte de la marca de tiempo de .234Z milisegundos Zulu / UTC? Respuesta: Matt Longs @ stackoverflow.com/a/42101630/3078330
smat88dd

1
@ smat88dd - consejo fantástico, gracias. No tenía idea de que había "opciones en un formateador", ¡extraño y salvaje!
Fattie

Estoy buscando una solución que funcione en Linux.
neoneye

@neoneye Simplemente use la versión anterior (simple DateFormatter) y cambie el calendario iso8601 a gregorian stackoverflow.com/a/28016692/2303865
Leo Dabus el

Respuestas:


393

Swift 4 • iOS 11.2.1 o posterior

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 posterior

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

Protocolo codificable

Si necesita codificar y decodificar este formato cuando trabaje con el protocolo Codificable, puede crear sus propias estrategias de codificación / decodificación de fecha personalizadas:

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

y la estrategia de codificación

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

Prueba de patio de recreo

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

codificación

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

descodificación

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

ingrese la descripción de la imagen aquí


3
Sería útil agregar una extensión de conversión opuesta:extension String { var dateFormattedISO8601: NSDate? {return NSDate.Date.formatterISO8601.dateFromString(self)} }
Vive

1
Solo una nota de que esto pierde un poco de precisión, por lo que es importante asegurarse de que la igualdad de fechas se compare a través de la cadena generada y no timeInterval. let now = NSDate() let stringFromDate = now.iso8601 let dateFromString = stringFromDate.dateFromISO8601! XCTAssertEqual(now.timeIntervalSince1970, dateFromString.timeIntervalSince1970)
pixelrevision

77
No hace falta decir que si no necesita milisegundos, el nuevo iOS 10 ISO8601DateFormattersimplifica el proceso. He emitido un informe de error (27242248) a Apple, solicitándoles que expandan este nuevo formateador para ofrecer la capacidad de especificar milisegundos también (ya que este nuevo formateador no es útil para muchos de nosotros sin los milisegundos).
Rob

1
En RFC3339 podemos encontrar una nota "NOTA: ISO 8601 define la fecha y la hora separadas por" T ". Las aplicaciones que usan esta sintaxis pueden elegir, en aras de la legibilidad, especificar una fecha completa y una hora completa separadas por (digamos) un personaje espacial ". ¿Cubre también el formato de fecha sin, Tpor ejemplo 2016-09-21 21:05:10+00:00:?
manRo

3
@LeoDabus sí, pero este es el primer resultado para "Swift iso8601". Mi comentario estaba destinado a advertir a otros desarrolladores que se encontrarían con esto en el futuro y no estaba dirigido a OP.
thislooksfun

39

Recuerde configurar la configuración regional en_US_POSIXcomo se describe en las Preguntas y respuestas técnicas 1480 . En 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))

La cuestión es que si estás en un dispositivo que utiliza un calendario no gregoriano, el año no cumplirá con RFC3339 / ISO8601 a menos que especifique el locale, así como el timeZoney dateFormatcadena.

O puede usarlo ISO8601DateFormatterpara salir de la maleza del entorno localey de timeZoneusted mismo:

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

Para la versión de Swift 2, vea la revisión anterior de esta respuesta .


¿Por qué deberíamos establecer la configuración regional en en_US_POSIX? incluso si no estamos en los Estados Unidos?
mnemonic23

2
Bueno, necesita un entorno local coherente y la convención de las normas ISO 8601 / RFC 3999 es ese formato que ofrece en_US_POSIX. Es la lengua franca para intercambiar fechas en la web. Y no puede hacer que malinterprete las fechas si se usó un calendario en el dispositivo al guardar una cadena de fecha y otro cuando la cadena se vuelve a leer más tarde. Además, necesita un formato que garantice que nunca cambiará (por eso lo usa en_US_POSIXy no en_US). Consulte las preguntas y respuestas técnicas 1480 o las normas RFC / ISO para obtener más información.
Rob

24

Si desea utilizar el ISO8601DateFormatter()con una fecha de un feed JSON de Rails 4+ (y no necesita millis, por supuesto), debe establecer algunas opciones en el formateador para que funcione correctamente; de ​​lo contrario, la date(from: string)función devolverá nil. Esto es lo que estoy 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
    }()
}

Aquí está el resultado de usar los versos de opciones que no están en una captura de pantalla del patio de juegos:

ingrese la descripción de la imagen aquí


Tendría que incluir en las opciones también el .withFractionalSecondspero ya lo intenté y sigue arrojando un error libc++abi.dylib: terminating with uncaught exception of type NSException.
Leo Dabus

@MEnnabah Me funciona bien en Swift 4. ¿Recibes un error?
Matt Long el

@LeoDabus, obtuvo el mismo error que el tuyo, ¿lo resolviste?
Freeman

JSONDecoder personalizado DateDecodingStrategy stackoverflow.com/a/46458771/2303865
Leo Dabus el

@freeman Si desea conservar la Fecha con todos sus segundos fraccionarios, sugiero utilizar un doble (intervalo de tiempo desde la fecha de referencia) al guardar / recibir su fecha en el servidor. Y use la estrategia de decodificación de fecha predeterminada .deferredToDatecuando use el protocolo Codificable
Leo Dabus el

6

Swift 5

Si está apuntando a iOS 11.0+ / macOS 10.13+, simplemente use ISO8601DateFormattercon las opciones withInternetDateTimey withFractionalSeconds, de esta manera:

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

Usos ISO8601DateFormatteren iOS10 o más reciente.

Usos DateFormatteren iOS9 o anterior.

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

Para felicitar aún más a Andrés Torres Marroquín y Leo Dabus, tengo una versión que conserva segundos fraccionarios. No puedo encontrarlo documentado en ningún lado, pero Apple trunca segundos fraccionarios al microsegundo (3 dígitos de precisión) tanto en la entrada como en la salida (aunque se especifique utilizando SSSSSSS, al contrario de Unicode tr35-31 ).

Debo enfatizar que esto probablemente no sea necesario para la mayoría de los casos de uso . Las fechas en línea generalmente no necesitan una precisión de milisegundos, y cuando lo hacen, a menudo es mejor usar un formato de datos diferente. Pero a veces uno debe interoperar con un sistema preexistente de una manera particular.

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

Esta es la mejor respuesta en mi opinión, ya que le permite a uno llegar a un nivel de precisión de microsegundos donde todas las otras soluciones se truncan en milisegundos.
Michael A. McCloskey

Si desea conservar la Fecha con todos sus segundos fraccionarios, debe usar solo un doble (intervalo de tiempo desde la fecha de referencia) al guardar / recibir su fecha en el servidor.
Leo Dabus

@LeoDabus sí, si controlas todo el sistema y no necesitas interoperar. Como dije en la respuesta, esto no es necesario para la mayoría de los usuarios. Pero no todos siempre tenemos control sobre el formato de datos en las API web, y como Android y Python (al menos) conservan 6 dígitos de precisión fraccional, a veces es necesario hacer lo mismo.
Eli Burke

3

En mi caso, tengo que convertir la columna DynamoDB - lastUpdated (Unix Timestamp) a Normal Time.

El valor inicial de lastUpdated fue: 1460650607601 - convertido a 2016-04-14 16:16:47 +0000 a través de:

   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

En el futuro, es posible que sea necesario cambiar el formato, lo que podría ser un pequeño dolor de cabeza con date.dateFromISO8601 en todas partes de una aplicación. Use una clase y un protocolo para ajustar la implementación, cambiar el formato de fecha y hora en un solo lugar será más simple. Utilice RFC3339 si es posible, es una representación más completa. DateFormatProtocol y DateFormat es ideal para la inyección de dependencias.

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

Hay una nueva ISO8601DateFormatterclase que le permite crear una cadena con solo una línea. Por compatibilidad con versiones anteriores, utilicé una antigua biblioteca C. Espero que esto sea útil para alguien.

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

Para complementar la versión de Leo Dabus, agregué soporte para proyectos escritos Swift y Objective-C, también agregué soporte para los milisegundos opcionales, probablemente no sea lo mejor, pero entendería el punto:

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

Sin algunas máscaras de cadena manuales o 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

Esta es una buena respuesta, pero el uso .iso8601no incluirá milisegundos.
Stefan Arentz

0

Basado en la respuesta aceptable en un paradigma de objeto

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

sitio de llamadas

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())")
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.