tc39 / proposal-temporal

Provides standard objects and functions for working with dates and times.
https://tc39.es/proposal-temporal/docs/
Other
3.21k stars 147 forks source link

What about the default calendar? #292

Closed sffc closed 3 years ago

sffc commented 4 years ago

Calendar support has landed in Temporal. Now we need to decide on the default calendar.

Option 1 is for the API to always assume the ISO (Gregorian) calendar. The programmer can opt-in to an alternative calendar system, but the API doesn't nudge them to do the right thing when a non-Gregorian calendar should actually be used.

However, it is bad i18n practice to assume that dates should be represented in the Gregorian calendar by default.

I have presented several ways that we could improve the API to nudge developers to do the right thing when it comes to non-Gregorian calendars:

  1. Require the programmer to explicitly specify the calendar.
  2. Default to a partial ISO calendar (detailed in the explainer).
  3. Default to Intl.defaultCalendar (a new symbol), or ISO if that field doesn't exist.
  4. Add ZonedAbsolute, a new type between Absolute (timestamp) and DateTime (calendar-dependent wall clock time).
  5. Split factory methods into ISO and non-ISO variants.
  6. Use Intl.defaultCalendar in Temporal.now, and explicit elsewhere. See https://github.com/tc39/proposal-temporal/issues/292#issuecomment-687551317

More details on these six options is documented in the explainer doc: https://github.com/tc39/proposal-temporal/blob/main/docs/calendar-draft.md#default-calendar

littledan commented 4 years ago

I want to suggest that, rather than a broad survey like @codehag created for the pipeline operator, we focus on some targeted outreach to developers who work with various non-ISO calendars and locales. This may be more in the form of interviews rather than a survey, since this information is pretty detailed. I met one such developer, and wrote about his feedback in https://github.com/tc39/proposal-temporal/issues/268#issuecomment-570670212 (sorry for posting on the wrong issue); my overall impression is that any of 1-4 would be acceptable for his needs, which span both Gregorian and Hijri (Islamic) calendars.

codehag commented 4 years ago

Sorry i totally missed this issue. Happy to help where I can though.

pipobscure commented 4 years ago

Yes I agree with @littledan in addition I'd ask people about their circumstance. I believe knowing whether respondents regularly deal with differing calendars will be a key input.

sffc commented 4 years ago

See also my PR in #309

littledan commented 4 years ago

We've been thinking about this for a while. Now that we have an extensive cookbook, and the polyfill for non-Gregorian calendars is coming along, I think we're coming to the understanding of the design space where we could really come to a decision, based on consulting with more people.

sffc commented 4 years ago

I've updated calendar-draft.md in #590 with relevant cookbook changes and some new material in the default calendar explainer, including a closer look at the different factory methods used for constructing Temporal types.

ptomato commented 4 years ago

