[Review] SF-0040: Calendar initializer with time zone, locale, first weekday, and minimum days in first week

Hello Swift community,

The review of Calendar initializer with time zone, locale, first weekday, and minimum days in first week begins now and runs through 2026-05-15.

Reviews are an important part of the Swift-Foundation evolution process. All review feedback should be either on this forum thread or, if you would like to keep your feedback private, directly to me as the review manager by DM. When contacting the review manager directly, please include proposal name in the subject line.

What goes into a review?

The goal of the review process is to improve the proposal under review through constructive feedback. When writing your review, here are some questions you might want to answer in your review:

  • How much effort did you put into your review?
  • If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?

More information about Swift-Foundation review process is available at

swift-foundation/CONTRIBUTING.md at main · swiftlang/swift-foundation · GitHub

Thank you,

Tina, Review Manager

5 Likes

This looks great! Agree with all the notes about ABI stability. To me, the low risk of properties being later added or deprecated/removed is far outweighed by the usefulness of the new initializer.

If to go with a fluent approach I'd drop a separate configuration object and allow chaining done on the calendar itself. With a separate configuration object it does feel heavier than needed and rightfully raises questions about the duplicity of Calendar.current vs Calendar(.current).

I like this configuration idea, though doesn’t that just shift the problem of any future changes? I’m probably missing something.

Also, would chaining happen on the configuration and not the calendar?

That's the strong point of fluent approach, it's incremental / scaleable.

That's how it is now laid out in the proposal's alternative section and this is what I question.

By way of example:


  1. init approach:
// changing from:
// init(... minimumDaysInFirstWeek: Int? = nil) // v1
// to:
// init(... minimumDaysInFirstWeek: Int? = nil, newFeature: Int = 0) ❌
// is probably no go due to ABI compatibility.
// Please correct me if that's not the case.
// We'd need adding another init in a non scaleable manner:

init(... , minimumDaysInFirstWeek: Int? = nil) // keep
init(..., minimumDaysInFirstWeek: Int? = nil, newFeature: Int) // add 😒

Calendar(identifier: "...", newFeature: 1)
❌ Calendar(..., newFeature: 1) // attempted usage with current calendar

  1. "fluent configuration" approach:
    extension Configuration {
        func newFeature(_ param: Int) -> Self // added
    }
    // Usage:
    Calendar(.identifier("...").newFeature(1))
    Calendar(.current.newFeature(1))

  1. "fluent calendar" approach:
    extension Calendar {
        func newFeature(_ param: Int) -> Self // added
    }
    // Usage:
    Calendar.identifier("...").newFeature(1)
    Calendar.current.newFeature(1)

PS. When I tried (1) I realised that it's not immediately clear how to create the "current" or "autoupdating" or "iso8601" calendar via the new initialiser.... Another illustration of inflexibility of that approach.

Otherwise the usage in (1) is Calendar(..., newFeature: 1)

2 Likes

Please expand on this.. how exactly will the new initialiser look, will it take an optional (and defaulted to nil) observerSite parameter or a non optional parameter or what?

If it takes an optional defaulted to nil parameter will there be no ambiguity of which initialiser to use?

Calendar(identifier: "", firstWeekday: 1)
// - .init(identifier:firstWeekday:) // this?
// - .init(identifier:firstWeekday:observerSite:) // or this?

Or is this ambiguity not an issue in Swift in practice?

since maintaining a family of initializers is messy.

Indeed!

That might well be just due to the fact that "maintaining a family of initialisers is messy" and that in Obj-C there was no good answer to that... That we now just might have in Swift... to be able establish a new precedent!

Correct, there is no ambiguity (for the compiler, at least!). If the call site omits observerSite, the initialiser without it is chosen. It does not matter if the argument defaults to a nil or non-nil value.

Thanks for framing it this way. I'll need a bit of time to experiment. The implementation details of Calendar make it hard to avoid creating fresh instances of the underlying storage reference type for each link in the chain.

1 Like

This is essentially a memberwise initializer and would be a natural fit on Calendar.

The decision to accept the deprecation risk is the right one, in my opinion. Deprecation is a normal part of any API's lifecycle. Multiplying initializers reminds me of writing n overloads before the introduction of parameter packs.

I think the proposal could be stronger if it put less weight on the efficiency argument. Calendar objects generally aren't created in hot loops or in large quantities, so any efficiency gains are probably immaterial. The transient-state avoidance feels like a much stronger motivation and could carry more of the proposal on its own.

I'd be curious to see what @tera's fluent-on-Calendar variant looks like in practice. Labeled arguments are usually the more idiomatic approach, but I can see the appeal of incremental extensibility.

They shouldn't be, but that doesn't stop people from creating and mutating instances when, say, scrolling a list. Formatters are far worse, but making common use of APIs more efficient is generally a good goal.

1 Like

Are you referring specifically to this issue?

TODO: We can't use isKnownUniquelyReferenced on an existential. For now we must always copy. n.b. we must also always copy if _calendar.isAutoupdating is true.

It is in the current timeZone property implementation and quite a few others:

    /// The time zone of the calendar.
    public var timeZone : TimeZone {
        get {
            _calendar.timeZone
        }
        set {
            guard newValue != _calendar.timeZone else {
                // Nothing to do
                return
            }

            // TODO: We can't use isKnownUniquelyReferenced on an existential. For now we must always copy. n.b. we must also always copy if _calendar.isAutoupdating is true.
            _calendar = _calendar.copy(changingLocale: nil, changingTimeZone: newValue, changingFirstWeekday: nil, changingMinimumDaysInFirstWeek: nil)
        }
    }

