tc39 / proposal-temporal

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

Proposed semantics for calendar system support in Temporal #268

Closed sffc closed 4 years ago

sffc commented 4 years ago

We had a discussion today about this at the Google i18n design meetup. I wanted to post an idea for a way to make Temporal support different calendar systems without making major changes to the API.

Lemma: We can think of a Temporal.Date as some discrete number of days from a universal epoch (say January 1, 1970). For example, today, November 19, 2019, is 18219 days since January 1, 1970.

Given this mindset, all calendar-sensitive operations can operate as though that discrete date were first mapped into the respective calendar. The fundamental calendar-sensitive operations are only:

  1. Add 1 month.
  2. Add 1 year.
  3. Get the day.
  4. Get the month.
  5. Get the year (which could be a pair of era, year).

Note that adding 1 day is not calendar-sensitive in my proposed framework.

What could this API look like? Here are some examples for the getters:

let date = Temporal.now.date();  // a Temporal.Date

console.log(date.as("iso").month);  // 11, i.e. November
console.log(date.as("hebrew").month);  // 2, i.e. Heshvan
console.log(date.as("japanese"));
/*
{
  calendar: "japanese",
  day: 19,
  month: 11,
  year: 1,
  era: "reiwa"
}
*/

For the arithmetic (Temporal.Duration):

let d1 = new Temporal.Duration({ days: 1 });  // OK
let d2 = new Temporal.Duration({ months: 1 });  // TypeError: calendar is required
let d3 = new Temporal.Duration({ months: 1, calendar: "iso" });  // OK
let d4 = new Temporal.Duration({ months: 1, calendar: "hebrew" });  // OK

d1.toString();  // OK
d3.toString();  // OK
d4.toString();  // TypeError: calendar is not iso

If a calendar is not specified, should we default to the ISO calendar? From an i18n perspective, no, we should not operate using a default calendar. Requiring users to specify the calendar makes them go through the thought process of making an informed decision.

@rxaviers @littledan @younies

gibson042 commented 4 years ago

What kind of object would be returned from Temporal.Date.prototype.as? Temporal.Date.prototype is currently Gregorian and specifically uses ISO 8601 conventions for weekOfYear and dayOfWeek. And it seems like string-valued era would have localization concerns, e.g. "reiwa" vs. "Reiwa" vs. "令和".

sffc commented 4 years ago

What kind of object would be returned from Temporal.Date.prototype.as?

A plain object, or possibly a Record. This object would only be returned from the getter and would have no other fancy operations on it.

Temporal.Date.prototype is currently Gregorian and specifically uses ISO 8601 conventions for weekOfYear and dayOfWeek.

weekOfYear can be added to the plain object returned by Temporal.Date.prototype.as.

We'd have to do more research on dayOfWeek in different calendar systems to see whether it could remain on Temporal.Date.prototype or whether it would need to be moved to the .as() object.

And it seems like string-valued era would have localization concerns, e.g. "reiwa" vs. "Reiwa" vs. "令和".

The string "reiwa" in my example would be an identifier that we define. That identifier could be passed to Intl.DisplayNames, for example, to get the localized string.

new Intl.DisplayNames("ja", { type: "era", calendar: "japanese" }).of("reiwa");
// "令和"
sffc commented 4 years ago

Bikeshed of other APIs for the getters:

// Alt 1a
date.inCalendar("hebrew")  // returns object

// Alt 1b
date.calendar("hebrew")  // returns object

// Alt 1c, if 1a and 1b are too long
date.cal("hebrew")  // returns object

// Alt 2
date.getMonth("hebrew")  // returns number
date.getEra("japanese")  // returns string

// Alt 3
date.getIsoMonth()
date.getHebrewMonth()
date.getJapaneseEra()

// Alt 4
date.getIsoObject()
/*
{
  calendar: "iso",
  month: 11,
  day: 19,
  year: 2019
}
*/

The fundamental concept is that the internal representation of Temporal.Date is always the same, but you have to specify the calendar upon input/output operations that are calendar-dependent. The exact API is flexible.

kaizhu256 commented 4 years ago

playing devil's advocate -- instead of universal-epoch, maybe lookup-tables between iso-year-ranges 1000-3000 (or even 1500-2500) are a more cost-effective approach? i don't think anyone realistically expects utc-accuracy (or consistency with external database-operations) outside of those date-ranges.

i'm also not certain utc-based time or the javascript-language (much less this proposal) would still be relevant 500-1000 years from now.

sffc commented 4 years ago