@sffc requested specific feedback on each of the 6 options, considered individually, so here's mine. I'd like to preface it by saying that I do see disadvantages in each of the options, so to me the question is picking the one with the least serious disadvantage.

  1. Full ISO: I agree that this is a potential source of bugs as detailed in calendar-draft.md. However, I believe there ought to be some other way to reduce the likelihood of those kinds of bugs, even if we choose this option. I believe that should be carefully considered, but I don't believe it's an insurmountable problem, and the fact that those mistakes may be possible to commit is not a disadvantage that I weight very highly.

  2. Explicit calendar: For the reason I commented elsewhere, I feel strongly that the calendar doesn't need to be explicit in Temporal.X.from(fields), so I would consider this option only for now and inTimeZone. So, concretely, we are talking about removing default values from arguments in the following four API signatures:

    • Temporal.now.dateTime(timeZone = Temporal.now.timeZone(), calendar = 'iso8601')Temporal.now.dateTime(calendar, timeZone = Temporal.now.timeZone())
    • Temporal.now.date(timeZone = Temporal.now.timeZone(), calendar = 'iso8601')Temporal.now.date(calendar, timeZone = Temporal.now.timeZone())
    • timeZone.getDateTimeFor(absolute, calendar = 'iso8601')timeZone.getDateTimeFor(absolute, calendar)
    • absolute.inTimeZone(timeZone, calendar = 'iso8601')absolute.inTimeZone(timeZone, calendar)

    I don't prefer this, because I think in practice the vast majority of programmers will be using ISO anyway, so having to write 'iso8601' explicitly will be a minor annoyance. At worst, it will confuse JavaScript beginners because it's a required parameter that they won't understand. (I'm assuming that inexperienced JavaScript programmers are barely going to understand the Temporal types to begin with, let alone non-ISO calendars.) I weight that argument very highly.

    I think in practice, tutorials and StackOverflow will tell people "oh, you have to pass 'iso8601' for that parameter, it's too complicated to explain why" and so any benefit to making it explicit will be lost. Specifically, for that reason I don't agree that this forces a decision point on the programmer.

  3. Partial ISO: I see this option as adding a flag to Temporal.Date and Temporal.DateTime saying "is this object broken Y/N" and the way you flip the flag is using a method named withCalendar(). If you write a function that takes a Temporal.Date or Temporal.DateTime, you have no way to know whether the object has its broken-flag set or not, so in practice your function will have to defensively call withCalendar('iso8601') on it anyway if you need to do one of the operations not supported by partial ISO. This violates predictability, which I weight highest of all, and for that reason this is a non-starter for me.

  4. Locale-defined default (whether via Intl.defaultCalendar or something else): I think this option does prevent the bugs detailed in calendar-draft.md, but it makes your arithmetic operations have potentially different results depending on what locale you're in. This violates predictability, making this one also a non-starter for me.

  5. Extra types: This one I haven't been able to fully study the advantages and disadvantages yet. But, I think just the fact that it's been several days and I haven't had time to wrap my head around it, is an indication that it's too complicated. If inexperienced JavaScript programmers are barely going to understand the existing Temporal types, then adding two more types is not going to improve things.

    What I do understand is that we'd need a new Date-type and DateTime-type without a calendar, to contrast Temporal.Date and Temporal.DateTime with a calendar. I think this will confuse inexperienced JavaScript programmers, and the advice on StackOverflow will be "oh, you have to do withCalendar('iso8601') to be able to subtract," and that advice will be applied indiscriminately.

  6. Separate API: Again assuming that the ISO calendar can be the default in Temporal.X.from(fields), we are talking concretely about doing option 2, removing the default value from some calendar arguments that would otherwise default to ISO, while also adding new methods for the convenience of programmers who only want the ISO calendar:

    • add Temporal.now.isoDateTime(timeZone = Temporal.now.timeZone())
    • add Temporal.now.isoDate(timeZone = Temporal.now.timeZone())
    • add timeZone.getISODateTimeFor(absolute)
    • add absolute.inTimeZoneISO(timeZone)

    Since the vast majority of programmers will want ISO, I think this prioritizes the wrong use case. It will also confuse inexperienced JavaScript programmers. Reading the docs, or seeing what pops up in their IDEs, they will think "Do I want inTimeZone or inTimeZoneISO?" and code in the wild will end up with mixtures of the two.

    I'd have less of an objection to this option if we kept the existing methods as they are, and instead added differently-named methods, that mention "calendar" in the name, which require a calendar. That way, the obvious way would be to call e.g. Temporal.now.date() and absolute.inTimeZone(). Programmers would see Temporal.now.dateWithCalendar() and absolute.inTimeZoneWithCalendar() in the docs or in their IDEs, but the thought process would be clearer, since if they don't need calendars then they also don't need the calendar-named methods.

Here's approximately how I'd weight the various criteria that the proposals are validated by:

I can make a pull request to calendar-draft.md showing how I'd change the emoji faces in the table if anyone wants that, but hopefully this illustrates my opinion.

littledan commented 4 years ago

I agree with all the points that @ptomato mentioned above, and his weighing of them. I want to mention a few more disadvantages of option 4, which I've mentioned in some calls but don't think I properly wrote down anywhere:

Across options 2, 3, 5 and 6, there is a common API design principle that, if we ask developers to explicitly invoke the "iso" calendar in one way or another, then that will lead to a decision point from them. I share @ptomato 's skepticism about what this would mean in practice--I think the meaning of this would remain confusing to many programmers, and wouldn't constitute a choice point in practice. However, this is just a hypothesis, which we might investigate by talking to more programmers, distributing sample implementations/documentation for API alternatives, etc.

Among options 2, 3, 5 and 6, I prefer 6--I think it has the lowest syntactic overhead in practice, and it still meets the goal of "providing an explicit choice point", if that's taken to be a goal. The alternate naming that @ptomato proposes in option 6 seems interesting to me--this alternative option 6 would still create a situation where, in the documentation listing, tab completion for methods, etc, you see an option where there's an explicit calendar parameter, so this may still lead developers to invoke the right calendar-specific APIs. Option 6 seems to me like, at worst, a very minor source of slight ugliness compared to Option 1.

