Swift 具有空字符串或 Nil 的可解码日期
Swift Decodable Dates with Empty Strings or Nil
情况
我正在处理一个具有多个日期字段的 API 服务器,我已经看到这些日期字段的 API 响应可以是:
{
"clicktimestamp": "",
"clicktimestamp": " ",
"clicktimestamp": "2020-08-08 16:13:17"
}
JSON 响应可以是:
• 字符串(无白space)
• 字符串(白色space)
• 具有某种日期格式的字符串。
我无权访问API服务器,我无法要求服务器端工程师更改它。我的情况不理想,不得不处理
工作解决方案(不是很 Swifty)
我写了一些代码来处理这种情况。它有效,但 感觉不太好 Swift。
考虑到我的 JSON 响应情况,我该如何改进整个解码过程?
还是很好?
这是一个可行的解决方案:
import UIKit
import Foundation
struct ProductDate: Decodable, Hashable {
var lastcheckedtime: Date?
var oktime: Date?
var clicktimestamp: Date?
var lastlocaltime: Date?
// I have more properties but I'm omitting them
}
extension ProductDate {
private enum Keys: String, CodingKey {
case lastcheckedtime
case oktime
case clicktimestamp
case lastlocaltime
}
init(from decoder: Decoder) throws {
let formatter = DateFormatter.yyyyMMdd
let container = try decoder.container(keyedBy: Keys.self)
let dateKeys: [KeyedDecodingContainer<Keys>.Key] = [
.lastcheckedtime,
.oktime,
.clicktimestamp,
.lastlocaltime
]
let parseDate: (String, KeyedDecodingContainer<Keys>.Key, KeyedDecodingContainer<Keys>) throws -> Date? = {(dateString, someKey, container) in
if !dateString.isEmpty {
if let date = formatter.date(from: dateString) {
return date
} else {
throw DecodingError.dataCorruptedError(forKey: someKey,
in: container,
debugDescription: "Date string does not match format expected by formatter.")
}
} else {
return nil
}
}
let datesResults: [Date?] = try dateKeys.map({ key in
// 1. decode as a string because we sometimes get "" or " " for those date fields as the API server is poorly managed.
let dateString = try container.decode(String.self, forKey: key)
.trimmingCharacters(in: .whitespaces)
// 2. now pass in our dateString which could be "" or " " or "2020-08-08 16:13:17"
// and try to parse it into a Date or nil
let result = try parseDate(dateString, key, container)
return result
})
// 3. Assign our array of dateResults to our struct keys
lastcheckedtime = datesResults[0]
oktime = datesResults[1]
clicktimestamp = datesResults[2]
lastlocaltime = datesResults[3]
}
}
extension DateFormatter {
static let yyyyMMdd: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "YYYY-MM-DD HH:mm:ss"
formatter.calendar = Calendar(identifier: .iso8601)
formatter.timeZone = TimeZone(secondsFromGMT: 0)
formatter.locale = Locale(identifier: "en_US_POSIX")
return formatter
}()
}
let json = """
{
"lastcheckedtime": "",
"oktime": " ",
"clicktimestamp": "",
"lastlocaltime": "2020-08-08 16:13:17"
}
""".data(using: .utf8)!
let decoder = JSONDecoder()
print(json)
do {
let decoded = try decoder.decode(ProductDate.self, from: json)
print(decoded)
} catch let context {
print(context)
}
太复杂了。
添加一个dateDecodingStrategy
并解码Date
。如果分配失败 nil
.
不需要修剪东西。
另请注意,您的日期格式有误。
struct ProductDate: Decodable, Hashable {
var lastcheckedtime: Date?
var oktime: Date?
var clicktimestamp: Date?
var lastlocaltime: Date?
}
extension ProductDate {
private enum CodingKeys: String, CodingKey {
case lastcheckedtime, oktime, clicktimestamp, lastlocaltime
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
lastcheckedtime = try? container.decode(Date.self, forKey: .lastcheckedtime)
oktime = try? container.decode(Date.self, forKey: .oktime)
clicktimestamp = try? container.decode(Date.self, forKey: .clicktimestamp)
lastlocaltime = try? container.decode(Date.self, forKey: .lastlocaltime)
}
}
extension DateFormatter {
static let yyyyMMdd: DateFormatter = {
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
formatter.timeZone = TimeZone(secondsFromGMT: 0)
return formatter
}()
}
let json = """
{
"lastcheckedtime": "",
"oktime": " ",
"clicktimestamp": "",
"lastlocaltime": "2020-08-08 16:13:17"
}
"""
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .formatted(.yyyyMMdd)
print(json)
do {
let decoded = try decoder.decode(ProductDate.self, from: Data(json.utf8))
print(decoded)
} catch let context {
print(context)
}
我会像这样使用 dateDecodingStrategy
为解码器设置自定义日期格式器
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .formatted(DateFormatter.yyyyMMdd)
然后在自定义 init
中,我首先解码为字符串并检查它是否为空,如果不是则解码为日期(使用上面的格式化程序)
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: Keys.self)
if try !container.decode(String.self, forKey: .lastcheckedtime).trimmingCharacters(in: .whitespaces).isEmpty {
lastcheckedtime = try container.decode(Date.self, forKey: .lastcheckedtime)
}
if try !container.decode(String.self, forKey: .oktime).trimmingCharacters(in: .whitespaces).isEmpty {
oktime = try container.decode(Date.self, forKey: .oktime)
}
if try !container.decode(String.self, forKey: .clicktimestamp).trimmingCharacters(in: .whitespaces).isEmpty {
clicktimestamp = try container.decode(Date.self, forKey: .clicktimestamp)
}
if try !container.decode(String.self, forKey: .lastlocaltime).trimmingCharacters(in: .whitespaces).isEmpty {
lastlocaltime = try container.decode(Date.self, forKey: .lastlocaltime)
}
}
情况
我正在处理一个具有多个日期字段的 API 服务器,我已经看到这些日期字段的 API 响应可以是:
{
"clicktimestamp": "",
"clicktimestamp": " ",
"clicktimestamp": "2020-08-08 16:13:17"
}
JSON 响应可以是:
• 字符串(无白space)
• 字符串(白色space)
• 具有某种日期格式的字符串。
我无权访问API服务器,我无法要求服务器端工程师更改它。我的情况不理想,不得不处理
工作解决方案(不是很 Swifty)
我写了一些代码来处理这种情况。它有效,但 感觉不太好 Swift。
考虑到我的 JSON 响应情况,我该如何改进整个解码过程?
还是很好?
这是一个可行的解决方案:
import UIKit
import Foundation
struct ProductDate: Decodable, Hashable {
var lastcheckedtime: Date?
var oktime: Date?
var clicktimestamp: Date?
var lastlocaltime: Date?
// I have more properties but I'm omitting them
}
extension ProductDate {
private enum Keys: String, CodingKey {
case lastcheckedtime
case oktime
case clicktimestamp
case lastlocaltime
}
init(from decoder: Decoder) throws {
let formatter = DateFormatter.yyyyMMdd
let container = try decoder.container(keyedBy: Keys.self)
let dateKeys: [KeyedDecodingContainer<Keys>.Key] = [
.lastcheckedtime,
.oktime,
.clicktimestamp,
.lastlocaltime
]
let parseDate: (String, KeyedDecodingContainer<Keys>.Key, KeyedDecodingContainer<Keys>) throws -> Date? = {(dateString, someKey, container) in
if !dateString.isEmpty {
if let date = formatter.date(from: dateString) {
return date
} else {
throw DecodingError.dataCorruptedError(forKey: someKey,
in: container,
debugDescription: "Date string does not match format expected by formatter.")
}
} else {
return nil
}
}
let datesResults: [Date?] = try dateKeys.map({ key in
// 1. decode as a string because we sometimes get "" or " " for those date fields as the API server is poorly managed.
let dateString = try container.decode(String.self, forKey: key)
.trimmingCharacters(in: .whitespaces)
// 2. now pass in our dateString which could be "" or " " or "2020-08-08 16:13:17"
// and try to parse it into a Date or nil
let result = try parseDate(dateString, key, container)
return result
})
// 3. Assign our array of dateResults to our struct keys
lastcheckedtime = datesResults[0]
oktime = datesResults[1]
clicktimestamp = datesResults[2]
lastlocaltime = datesResults[3]
}
}
extension DateFormatter {
static let yyyyMMdd: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "YYYY-MM-DD HH:mm:ss"
formatter.calendar = Calendar(identifier: .iso8601)
formatter.timeZone = TimeZone(secondsFromGMT: 0)
formatter.locale = Locale(identifier: "en_US_POSIX")
return formatter
}()
}
let json = """
{
"lastcheckedtime": "",
"oktime": " ",
"clicktimestamp": "",
"lastlocaltime": "2020-08-08 16:13:17"
}
""".data(using: .utf8)!
let decoder = JSONDecoder()
print(json)
do {
let decoded = try decoder.decode(ProductDate.self, from: json)
print(decoded)
} catch let context {
print(context)
}
太复杂了。
添加一个dateDecodingStrategy
并解码Date
。如果分配失败 nil
.
不需要修剪东西。
另请注意,您的日期格式有误。
struct ProductDate: Decodable, Hashable {
var lastcheckedtime: Date?
var oktime: Date?
var clicktimestamp: Date?
var lastlocaltime: Date?
}
extension ProductDate {
private enum CodingKeys: String, CodingKey {
case lastcheckedtime, oktime, clicktimestamp, lastlocaltime
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
lastcheckedtime = try? container.decode(Date.self, forKey: .lastcheckedtime)
oktime = try? container.decode(Date.self, forKey: .oktime)
clicktimestamp = try? container.decode(Date.self, forKey: .clicktimestamp)
lastlocaltime = try? container.decode(Date.self, forKey: .lastlocaltime)
}
}
extension DateFormatter {
static let yyyyMMdd: DateFormatter = {
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
formatter.timeZone = TimeZone(secondsFromGMT: 0)
return formatter
}()
}
let json = """
{
"lastcheckedtime": "",
"oktime": " ",
"clicktimestamp": "",
"lastlocaltime": "2020-08-08 16:13:17"
}
"""
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .formatted(.yyyyMMdd)
print(json)
do {
let decoded = try decoder.decode(ProductDate.self, from: Data(json.utf8))
print(decoded)
} catch let context {
print(context)
}
我会像这样使用 dateDecodingStrategy
为解码器设置自定义日期格式器
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .formatted(DateFormatter.yyyyMMdd)
然后在自定义 init
中,我首先解码为字符串并检查它是否为空,如果不是则解码为日期(使用上面的格式化程序)
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: Keys.self)
if try !container.decode(String.self, forKey: .lastcheckedtime).trimmingCharacters(in: .whitespaces).isEmpty {
lastcheckedtime = try container.decode(Date.self, forKey: .lastcheckedtime)
}
if try !container.decode(String.self, forKey: .oktime).trimmingCharacters(in: .whitespaces).isEmpty {
oktime = try container.decode(Date.self, forKey: .oktime)
}
if try !container.decode(String.self, forKey: .clicktimestamp).trimmingCharacters(in: .whitespaces).isEmpty {
clicktimestamp = try container.decode(Date.self, forKey: .clicktimestamp)
}
if try !container.decode(String.self, forKey: .lastlocaltime).trimmingCharacters(in: .whitespaces).isEmpty {
lastlocaltime = try container.decode(Date.self, forKey: .lastlocaltime)
}
}