[Pitch] Calendar Sequence Enumeration

But that's actually 90% of what we want. It's the confusion when looking at the API, that I'm most concerned with. What legacy cruft is hiding in the binary is far less important.

So if what you say is indeed an option, then I push strongly for it.

(note that measures like marking things as deprecated, or anything involving / relying on documentation, are comparatively ineffective - there's just too many places in which that information is not conveyed or conveyed too late to solve the problem at hand, such as code-completion suggestions)

Apple's SDKs haven't been considered long-term stable or reliable in, well, as long as I can remember. Decades, at least. Apple are [in]famous for breaking backwards compatibility. It's kind of their thing. Might as well lean into it, as otherwise you get all the downsides (confusion and incompatibility) without the upsides (clean APIs).

(I realise Swift is greater than just Apple platforms, but we're talking about Foundation in particular here, which is predominately used on Apple platforms, and doesn't really have a meaningful existing install base on non-Apple platforms as far as I've seen)

I assure you, if we actually did this (stopped investing unreasonably large amounts of engineering time into compatibility), it would last precisely one release before massive community outcry forced us to reinstate it. I've carefully preserved the behavior of Foundation APIs that were introduced in the mid-90s but never actually implemented, just in case someone was relying on them doing nothing in a particular way.

3 Likes

To add to David's comment: Apple is an enormous company, with a huge number of teams supporting mind-boggling amounts of code. Even with extremely rigorous focus on backwards-compatibility, sometimes people make mistakes, or things slip through the cracks, or a bug fix now breaks someone's workaround for the fixed bug. Are there teams that are less scrupulous than others? Absolutely, can't deny that. But having been inside and out, I can also absolutely tell you that heroic levels of effort go in to keeping things backwards compatible.

From the outside, it's easy to latch on to the breakages you do come across, but the alternative is much, much worse :sweat_smile: :fire::wastebasket::fire:


In any case, back on topic, I personally think the existing API is sufficiently unpalatable to use from Swift that the alternative is an easy pick for new adoption at call sites. I'd hope that documentation can be updated as part of this effort, to much more clearly highlight the existence of this new method of the older enumerate API, given that the methods have different base names (so if you're used to writing calendar.enumerate..., you won't necessarily discover this API if you're not already aware of it — but there's only so much we can do).

Thanks Tony for your replies! I've reviewed the latest proposal and things look great.

I do agree that some refinement of startingAt + range may be necessary — it feels a bit strange that either the start or the end of the range (depending on search direction) may be working against you: if the range contains the start date, then is the start date not the real starting bound of the range? And if the range doesn't contain the start date, then you'll just silently get back an empty sequence.

Instead of a bounding range, would it make sense to include only the relevant boundary with something like

func dates(startingAt start: Date,
           until end: Date? = nil,
           ...)

(Apologies if this was already suggested somewhere and I've missed it.)


Alternatively, one solution to

We considered a PartialRangeFrom based API instead of a Date plus optional Range. However, we felt that a "backwards" search would be confusing:

could be to take all forms of RangeExpression, with concrete overloads to refine the API.

So calendar.dates(in: start..., matching: components) would iterate forwards, while calendar.dates(in: ...end, matching: components) would iterate backwards. (Concrete overloads taking closed ranges can still take a direction: to allow for clearer iteration there).

Though that might be a bit too "clever".

1 Like

Hey Itai,

I played around with inferring the start and end from the range, but in the end I did indeed think it was too clever. Date calculation is hard enough to wrap your head around (as you know), so I thought it would be wise here to just surface the three questions we need directly:

  1. Where are you starting?
  2. Which direction do you want to go?
  3. What range of output are you willing to accept?

These have the sensible default values for 2/3 that make the obvious API still pretty easy to use, e.g.:

let dates = cal.dates(startingAt: Date.now, byAdding: .day)

Personally I don't think attempting to reduce the number of arguments for the non-default-value case by inferring direction from ranges is really worth the tradeoff vs. direct meaning in the API. Also, I would like to embrace Swift's Range type for this API, since we then reuse a concept that most Swift developers already understand. It helps reinforce what Date actually is (a point on a continuous timeline).

I also think there are legitimate use cases for specifying a starting point outside the range for either matching or adding. The result is simply that if the first result after start is outside the range, then your result is empty.

1 Like

I'm pleased with the shape of this so far as well. As I was reading the "addition" sequence, it occurred to me that some conveniences on DateComponents might make the API even more fluent:

extension DateComponents {
    public static func seconds(_ value: Int) -> Self
    public static func minutes(_ value: Int) -> Self
    public static func hours(_ value: Int) -> Self
    public static func days(_ value: Int) -> Self
    public static func weeks(_ value: Int) -> Self
    public static func months(_ value: Int) -> Self
    public static func years(_ value: Int) -> Self
}

This would allow the usage of:

let dates = cal.dates(startingAt: somePoint, byAdding: .days(1))

That reads nicer to me than

let dates = cal.dates(startingAt: somePoint, byAdding: DateComponents(day: 1))
2 Likes

I updated the proposal to reorder the arguments as suggested:

    public func dates(byMatching components: DateComponents,
                      startingAt start: Date,
                      in range: Range<Date>? = nil,                      
                      matchingPolicy: MatchingPolicy = .nextTime,
                      repeatedTimePolicy: RepeatedTimePolicy = .first,
                      direction: SearchDirection = .forward) -> some (Sequence<Date> & Sendable)

    public func dates(byAdding component: Calendar.Component,
                      value: Int = 1,
                      startingAt start: Date,
                      in range: Range<Date>? = nil,                      
                      wrappingComponents: Bool = false) -> some (Sequence<Date> & Sendable)

    public func dates(byAdding components: DateComponents,
                      startingAt start: Date,
                      in range: Range<Date>? = nil,                      
                      wrappingComponents: Bool = false) -> some (Sequence<Date> & Sendable)
3 Likes

Sometimes, but beside my intended point.

I guess I'm not certain, now, if this pitch is for 'new' Foundation or 'old' Foundation? I was presuming the former. As such there is - at least hypothetically - an opportunity to refactor things, not merely reimplement them.

Think, also, more broadly about Apple's API history - we're not still using Toolbox APIs or QuickDraw, for example, and heck there's been like a billion different multimedia frameworks over the last few years.

Try running Mac software from more than five years ago. Even aside from things like CPU architecture changes, plenty of apps just don't launch at all due to dyld failures. Many more don't work as intended due to more subtle API changes.

And again, I'm not even saying that's a bad thing, I'm just saying: either lean into it and get the proper benefits, or keep away from it and get other benefits instead.

In fact I worked at Apple at one time, including on public frameworks, so I'm well aware of what's involved.

I meant no disrespect to those currently (or previously) working on Apple's public frameworks.

1 Like