swift swift4 nsdateformatter codable dateformatter

Cómo convertir una cadena de fecha con segundos fraccionales opcionales usando Codificable en Swift4



nsdateformatter codable (2)

Alternativamente a la respuesta de @ Leo, y si necesita proporcionar soporte para sistemas operativos más antiguos ( ISO8601DateFormatter está disponible solo a partir de iOS 10, mac OS 10.12), puede escribir un formateador personalizado que use ambos formatos al analizar la cadena:

class MyISO8601Formatter: DateFormatter { static let formatters: [DateFormatter] = [ iso8601Formatter(withFractional: true), iso8601Formatter(withFractional: false) ] static func iso8601Formatter(withFractional fractional: Bool) -> 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/(fractional ? ".SSS" : "")XXXXX" return formatter } override public func getObjectValue(_ obj: AutoreleasingUnsafeMutablePointer<AnyObject?>?, for string: String, errorDescription error: AutoreleasingUnsafeMutablePointer<NSString?>?) -> Bool { guard let date = (type(of: self).formatters.flatMap { $0.date(from: string) }).first else { error?.pointee = "Invalid ISO8601 date: /(string)" as NSString return false } obj?.pointee = date as NSDate return true } override public func string(for obj: Any?) -> String? { guard let date = obj as? Date else { return nil } return type(of: self).formatters.flatMap { $0.string(from: date) }.first } }

, que puedes usar como estrategia de decodificación de fecha:

let decoder = JSONDecoder() decoder.dateDecodingStrategy = .formatted(MyISO8601Formatter())

Aunque es un poco más feo en la implementación, esto tiene la ventaja de ser consistente con los errores de decodificación que Swift arroja en caso de datos mal formados, ya que no modificamos el mecanismo de informe de errores).

Por ejemplo:

struct TestDate: Codable { let date: Date } // I don''t advocate the forced unwrap, this is for demo purposes only let jsonString = "{/"date/":/"2017-06-19T18:43:19Z/"}" let jsonData = jsonString.data(using: .utf8)! let decoder = JSONDecoder() decoder.dateDecodingStrategy = .formatted(MyISO8601Formatter()) do { print(try decoder.decode(TestDate.self, from: jsonData)) } catch { print("Encountered error while decoding: /(error)") }

imprimirá TestDate(date: 2017-06-19 18:43:19 +0000)

Agregar la parte fraccional

let jsonString = "{/"date/":/"2017-06-19T18:43:19.123Z/"}"

dará como resultado la misma salida: TestDate(date: 2017-06-19 18:43:19 +0000)

Sin embargo, usando una cadena incorrecta:

let jsonString = "{/"date/":/"2017-06-19T18:43:19.123AAA/"}"

imprimirá el error Swift predeterminado en caso de datos incorrectos:

Encountered error while decoding: dataCorrupted(Swift.DecodingError.Context(codingPath: [__lldb_expr_84.TestDate.(CodingKeys in _B178608BE4B4E04ECDB8BE2F689B7F4C).date], debugDescription: "Date string does not match format expected by formatter.", underlyingError: nil))

Estoy reemplazando mi antiguo código de análisis JSON con Swift''s Codable y me encuentro con un pequeño inconveniente. Supongo que no es tanto una pregunta codificable como una pregunta de DateFormatter.

Comience con una estructura

struct JustADate: Codable { var date: Date }

y una cuerda json

let json = """ { "date": "2017-06-19T18:43:19Z" } """

ahora vamos a decodificar

let decoder = JSONDecoder() decoder.dateDecodingStrategy = .iso8601 let data = json.data(using: .utf8)! let justADate = try! decoder.decode(JustADate.self, from: data) //all good

Pero si cambiamos la fecha para que tenga segundos fraccionarios, por ejemplo:

let json = """ { "date": "2017-06-19T18:43:19.532Z" } """

Ahora se rompe. Las fechas a veces vuelven con segundos fraccionarios y otras no. La forma en que solía resolverlo era en mi código de mapeo. Tenía una función de transformación que probaba ambos dateFormats con y sin los segundos fraccionarios. Sin embargo, no estoy muy seguro de cómo abordarlo usando Codable. ¿Alguna sugerencia?


Puede usar dos formateadores de fecha diferentes (con y sin fracción de segundos) y crear una DateDecodingStrategy personalizada. En caso de falla al analizar la fecha devuelta por la API, puede lanzar un DecodingError como lo sugiere @PauloMattos en los comentarios:

iOS 9, macOS 10.9, tvOS 9, watchOS 2, Xcode 9 o posterior

El ISO8601 DateFormatter personalizado:

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 }() static let iso8601noFS: 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:ssXXXXX" return formatter }() }

El DateDecodingStrategy personalizado y Error :

extension JSONDecoder.DateDecodingStrategy { static let customISO8601 = custom { let container = try $0.singleValueContainer() let string = try container.decode(String.self) if let date = Formatter.iso8601.date(from: string) ?? Formatter.iso8601noFS.date(from: string) { return date } throw DecodingError.dataCorruptedError(in: container, debugDescription: "Invalid date: /(string)") } }

El DateEncodingStrategy personalizado:

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

editar / actualizar :

Xcode 9 • Swift 4 • iOS 11 o posterior

ISO8601DateFormatter ahora es compatible con formatOptions .withFractionalSeconds en iOS11 o posterior:

extension Formatter { static let iso8601: ISO8601DateFormatter = { let formatter = ISO8601DateFormatter() formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] return formatter }() static let iso8601noFS = ISO8601DateFormatter() }

Las costumbres DateDecodingStrategy y DateEncodingStrategy serían las mismas que se muestran arriba.

// Playground testing struct ISODates: Codable { let dateWith9FS: Date let dateWith3FS: Date let dateWith2FS: Date let dateWithoutFS: Date } let isoDatesJSON = """ { "dateWith9FS": "2017-06-19T18:43:19.532123456Z", "dateWith3FS": "2017-06-19T18:43:19.532Z", "dateWith2FS": "2017-06-19T18:43:19.53Z", "dateWithoutFS": "2017-06-19T18:43:19Z", } """ let isoDatesData = Data(isoDatesJSON.utf8) let decoder = JSONDecoder() decoder.dateDecodingStrategy = .customISO8601 do { let isoDates = try decoder.decode(ISODates.self, from: isoDatesData) print(Formatter.iso8601.string(from: isoDates.dateWith9FS)) // 2017-06-19T18:43:19.532Z print(Formatter.iso8601.string(from: isoDates.dateWith3FS)) // 2017-06-19T18:43:19.532Z print(Formatter.iso8601.string(from: isoDates.dateWith2FS)) // 2017-06-19T18:43:19.530Z print(Formatter.iso8601.string(from: isoDates.dateWithoutFS)) // 2017-06-19T18:43:19.000Z } catch { print(error) }