Jens
1
For example:
let formatter = ISO8601DateFormatter()
formatter.timeZone = TimeZone(identifier: "Europe/Stockholm")
formatter.formatOptions = [.withFullDate]
// 2023 is not a leap year, so only 28 days in February, yet this won't crash:
let date = formatter.date(from: "2023-02-29")!
print(formatter.string(from: date)) // Prints 2023-03-01
Even "2023-02-31" is accepted (as a date which the formatter will format as "2023-03-03").
But, as expected, "2023-02-32" is not accepted, and neither is "2023-13-01" or "2023-00-01".
So if I want to verify that someDateInput is a valid date string, then I can't just do formatter.date(from: someDateInput) != nil. I have to do something like:
guard let date = formatter.date(from: someDateInput),
formatter.string(from: date) == someDateInput
else {
return false
}
return true
Or is there a better way?
I think the formatter is just checking that the string has a day value in the valid range of days (1-31), and then when it finds out that there aren't enough days in February to actually create that day, it just offsets to get to March. Same thing with the months: 2023-13-01 probably isn't accepted just because it isn't in the valid range of months (1-12). But it doesn't seem like there's a better way to do that.
Strangely, it seems like DateComponents does the same thing when given an invalid date.
let components = DateComponents(calendar: .current, year: 2023, month: 2, day: 32)
let date = components.date!
// 2023-03-04 05:00:00 +0000
1 Like
Jens
3
That DateComponents initializer isn't failable though, and there's nothing in its documentation saying anything about validation of the values, so you can give it any values:
let components = DateComponents(calendar: .current, year: .max, month: .max, day: .max)
let date = components.date!
print(date) // 0000-12-31 23:06:32 +0000
The formatter.date(from:) on the other hand returns an optional date, and I just expected it to fail for all invalid date strings, not just for some invalid date strings.
The documentation says:
Creates and returns a date object from the specified ISO 8601 formatted string representation.
Parameters
string
The ISO 8601 formatted string representation of a date.
Return Value
A date object, or nil if no valid date was found.
I guess the documentation is a bit open for interpretation, but I still find its current behavior surprising. Ie, that:
ISO8601DateFormatter().date(from: "2023-02-31T12:00:00Z")
results in a Date value for
2023-03-03 12:00:00 +0000
rather than nil.
Yeah, but the date property on DateComponents is optional, so theoretically it should return nil for an invalid date.
Jens
5
Ah, right! So now we have two similar questions.
eskimo
(Quinn “The Eskimo!”)
6
The DateComponents behaviour is expected (at least by me :-). AFAICR it’s always worked this way. I did a quick check of the docs to see if I could find anything concrete to support that recollection but turned up blank )-:
And yeah, I suspect that ISO8601DateFormatter just parses the components and passes them to the Gregorian calendar’s date(from:) method to get a date.
As to what you can do about this, the FormatStyle API seems to do the right thing here. That is, this:
import Foundation
func test(_ input: String) {
do {
let fs = Date.ISO8601FormatStyle()
let d = try fs.parseStrategy.parse(input)
print("\(input) -> \(d)")
} catch {
print("\(input) -> NG")
}
}
test("2023-02-28T12:00:00Z")
test("2023-02-29T12:00:00Z")
test("2024-02-29T12:00:00Z")
test("2023-02-30T12:00:00Z")
prints this:
2023-02-28T12:00:00Z -> 2023-02-28 12:00:00 +0000
2023-02-29T12:00:00Z -> NG
2024-02-29T12:00:00Z -> 2024-02-29 12:00:00 +0000
2023-02-30T12:00:00Z -> NG
Share and Enjoy
Quinn “The Eskimo!” @ DTS @ Apple
3 Likes
tera
7
Thanks for the workaround.
I found it strange that the two API's with "ISO8601" in their names behave differently irt Feb 29 / Feb 30 dates.
1 Like