如何在 Swift 中使用 Codable 转换带有可选小数秒的日期字符串?
Posted
技术标签:
【中文标题】如何在 Swift 中使用 Codable 转换带有可选小数秒的日期字符串?【英文标题】:How to convert a date string with optional fractional seconds using Codable in Swift? 【发布时间】:2018-03-09 13:38:12 【问题描述】:我正在用 Swift 的 Codable 替换我的旧 JSON 解析代码,并且遇到了一些障碍。我想这不是一个可编码的问题,而是一个 DateFormatter 问题。
从结构开始
struct JustADate: Codable
var date: Date
和一个 json 字符串
let json = """
"date": "2017-06-19T18:43:19Z"
"""
现在让我们解码
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
let data = json.data(using: .utf8)!
let justADate = try! decoder.decode(JustADate.self, from: data) //all good
但如果我们更改日期,使其具有小数秒,例如:
let json = """
"date": "2017-06-19T18:43:19.532Z"
"""
现在它坏了。日期有时会以小数秒返回,有时则不会。我用来解决它的方法是在我的映射代码中,我有一个转换函数,它尝试使用和不使用小数秒的 dateFormats。但是,我不太确定如何使用 Codable 来处理它。有什么建议吗?
【问题讨论】:
正如我在上面的帖子中所分享的,我没有格式本身的问题,而是 API 返回两种不同的格式。有时有小数秒,有时没有。我一直无法找到处理这两种可能性的方法。 【参考方案1】:您可以使用两种不同的日期格式化程序(带和不带小数秒)并创建自定义 DateDecodingStrategy。如果解析 API 返回的日期失败,您可以按照 @PauloMattos 在 cmets 中的建议抛出 DecodingError:
iOS 9、macOS 10.9、tvOS 9、watchOS 2、Xcode 9 或更高版本
自定义ISO8601DateFormatter:
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
()
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:ssXXXXX"
return formatter
()
自定义DateDecodingStrategy
:
extension JSONDecoder.DateDecodingStrategy
static let customISO8601 = custom
let container = try $0.singleValueContainer()
let string = try container.decode(String.self)
if let date = Formatter.iso8601withFractionalSeconds.date(from: string) ?? Formatter.iso8601.date(from: string)
return date
throw DecodingError.dataCorruptedError(in: container, debugDescription: "Invalid date: \(string)")
自定义DateEncodingStrategy
:
extension JSONEncoder.DateEncodingStrategy
static let customISO8601 = custom
var container = $1.singleValueContainer()
try container.encode(Formatter.iso8601withFractionalSeconds.string(from: $0))
编辑/更新:
Xcode 10 • Swift 4.2 或更高版本 • iOS 11.2.1 或更高版本
ISO8601DateFormatter
现在支持formatOptions
.withFractionalSeconds
:
extension Formatter
static let iso8601withFractionalSeconds: ISO8601DateFormatter =
let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
return formatter
()
static let iso8601: ISO8601DateFormatter =
let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withInternetDateTime]
return formatter
()
海关DateDecodingStrategy
和DateEncodingStrategy
与上图相同。
// 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.iso8601withFractionalSeconds.string(from: isoDates.dateWith9FS)) // 2017-06-19T18:43:19.532Z
print(Formatter.iso8601withFractionalSeconds.string(from: isoDates.dateWith3FS)) // 2017-06-19T18:43:19.532Z
print(Formatter.iso8601withFractionalSeconds.string(from: isoDates.dateWith2FS)) // 2017-06-19T18:43:19.530Z
print(Formatter.iso8601withFractionalSeconds.string(from: isoDates.dateWithoutFS)) // 2017-06-19T18:43:19.000Z
catch
print(error)
【讨论】:
好主意。不知道为什么我没想到那个!!!有时只需要多一双眼睛就能看到明显的...... 是的,我的计划是在自定义解码器中使用 nil 合并运算符方法。应该很好用。再次感谢。 创建一个新的错误类型最初可能看起来是个好主意,但我想说你最好还是扔掉标准的DecodingError
——case dataCorrupted
可能正是你要找的东西;)
不错的扩展~~
@leodabus 仅供参考:Swift4/ios 11 解码不适用于dateWithoutFS
。另一方面,原始扩展运行良好。【参考方案2】:
作为@Leo 的答案的替代方案,如果您需要为旧操作系统提供支持(ISO8601DateFormatter
仅从 iOS 10、mac OS 10.12 开始可用),您可以编写一个自定义格式化程序,在解析时使用这两种格式字符串:
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
,您可以将其用作日期解码策略:
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .formatted(MyISO8601Formatter())
虽然在实现上有点丑陋,但这样做的好处是与 Swift 在出现格式错误的数据时抛出的解码错误一致,因为我们不会更改错误报告机制。
例如:
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)")
将打印TestDate(date: 2017-06-19 18:43:19 +0000)
添加小数部分
let jsonString = "\"date\":\"2017-06-19T18:43:19.123Z\""
将产生相同的输出:TestDate(date: 2017-06-19 18:43:19 +0000)
但是使用了不正确的字符串:
let jsonString = "\"date\":\"2017-06-19T18:43:19.123AAA\""
如果数据不正确,将打印默认的 Swift 错误:
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))
【讨论】:
这对两种情况都不起作用(有或没有小数秒) 唯一的选择是对于 iOS 11 或更高版本,您可以使用ISO8601DateFormatter
formatOptions .withFractionalSeconds
而不是使用 DateFormatter
但自定义 dateDecodingStrategy
方法保持不变,因此使用它没有优势考虑到 iOS 11 或更高版本限制的缺点。
你为什么要浪费你的时间呢? kkkk 自定义实现更简洁,它会抛出 DecodingError。顺便说一句,我的原始答案确实适用于 iOS10,并且不需要 ISO8601DateFormatter
编码时我也可以选择任何dateEncodingStrategy
别担心,我只是想向您展示如何以两种方式实现它。最好使用适当的方法而不是绕过它【参考方案3】:
一个新选项(从 Swift 5.1 开始)是一个属性包装器。 CodableWrappers 库有一个简单的方法来处理这个问题。
对于默认 ISO8601
@ISO8601DateCoding
struct JustADate: Codable
var date: Date
如果您想要自定义版本:
// Custom coder
@available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *)
public struct FractionalSecondsISO8601DateStaticCoder: StaticCoder
private static let iso8601Formatter: ISO8601DateFormatter =
let formatter = ISO8601DateFormatter()
formatter.formatOptions = .withFractionalSeconds
return formatter
()
public static func decode(from decoder: Decoder) throws -> Date
let stringValue = try String(from: decoder)
guard let date = iso8601Formatter.date(from: stringValue) else
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Expected date string to be ISO8601-formatted."))
return date
public static func encode(value: Date, to encoder: Encoder) throws
try iso8601Formatter.string(from: value).encode(to: encoder)
// Property Wrapper alias
public typealias ISO8601FractionalDateCoding = CodingUses<FractionalSecondsISO8601DateStaticCoder>
// Usage
@ISO8601FractionalDateCoding
struct JustADate: Codable
var date: Date
【讨论】:
这是错误的。您只指定.fractionalSeconds
。应该是[.withInternetDateTime, .withFractionalSeconds]
。另一种选择是简单地将.withFractionalSeconds
插入ISO8601DateFormatter
默认选项。【参考方案4】:
斯威夫特 5
要将 ISO8601 字符串解析为日期,您必须使用 DateFormatter。在较新的系统(例如 iOS11+)中,您可以使用 ISO8601DateFormatter。
只要您不知道日期是否包含毫秒,您就应该为每种情况创建 2 个格式化程序。然后,在将 String 解析为 Date 的过程中,同时使用两者。
旧系统的 DateFormatter
/// Formatter for ISO8601 with milliseconds
lazy var iso8601FormatterWithMilliseconds: DateFormatter =
let dateFormatter = DateFormatter()
dateFormatter.locale = Locale(identifier: "en_US_POSIX")
dateFormatter.timeZone = TimeZone(abbreviation: "GMT")
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"
return dateFormatter
()
/// Formatter for ISO8601 without milliseconds
lazy var iso8601Formatter: DateFormatter =
let dateFormatter = DateFormatter()
dateFormatter.locale = Locale(identifier: "en_US_POSIX")
dateFormatter.timeZone = TimeZone(abbreviation: "GMT")
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ"
return dateFormatter
()
适用于较新系统的 ISO8601DateFormatter(例如 iOS 11+)
lazy var iso8601FormatterWithMilliseconds: ISO8601DateFormatter =
let formatter = ISO8601DateFormatter()
// GMT or UTC -> UTC is standard, GMT is TimeZone
formatter.timeZone = TimeZone(abbreviation: "GMT")
formatter.formatOptions = [.withInternetDateTime,
.withDashSeparatorInDate,
.withColonSeparatorInTime,
.withTimeZone,
.withFractionalSeconds]
return formatter
()
/// Formatter for ISO8601 without milliseconds
lazy var iso8601Formatter: ISO8601DateFormatter =
let formatter = ISO8601DateFormatter()
// GMT or UTC -> UTC is standard, GMT is TimeZone
formatter.timeZone = TimeZone(abbreviation: "GMT")
formatter.formatOptions = [.withInternetDateTime,
.withDashSeparatorInDate,
.withColonSeparatorInTime,
.withTimeZone]
return formatter
()
总结
如您所见,需要创建 2 个格式化程序。如果你想支持旧系统,它有 4 个格式化程序。为了使其更简单,请查看Tomorrow on GitHub,您可以在其中查看此问题的完整解决方案。
要将字符串转换为您使用的日期:
let date = Date.fromISO("2020-11-01T21:10:56.22+02:00")
【讨论】:
"如果你想支持旧系统,它有 4 个格式化程序。"为什么 ?旧方法适用于所有人 顺便说一句,为什么您在一种格式中使用单个Z
而在另一种格式中使用 ZZZZZ
?请注意,Z
不使用Z
作为UTC
时区表示,它使用+0000
而ZZZZZ
与XXXXX
使用Z
相同。换句话说,您应该对它们都使用XXXXX
或ZZZZZ
。
@LeoDabus 有毫秒和没有日期的解析器。并且因为有新旧方式来做这件事,它产生了 4 个格式化程序。如果您问为什么要使用新的格式化程序而不是旧的格式化程序,您可以在 Apple 文档中找到声明 When working with date representations in ISO 8601 format, use ISO8601DateFormatter instead.
。由于 ISO8601DateFormatter 仅适用于 iOS10 及更新版本,因此该解决方案同时支持新旧平台。
No ISO8601DateFormatter withFractionalSeconds
不适用于 iOS 10 和/或 iOS11。它仅适用于 iOS 11.2.1 或更高版本,只有没有小数秒。
@LeoDabus 你是对的。 Apple 文档中可能存在错误。我检查了 11.0.1,确实出现了错误。以上是关于如何在 Swift 中使用 Codable 转换带有可选小数秒的日期字符串?的主要内容,如果未能解决你的问题,请参考以下文章
使用 Swift 4 的 Codable 进行解码时,是啥阻止了我从 String 到 Int 的转换?
使用 swift Codable 以值作为键来解码 JSON
Swift 4 Codable:将 JSON 返回字符串转换为 Int/Date/Float