The design doesn't seem to work in practice. For example, suppose I use DataInterval to represent a day (i.e. 10/14/2023). The interval's start is 10/14/2023 00:00:00, duration is 86400 seconds. That leads to its end at 10/15/2023 00:00:00. If I use contains() method to check if the interval contains 10/15/2023 00:00:00, it will return true. That's incorrect because the interval is supposed to represent 10/14/2023 only.
Am I missing something? I thought this would be a frequently asked question, but it's odd I couldn't find any discussion on it.
UPDATE: a high level summary of the issue
Day, month, year, etc. are half-open ranges.
DateInterval is implemented as closed range. So it shouldn't be used to represent day, month, and year.
But there are Foundation APIs, e.g. Calendar.dateInterval(of:, for:), that use DateInterval to represent day, month, and year. That causes issues.
I'm afraid your interpretation is incorrect. DateInterval is a general struct and can represent any interval (e.g. a few minutes or several days, etc.). In my example, I'd like to use it to represent a day but as I explained above, it doesn't work. My point is that's an inherent issue in DateInterval design - it should consider the range as half-open, instead of closed.
Welcome to the rich world of calendar computations!
It is not suited for the particular test you want to perform.
There are other calendar apis, that such as Calendar.isDate(_:inSameDayAs:), that are a better fit. Here you can represent a "day" by any Date inside (instead of a DateInterval). And you get free support for daylight saving time.
In some apps, a "date" is better represented by its components (year, month, day) than by a Date: Calendar has plenty of apis that deal with DateComponents.
All in all, it may be time to make a step back, and model your data in a way that makes it possible to perform the computations you need.
As others said Date alone is not really suited for this. Though calendar api are not super easy to use and can be a footgun too if misused. I’d recommend GitHub - davedelong/time: Building a better date/time library for Swift which is a fin wrapper around calendar apis encoding some constraints in the type system so you can make far less mistakes. It gives you really nice APIs too for other related date manipulation and formatting task.
Thank for all suggestions. So it seems DateInterval should only be used for calculating or comparing intervals? That seems quite limited. But even so, I still don't understand why it doesn't use half-open range.
In my app, the minimal time unit is day and I use the start time of a day to represent it. I also need to represent a month. My old approach was using the first day of that month. While refactoring my code, I'm thinking to use interval to represent a month, because I think it suites the nature of the problem well and facilitates generating days in the month or checking if a day is in the month. I'm going to write my own version of interval by wrapping DateInterval but implementing all APIs I need. Thanks.
PS: while I use day as an example, the question applies in others scenarios. In general, if we think DateInterval as a range, I find it's difficult to understand why Foundation developers chose a design where both the following items are true at the same time:
I don't have background on the closed nature of this interval, but as an aside: please don't hardcode duration to be 86400 seconds — days may be shorter or longer depending on DST and other factors.
For both your day and month scenarios, the right API to use would be Calendar.dateInterval(of:for:), which returns the range of the date component you ask for which contains the given date. e.g.,
let calendar = Calendar(identifier: .gregorian)
// Ran on 2023-10-14 08:20:00 -0400
// Printed dates are in UTC, hence the 4-hour offset.
let rangeOfToday = calendar.dateInterval(of: .day, for: .now)
// => 2023-10-14 04:00:00 +0000 to 2023-10-15 04:00:00 +0000
let rangeOfThisMonth = calendar.dateInterval(of: .month, for: .now)
// => 2023-10-01 04:00:00 +0000 to 2023-11-01 04:00:00 +0000
You can then make adjustments to the interval as needed to contain only the dates you need, e.g., when checking for membership, check date != interval.end
FWIW, I would consider filing Feedback for this. Change in this area is particularly difficult because of backwards compatibility concerns, but it's worth investigating, especially as part of the updated Foundation efforts; I haven't done a thorough audit by any means, but it looks like many (most? all?) APIs actually treat DateInterval as an open range, not a closed one (e.g., dateInterval(of:for:) above). Depending on backwards-compatibility constraints, it's worth considering:
Updating DateInterval to be an open range, updating its documentation and implementation, so APIs can continue using it as such; or
Updating APIs which return DateInterval to ensure their results match DateInterval's closed nature; or
Updating the documentation for those APIs to indicate that despite returning a closed DateInterval, the result is actually meant to be an open one
Thanks for the catch. I use this approach for demonstration purpose. I use Calendar.dateInterval(of:for:) API to generate interval in my actual code.
That's the approach I'm using. I use Calendar.dateInterval(of:for:) to generate DateInterval. But I don't use DateInterval's API. I implement a set of custom API where I treat DateInterval as half-open range. This works well because, as you mentioned, Calendar.dateInterval(of:for:) treat DateInterval as a half-open range, instead of a closed range.
BTW, it doesn't work to modify DateInterval returned by Calendar.dateInterval(of:for:) to a closed range, because day, month, year, etc. are half-open ranges on concept.