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