playing devil's advocate -- instead of universal-epoch, maybe lookup-tables between iso-year-ranges 1000-3000 (or even 1500-2500) are a more cost-effective approach?

By "cost-effective" do you mean better for performance? I would assume that yes, anyone implementing this proposal would probably use some kind of lookup table to convert between calendars. That's an implementation detail that doesn't need to go into the spec IMO.

An interesting question would be, if we were to move forward with my proposal in this thread, would we change the spec to make Temporal.Date store the "# of days since epoch" as a BigInt instead of the year/month/day tuple? Again, that's an implementation detail, and I claim my proposal can be implemented regardless of the internal representation of Temporal.Date. However, we may want to at least document that Temporal.Date is defined based on "# of days since epoch" to make the logic for calendar math more clear to users.

pipobscure commented 4 years ago

I've experimented with supporting different calendars. Specifically I investigated the Hebrew, and Ethiopian calendar system. I do not believe that this suggestion works consistently especially once one adds time information.

We did discuss this (and I was a vocal advocate for enabling support for multiple calendar systems) about a year ago and concluded that we would keep that aspect out of scope for this proposal.

I would therefore ask if you can agree that this can be part of a follow-on, and not cram it into this proposal.

sffc commented 4 years ago

I do not believe that this suggestion works consistently especially once one adds time information.

For calendars where the day does not start at midnight, I proposed an approximation based on long-standing ICU behavior where we treat days starting at sunset as starting at midnight approximately 6 hours later.

I would therefore ask if you can agree that this can be part of a follow-on, and not cram it into this proposal.

Although full calendar support could be a follow-on, I do think we should consider carefully the assumptions that would be hard to take back later. I claim that the bulk of Temporal actually works fine even if we're not in the Gregorian calendar. And, the best way to think about how to prevent bad assumptions is to design calendar support in v1.

littledan commented 4 years ago

Really interesting research. The big place where I see a mismatch with the current Temporal is in whether there's a default calendar:

If a calendar is not specified, should we default to the ISO calendar? From an i18n perspective, no, we should not operate using a default calendar. Requiring users to specify the calendar makes them go through the thought process of making an informed decision.

Can we go into this a bit more? I think the "iso" calendar is used pretty widely, in a way that's a bit different from "en" locales being used widely.

I guess the other mismatch is, if a Temporal.Date has a calendar as part of its data model, how does that interact with Intl.DateTimeFormat, which also has a calendar?

I'd suggest that we make a separate state from the explicitly provided calendar, which was, "no calendar was provided". In this state, the calendar of the Intl.DateTimeFormat is used. Otherwise, there is a check that the two calendars match, with an exception thrown on a mismatch.

If we went with these semantics, then I don't see any mismatch from the current Temporal proposal to Temporal which includes these calendars. Would this work acceptably, from your perspective?

(Bikeshed suggestion on the name of that .as() method: How about withCalendar?)

sffc commented 4 years ago

Can we go into this a bit more? I think the "iso" calendar is used pretty widely, in a way that's a bit different from "en" locales being used widely.

It could be seen as incorrect in ar-SA for date arithmetic to operate in the Gregorian calendar, when dates are displayed to the user in their local calendar. In fact, the best default for the calendar may be the locale's default calendar, but that might introduce SES problems.

Making the calendar explicit solves any ambiguity. If you want your operations to be in the user's preferred calendar, you can do,

let calendar = new Intl.DateTimeFormat().resolvedOptions().calendar;
// use that calendar for Temporal operations

And if you really want your operations to be in Gregorian, then you just let calendar = "iso" and use that everywhere.

I guess the other mismatch is, if a Temporal.Date has a calendar as part of its data model, how does that interact with Intl.DateTimeFormat, which also has a calendar?

I'm specifically suggesting that Temporal.Date does not have calendar as part of its data model. There would be no calendar-specific types in my proposal. Rather, conceptualize Temporal.Date as a calendar-agnostic number of days since an epoch, which can be mapped without loss of precision into any calendar system by use of getters.

If a Temporal.Date is passed into an Intl.DateTimeFormat, that's perfectly fine, because the calendar used for display is the one coming from the Intl.DateTimeFormat.

littledan commented 4 years ago

It could be seen as incorrect in ar-SA for date arithmetic to operate in the Gregorian calendar, when dates are displayed to the user in their local calendar.

Sounds like that correctness issue could be solved by the programmer supplying the calendar. I don't think we need to force the strictest approach here to give programmers a path to correctness.

sffc commented 4 years ago

