Presently, if we want a Calendar object configured with a specific time zone, we have to write this:
let sydney = TimeZone(identifier: "Australia/Sydney")!
var calendar = Calendar(identifier: .gregorian)
calendar.timeZone = sydney
A developer reading this instantly encounters friction understanding some scope. They would be left wondering and checking if calendar is ever mutated, or assuming that it was mutable only to set the initial time zone.
Swift gives us lets and vars to help us reason more intuitively. So to distinguish between these intentions, we might like to write instead:
let sydney = TimeZone(identifier: "Australia/Sydney")!
let calendar = Calendar(identifier: .gregorian, timeZone: sydney)
Some questions for the community.
Firstly, would such an initialiser on Calendar help make your code more expressive?
Another question: do you already have such an initialiser, or are you repeating calendar.timeZone = where you need a calendar for a specific time zone?
Any thoughts or comments about pitching such a small improvement to Foundation?
More flexible initializers are always good, but in their absence, the practical solution is a post-applicative that works on anything, not just one type:
and only set calendar's timeZone/locale if they are none nil.
I'd say yes, why not.
Note that, strictly speaking:
{
let sydney = Calendar(
identifier: .gregorian,
timeZone: TimeZone(identifier: "Australia/Sydney")!)
// ... some lines here
// but this is in the same scope:
precondition(sydney.timeZone.identifier == "Australia/Sydney")
}
the precondition here could still fail (hint: use guard let to redefine sydney to something else), so let alone is not a panacea.
The concern raised in the PR about potential clash between API/ABI stability vs a new public field being added in the future is valid. IOW, how could we make this future proof? Not sure what best to do, but let me think out loud with this alternative "fluent" approach:
let calendar = Calendar(identifier: .gregorian).timeZone(sydney).firstWeekday(2)
Note that it's not even longer than the initialiser form:
let calendar = Calendar(identifier: .gregorian, timeZone: sydney, firstWeekday: 2)
whether it is (or could be) as efficient (ideally not via creating and dropping the underlying NSCalendar heap objects for every fluent chain element) – to be seen.
Edit: Yeah, seems plausible.
Possible implementation 1
extension Calendar {
public func timeZone(_ v: TimeZone) -> Self {
var copy = self
copy.timeZone = v
return copy
}
public func locale(_ v: Locale) -> Self {
var copy = self
copy.locale = v
return copy
}
}
Possible implementation 2
extension Calendar {
private var internalCalendar: NSCalendar {
// TODO: get the underlying NSCalendar somehow
fatalError("TODO")
}
public func timeZone(_ v: TimeZone) -> Self {
internalCalendar.timeZone = v
return self
}
public func locale(_ v: Locale) -> Self {
internalCalendar.locale = v
return self
}
}
I did some testing and I do not think there is show-stopping problem. Swift does not find the following choice of initialisers ambiguous such that the introduction of a second or third init breaks source or ABI:
struct Thing {
init(arg1: Int = 1) { print(#function) }
init(arg1: Int = 1, arg2: Int = 2) { print(#function) }
init(arg1: Int = 1, arg2: Int = 2, arg3: Int = 3) { print(#function) }
}
let _ = Thing()
// init(arg1:)
This means we should be able to shape the initializer with an optional time zone.
Imagine a situation you'd like to deprecate / remove arg2. Eventually you'd be able to make init(arg1: Int = 1, arg2: Int = 2)unavailable... but in everything from init(arg1: Int = 1, arg2: Int = 2, arg3: Int = 3) and up you'd have to keep arg2 forever...
I'd seriously consider the fluent approach (at least add it to the alternatives considered). For me it feels more scalable and DRY. Do you see any shortcomings of that compared to init?
Yup, but it uses the user's current timezone if you leave it unset. In other words, I would expect it to work just fine if you leave it unset. But if you feel you need to set it for it to work correctly, then there's clearly something I'm missing. That's why I'm asking about the scenarios that you find it necessary.
Are you suggesting that if the scenarios for computing absolute moments in time zones other than the users are non-obvious/specialised, then Foundation should not offer such an initialiser?
I would be happy to hear from others. When I started this thread, I thought by asking if people had their own initialisers on Calendar, and if people indicated they did, then this would be a sign those scenarios are less obvious.
For me, I tend to need these computations for apps I have worked on that schedule people, work or events in other people's or organisation's time zones, or review those kinds of things in the past. Other categories of apps could be multiplied, though I have less experience developing some of them: project management, financial, logging and auditing, delivery, travel, alarms, calendars, event planners, etc.
It works just fine, until it doesn't.. Is it's timezone's current or autoupdatingCurrent? It might matter for you depending upon how you want the app to react when user is changing the timezone. You might want to care about calendar's timezone if you are making an app to estimate the time of arrival and the destination is in a different time zone, or an app that schedules a call between participants in different time zones, or making calendar calculations that should not be affected by the current time zone, or making a world clock (see below), or for a dozen of other good reasons.
@main struct WorldClockApp: App {
var locations = ["Europe/London", "America/Phoenix", "Asia/Tokyo"]
var body: some Scene {
WindowGroup {
let currentTime = Date()
VStack(alignment: .leading) {
ForEach(locations) { location in
var calendar: Calendar {
var calendar = Calendar(identifier: .gregorian)
calendar.timeZone = TimeZone(identifier: location)!
return calendar
}
let hour = calendar.component(.hour, from: currentTime)
let minute = calendar.component(.minute, from: currentTime)
Text(String(format: "%02d:%02d in %@", hour, minute, calendar.timeZone.identifier))
}
}
}
}
}
10:42 in Europe/London
02:42 in America/Phoenix
18:42 in Asia/Tokyo
The example above is particularly relevant as this simple option is not even available:
var calendar = Calendar(identifier: .gregorian)
calendar.timeZone = TimeZone(identifier: location)! // ❌
let hour = calendar.component(.hour, from: currentTime)
let minute = calendar.component(.minute, from: currentTime)
Text(String(format: "%02d:%02d in %@", hour, minute, calendar.timeZone.identifier))
as you'd be getting a compilation error "No exact matches in reference to static method 'buildExpression'" when you do that in a view builder context.
Are you suggesting that if the scenarios for computing absolute moments in time zones other than the users are non-obvious/specialised, then Foundation should not offer such an initialiser?
No, that is not what I'm suggesting at all. I'm just following up on your question to understand what people tend to do. I also wonder if you ever feel that you need to set other calendar properties such as firstWeekday and minimumDaysInFirstWeek as well. If so, would it also make sense to take in other properties in the initialiser too, like I commented in the PR?
(I can imagine firstWeekday to be useful if you're building a calendar app that allows setting the week to start on Monday or Sunday. The latter... I'm not sure, besides using ISO8601 or if you are implementing a calendar on your own.)
Speaking personally, setting timeZone is a very common operation for me when working with Calendar and setting basically any other property is exceedingly rare.
In my own use cases, firstWeekday is a must. For example, when scheduling people, work, or things, users like to start their weeks on non-default weekdays, such as a cafe that is only open from Wednesdays to Sundays.
I have never needed to set minimumDaysInFirstWeek, but the PR in its current state includes it for completeness.
The PR has changed in response to those comments and I think you'd have some great feedback, so I'm hoping you'll have a chance to share it. In particular, I'd like to draw your (and the community's) attention to the Alternatives Considered section, where I think a compelling alternative is presented, but I need some more feedback from you, code owners and others in the community to settle the direction one way over the other.
Testing. Setting a time zone allows one to prove logic is correct with different time zones. Or, to set a specific time zone that will be used no matter where team members reside.
So an init on Calendar offering time zone (and locale) would be great.