If so that means that the issue is already there in the setters... the fluent approach (even if it uses a similar machinery) will not make the situation meaningfully worse...

And once this "TODO" is finally fixed – both setters and fluent chains (if we decide to go with them) become efficient and won't involve extra objects creation.


This one we could probably save by introducing "pseudo" identifiers:

public enum Identifier : Sendable, CustomDebugStringConvertible {
    case gregorian
    case buddhist
    ...
    // psuedo identifiers:
    case current             // 🆕
    case autoupdatingCurrent // 🆕
}

// Usage:
Calendar(identifier: .gregorian)
Calendar(identifier: .autoupdatingCurrent, ...)

Let me expand on this. Imagine we were talking about not 5 but 20 or 50 parameters... Ridiculous, I know, but helps to illustrate the point.

  • Some of those parameters were added over the years, so we might end up having say 10 or 20 different versions of inits at the end of the day.
  • No proper way to deprecate some of those parameters that we no longer need.
  • If the caller wants to pass just two parameters one of which happens to be in the most modern ("widest") version of init - there'll be a toll of passing all 20 parameters (perhaps 18 of them defaulted to nil).

Fluent approach solves this nicely – easy to expand, and deprecate, and you only "pay for what you use".

Having said that, obviously there's a difference between 5 and 20 parameters, and chances are low that Calendar ever gets that many.

I have needed this initializer almost every time I’ve had to use Calendar. It’s always been baffling to me that it doesn’t exist.

Yes. A Fluent styled expression like the following would (today) have these performance characteristics:

let calendar = Calendar.current.timeZone(timeZone).locale(locale)
               |-- cache hit --|--- wasted copy --|---- copy ---|

This has the same performance characteristics as setting the properties one by one has today. To go with the Fluent inspired initialiser expressions, the first question we need to answer is: do we want to avoid the extra internal allocations? If we do not, then adding these Fluent inspired initialisers is straightforward and we turn to matters of the idiomatic, weighting the risk a new property will be added, and this becoming a potential precedent for other instances of ABI stability initialiser gymnastics.

If we want to avoid the extra allocations, then the internals of Calendar would need to change. There look to be two options for that. First, we can wait for isKnownUniquelyReferenced to work with existentials and update accordingly. Second, Calendar would need to become a value type more purely, so time zone, locale, etc properties could be added to a struct.

The second option is very involved. One way this could be done is by removing the _CalendarProtocol conformance from _GregorianCalendar, making _GregorianCalendar a value rather than a reference type, placing the properties there, and modifying Calendar to store either a Gregorian or calendar reference type. Then, getting and setting properties becomes a case of switching on the calendar's storage and accessing the property as appropriate. For example:

  public var timeZone: TimeZone {
      get {
          switch storage {
          case .gregorian(let g): return g.timeZone // reads from struct
          case .other(let c):     return c.timeZone // reads from reference type
          }
      }
      set {
          switch storage {
          case .gregorian(var g):
              guard newValue != g.timeZone else { return }
              g.timeZone = newValue
              storage = .gregorian(g)       // simply sets a struct's property
                                            // before, heap allocation as below
          case .other(let c):
              guard newValue != c.timeZone else { return }
              storage = .other(c.copy(changingLocale: nil,     // heap allocation
                                      changingTimeZone: newValue,
                                      changingFirstWeekday: nil,
                                      changingMinimumDaysInFirstWeek: nil))
          }
      }
  } 

The details for this option could vary but I think the substance would remain. A few things to note about that substance, though. First, we can't avoid making a copy from the reference type calendars. I do not know enough about how swift-foundation is used or its obligations to say how big of a deal that is. Second, the Calendar struct gets heavier to copy around the stack and I'm not sure what subtle implication that'll have on pre-existing, high performance, memory sensitive code. Third, this option makes Gregorian calendars cheap to create, simplifying the internal caching story. Finally, another advantage is that Gregorian calendars can be used on platforms where existentials are unavailable.

I hope this detail helps the community decide if the proposal has value in itself that is not short-sighted. The proposal agrees that some sort of fluent configuration (or calendars) approach is most flexible, but suggests the need for that flexibility is unlikely. In addition to that, it seems the technical complexity of creating such an affordance (in this case) is also quite large. So I think the proposed initialiser is not unreasonably short-sighted. But I hope others can help me see where I could be wrong.

1 Like

In my opinion, a ”fluent” style API is absolutely not needed, or even desirable. This is for configuring an instance at initialization. The idiomatic way to express that in Swift is with an initializer.

It sounds highly unlikely that Calendar would suddenly start gaining so many new properties that adding new initializers would be problematic.

4 Likes

I believe the idea initially was that we didn't expect it to be so urgently needed, so it was probably left out (unintentionally).

I agree with this. We have not added any new settable properties since the introduction of Calendar. Even if we did add new settable properties, we have precedents in other types where new properties are omitted from the initializer when setting them is uncommon, as well as precedents for simply adding another initializer that takes everything. So I wouldn't use this as a deciding factor to oppose a memberwise initializer.

2 Likes

Thank you for laying this out so crystal clear.

I wonder if having those TODO's already fixed would influence our choice of API in this PR?

I like the proposed approach. It's simple to implement and easy to understand.

1 Like