It could be seen as incorrect in ar-SA for date arithmetic to operate in the Gregorian calendar, when dates are displayed to the user in their local calendar.

Sounds like that correctness issue could be solved by the programmer supplying the calendar. I don't think we need to force the strictest approach here to give programmers a path to correctness.

To be clear, are you suggesting that the default calendar remain Gregorian when the programmer performs arithmetic? Or would the default calendar come from the locale?

Here is the use case: you are building a web site in which you have a month/day picker. You use toLocaleString() to show a Temporal.Date to the user. You have a button that says, "add 1 month". If a user in ar-SA clicks that button, if the calculation is implicitly performed in Gregorian space, the user will see a date that is not simply 1 month in the future from their point of view. Unless we require programmers to explicitly specify the calendar, this type of bug will be commonplace.

littledan commented 4 years ago

I'm sorry, I can see that my above comments were based on various misunderstandings of the proposal. I plan to revisit this proposal with more time and rest. Thanks for thinking this all through.

rxaviers commented 4 years ago

I would therefore ask if you can agree that this can be part of a follow-on, and not cram it into this proposal.

Although full calendar support could be a follow-on, I do think we should consider carefully the assumptions that would be hard to take back later. I claim that the bulk of Temporal actually works fine even if we're not in the Gregorian calendar. And, the best way to think about how to prevent bad assumptions is to design calendar support in v1.

Yes, I totally agree with @sffc and also encourage NOT to defer to the future designing calendar support. It's going to be just harder compared to doing it now in v1 where the API freedom is bigger.

rxaviers commented 4 years ago

It's important to emphasize that "No support for non-Gregorian calendars" is one of the six major problems stated in the motivation section for Temporal. Therefore, such support goes in line with this proposal's mission. We all probably don't want to take the risk of needing a Temporal2 in the future 😄

rxaviers commented 4 years ago

@sffc proposal looks good to me in terms of i18n. It would be great if the proposal experts can help improve it. Thank you

pipobscure commented 4 years ago

Way back when (like 2 years ago) a younger, naiver and much less cynical me entered Temporal-land with the idea that we should have types to reflect all calendar systems in an effort to a as multicultural and inclusive as we could possibly be.

I did about 3 months of research to get to know different calendars, by the end of which I came to the conclusion that supporting a subset of the ISO-Proleptic-Gregorian calendar is more than enough of a challenge and more than enough to take on for starters.

I did for example investigate the Hebrew calendar. Depending on which rabbi you speak to you get differing rules:

And that's just the rough outline of the Hebrew calendar; we haven't even gotten started on the astronomical database and required geo-location required to actually implement this. In short I am unwilling to start the Hebrew calendar wars of 5780!

And that's before getting started of the Ethiopian calendar/time and the slight differences to the Eritreans. There's a whole other sets of conflicts in that.

In short: while I am very sympathetic to supporting multiple different calendar/time systems, I would definitely want to declare this out of scope for this proposal. I encourage anyone reading this to contribute to a dedicated issue to discuss how to support different calendars and bring that to both a future Temporal follow-on proposal as well as ECMA402

sffc commented 4 years ago

In short I am unwilling to start the Hebrew calendar wars of 5780!

About the Hebrew calendar, I've stated that my opinion is that we should follow what the industry has been already doing for 20+ years, which is what you refer to as the "progressive" option. This is not a question we can avoid, because we have to decide one way or another in order to implement #262, toLocaleString().

And that's before getting started of the Ethiopian calendar/time and the slight differences to the Eritreans. There's a whole other sets of conflicts in that.

We would not want to get in depth on every corner of every calendar system conflict. We only need first-class support for the most common calendars, which I propose to be the ones that are preferred in at least one region according to CLDR data. We could even narrow our scope further and only support calendars that are the first choice in at least one region, which a shorter list of only 4 calendars (gregorian, islamic-umalqura, persian, and buddhist).

In short: while I am very sympathetic to supporting multiple different calendar/time systems, I would definitely want to declare this out of scope for this proposal. I encourage anyone reading this to contribute to a dedicated issue to discuss how to support different calendars and bring that to both a future Temporal follow-on proposal as well as ECMA402

The problem with this, as I've stated before, is that if we don't think about this now, we are likely to build in assumptions that would make adding calendar support more challenging in the future.

nciric commented 4 years ago

I like sffc@ proposal, because it sidesteps some of the problems with current Temporal approach by using days since epoch, and keeps door open for more i18n friendly APIs. If we marry Temporal to gregorian (because it solves 90% of use cases for example) then that door is going to be much harder to adjust later on.

