代码之家  ›  专栏  ›  技术社区  ›  Guillermo Alvarez

如何使用Swift中的Codable转换具有可选分数秒的日期字符串?

  •  60
  • Guillermo Alvarez  · 技术社区  · 7 年前

    我正在用Swift的Codable替换我的旧JSON解析代码,并且遇到了一些障碍。我想这不是一个可编辑的问题,而是一个日期格式化程序问题。

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

    现在它坏了。日期有时以小数秒返回,有时则不返回。我用来解决这个问题的方法是在我的映射代码中,我有一个转换函数,它可以尝试使用和不使用小数秒的两种日期格式。然而,我不太确定如何使用Codable实现它。有什么建议吗?

    4 回复  |  直到 3 年前
        1
  •  92
  •   Leo Dabus    4 年前

    您可以使用两种不同的日期格式化程序(带和不带分数秒)并创建自定义DateDecodingStrategy。如果解析API返回的日期失败,您可以抛出DecodingError,正如@PauloMattos在注释中所建议的那样:

    习俗 ISO8601

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

    海关 与上图相同。


    // 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)
    }
    
        2
  •  4
  •   lukszar    4 年前

    Swift 5

    要将ISO8601字符串解析到日期,必须使用DateFormatter。在较新的系统(例如iOS11+)中,您可以使用ISO8601DateFormatter。

    只要不知道日期是否包含毫秒,就应该为每种情况创建2个格式化程序。然后,在分析字符串到日期的过程中,使用这两种方法。

    旧系统的日期格式化程序

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

    适用于较新系统的ISO8601数据格式(如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
    }()
    

    总结

    Tomorrow on GitHub 你们可以看到这个问题的整个解决方案。

    要将字符串转换为您使用的日期:

    let date = Date.fromISO("2020-11-01T21:10:56.22+02:00")

        3
  •  3
  •   GetSwifty    5 年前

    一个新选项(从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
     }
    
        4
  •  0
  •   Cristik    7 年前

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

    例如:

    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\"}"
    

    将产生相同的输出:

    但是,使用不正确的字符串:

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