¿Cómo obtengo una fecha ISO 8601 en iOS?

115

Es bastante fácil obtener la cadena de fecha ISO 8601 (por ejemplo, 2004-02-12T15:19:21+00:00) en PHP a través de date('c'), pero ¿cómo se obtiene en Objective-C (iPhone)? ¿Existe una forma igualmente corta de hacerlo?

Aquí está el camino largo que encontré para hacerlo:

NSDateFormatter* dateFormatter = [[[NSDateFormatter alloc] init] autorelease];
dateFormatter.dateFormat = @"yyyy-MM-dd'T'HH:mm:ssZ";

NSDate *now = [NSDate date];
NSString *formattedDateString = [dateFormatter stringFromDate:now];
NSLog(@"ISO-8601 date: %@", formattedDateString);

// Output: ISO-8601 date: 2013-04-27T13:27:50-0700

Parece una tremenda confusión para algo tan central.

JohnK
fuente
1
Supongo que lo corto está en el ojo del espectador. Tres líneas de código son bastante cortas, ¿no? Hay una subclase de C NSDate que puede agregar que lo hará con una línea, simplemente me tropecé con ella: github.com/soffes/SAMCategories/tree/master/SAMCategories/… . Dicho esto, realmente, obtuviste respuestas bastante buenas (en mi humilde opinión) y deberías otorgarle a Etienne o rmaddy la respuesta. Es una buena práctica y recompensa a las personas que brindan información realmente útil (me ayudó, necesitaba esta información hoy). Etienne podría usar los puntos más que rmaddy. ¡Como señal de buena medida, incluso votaré a favor de tu pregunta!
David H

Respuestas:

212

Utilizar NSDateFormatter:

NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];   
NSLocale *enUSPOSIXLocale = [NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"];
[dateFormatter setLocale:enUSPOSIXLocale];
[dateFormatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ssZZZZZ"];
[dateFormatter setCalendar:[NSCalendar calendarWithIdentifier:NSCalendarIdentifierGregorian]];

NSDate *now = [NSDate date];
NSString *iso8601String = [dateFormatter stringFromDate:now];

Y en Swift:

let dateFormatter = DateFormatter()
let enUSPosixLocale = Locale(identifier: "en_US_POSIX")
dateFormatter.locale = enUSPosixLocale
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ"
dateFormatter.calendar = Calendar(identifier: .gregorian)

let iso8601String = dateFormatter.string(from: Date())
rmaddy
fuente
1
Gracias, Maddy. Acabo de ver su respuesta ahora, después de publicar mi código. ¿Es lo setLocaleimportante?
JohnK
2
@rmaddy Puede editar su ejemplo para usar ZZZZZ para que las personas que copian y pegan no se quemen.
Jesse Rusak
4
@jlmendezbonini No. Es muy raro que quieras YYYY. Normalmente quieres aaaa.
rmaddy
7
Fuente de por qué la configuración regional debe establecerse en en_US_POSIX: developer.apple.com/library/mac/qa/qa1480/_index.html
yood
11
@maddy: quería que los demás pudieran descubrirlo fácilmente a través de Google y quería darle crédito por haber descubierto el formato correcto, la configuración regional, etc. Pero feliz de publicarlo como una respuesta separada si lo desea
Robin
60

iOS 10 introduce una nueva NSISO8601DateFormatterclase para manejar precisamente esto. Si está utilizando Swift 3, su código sería algo como esto:

let formatter = ISO8601DateFormatter()
let date = formatter.date(from: "2016-08-26T12:39:00Z")
let string = formatter.string(from: Date())
Dos pajitas
fuente
y que formato le da? También sería bueno saber si puede manejar desde: 2016-08-26T12: 39: 00-0000 y 2016-08-26T12: 39: 00-00: 00 y 2016-08-26T12: 39: 00.374-0000
Malhal
1
Después de que se ejecuten las tres líneas en mi respuesta, stringcontiene "2016-09-03T21: 47: 27Z".
TwoStraws
3
que NO puede manejar milisegundos con este formato.
Enrique
3
@Enrique: debes agregar el NSISO8601DateFormatWithFractionalSeconds a las opciones del formateador para poder trabajar con milisegundos, consulta la documentación.
PakitoV
1
NSISO8601DateFormatWithFractionalSeconds funciona solo en 11.2+. De lo contrario, ¡tienes un accidente!
hash3r
27

Como complemento a la respuesta de Maddy, el formato de zona horaria debería ser "ZZZZZ" (5 veces Z) para ISO 8601 en lugar de una sola "Z" (que es para el formato RFC 822). Al menos en iOS 6.

(ver http://www.unicode.org/reports/tr35/tr35-25.html#Date_Format_Patterns )

Etienne
fuente
¡Gran captura! Cualquier otra persona interesada en esto, vaya al enlace, busque 8601 y lo verá.
David H
Cuando la zona horaria de la fecha es UTC, ¿hay alguna forma de hacer que muestre las 00:00 en lugar de la Z (que son equivalentes)?
shim
Muchas gracias;) me estaba golpeando la cabeza contra la pared con un dateformatter que regresaba sin fecha !!! :)
polo987
19

Un problema que a menudo se pasa por alto es que las cadenas en formato ISO 8601 pueden tener milisegundos o no.

En otras palabras, tanto "2016-12-31T23: 59: 59.9999999" y "2016-12-01T00: 00: 00" son legítimos, pero si está utilizando un formateador de fecha de tipo estático, uno de ellos no se analizará .

A partir de iOS 10 , debe usar ISO8601DateFormatterque maneje todas las variaciones de cadenas de fecha ISO 8601. Vea el ejemplo a continuación:

let date = Date()
var string: String

let formatter = ISO8601DateFormatter()
string = formatter.string(from: date)

let GMT = TimeZone(abbreviation: "GMT")
let options: ISO8601DateFormatOptions = [.withInternetDateTime, .withDashSeparatorInDate, .withColonSeparatorInTime, .withTimeZone]
string = ISO8601DateFormatter.string(from: date, timeZone: GMT, formatOptions: options)

Para iOS 9 y versiones anteriores, utilice el siguiente enfoque con varios formateadores de datos.

No he encontrado una respuesta que cubra ambos casos y abstraiga esta sutil diferencia. Aquí está la solución que lo aborda:

extension DateFormatter {

    static let iso8601DateFormatter: DateFormatter = {
        let enUSPOSIXLocale = Locale(identifier: "en_US_POSIX")
        let iso8601DateFormatter = DateFormatter()
        iso8601DateFormatter.locale = enUSPOSIXLocale
        iso8601DateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"
        iso8601DateFormatter.timeZone = TimeZone(secondsFromGMT: 0)
        return iso8601DateFormatter
    }()

    static let iso8601WithoutMillisecondsDateFormatter: DateFormatter = {
        let enUSPOSIXLocale = Locale(identifier: "en_US_POSIX")
        let iso8601DateFormatter = DateFormatter()
        iso8601DateFormatter.locale = enUSPOSIXLocale
        iso8601DateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss'Z'"
        iso8601DateFormatter.timeZone = TimeZone(secondsFromGMT: 0)
        return iso8601DateFormatter
    }()

    static func date(fromISO8601String string: String) -> Date? {
        if let dateWithMilliseconds = iso8601DateFormatter.date(from: string) {
            return dateWithMilliseconds
        }

        if let dateWithoutMilliseconds = iso8601WithoutMillisecondsDateFormatter.date(from: string) {
            return dateWithoutMilliseconds
        }

        return nil
    }
}

Uso:

let dateToString = "2016-12-31T23:59:59.9999999"
let dateTo = DateFormatter.date(fromISO8601String: dateToString)
// dateTo: 2016-12-31 23:59:59 +0000

let dateFromString = "2016-12-01T00:00:00"
let dateFrom = DateFormatter.date(fromISO8601String: dateFromString)
// dateFrom: 2016-12-01 00:00:00 +0000

También recomiendo consultar el artículo de Apple sobre formateadores de fecha.

Vadim Bulavin
fuente
¿Podría indicar también cómo obtener NSISO8601DateFormtter en Obj-C? (en iOS 10 y superior, por supuesto)
Motti Shneor
8

Así que use la categoría de Sam Soffee en NSDate que se encuentra aquí . Con ese código agregado a su proyecto, a partir de ese momento puede usar un solo método en NSDate:

- (NSString *)sam_ISO8601String

No solo es una línea, es mucho más rápido que el enfoque NSDateFormatter, ya que está escrito en C.

David H
fuente
1
La ubicación actual de la categoría está aquí
Perry
1
No lo recomendaría, tiene un problema de excepción de memoria que ocurre de manera muy aleatoria
M_Waly
1
@M_Waly, ¿le informaste de esto? Construí su código sin advertencias y pasó bien las comprobaciones de análisis.
David H
6

Desde IS8601 , los problemas son la representación y la zona horaria

  • ISO 8601 = year-month-day time timezone
  • Para la fecha y la hora, hay formato básico (AAAAMMDD, hhmmss, ...) y extendido (AAAA-MM-DD, hh: mm: ss, ...)
  • La zona horaria puede ser zulú, offset o GMT
  • El separador de fecha y hora puede ser un espacio o T
  • Hay un formato de semana para la fecha, pero rara vez se usa
  • La zona horaria puede tener muchos espacios después
  • El segundo es opcional

Aquí hay algunas cadenas válidas

2016-04-08T10:25:30Z
2016-04-08 11:25:30+0100
2016-04-08 202530GMT+1000
20160408 08:25:30-02:00
2016-04-08 11:25:30     +0100

Soluciones

NSDateFormatter

Así que aquí está el formato que estoy usando en onmyway133 ISO8601

let formatter = NSDateFormatter()
formatter.locale = NSLocale(localeIdentifier: "en_US_POSIX")
formatter.dateFormat = "yyyyMMdd HHmmssZ"

Acerca de la tabla de símbolos del campo de fecha del Zidentificador

Z: The ISO8601 basic format with hours, minutes and optional seconds fields. The format is equivalent to RFC 822 zone format (when optional seconds field is absent)

Acerca del locale formato de datos mediante la configuración regional

Las configuraciones regionales representan las opciones de formato para un usuario en particular, no el idioma preferido del usuario. Suelen ser iguales, pero pueden ser diferentes. Por ejemplo, un hablante nativo de inglés que vive en Alemania puede seleccionar el inglés como idioma y Alemania como región.

Acerca de en_US_POSIX preguntas y respuestas técnicas QA1480 NSDateFormatter y fechas de Internet

Por otro lado, si está trabajando con fechas de formato fijo, primero debe establecer la configuración regional del formateador de fechas en algo apropiado para su formato fijo. En la mayoría de los casos, la mejor configuración regional para elegir es "en_US_POSIX", una configuración regional diseñada específicamente para producir resultados en inglés de EE. UU. Independientemente de las preferencias del usuario y del sistema. "en_US_POSIX" también es invariante en el tiempo (si EE. UU., en algún momento en el futuro, cambia la forma en que da formato a las fechas, "en_US" cambiará para reflejar el nuevo comportamiento, pero "en_US_POSIX" no lo hará) y entre máquinas ( "en_US_POSIX" funciona igual en iOS que en OS X y como lo hace en otras plataformas).

Interesantes consultas relacionadas

onmyway133
fuente
Lo intenté con 2016-07-23T07: 24: 23Z, no funciona
Kamen Dobrev
@KamenDobrev ¿qué quieres decir con "no funciona"? Acabo de agregar su ejemplo a las pruebas github.com/onmyway133/ISO8601/blob/master/ISO8601Tests/… y funciona
onmyway133
3

Basado en esta esencia: https://github.com/justinmakaila/NSDate-ISO-8601/blob/master/NSDateISO8601.swift , el siguiente método se puede utilizar para convertir NSDate a la cadena de fecha ISO 8601 en el formato de aaaa-MM -dd'T'HH: mm: ss.SSSZ

-(NSString *)getISO8601String
    {
        static NSDateFormatter *formatter = nil;
        if (!formatter)
        {
            formatter = [[NSDateFormatter alloc] init];
            [formatter setLocale: [NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"]];
            formatter.timeZone = [NSTimeZone timeZoneWithAbbreviation: @"UTC"];
            [formatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ss.SSS"];

        }
        NSString *iso8601String = [formatter stringFromDate: self];
        return [iso8601String stringByAppendingString: @"Z"];
    }
Zack Zhu
fuente
El límite Spara el sub-segundo fue clave aquí
Mark Meyer
Tenga en cuenta que si necesita más de 3 S, no puede usar el formateador de fecha para analizarlo manualmente.
Malhal
-1

Esto es un poco más simple y pone la fecha en UTC.

extension NSDate
{
    func iso8601() -> String
    {
        let dateFormatter = NSDateFormatter()
        dateFormatter.timeZone = NSTimeZone(name: "UTC")
        let iso8601String = dateFormatter.stringFromDate(NSDate())
        return iso8601String
    }
}

let date = NSDate().iso8601()
Ifaaw
fuente
-1

Usando Swift 3.0 y iOS 9.0

extension Date {
    private static let jsonDateFormatter: DateFormatter = {
        let formatter = DateFormatter()
        formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ"
        formatter.locale = Locale(identifier: "en_US_POSIX")
        formatter.timeZone = TimeZone(identifier: "UTC")!
        return formatter
    }()

    var IOS8601String: String {
        get {
            return Date.jsonDateFormatter.string(from: self)
        }
    }

    init?(fromIOS8601 dateString: String) {
        if let d = Date.jsonDateFormatter.date(from: dateString) {
            self.init(timeInterval: 0, since:d)
        } else {
            return nil
        }
    }
}
Denis Krivitski
fuente