Similar thing happened with original JS spec - there was lots of pain to push ES 402 through because of baked in assumptions, and English first approach (which did solve 90% of problems at the time).

littledan commented 4 years ago

Thanks for this background on a way forward for the start of days, @sffc . Are there any other technical concerns people have with the proposal?

gibson042 commented 4 years ago

I think it can work, but I'm not 100% certain that I understand the entirety of how currently-planned interfaces would change. Ignoring new methods, what if anything is missing from this list:

sffc commented 4 years ago

@gibson042 Thanks for the reply :)

  • Temporal.Duration instances expose a new calendar property, which must be specified with input including any unit larger than "days" (i.e., "months" and/or "years") but otherwise can be empty.

Correct.

Temporal.{Date,DateTime,YearMonth} instances stop exposing the Gregorian-specific leapYear property and ISO-specific weekOfYear and dayOfWeek properties,

These would become accessible via the .as() object or some other calendar-aware API discussed in https://github.com/tc39/proposal-temporal/issues/268#issuecomment-555722744. As mentioned above, it is likely OK for dayOfWeek to remain on the main object.

and start exposing a new never-empty calendar property and a new era property.

Not correct. Temporal.{Date,DateTime} would not have a calendar field because they would be defined in a calendar-agnostic way.

They also either stop exposing the currently Gregorian and/or ISO-specific properties (dayOfYear, daysInMonth, daysInYear) OR generalize them to every supported calendar.

These properties could also go on the .as() object.

Temporal.MonthDay instances expose a new never-empty calendar property.

Temporal.{YearMonth,MonthDay}, if we keep them, would need to have a calendar field, as discussed in #264.

toString serialization fails when the instance uses a non-ISO calendar and has a nonzero value for month(s) and/or year(s). plus and minus methods fail when the input uses a different calendar than the receiver and includes a nonzero value for months and/or years.

Irrelevant for Temporal.{Date,DateTime}. toString, plus, and minus would always work for those types since they are calendar-agnostic. toString would always serialize to the universal standard ISO calendar string representation.

Would need to think about what behavior to adopt for those methods on Temporal.{YearMonth,MonthDay}, if we were to keep those types.

Temporal.{Duration,Date,DateTime,YearMonth,MonthDay} difference methods fail when the input uses a different calendar than the receiver.

Irrelevant for Temporal.{Date,DateTime}. For the other types, yes, we'd probably have to fail if difference is called on them when their calendar systems differ. (This is an edge case)

Temporal.{Duration,Date,DateTime} string deserialization is always based on ISO 8601, and explicitly exposes that in the resulting instance's calendar property.

Correct, although Temporal.{Date,DateTime} have no calendar field.

gibson042 commented 4 years ago

it is likely OK for dayOfWeek to remain on the main object

I don't think so, because the interpretation is calendar-dependent (even in Gregorian systems, USA and perhaps all of North America conventions start weeks on Sunday but ISO 8601 starts them on Monday).

Temporal.{Date,DateTime} would not have a calendar field because they would be defined in a calendar-agnostic way.

That might have bad ergonomics for arithmetic since Temporal.Duration does have a calendar property, but I can't come up with a concrete example right now.

Still, I don't see any fatal flaws with the approach.

rxaviers commented 4 years ago

dayOfWeek

