Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Swift DateFormatter Optional Milliseconds [duplicate]

I have the following code to parse an ISO8601 date.

let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ"

Problem is sometimes the date is in a format like 2018-01-21T20:11:20.057Z, and other times it's in a format like 2018-01-21T20:11:20Z.

So basically part of the time it has the .SSS millisecond part, and other times it does not.

How can I setup the date formatter to make that part optional?

Edit

I forgot to mention a few details tho in my question I just realized. So I'm using the JSON Codable feature in Swift 4. So it just throws an error if it fails.

So I basically have the following code.

let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ"
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .formatted(isoMilisecondDateFormatter())

return try decoder.decode([Person].self, from: _people)

An example JSON object for _people is the following.

[
    {
        "name": "Bob",
        "born": "2018-01-21T20:11:20.057Z"
    },
    {
        "name": "Matt",
        "born": "2018-01-21T20:11:20Z"
    }
]

The API I'm working with is pretty inconsistent so I have to deal with multiple types of data.

like image 467
Charlie Fish Avatar asked Jan 21 '18 19:01

Charlie Fish


2 Answers

I created a DateFormatter subclass that tries to parse with fractional seconds, and then falls back on a second internal DateFormatter that parses without.

class OptionalFractionalSecondsDateFormatter: DateFormatter {

     // NOTE: iOS 11.3 added fractional second support to ISO8601DateFormatter, 
     // but it behaves the same as plain DateFormatter. It is either enabled
     // and required, or disabled and... anti-required
     // let formatter = ISO8601DateFormatter()
     // formatter.timeZone = TimeZone(secondsFromGMT: 0)
     // formatter.formatOptions = [.withInternetDateTime ] // .withFractionalSeconds

    static let withoutSeconds: DateFormatter = {
        let formatter = DateFormatter()
        formatter.calendar = Calendar(identifier: .iso8601)
        formatter.locale = Locale(identifier: "en_US_POSIX")
        formatter.timeZone = TimeZone(identifier: "UTC")
        formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssXXX"
        return formatter
    }()

    func setup() {
        self.calendar = Calendar(identifier: .iso8601)
        self.locale = Locale(identifier: "en_US_POSIX")
        self.timeZone = TimeZone(identifier: "UTC")
        self.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSSSSXXX" // handle up to 6 decimal places, although iOS currently only preserves 2 digits of precision
    }

    override init() {
        super.init()
        setup()
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        setup()
    }

    override func date(from string: String) -> Date? {
        if let result = super.date(from: string) {
            return result
        }
        return OptionalFractionalSecondsDateFormatter.withoutSeconds.date(from: string)
    }
}

I keep a static copy around since it is a bit heavy.

extension DateFormatter {
    static let iso8601 = OptionalFractionalSecondsDateFormatter()
}

let str1 = "2018-05-10T21:41:30Z"
let str2 = "2018-05-10T21:41:30.54634Z"
let d1 = DateFormatter.iso8601.date(from: str1)
let d2 = DateFormatter.iso8601.date(from: str2)
DDLogInfo("d1 is \(String(describing: d1))")
DDLogInfo("d2 is \(String(describing: d2))")

Obviously, you can customize it to suit your own formatting needs. In particular, you should structure the two parsers based on your typical date format (whether you expect MOSTLY fractional seconds, or mostly whole seconds)

like image 86
Eli Burke Avatar answered Oct 26 '22 16:10

Eli Burke


Two suggestions:

  • Convert the string with the date format including the milliseconds. If it returns nil convert it with the other format.

  • Strip the milliseconds from the string with Regular Expression:

    var dateString = "2018-01-21T20:11:20.057Z"
    dateString = dateString.replacingOccurrences(of: "\\.\\d+", with: "", options: .regularExpression)
    // -> 2018-01-21T20:11:20Z
    

Edit:

To use it with Codable you have to write a custom initializer, specifying dateDecodingStrategy does not work

struct Foo: Decodable {
    let birthDate : Date
    let name : String

    private enum CodingKeys : String, CodingKey { case born, name }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        var rawDate = try container.decode(String.self, forKey: .born)
        rawDate = rawDate.replacingOccurrences(of: "\\.\\d+", with: "", options: .regularExpression)
        birthDate = ISO8601DateFormatter().date(from: rawDate)!
        name = try container.decode(String.self, forKey: .name)
    }
}

let jsonString = """
[{"name": "Bob", "born": "2018-01-21T20:11:20.057Z"}, {"name": "Matt", "born": "2018-01-21T20:11:20Z"}]
"""

do {
    let data = Data(jsonString.utf8)
    let result = try JSONDecoder().decode([Foo].self, from: data)
    print(result)
} catch {
    print("error: ", error)
}
like image 41
vadian Avatar answered Oct 26 '22 18:10

vadian