Overall, I prefer option 1, but the only option I am very strongly concerned about is option 4.

justingrant commented 4 years ago

I think the high-order bit for defaults is (as @littledan notes above) that calendar defaults are nuanced and can affect application logic more than other localization domains. By contrast, a "default locale" is simpler, e.g. Japanese user only wants to see Japanese text or number formats. In that case, a developer's job is mostly to pass the right locale parameter to the right APIs, but otherwise their application logic could be almost identical across locales.

I don't think calendars will ever work like that. In markets using non-ISO calendars, both developers and end-users must navigate the complexity of local and/or ISO calendars used depending on the user (e.g. young vs. old users) or the app (e.g. government websites vs. ecommerce). A "default calendar" at the user or app level may not really exist. It could be a toggle, like @littledan's Jordanian friend describes. Or could even be 2 calendars side-by-side in the same UI.

Also, I think it's important to think of defaults in context of which users will need them:

With those customer profiles in mind, here's some specific suggestions:

Overall I agree with @littledan's and @ptomato's prioritization so I won't rehash the pro/con arguments for each option, other than to be careful not to make an already-complex API even more complicated for the vast majority of developers who only use ISO.

ptomato commented 4 years ago

Meeting, May 28: We'll initially ship the polyfill with option 1 (default is full ISO) with the understanding that it is revisited for Stage 3 based on feedback. It would be helpful to think about how we can most effectively gather that feedback (talk to individual developers using these calendars?) and what sort of feedback would change our minds vs. what sort of feedback would not.

ptomato commented 4 years ago

(Moving to Stage 3 milestone since option 1, like 2 or 6, doesn't require writing any significant extra code, I'll just implement it that way in the calendar branch)

macchiati commented 3 years ago

First, it is great to have support for non-Gregorian calendar systems; that is an important feature for many cultures.

However, I do have concerns about having the calendar support default to Gregorian. I'm afraid that that is repeating what happened with other areas, such as locales. We've had unpleasant (and costly) experiences in internationalization where programmers have to go out of their way to do the right thing. It is very easy for a programmer to not realize, for example, that numbers are formatted very differently for different locales, when the default formatting API for them doesn't need a locale.

When the programmer needs to supply a locale, then that at least raises the question for them as to how to get it, and makes it more likely than they will use the locale of the user, rather than simply default to (say) English.

Option #1 feels quite similar to that case, and I'd urge the group to take a second look at some of the options where the calendar choice needs to be made. None of them feel that onerous: although I do have some preferences among them, but that is less important than the fundamental issue of whether to require a calendar choice or use a default.

justingrant commented 3 years ago

Thanks to @sffc's friendly but persistent feedback, I've been giving this topic a lot of thought. Here's a short writeup of my understanding of the problem and my current opinion about a solution.

TL;DR - I think we should require a calendar in toLocaleString, but default to ISO when creating objects. This means that using non-ISO calendars for business logic would require an opt-in if the calendar isn't already attached to the instance by whoever created it.

AFAIK, there are two logical places where calendars could be required: 1) When formatting an ISO-calendar date via toLocaleString() methods of Date, DateTime, LocalDateTime, MonthDay, and YearMonth. 2) When creating new Temporal object instances, which primarily happens via .from() or less-common APIs like Time.toDateTime.

Did I miss any others?

Requiring a calendar-enabled locale in toLocaleString() methods of Temporal types seems reasonable to consider.

I'd assume that the calendar could come from one of three places:

toLocaleString() would throw if none of the three ways above provided a calendar.

Here's a few reasons why I think requiring a calendar for toLocaleString() could be OK:

toLocaleString() works like other localization APIs that we know are successful. Let's follow the same path.

However, I'm not sold on requiring a calendar when creating Temporal objects.

This is the status quo: Temporal objects that don't opt in to a specific calendar would default to ISO-valued properties for year, month, day, and era.

Use cases that only display date/time data fetched from somewhere else (e.g. a date picker, or a database) and don't change that data before sending it to toLocaleString() would get the localized results in the desired calendar, because toLocaleString() would require a calendar.

Developers who refuse to use toLocaleString (e.g. ${date.month}/${date.day}/${date.year}) will show ISO values for month/day/year to users. This is the same problem as lazy developers writing code like £${price} instead of using an Intl currency API. IMHO we can only fix this problem via docs and evangelism, not APIs.