Note that the first day of the week is region-dependent (not calendar-dependent) (CLDR ref) (UTS#35 ref). For example, it's "sun" for US but "mon" for DE.

That being said, I believe that's irrelevant for Temporal, which is expected to always return the same ISO-8601 numeric index for dayOfWeek, e.g., mon is 1 and sun is 7? http://tc39.es/proposal-temporal/#sec-temporal-todayofweek

PS: weekOfYear is analogous (CLDR ref) (UTS#35 ref)

sffc commented 4 years ago

That being said, I believe that's irrelevant for Temporal, which is expected to always return the same ISO-8601 numeric index for dayOfWeek, e.g., mon is 1 and sun is 7? http://tc39.es/proposal-temporal/#sec-temporal-todayofweek

Right, this is my understanding for dayOfWeek.

That might have bad ergonomics for arithmetic since Temporal.Duration does have a calendar property, but I can't come up with a concrete example right now.

My proposed arithmetic semantics are:

let date = Temporal.now.date();  // a Temporal.Date

// d1 = date minus 1 gregorian month
// From November to October, this equates to 31 days
let d1 = date.minus(new Temporal.Duration({
    calendar: "iso",
    months: 1
}));

// d2 = date minus 1 hebrew month
// From Cheshvan to Tishrei, this equates to 30 days
let d2 = date.minus(new Temporal.Duration({
    calendar: "hebrew",
    months: 1
}));

date.toLocaleString("en");  //  11/22/2019
date.toLocaleString("en-u-ca-hebrew");  //  2/24/5780

// `d1` has the same day as `date` in Gregorian; the Hebrew day changes
d1.toLocaleString("en");  //  10/22/2019
d1.toLocaleString("en-u-ca-hebrew");  // 1/23/5780

// `d2` has the same day as `date` in Hebrew; the Gregorian day changes
d2.toLocaleString("en");  //  10/23/2019
d2.toLocaleString("en-u-ca-hebrew");  // 1/24/5780
pipobscure commented 4 years ago

Ok so let me review:

Temporal.Date currently has the following:

Of those we'd have to drop a whole bunch because they are intrinsically calendar specific leaving:

Except that a lot of those are actually calendar specific as well:

Which leaves us with an object wit the properties:

with which I cannot actually do anything useful beyond what I could do with a plain JS object.

Beside that, it takes away a lot of the comfort and guaranteed correctness away.

Temporal currently guarantees that a Date object is actually fully correct when it is created. It guarantees that comparison and arithmetic will be correct for any two Date objects. It guarantees that combination with a Time object will result in a valid DateTime. It guarantees that it will complain (throw) at the earliest moment possible when something would result in an incorrect object.

Beyond that this proposal becomes very complicated in actual use. So that while it's indubitably correct, it's also something that would be so hard to understand and work with as to be entirely irrelevant.


Now as to why we originally decided to make differing calendars out of scope and what we have done to make adding them later easy.

The Temporal.Absolute type is the one that actually ties things to a fixed timeline. It is also the only thing that can reliably be used to translate between possible different calendar systems. You will find that it does not have ANY properties that rely in any way on the ISO calendar. Instead these are all split off into the DateTime object.

So to implement a Hewbrew calendar (or any other) one would have to specify 2-3 objects (HewbewDateTime, HewbrewDate & possibly HebrewTime if one were orthodox). These objects would encompass all the properties, limits and algorithms that define the Hewbrew calendar.

That's it, and it becomes easy and quite clear that there is a difference between Date and HebrewDate. More than that it would be possible to correctly convert between the two via an Absolute. If anything the argument that could be made is that the name Date is too generic. And for that I'd be happy to rename to ISODate, ISODateTime, ISOTime.


As for toLocaleString(): What would be your proposal to allow Date.prototype.toLocalString() to work if the calendar of the Date object is unknown. It would be all too easy to new Date(2019, 11, 22).toLocaleString('en', { calendar: 'hebrew' }) and in the Temporal-proposal as it stands that should result in "22 Heshvan 5780". However if we adopt this multi-calendar proposal the result would instead be "20 Kislev 2019". So it would produce entirely unexpected output.


In short: we had a lot of in person conversations where I was trying to push for multi-calendar support and calendar agnosticism. It took several months of conversations and lots of research to convince me that while possible, and possible to do correctly, the result would be entirely impractical.

So again: I urge those present here to take their time and think through a lot of the edge-cases. And hopefully come to realize that while possible, this is not something that can be easily done. At the same time, I believe Temporal as it already stands can function for the 99% case quite well and does not (because I've taken great care to ensure that) make it impossible or even hard to add support for other calendars in a follow on proposal.

Follow-On proposals: Please don't misunderstand me. When I say follow-on proposal, I don't envision this to be a thing of additional years, but rather something that could be started immediately after Temporal. I'm very much with Shu who is thinking of proposing a more iterative modus operandi for built-in Libraries.


I see Temporal as building a house. What I propose is to defer this to a next step; akin to waiting to put on the roof until the walls are standing. What I see this proposal as is trying to put the roof on while still holding the wall-bricks in laborer hands because we haven't even put the mortar in let alone let it cure.


Apologies if this has been long and possibly harsh. I admit to being a bit exhausted right now and didn't really want to write this comment until some-time later; However I felt this was getting off to now good place and beginning to be detrimental. Be patient with me and take this in the spirit in which it is meant: one of trying to reproduce the content & arguments of discussions had and resolved long ago.

sffc commented 4 years ago

I want to be clear: in the OP, I made a concrete proposal with semantics on how to handle calendar systems in an uninvasive way. Your reply above lists out many points about generally why calendar systems are hard to support, but it does not address the substance of my specific proposal. I claim that my proposal addresses most of the issues you raised.

kaizhu256 commented 4 years ago

nit -- adding Date.weekOfMonth would improve ux in business-intelligence-aggregation scenarios.

again, its complicated by region-specific sunday/monday start-of-week. for all the week-methods, the user would probably want [usability] option to override iso-behavior with sunday-behavior.

pipobscure commented 4 years ago

I claim that your proposal does not address the issues I mentioned.

In short I did mean to interact with your proposal directly. I accept that the explanations I gave were more in terms of calendars in general. I am however definitely claiming that your proposal solves none (or very few) of those issues while at the same time significantly impacting ergonomics.

sffc commented 4 years ago

I am claiming that NOT adding the calendar to Date itself, makes Date unworkable. Days since epoch is exactly the wrong paradigm.

Ok. This is our fundamental disagreement.

direct conversions are not trivial and sometimes not possible.

Could you share an example where conversion between an ISO 8601 Date and some other modern-use calendar system is not possible?

plus/minus & difference all depend on the calendar in pretty much all cases.

This is objectively only true if months and years are in play, which is often not the case.


Richard's cookbook suggests that only a small portion of Temporal use cases are calendar-sensitive. My proposal has zero impact on the ergonomics of the majority of use cases.

pipobscure commented 4 years ago

In the meantime it’s probably worth it to have a bit of a think about time

hax commented 4 years ago

I agree with @pipobscure .

Though most (all?) calendars have very similar concepts like year, month, day, most programmers would think d.as(anyCal).year/month/day always returns number. But they differ in tricky way.

We already see japanese year is not a number but number plus era, it's very unclear how Japanese programmers expect d.as('japanese').year should return. Similarly, it's also very unclear what d.as('chinese').year should return, the notation of 干支 rarely expressed in one number, and it's also unclear how to express era. And it's also unclear what d.as('chinese').monthshould return, there are two "fourth month" (leap month) in 2020.

So it's very unclear to me that how the surface consistency like d.as(cal) would bring any real benefit than new JapaneseDate/ChineseDate.

sffc commented 4 years ago

Hi, hax,

If I read your post correctly, it sounds like you believe true JapaneseDate and ChineseDate types present better ergonomics than a generic record as I had proposed returning by .as().

This is a good point to discuss. In my proposal, I had preferred generic records rather than fully powered types, because:

  1. I see Temporal.Date as a lower-level data type appropriate for storing in a database, sending over the wire, serializing as an ISO string, and performing calendar-agnostic operations. The calendar-specific records are available via a last-minute conversion when the programmer needs that extra information.
  2. If we had GregorianDate, JapaneseDate, ChineseDate, etc., we give programmers an undesirable choice when it comes to representing their date in a computer. If programmers choose GregorianDate as their preferred data type, and a user comes along with locale ja-JP-u-ca-japanese, which calendar to we use to render the date: the programmer-specified gregorian calendar, or the user-specified japanese calendar? A calendar-agnostic Temporal.Date eliminates this problem because the calendar field only comes from one place: the user's locale preference.
  3. I really like the ergonomics of the Temporal types and how they focus on making the cookbook operations (#240) easy for the programmer. My proposal keeps the majority of the well-researched ergonomics the same, instead adding one extra step only when the programmer wishes to perform some operation that is not calendar-agnostic.

However, I can see the other side of the argument: perhaps the idea of a calendar-agnostic "epoch days" type is too abstract for the average programmer. My counters to that argument would be:

Thanks for the discussion.


P.S. I've already been in touch with the Temporal champions, but I'm planning a deep-dive session on this topic this Monday before TC39. If you are interested in joining, please DM me (e.g., send an email to sffc at google.com).

ryzokuken commented 4 years ago

@sffc please feel free to correct me if I'm wrong, but do programmers using Temporal.Date even need to be aware of how it works internally (i.e. epochDays)? I had imagined that both Date.from and Date.as would accept a calendar as a param and encode/decode the date based on that parameter.

That said, even if average programmers need to be made aware of epochDays, I personally don't think

This becomes part of the unavoidable onboarding process that programmers have to go through anyway when learning Temporal

would be a big deal, since it's not a very different idea than unix time and tbqh that's also a similar abstract idea that is expected to be understood by most if not all programmers anyway, so...

kaizhu256 commented 4 years ago

for most programmers, Temporal source-of-truth is what goes on inside databases rather than inside javascript/c#/java/etc. even if the database is wrong, you often still go with it, because you don't want customers to see different answers (and perceive bugs) who use the same database on their end.

speccing Temporal's internal behavior is beneficial not because i care to understand it (i don't), but because i care to know which operations are inconsisent with their equivalents in sqlite3/sqlserver/mysql etc. (so i can avoid them).

sffc commented 4 years ago

@sffc please feel free to correct me if I'm wrong, but do programmers using Temporal.Date even need to be aware of how it works internally (i.e. epochDays)?

You're correct. Most programmers don't need to know this. In fact, the internal representation of Temporal.Date could remain as it is today as a proleptic Gregorian date. I'm using "epoch days" in this thread to make more apparent the 1-to-1 mapping between Temporal.Date and arbitrary calendar systems.

I had imagined that both Date.from and Date.as would accept a calendar as a param and encode/decode the date based on that parameter.

Yep, pending full API design. I expect to embark on the naming of methods, parameter order, etc., if/when we reach consensus on this being a good idea.

sffc commented 4 years ago

Here is a photo of the whiteboard from today's deep dive. We hashed out some of our differences in understanding. Some key insights:

Discussions will continue this week at TC39.

IMG_20191202_170015

ryzokuken commented 4 years ago

The ISO calendar should be spec'd in 262, and all other calendars should be spec'd in 402.

To be more precise: I'd personally prefer ISO to be spec'd in 262, CLDR/ICU-supported calendars in 402 and everything else in userland. It's important to mention the latter and think about third-party calendars during the design process.

My proposal is limited to so-called ISO-compatible calendars. An ISO-compatible calendar is one in which days start at midnight, days are exactly 24 hours long, and there are no discontinuities in the instants that are able to be represented. (This definition is subject to change based on additional research.) Based on my research thusfar, all modern-use calendars are ISO-compatible. A modern-use calendar is defined as one sanctioned by CLDR, linked in an earlier comment.

Philipp and others would like Temporal to be extensible in a way to support calendars that are not ISO-compatible.

I see the sense in both the arguments. While we shouldn't be too biased against non ISO-compatible calendars in that we make them un-implementable (there's well-documented use-cases, IIUC, like financial calendars, or hotel calendars (days begin at noon)), I understand that the percentage of users/developers who will stick to an ISO-compatible calendar would dwarf the rest. What about some sort of middle-ground?

What about giving a preference to ISO-compatible calendars? Since incompatible calendars aren't sanctioned by CLDR (going by @sffc's assumption here), they'd likely always be implemented in userland. It wouldn't therefore be too outrageous if people implementing these special calendars were expected to jump through an extra set of hoops IMO...

Just spitballing here, but here's an idea:

What if a FooDateTime class was a composition of a BarDate (subclass of Temporal.Date) and BazTime (subclass of Temporal.Time) and all operations on an instance was appropriately passed onto the composed instances? All ISO-compatible DateTime instances would just have to implement their own Date subclass (the whole point of being ISO-compatible is that they agree on everything below day, right?) and related arithmetic, while they could just use ISOTime for time arithmetic. Special calendars (FooDateTime) would have to provide classes for both underlying instances (FooDate and FooTime) and this should work, I think, without making us too opinionated or making any use-case exceptionally hard to achieve.

maggiepint commented 4 years ago

Sorry to chip in with my 2 cents here after some absence, but some thoughts:

littledan commented 4 years ago

To be more precise: I'd personally prefer ISO to be spec'd in 262, CLDR/ICU-supported calendars in 402 and everything else in userland.

It's hard for me to make sense of this comment. This is a bit of a tangent, but: We haven't been normatively referencing CLDR/ICU in the specifications, and I don't think we should change this: implementations may tailor their data. As far as "userland", are you picturing a "data-based API" for calendars, or a polyfill of Temporal?

maggiepint commented 4 years ago

One other small thought. I think that when we say "ISO Compatible" what we really mean is "Does not require time to make calendar computations". The differentiation between "Date Math" - math that involves units of days and greater - and "Time Math" - fuzzy because it includes things like DST, or possible sunrise/sunset times - is a distinction I often make in conference talks and the like. I think ISO compatible alternatively means "we are able to do date math independent from time math". I'm not sure that it necessarily requires 24 hours in a day (DST tells me it doesn't) - so the requirement of 24 hours in a day seems incongruous.

