Foundation's calendar.dateInterval() returns too large interval

The documentation for Foundation claims that DateInterval represents closed ranges. Thus, I would expect an interval's end returned from calendar.dateInterval(of:for:) to be the last possible instance of the unit (say, 31th of December 23:59 when passed a .year). However, the actual behaviour is:

var calendar = Calendar(identifier: .gregorian)
calendar.locale = .current
let components = DateComponents(year: 2020, month: 1, day: 1)

let startDate = components)!
let interval = calendar.dateInterval(of: .year, for: startDate)!

let endComponents = calendar.dateComponents([.year], from: interval.end)
print(endComponents.year!) // prints "2021"

// or:
let formatter = DateFormatter()
formatter.calendar = calendar
formatter.dateStyle = .full

// prints "Friday, January 1, 2021 at 12:00:00 AM Central European Standard Time"
print(formatter.string(from: interval.end))

— that is, if DateInterval is a closed interval, then January 1, 2021 at 12:00:00 AM belongs to the year 2020, which is very confusing to me. At least, all APIs extracting DateComponents will return very unexpected values, for instance, querying a date interval for any month and then looking at components' day will always deliver 1 and 1 on both ends.

The way it works, as I deduced from the source code, is that it simply adds the full duration of (in this case, year) and then deliberately does not jump back to the previous day (or whatever instance would make sense), only sets the hour-minute-second to 00:00:00. I would expect the interval's end to be December 31, 2020 at 23:59:59 PM in this case, i.e., subtracting 1 second from the instance the method actually returns.

So, I wonder, if it's a bug in the implementation or is it that DateInterval was misdocumented, because it actually was meant to represent open intervals? Should this be filed as a bug — or is this all completely intentional?

It's almost certainly a mistake (even a typo) in the documentation. What it actually says is:

DateInterval represents a closed date interval in the form of [startDate, endDate]. It is possible for the start and end dates to be the same with a duration of 0. [my emphasis]

The writer clearly understands that it's an open range in the Swift sense, otherwise the duration couldn't be 0.

I suspect the writer actually meant to call it "a finite date interval".

1 Like

Do you mind filing a bug against the documentation for DateInterval and posting the feedback number here? I can help direct that bug to the right place.

Submitted, the number is FB8908429 — thank you!

As someone who's done quite a bit of digging into the calendar APIs, it's my opinion that you should consider the DateInterval type as an open interval.

It's actually really difficult to make a proper "closed" interval for calendar types, because of how the current API works. If you create an interval from 00:00:00 to 23:59:59, you're still technically leaving off a large number of valid Date instances that likely should be interval, but are not. These would be the Date values that occur after 23:59:59, but before 00:00:00. For example: 23:59:59.0000000000001, 23:59:59.0000000000002, 23:59:59.0000000000003, etc. There are technically an infinite number of such dates.

On the other hand, if you use intervals as half-open ranges, then the comparison makes more sense: you can reliably get the first instant that corresponds to a known calendar value (ie, the 00:00:00.000000000... value), but you cannot get the last instant that corresponds to known calendar value (Xeno's paradox raises its head here). Thus, the proper comparison for a "is this date contained in the range" is technically a start <= candidate && candidate < end check. In other words, it's a half-open range, as you've discovered.

This all was indeed what I suspected, it still wasn't a good enough guarantee for me to "start treating" DateInterval as half-open because I couldn't make sure this assumption won't break for any other API without literally testing/reading the code of any method that returns DateInterval — given that working dates and times already has multiple exceptions to almost every rule. The discussion has sorted that for me, thank you!

Thank you!

Terms of Service

Privacy Policy

Cookie Policy