Thanks for bringing up this topic, Dave. Some thoughts on this:
As with all new API we might introduce, we would need strong motivation to bring it forward — especially in an area where there is so much existing API and convention, it would need to really carry its own weight.
One of your main concerns here is that Date.now(), however spelled, is an implicit dependency; my question to you is: in what ways would a new Clock API solve this concern? Presumably, the idea is to pass a Clock around between function calls to mock out the underlying datetime calculations and make it easier to test:
// Old
func myDateThing() -> Result {
// ...
let date = Date.now()
// calculate based on date
}
// New
func myDateThing(_ clock: Clock) -> Result {
// ...
let date = clock.now()
// calculate based on date
}
Passing in the clock could allow you to set up a different clock in your tests, letting you inject the dependency.
However, this is the solution to all dependency injection issues: making your functions referentially transparent by passing in your dependencies lets you mock and test more easily. Is this in an appreciable way different from what you can already do today?
// Today
func myDateThing(_ date: Date) -> Result {
// calculate based on date
}
Similarly, what, if anything, would prevent someone from replacing all Date.now() calls with Clock.system.now()? Does Clock.system.now() imply more global state than Date.now()?
Separately, like @pvieito, I fundamentally disagree with your statement here. Dates in colloquial usage can indeed represent ranges: when someone asks "what day was last Tuesday", they're likely referring to the range of time starting on Tuesday at 00:00:00.000 and ending at 23:59:59.99999..., and this is well represented by DateInterval. However, the date represented by "May 5th, 2018, at 9:06:02 AM in America/Los_Angeles" (2018-05-21T09:06:02.000-0700) is an absolute instant in time, with no associated duration. It's not useful to discuss that instant in terms of a range.
Where I do agree with you is the difficulty in separating those concepts. Given a DateComponents, it isn't possible for Calendar to guarantee statically whether the components you're asking for correspond to a single instant in time (2018-05-21T09:06:02.000-0700), or a supposed range ("the month of May, 2018"). It's also not possible for it to do so, unless you bifurcate the methods in your system to deal either in terms of absolute points in time or in intervals.
Calendar opts for consistency here. Given a request for a date, it returns a Date representing the first (or last, depending on how you search) instant in time matching the components you're interested in. If you want to turn that into an interval, it's always possible to do so with -[NSCalendar rangeOfUnit:startDate:interval:forDate:], which would return to you the interval. One great place to make progress here, I think, is exposing that method in a better way for Swift, and potentially adding convenience methods to make this process automatic — given a DateComponents, give me back a DateInterval representing the range of this Calendar.Unit inside those components, e.g. calendar.interval(for: .year, containing: DateComponents(year: 2018, month: 5))
There's always room for improvement there, and we'd be happy to accept suggestions that make correctness easier and more ergonomic.