Here's the part that @sffc won't like: if developers want to create or manipulate date/time data using non-ISO-calendar-aware business logic (e.g. add 1 Hebrew month, or set date to the first of the Islamic year) then the developer will need to explicitly set the calendar, either when constructing the object or after construction. Non-ISO developers won't have to do this step.

The main reason I think this is the only viable option is because I don't believe that most developers will be successful with non-ISO-calendar business logic unless they already understand non-ISO calendars or are motivated to learn-- and these developers are the same people who will be OK to add a non-default calendar parameter! I outlined these concerns in more detail here: https://github.com/tc39/proposal-temporal/issues/292#issuecomment-633736952. Happy to discuss further.

sffc commented 3 years ago

Thanks for the reply @justingrant, but unfortunately I don't think it moves us closer to a solution.

toLocaleString takes a locale parameter, and the locale carries a calendar (for example, fa-IR implicitly resolves to the persian calendar; en-US implicitly resolves to the gregory calendar; and en-US-u-ca-hebrew explicitly resolves to the hebrew calendar). We are already discussing in #262 about whether the locale's calendar or Temporal's calendar should win when formatting. It is therefore unnecessary for toLocaleString to take a calendar parameter.

Furthermore, this is actively detrimental to good i18n, because we should in general rely on the locale as the source for the calendar. On the client, the locale is environment-determined, and in TC39-TG2, we are actively working on having it explicitly reflect the user's preferred calendar system based on browser/OS settings (see, e.g., https://github.com/tc39/ecma402/issues/68). @littledan is closely involved in those discussions. On the server, the locale should be determined from the request (either a ?lang= URL parameter or the Accept-Language header as a fallback), and that locale should be used whenever you use toLocaleString anywhere in your code. Again, although Accept-Language does not yet carry an explicit calendar system, we're working on a solution (https://github.com/tc39/ecma402/issues/416).

Here's the part that @sffc won't like: if developers want to create or manipulate date/time data using non-ISO-calendar-aware business logic (e.g. add 1 Hebrew month, or set date to the first of the Islamic year) then the developer will need to explicitly set the calendar, either when constructing the object or after construction. Non-ISO developers won't have to do this step.

Right, that's the crux. Not only is this an extra step for non-Gregorian programmers, but more importantly, the API won't give the programmer a clear signal of "hey! you need to specify a calendar argument here!" It requires careful coding to make sure that you find everywhere in your code where you need to specify the calendar argument.

The main reason I think this is the only viable option is because I don't believe that most developers will be successful with non-ISO-calendar business logic unless they already understand non-ISO calendars or are motivated to learn-- and these developers are the same people who will be OK to add a non-default calendar parameter! I outlined these concerns in more detail here: https://github.com/tc39/proposal-temporal/issues/292#issuecomment-633736952. Happy to discuss further.

You're bringing me over to your point of view on calendars being an explicit choice for programmers. But, I don't see why this means that calendar should be defaulted. Having calendars be required helps these programmers by showing them everywhere they need to put their calendar argument.

sffc commented 3 years ago

I wanted to add Option 7 based on discussions from today.

As I've previously noted, there are really only two functions affected by an implicit calendar argument: Temporal.now.xxx(), and Temporal.Absolute.prototype.toLocalDateTime().

Option 7 is to make Temporal.now.xxx() use the environment's calendar (from Intl), like it already does with the time zone. Temporal.Absolute.prototype.toLocalDateTime() would require both the time zone and the calendar, since those are the two fields added when transitioning from Temporal.Absolute to Temporal.LocalDateTime.

Option 7 is therefore a hybrid between Option 4 (applied to Temporal.now) and Option 2 (applied to Temporal.Absolute.prototype.toLocalDateTime).

justingrant commented 3 years ago

Option 7 looks promising.

There's one more problematic method:Time. prototype.toDateTime. This problem goes away if Time gets a calendar as @pipobscure has assured we should do for reasons unrelated to calendars in #522.

sffc commented 3 years ago

Time. prototype.toDate

There is Time.prototype.toDateTime, which takes a Temporal.Date, which has a calendar slot. There is no Time.prototype.toDate.

justingrant commented 3 years ago

Oops yes. Typo. Fixed.

sffc commented 3 years ago

Right, except that Time.prototype.toDateTime is not problematic, even without #522, because it can use the calendar from its required Temporal.Date argument.

justingrant commented 3 years ago

Ahhh, you're right. You always have a date to combine with. Never mind.

justingrant commented 3 years ago

there are really only two functions affected by an implicit calendar argument: Temporal.now.xxx(), and Temporal.Absolute.prototype.toLocalDateTime().

What about Absolute.prototype.toDateTime ?

sffc commented 3 years ago

What about Absolute.prototype.toDateTime ?

If we still have such a function, then yes, that one would be affected. We don't really need that function, though, since you can go to LocalDateTime first and then down to DateTime. We should probably remove that function.

sffc commented 3 years ago

@justingrant asked me to give more examples of where i18n-related functionality was used outside of the context of formatting.

I just came across one in my coding today: charsets when reading files from the filesystem. Node.js and Python now expect you to pass the character encoding when reading a file from disk. Often, you just hard-code "utf-8". However, I can think of a couple times where this has really saved me: when my file wasn't UTF-8, I knew exactly where I needed to fix it in code.

macchiati commented 3 years ago

Formatting is a big one, and you mention charsets (diminishing but still necessary). There's also parsing, comparison (collation, searching,...), transforms (lowercasing, transliteration,..), choice of units per locale+scope (currency, measures, ...), etc.

On Mon, Sep 7, 2020, 13:38 Shane F. Carr notifications@github.com wrote:

@justingrant https://github.com/justingrant asked me to give more examples of where i18n-related functionality was used outside of the context of formatting.

I just came across one in my coding today: charsets when reading files from the filesystem. Node.js and Python now expect you to pass the character encoding when reading a file from disk. Often, you just hard-code "utf-8". However, I can think of a couple times where this has really saved me: when my file wasn't UTF-8, I knew exactly where I needed to fix it in code.

— You are receiving this because you commented. Reply to this email directly, view it on GitHub https://github.com/tc39/proposal-temporal/issues/292#issuecomment-688503445, or unsubscribe https://github.com/notifications/unsubscribe-auth/ACJLEMHZGEQ66HMJFTTUWLLSEVAFXANCNFSM4JYACW2A .

littledan commented 3 years ago

@macchiati Can you say more about your preferences among the non-1 options?

macchiati commented 3 years ago

I'll weigh in later (not feeling well now).

Mark

On Mon, Sep 7, 2020 at 9:19 PM Daniel Ehrenberg notifications@github.com wrote:

@macchiati https://github.com/macchiati Can you say more about your preferences among the non-1 options?

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/tc39/proposal-temporal/issues/292#issuecomment-688608718, or unsubscribe https://github.com/notifications/unsubscribe-auth/ACJLEMA6MV7YRBNC3HCMRZ3SEWWDZANCNFSM4JYACW2A .

littledan commented 3 years ago

I'm a bit concerned about this option 7, as I am with 4, because I don't think we're going to be able to get a correct default calendar in a reliable way.

In general, my understanding is that communicating users' calendar preference is a lot less mature than communicating users' default timezone around computers in general, with a common pattern of higher layers ending up maintaining preferences. I'm not really sure if browsers will be able to get an accurate enough view of which calendar they should choose, even if we add web APIs to communicate this to the client and HTTP headers for the server. It would definitely be a good data source, and I support these efforts, but I'm not sure if having it "just work" will be feasible (e.g., given how many people and UIs juggle multiple calendar systems at once).

In JavaScript server environments, I don't see how a default calendar would work: there would be no way to change the default calendar within a particular scope, since JS doesn't support any kind of dynamic scoping that you'd need to make that work. The default calendar of the surrounding environment may show through, rather than that of the client. So, if you try to evaluate the same JS code on the server and client, you'll often get mismatching results, which could potentially lead to bugs.

In browsers, the default calendar would currently be chosen based on just the region, which we know is inaccurate. Even if we move it to OS preferences, I believe that this would not always be set as the user would like--this is why some applications have user-level preferences for calendars.

I believe having a global default locale was a design mistake in ECMA-402, which we shouldn't reproduce in Temporal. Instead, it would be better to read the locale from navigator.languages thread these things through explicitly. We've been grandfathering in the default locale in in that one area (Intl constructors), but shouldn't extend the pattern everywhere. Defaults lead to unreliable behavior, difficulty testing/reproducing results. They're "too cute", working just enough for easier examples, sacrificing correctness in the harder cases.

For those reasons, I would strongly prefer that we use an explicit calendar for those cases or default to the Gregorian calendar, and I would disprefer having a locale-sensitive default calendar used when no calendar is provided. (I have a slight preference for default-ISO over locale-sensitive, but that's more a matter of taste than the more objective concerns I listed above.)

sffc commented 3 years ago

I'm a bit concerned about this option 7, as I am with 4, because I don't think we're going to be able to get a correct default calendar in a reliable way.

In general, my understanding is that communicating users' calendar preference is a lot less mature than communicating users' default timezone around computers in general, with a common pattern of higher layers ending up maintaining preferences.

My hope and intention is that the environment calendar will evolve to become more reliable over time. For example, the User Preferences proposal makes the user's OS preference available to the web platform (via navigator.locales, which Temporal.now would use).

I'm not really sure if browsers will be able to get an accurate enough view of which calendar they should choose, even if we add web APIs to communicate this to the client and HTTP headers for the server. It would definitely be a good data source, and I support these efforts, but I'm not sure if having it "just work" will be feasible (e.g., given how many people and UIs juggle multiple calendar systems at once).

Agreed that the data source isn't perfect. However, it should have a higher precision than "just assume Gregorian".

In JavaScript server environments, I don't see how a default calendar would work: there would be no way to change the default calendar within a particular scope, since JS doesn't support any kind of dynamic scoping that you'd need to make that work. The default calendar of the surrounding environment may show through, rather than that of the client. So, if you try to evaluate the same JS code on the server and client, you'll often get mismatching results, which could potentially lead to bugs

In browsers, the default calendar would currently be chosen based on just the region, which we know is inaccurate. Even if we move it to OS preferences, I believe that this would not always be set as the user would like--this is why some applications have user-level preferences for calendars.

No matter which option we choose, server-side and client-side applications can provide the calendar via an explicit argument, which will be better than any default.

I would be open to consider allowing Temporal.now.calendar() to be overriden by user code as a way to set the default calendar for other Temporal.now functions to use. Main issue: #873

Option 2, explicit everywhere, is the only one that encourages best server-side practices.

I believe having a global default locale was a design mistake in ECMA-402, which we shouldn't reproduce in Temporal. Instead, it would be better to read the locale from navigator.languages thread these things through explicitly. We've been grandfathering in the default locale in in that one area (Intl constructors), but shouldn't extend the pattern everywhere. Defaults lead to unreliable behavior, difficulty testing/reproducing results. They're "too cute", working just enough for easier examples, sacrificing correctness in the harder cases.

I generally agree, which is why Option 2 remains my first choice.

For those reasons, I would strongly prefer that we use an explicit calendar for those cases or default to the Gregorian calendar, and I would disprefer having a locale-sensitive default calendar used when no calendar is provided. (I have a slight preference for default-ISO over locale-sensitive, but that's more a matter of taste than the more objective concerns I listed above.)

I agree with your arguments that any default calendar is bad in principle. However, if we had to pick one for the purposes of Temporal.now (and I want to emphasize that this is the only case we are debating), your arguments don't lead to the conclusion that default-ISO is better than locale-sensitive.

macchiati commented 3 years ago

I agree with Shane.

Ideal for avoiding potential problems is to pass in explicit locale, timezone, calendar, etc parameters always instead of defaulting (Option 2). And in a server environment that is especially the case.

But if the API is to have defaults, Option 7 is far better than Option 1, since it is much more likely to get the right answer. Having Temporal.now default to a locale-sensitive choice of calendar won't make things any worse than defaulting to Gregorian. If the user's choice of calendar is available, then the user would get it. If it isn't available, then the user gets the 'ultimate default': Gregorian.

Mark

On Tue, Sep 8, 2020 at 2:52 PM Shane F. Carr notifications@github.com wrote:

I'm a bit concerned about this option 7, as I am with 4, because I don't think we're going to be able to get a correct default calendar in a reliable way.

In general, my understanding is that communicating users' calendar preference is a lot less mature than communicating users' default timezone around computers in general, with a common pattern of higher layers ending up maintaining preferences.

My hope and intention is that the environment calendar will evolve to become more reliable over time. For example, the User Preferences proposal makes the user's OS preference available to the web platform (via navigator.locales, which Temporal.now would use).

I'm not really sure if browsers will be able to get an accurate enough view of which calendar they should choose, even if we add web APIs to communicate this to the client and HTTP headers for the server. It would definitely be a good data source, and I support these efforts, but I'm not sure if having it "just work" will be feasible (e.g., given how many people and UIs juggle multiple calendar systems at once).

Agreed that the data source isn't perfect. However, it should have a higher precision than "just assume Gregorian".

In JavaScript server environments, I don't see how a default calendar would work: there would be no way to change the default calendar within a particular scope, since JS doesn't support any kind of dynamic scoping that you'd need to make that work. The default calendar of the surrounding environment may show through, rather than that of the client. So, if you try to evaluate the same JS code on the server and client, you'll often get mismatching results, which could potentially lead to bugs

In browsers, the default calendar would currently be chosen based on just the region, which we know is inaccurate. Even if we move it to OS preferences, I believe that this would not always be set as the user would like--this is why some applications have user-level preferences for calendars.

No matter which option we choose, server-side and client-side applications can provide the calendar via an explicit argument, which will be better than any default.

I would be open to consider allowing Temporal.now.calendar() to be overriden by user code as a way to set the default calendar for other Temporal.now functions to use. Main issue: #873 https://github.com/tc39/proposal-temporal/issues/873

Option 2, explicit everywhere, is the only one that encourages best server-side practices.

I believe having a global default locale was a design mistake in ECMA-402, which we shouldn't reproduce in Temporal. Instead, it would be better to read the locale from navigator.languages thread these things through explicitly. We've been grandfathering in the default locale in in that one area (Intl constructors), but shouldn't extend the pattern everywhere. Defaults lead to unreliable behavior, difficulty testing/reproducing results. They're "too cute", working just enough for easier examples, sacrificing correctness in the harder cases.

I generally agree, which is why Option 2 remains my first choice.

For those reasons, I would strongly prefer that we use an explicit calendar for those cases or default to the Gregorian calendar, and I would disprefer having a locale-sensitive default calendar used when no calendar is provided. (I have a slight preference for default-ISO over locale-sensitive, but that's more a matter of taste than the more objective concerns I listed above.)

I agree with your arguments that any default calendar is bad in principle. However, if we had to pick one for the purposes of Temporal.now (and I want to emphasize that this is the only case we are debating), your arguments don't lead to the conclusion that default-ISO is better than locale-sensitive.

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/tc39/proposal-temporal/issues/292#issuecomment-689156439, or unsubscribe https://github.com/notifications/unsubscribe-auth/ACJLEMFYA6IE5TA467QJRRDSE2RQPANCNFSM4JYACW2A .

justingrant commented 3 years ago

Some ideas stemming from discussions in last Friday's meeting:

First, we decided to add (see #625) an equalsISO method on the Temporal.Date type to mirror the existing getISOFields method. Could this XxxISO pattern be useful in other calendar-related places? For example, should there be a Temporal.nowISO object which is optimized for servers or other environments where an ISO default is desired, leaving Temporal.now to pick up the environment's calendar?

Second, in Friday's meeting we discussed using an ESLint rule to catch cases where users specify string or object literals without specifying a calendar. The more I thought about this approach, the more I liked it, because ESLint rules are highly configurable. The developer could dial up or dial down the "strictness" of the rule, e.g. whether to require the a calendar to be explicitly specified for object bags vs. strings vs. now() vs. toLocalDateTime/toDateTime.

If we had such a lint rule, @sffc how would you feel about Absolute.prototype.toLocalDateTime accepting an optional calendar (defaulting to ISO) instead of a required calendar? I'm asking because the pattern that we both like in #889 gets a lot more ergonomic if the calendar were optional in that method.

// 2 required parameters, so object bag required
absolute.toLocalDateTime({timeZone: 'America/Los_Angeles', calendar: 'iso8601'});
// optional calendar, so can provide only a string
absolute.toLocalDateTime('America/Los_Angeles');
sffc commented 3 years ago

First, we decided to add (see #625) an equalsISO method on the Temporal.Date type to mirror the existing getISOFields method. Could this XxxISO pattern be useful in other calendar-related places? For example, should there be a Temporal.nowISO object which is optimized for servers or other environments where an ISO default is desired, leaving Temporal.now to pick up the environment's calendar?

Works for me.

Second, in Friday's meeting we discussed using an ESLint rule to catch cases where users specify string or object literals without specifying a calendar. The more I thought about this approach, the more I liked it, because ESLint rules are highly configurable. The developer could dial up or dial down the "strictness" of the rule, e.g. whether to require the a calendar to be explicitly specified for object bags vs. strings vs. now() vs. toLocalDateTime/toDateTime.

If we had such a lint rule, @sffc how would you feel about Absolute.prototype.toLocalDateTime accepting an optional calendar (defaulting to ISO) instead of a required calendar? I'm asking because the pattern that we both like in #889 gets a lot more ergonomic if the calendar were optional in that method.

I would rather not rely solely on ESLint rules to enforce best i18n practices. The comment I made about strings is intended as a general comment about strings in code. My hope is that as more platforms adopt our convention for expressing calendars in strings, issues involving strings will diminish over time.

I also acknowledge the brevity argument, since it is the number 1 complaint about Temporal overall. However, I do think that this particular case warrants the extra code. It's now the one and only place where we're still talking about making the calendar a required option, and I think it will go great lengths to help developers understand this crucial difference between Absolute and the rest of Temporal.

I would likely be happy with toISOLocalDateTime("America/Chicago") coexisting as a briefer alternative to toLocalDateTime({ timeZone: "America/Chicago", calendar: "iso8601" }).

sffc commented 3 years ago

Okay. Here is my latest proposal, based on a synthesis of suggestions from @justingrant, @macchiati, and @pipobscure. Thanks everyone for hanging with me over the last several months. I truly hope that this solution makes everyone happy.

Instant → LocalDateTime

Since this is the only transition that adds a calendar field to the Temporal data model, it is the one and only transition that needs to get a calendar from somewhere. My proposal is to have two methods:

  1. Temporal.Instant.prototype.toLocalDateTime({ timeZone, calendar }) takes an option bag with two required fields (timeZone and calendar).
  2. Temporal.Instant.prototype.toLocalDateTimeISO(timeZone) takes only one option, the timeZone (which may be specified in the option bag as per #889).

Temporal.now

This is a situation where the current time and time zone already come from the environment, so it makes intuitive sense that the calendar system also come from the environment. However, @littledan feels strongly that the ecosystem for user preferences hasn't evolved far enough yet to make the environment calendar high quality. I therefore propose two separate pieces:

  1. Add new functions with the "iso" suffix that return objects with the ISO-8601 calendar system:
    • Temporal.now.dateISO()
    • Temporal.now.dateTimeISO()
    • Temporal.now.localDateTimeISO()
    • Temporal.now.timeISO()
  2. Either remove the existing functions in Temporal.now that take an implicit calendar (Daniel's preference if I understand correctly), or change them to use the environment's calendar. If we remove them, we could always add them in the future when the user preferences ecosystem is more fully developed.
    • Temporal.now.date()
    • Temporal.now.dateTime()
    • Temporal.now.localDateTime()
    • Temporal.now.time()

Temporal.*.from(string)

As discussed in #293, I am engaging the Calsify group in IETF to discuss an extension to RFC 3339 that adds support for IANA time zone identifiers as well as calendars. As more of the ecosystem adopts this convention, strings are more and more likely to contain the correct calendar information when that information is pertinent.

Therefore, I do not feel it is warranted to have a separate calendar argument in this method.

Temporal.*.from(fields)

@ptomato astutely observed that at the point when the field object is created, the calendar system was already considered. For example, if a developer wrote { year: 2020, month: 9, day: 18 }, they clearly intended that to be a date in the ISO-8601 calendar, even though the calendar is not a slot (if this was not their intention, the developer should find out quickly because the date will be wrong). Likewise, { year: 5780, /* ... */, calendar: "hebrew" } accurately expresses the author's intent that the fields are expressed in the Hebrew calendar.

Therefore, I do not feel it is warranted to have a separate calendar argument in this method.

sffc commented 3 years ago

2020-09-18: Consensus on https://github.com/tc39/proposal-temporal/issues/292#issuecomment-694670080, with the following amendment: keep Temporal.now.date() and friends, and they take an explicit calendar argument.

// Current date in the ISO calendar
Temporal.now.dateISO();
Temporal.now.date({ calendar: "iso8601" });
Temporal.now.date("iso8601");  // shortcut according to #889

// Current date in the Hebrew calendar
Temporal.now.date({ calendar: "hebrew" });
Temporal.now.date("hebrew");  // shortcut

// Current date in the environment calendar
Temporal.now.date({ calendar: new Intl.DateTimeFormat().resolvedOptions().calendar });
Temporal.now.date(new Intl.DateTimeFormat().resolvedOptions().calendar);  // shortcut

// Throws an exception: calendar must be provided
Temporal.now.date()