sffc commented 4 years ago

Default Calendars

I think there are two sensible default calendars:

  1. The ISO calendar, e.g., for operations that are restricted to the "calendarless" space.
  2. The user's preferred calendar, which can be found via Intl APIs.

I suspect that (2) is actually what most users building global-ready web sites should prefer as the default. Therefore, I feel that it is bad i18n design to have an implicit ISO calendar default.

However, in my proposal, I discuss "calendarless" operations, for which the user would not have to specify a calendar, because those operations are calendar-agnostic. More specifically, they are calendar-agnostic insofar as the target calendar is ISO-compatible, a property shared by all modern-use calendars I have researched. The programmer, such as a bootcamp student, would need to explicitly specify the calendar only when performing a calendared operation, which is the minority of use cases according to @gibson042's research.

So, it's a choice between (1) accepting the idea of making ISO-compatible calendars more equal than non-ISO-compatible calendars, (2) accepting that all call sites would require some boilerplate to specify the calendar, and (3) accepting behavior that is not i18n-optimal.

262 vs 402

There are a few reasons why putting non-ISO calendar definitions in 402 makes sense:

  1. It would add an ICU or other complex dependency to 262 implementations
  2. Many non-ISO calendars are not well specified; in 402, they can simply be covered as "implementation-dependent"
  3. The primary use case for the proposed non-ISO calendars is i18n
littledan commented 4 years ago

I had the good fortune of meeting @amireh, who worked for many years in Jordan as a software developer. My understading from this conversation (please correct me, @amireh!):

We're doing a very good thing for developers working to produce work for these locales by providing support for the Hijri calendar. It'll be really appreciated by people who have to work with these calendars. (Probably there would be a lot of benefit just by telling more people about Intl.DateTimeFormat's existing support, too...)

In many places across the Middle East and North Africa, the Hijri and Gregorian calendars are used side by side; different people have preferences for different calendars (e.g., a trend might be younger people preferring Gregorian and older people preferring Hijri, in certain places). Sometimes, multiple forms of the same date are presented (e.g., one on top of the other). Sometimes, an application's settings would let someone toggle their preferred calendar. In Jordan, if I understood correctly, the official calendar is Gregorian, but many people really prefer Hijri--it makes more intuitive sense for them. Software developers just have to think about the two different calendars all the time.

When we discussed the default calendar, Ahmed was initially really excited about automatically getting the right default calendar based on the locale. However, as we chatted about how this works out in practice (no forwarding of OS preferences, just based on the region, and of course no application-specific preferences taken into account), it started to seem like the important thing is to have built-in Hijri calendar support, that an ISO default is understandable and not harmful, and that a region-based Hijri default wouldn't really be enough to solve the problems that come up in practice.


CLDR includes some really interesting calendar preference data. In CLDR 36, the four regions which have a first-ranked non-gregorian calendar are:

If we're considering a non-gregorian default locale to software developers working for these regions, I want to suggest that we see if we can get in touch with some of those developers; they may have some interesting feedback for us.

Sidenote: 262 vs 402

I think we're getting at a bit of an editorial issue, not one with normative/observable semantics. Right now, 402 patches 262, e.g., with toLocaleString() implementations. You don't need ICU to implement 262, just 402; you can use the dummy toLocaleString implementations that ECMA-262 has. I think we could do the same here for Temporal, and just use the ISO calendar in 262. But it's really really hard for me to think of any observable consequences of this spec layering. Has anyone argued for some other spec layering or observable behavior?

pipobscure commented 4 years ago

Awesome!

I don’t think anyone has suggested a different layering.

kaizhu256 commented 4 years ago

i feel iso-gregorian should be the default for interoperability with databases. this proposal has lots of nice-to-have features, but lets be honest -- most temporal business-logic will not be done in javascript -- in-practice we rely on external databases (or client-side sqlite3) for most temporal heavy-lifting.

@littledan @amireh, imagine the pain of having to explicitly set calendar=iso everytime you have to interface temporals with sqlserver/mysql/postgres/sqlite3. the cost in bugs if you forget that chore is not worth it.

sffc commented 4 years ago

This issue is pre-December TC39. A fair bit has changed since then, which addresses some of what is posted above. I'm going to close this issue, since I triaged these discussions into their own issues.

Default calendar: https://github.com/tc39/proposal-temporal/issues/292

Set of calendars to support: https://github.com/tc39/ecma402/issues/395

@kaizhu256, the "pain" of explicitly setting calendar = "iso" is at the forefront of the default calendar discussion, and we have a couple of options on the table to address that. Read more in #292 and the linked section in calendar-draft.md.

danwdart commented 4 years ago

Discordian ftw.