Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Safer handling of negative years in non-ISO calendars #1231

Closed
justingrant opened this issue Dec 14, 2020 · 24 comments · Fixed by #1319
Closed

Safer handling of negative years in non-ISO calendars #1231

justingrant opened this issue Dec 14, 2020 · 24 comments · Fixed by #1319
Assignees
Labels
calendar Part of the effort for Temporal Calendar API documentation Additions to documentation non-prod-polyfill THIS POLYFILL IS NOT FOR PRODUCTION USE! spec-text Specification text involved
Milestone

Comments

@justingrant
Copy link
Collaborator

While building non-ISO calendar prototypes, one problem I keep running into is the complexity of era-dependent year numbering, specifically because it introduces the possibility of years counting backwards. For formatting, "12 B.C." is desirable, but for calculation use cases (aka the main reason Temporal exists!) these era-based years add a lot of problems. For example:

  • Possibility to introduce infinite loops or other issues because incrementing years goes the "wrong" way. For example: an app displays a year calendar by starting at month: 1, day: 1 and .add-ing one day until date.year > originalDate.year. For BC years, that's an infinite loop.
  • Possible security/reliability issues: an app has one behavior for old dates (e.g. before 2000) and another for later dates. An attacker could send a BC date input that fools the app into using the "new date" behavior even though the date was actually old, thus running an unexpected codepath that could break the app, store invalid data, etc.

So far, the best solution I can come up with is:

  1. Each calendar defines a "default era" (suggestion: the era active as of 2020-01-01T00:00Z)
  2. Temporal offers two different year properties on types that currently expose year: an "era year" property and a "signed year". For 30BC, "era year" would be 30 while "signed year" would be -31.
  3. It could work similarly to my dual-property counter-proposal in Replace isLeapMonth with monthCode #1203 (comment) for months: there could be a math-friendly year field (signed) and a formatting-friendly convenience property (e.g. eraYear or yearInEra). Either property could be used to represent the year in with and from (but not both together).

An alternative to (1) above would be to do what's recommended in the eraDisplay proposal which is to set the default era to the current time. In Temporal use cases, this seems like it'd introduce quite a bit of complexity, perf, and maybe security issues. Not sure dynamic era selection is worth it, esp. since the vast majority of non-ISO calendars wouldn't be affected. (Japanese would, though.) For formatting, dynamic era selection seems appropriate. But for calculation and back-end use (aka Temporal) I'm not sure it's worth it.

@Louis-Aime
Copy link

I love the idea of having a "signed year", I call it "algebraic year", meaning that you can easily make computations with it.

As far as I am aware, all calendar can define such a year enumeration, using signed integers from a well defined and fixed origin. A number of calendars count years before the origin as negative integers, the origin being year zero. So does the modern Indian calendar (not surprising from the people that introduced zero to mankind), so do by default calendars for which the origin is "the world's creation", like the Hebrew calendar and also the Ethiopic ("Amete amet" means "from the world's creation").

As for the "default era", this is a "subjective" issue. The default era is the era we are presently in. Three years ago, the default era for the Japanese was Heisei, but now it is Reiwa. There is a pending proposal on this issue, your comments are most welcome at https://github.com/Louis-Aime/proposal-intl-eradisplay.

It could an interesting idea to suggest that each calendar defines an "origin era", the era of the "signed year" 1. For a number of calendars, this era is the first one, e.g. ethiopic, the best example, because there is a second era starting after year 5500 of the first one, but also hebrew, persian, all islamic and also iso8601 as defined by the ISO standard. All of them count with negative integers before the origin year. For other calendars, the origin era is in fact the second one. In the first era years are counted backwards starting with 1. This is the case for the Julian calendar, and for the coptic, roc, and gregory (IMHO iso8601 should not work that way).

IMHO the "signed year" or "algebraic year", defined by the calendar's author, would fulfil the needs.

@justingrant
Copy link
Collaborator Author

As for the "default era", this is a "subjective" issue. The default era is the era we are presently in. Three years ago, the default era for the Japanese was Heisei, but now it is Reiwa. There is a pending proposal on this issue, your comments are most welcome at https://github.com/Louis-Aime/proposal-intl-eradisplay.

Hi @Louis-Aime - @sffc pointed me to your proposal and from a quick review it looks good to me. Note that the use cases of Temporal are subtly different from the use cases of Intl's formatting APIs (and hence your eraDisplay proposal). For formatting, using a dynamic choice of era based on the current date seems like a good idea.

For Temporal, dynamically choosing the era based on the current date seems more problematic

It could an interesting idea to suggest that each calendar defines an "origin era", the era of the "signed year" 1. For a number of calendars, this era is the first one, e.g. ethiopic, the best example, because there is a second era starting after year 5500 of the first one, but also hebrew, persian, all islamic and also iso8601 as defined by the ISO standard. All of them count with negative integers before the origin year. For other calendars, the origin era is in fact the second one. In the first era years are counted backwards starting with 1. This is the case for the Julian calendar, and for the coptic, roc, and gregory (IMHO iso8601 should not work that way).

Yep, this is essentially what I'm recommending: each calendar defines one era whose 1 (or maybe 0 in some calendars) represents the epoch of the signed year field in Temporal. For most calendars, the "anchor era" (or "origin era" or "epoch era" or "default era" or "home era" or any other term that's best) will be obvious and will be the era that we're currently in right now. Among the ICU calendars there are only two exceptions where the anchor era involves a judgement call.

For Ethiopic, both the "start of creation" era and the modern era seem to have pros and cons as the anchor era. Given that there's an ethioaa calendar that only refers to the "start of the world" era, it seems reasonable to make the assumption that the anchor era in ethiopic should be the current era. So I assume the better choice would be the era used most, which is probably the modern era. But If there's already a convention used among scholars then that one should win. Regardless, choosing one of those two seems reasonable and I don't expect either choice would cause significant problems for developers.

For Japanese, the choice seems more ambiguous, especially because (unlike Ethiopic) there's going to be another Japanese era within a few more decades. If there's already a logical anchor era that scholars use, then we can use that one, but it seems reasonable to just pick the current (Reiwa) era, or perhaps Shōwa which was active at the UNIX epoch date? Regardless, if only the Japanese calendar has this ambiguity, I don't think that one case is enough to avoid offering a signed year field on all calendars.

Another interesting aspect of Japanese eras is that they don't start on year boundaries. For example: the current Reiwa era began on 2019-05-01.

IMHO the "signed year" or "algebraic year", defined by the calendar's author, would fulfil the needs.

Agreed. What do you think about my proposal above to use Temporal's year field for the signed year, and to offer a separate convenience property (e.g. eraYear or yearInEra) for the era-specific year? My assumption that this would be better than the reverse because in-era years are both more bug-prone and are mostly used for formatting and UI which is not as central to Temporal use cases as calculations are. Either one could be used when setting the year in with and from.

@Louis-Aime
Copy link

Hello @justingrant , I think we have the same point of view about the issues. Here is what I belief.

Temporal documentation should clearly state to calendar's author whether the era, year, month and day they use should be "computations-oriented" or "display-oriented".

The first option means clearly: year is a signed integer with no gap. It also probably means the same for month. In this model, a bissed or intercalary month has its own number, and subsequent months are represented with numbers that are shifted with respect to a common year. Nisan is 8 instead of 7 etc. Here transcalendar code developpers can write year++ instead of date.Add ({ year : 1}).

In the second option, year must be associated with era. And I would say: month must be associated with monthType, in most if not all luni-solar calendars.

Personally, I am in favour of option 2, because one expectation of the calendar's user is to easily convert from ISO 8601 to any calendar and the reverse, and to get the results without using DateTimeFormat.
But as you suggest I would add these features:

  1. Each calendar provides a signedYear and an orderedMonth, or whatever you call them. These are the "computations-oriented" versions of year and month.
  2. Calendar's author should design dateFromFields() in such a way that year without era is deemed signedYear, and month without monthType means orderedMonth. BTW this is the way I designed the WesternCalendar class' dateFromField method in my Temporal sand box. You specify the real calendar of most European places - except for the French revolution period and for Sweden in 1700-1712 - by instantiating the class with the date of switching to Gregorian. You specify Julian dates as being in era "Ancient Style" whereas Gregorian dates will be "New Style". But if you call dateFromFields() without specifying any era, the method will do its best to compute the right date.

Morevover, as a calendar's author, I would be glad to add other characteristics. I personally added "week characteristics" like yearOfWeek to tell the "week-year" a date belongs to when at the very end or the very beginning of a year. I would also add epact and goldenNumber, which are very useful for computing Easter in Julian or Gregorian calendars. But in this case, there should be a list of available properties for this calendar, given with the fields method, or with fullFields.

In short I am in favour of having year, era, month and monthType reflect the way years and months are expressed in the target calendar; but I still would like to have a "computable" signedYear and orderedMonth that I can use for trans-calendrical routines.

@justingrant
Copy link
Collaborator Author

justingrant commented Dec 16, 2020

Temporal documentation should clearly state to calendar's author whether the era, year, month and day they use should be "computations-oriented" or "display-oriented".

This is very well stated! I agree.

The first option means clearly: year is a signed integer with no gap. It also probably means the same for month. In this model, a bissed or intercalary month has its own number, and subsequent months are represented with numbers that are shifted with respect to a common year. Nisan is 8 instead of 7 etc. Here transcalendar code developpers can write year++ instead of date.Add ({ year : 1}).

Agreed also.

In the second option, year must be associated with era. And I would say: month must be associated with monthType, in most if not all luni-solar calendars.

For the "display-oriented" option, there is an existing standard RFC 7529 that defines a simplified combination of month and type by using a string that's either an integer (normal month) or an integer with an "L" suffix (e.g. "5L" for Adar I, or "7L" for a leap month added after the 7th chinese month) to denote an unusual month. We may want to add a leading zero for better comparability if the standard is OK with it. Anyway, I'd be inclined to follow this standard in Temporal too for a monthCode property. What do you think?

BTW I'm OK with having monthType and relatedMonth convenience properties as well, which could be used in with or from as a pair in place of month or monthCode which could also be used.

Personally, I am in favour of option 2, because one expectation of the calendar's user is to easily convert from ISO 8601 to any calendar and the reverse, and to get the results without using DateTimeFormat.

AFAIK, either option could enable this use case. Here's how I was assuming this case could work with option 1. What's missing?

// ISO => Calendar
const isoDate = Temporal.PlainDate.from('2022-05-01[c=hebrew]');
const {
  year,          // 5782
  month,         // 6 (ordinal index of Adar I in this year)
  monthCode,     // "5L"
  monthType,     // "leap"
  regularMonth,  // 6 ("normal month" that this month is related to)
  day            // 29
} = calendarDate;

// Calendar => ISO (the developer has several choices depending on which data they have)
date = Temporal.PlainDate.from({ year: 5782, month: 6, day: 29 });
date = Temporal.PlainDate.from({ year: 5782, monthCode: '5L', day: 29 });
date = Temporal.PlainDate.from({ year: 5782, regularMonth: 6, monthType: 'leap', day: 29 });

BTW, here's the reasons why I prefer Option 1:

  • The main reason Temporal exists is to support business-logic calculations that today require costly userland libraries. So it makes sense to optimize Temporal APIs for calculation use cases, as long as display cases are also supported.
  • The vast majority of developers worldwide (which includes developers working with solar or lunar non-ISO calendars!) won't proactively write code to correctly handle BC years or lunisolar calendars. If the most possible Temporal code can "just work" with all built-in calendars and all dates, the ecosystem will function better for everyone, including lunisolar-using/BC-using developers who want to use popular userland libraries that weren't developed with lunisolar/BC cases in mind.
  • Because the vast majority of developers worldwide won't be using lunisolar calendars, it'd be bad to impose a significant ergonomics/learning tax on them if we don't need to. It's better for the ecosystem to impose that tax on developers who actually intend to use lunisolar calendars or BC years. Those developers are incentivized to learn how to properly use Temporal in their preferred calendar and use cases. If the tradeoff for that extra work is that more userland libraries "just work" for their use case, then IMHO most developers would be OK with that trade.

Note that the last two point above may be in tension. For example, we could choose to use an opaque string value for month and year in all calendars. This would ensure that developers using all calendars would be forced to do extra work to support a string value in a place where most APIs only use numbers. This could help with the second bullet above (more code would probably "just work" for lunisolar/BC) but would hurt the third bullet (don't make the API worse for the vast majority). IMHO, Option 1 is the sweet spot where as much code as possible "just works" while imposing no tax on developers who don't target BC-like eras and/or lunisolar calendars.

@justingrant
Copy link
Collaborator Author

justingrant commented Dec 17, 2020

For Japanese, the choice seems more ambiguous, especially because (unlike Ethiopic) there's going to be another Japanese era within a few more decades. If there's already a logical anchor era that scholars use, then we can use that one, but it seems reasonable to just pick the current (Reiwa) era, or perhaps Shōwa which was active at the UNIX epoch date? Regardless, if only the Japanese calendar has this ambiguity, I don't think that one case is enough to avoid offering a signed year field on all calendars.

Another idea for Japanese: January 1 of the Gregorian year 1 could be used as the "anchor epoch". In other words, the signed year for Japanese would always match the Gregorian year. Given that Japan already uses the Gregorian calendar for months and days, this might be a logical choice to avoid having to pick a particular era as the zero-date. Given that the signed year is never going to satisfy all Japanese developers, then at least picking a familiar epoch seems reasonable and would probably make coding easier with that calendar.

@Louis-Aime
Copy link

I feel like we have discussed most arguments. Now the Temporal design team can take reasonable decisions.

I think everyone should stick to existing standards like RFC 7529 unless there are solid arguments for not doing so.

Personally, I am in favour of option 2, because one expectation of the calendar's user is to easily convert from ISO 8601 to any calendar and the reverse, and to get the results without using DateTimeFormat.

AFAIK, either option could enable this use case. Here's how I was assuming this case could work with option 1. What's missing?

This works indeed. The small difference I suggested is that developers and users may be "ambiguous", that is, they could use the same word, e.g. years for slightly different data. This works today, and I find it nice to make both historians and astronomers happy:

dauc = Temporal.PlainDate.from ( {calendar: julian, era: 'bc', year: 753, month: 4, day: 21});
dauc1 = Temporal.PlainDate.from ({calendar: julian, year: -752, month:4, day:21});    
dauc.equals(dauc1) //> true

This works also:
dauc.year //> 753
But users (and trans-calendar developers) would also like to have something like:
dauc.signedYear //> -752
No problem if Temporal calls it a different way.

@Louis-Aime
Copy link

About the Japanese calendar:

Another idea for Japanese: January 1 of the Gregorian year 1 could be used as the "anchor epoch". In other words, the signed year for Japanese would always match the Gregorian year. Given that Japan already uses the Gregorian calendar for months and days, this might be a logical choice to avoid having to pick a particular era as the zero-date. Given that the signed year is never going to satisfy all Japanese developers, then at least picking a familiar epoch seems reasonable and would probably make coding easier with that calendar.

I have two remarks:

  1. The japanese calendar uses the Julian calendar, not the Gregorian one, for dates prior to ISO 1582-10-15:
    new Intl.DateTimeFormat('en-US-u-ca-japanese',{era: "short", year:"numeric", month:"short", day:"numeric"}).format(new Date('1582-10-14')) //> "Oct 4, 10 Tenshō (1573–1592)" .
    (As I wrote in my blog, we, the European countries, have a lesser service than Japan, because we can't express historical dates before 1582-10-15, whereas Japanese can... But this another story, with Temporal we shall be able to do that).

  2. For the "anchor epoch", I would rather recommend Julian 1/1/645, which is ISO 645-01-04. It seems to be a suitable origin for the "era name" system (gengō) that the imperial Japanese calendar uses. Seireki refers to Anno Domini (the origin set by _Dionysius Exiguus) and is also well known in Japan (after Wikipedia ), but seems a different system. How would we represent year between 1 and 645 A.D. ? Today, the formatting API yields a signed year from the ISO 645-01-04 epoch, that sounds OK to me. I think the opinion of Japanese scholars would be welcome here.

@justingrant
Copy link
Collaborator Author

Meeting 2020-12-17: we don't have consensus on this one. Not yet at least! More discussion needed.

A quick note about invariants: if we made year into a signed year, then the following additional invariants (all of which hold for the ISO calendar) would hold for all calendars:

  • Temporal.PlainDate.from({year, month, day, calendar}) would be sufficient to initialize a date in any calendar. Multiple fields would never be required to initialize a date. This would enable simpler data structures and logic for trans-calendar apps. (Note: this invariant assumes that month also supports one-field initialization-- see Replace isLeapMonth with monthCode #1203.)
  • When you initialize a date with a particular numeric year, the year of the resulting date is always the same as the input. Currently: Temporal.PlainDate.from({year: -100, month: 1, day: 1, calendar: 'gregory'}).year === 101.
  • The next year after year X is always X+1, and the previous year is always X-1. This ensures that naive comparisons to the next/previous year will always work, and avoids infinite loops that are possible if developers make date.year > old.year the stop condition of the loop.

I'd expect that the biggest benefit would be lowering the bar for developers to write trans-calendar code, even if they don't intend to write trans-calendar code. Having more trans-calendar code will improve the reliability of JS apps for end-users who prefer non-ISO calendars. It'd also ensure that developers who who are using era-dependent calendars will have a wider range of 3rd-party libraries that they can successfully use.

In the meeting, we also discussed downsides of making this change:

  • The complexity of an additional eraYear property.
  • If developers are (incorrectly) directly showing Temporal property values to users instead of going through toLocaleString or similar Intl APIs, then this change would break those apps for far-past dates or for calendars like japanese or roc where era changes happened relatively recently. These breaks would likely be cosmetic issues, while the problems resolved by the invariants list above are more likely to be business logic problems.

@sffc
Copy link
Collaborator

sffc commented Dec 17, 2020

A Japanese colleague of mine suggested that if we need to set an epoch for the Japanese calendar, Meiji 1 is a good choice (Gregorian year 1868), since it is the first era in Modern Japan.

To me, having an algebraic year (a monotonically increasing integer with both positive and negative values, rooted at an epoch) seems convenient to supplement the calendar year (a year number in the current era), although it wasn't clear if we have consensus on that point. If we did decide to have both an algebraic year and a calendar year, it's not clear which one should be used as the "default": we could have .year return calendar year and .yearsFromEpoch return the signed year, or .year could return the signed year and .yearOfEra could return the calendar year.

Also, here's a little table comparing this discussion with #1203:

Property Type Arithmetic Property Calendar Property
Year .year or .yearsFromEpoch .year or .yearOfEra
Month .month or .monthNumber .month or .monthCode

We should think about being consistent with regard to whether we make the arithmetic property or the calendar property the "default" for both years and months.

@justingrant
Copy link
Collaborator Author

We should think about being consistent with regard to whether we make the arithmetic property or the calendar property the "default" for both years and months.

Agreed.

If we did decide to have both an algebraic year and a calendar year, it's not clear which one should be used as the "default": we could have .year return calendar year and .yearsFromEpoch return the signed year, or .year could return the signed year and .yearOfEra could return the calendar year.

For developers who intend to write trans-calendar code, IMHO either option would be fine, because those developers already have to think about eras in their app, already have to pair era with another property, etc.

But for developers who don't intend to write trans-calendar code (or who don't even know about other calendars) making the signed year the default would seem to make it much more likely that their code will work across calendars because more ISO business logic can be used as-is. As noted above, this isn't just for apps, it's for libraries too. If it's easier to write trans-calendar code in libraries, then more libraries will work trans-calendar and developers who are working in an era-dependent calendar will have more libraries they can use.

`` .year or `.yearOfEra`

I'd suggest eraYear (or any short name that starts with era) because to use that property it must be paired with era, so having it next to era in the docs and IDE autocomplete will help with discoverability. Conversely, having the property name be yearXxx could add confusion about which is used for which case.

@sffc
Copy link
Collaborator

sffc commented Dec 17, 2020

So, pending further research (and I do expect further research to be coming), I see three choices emerging:

  1. Calendar-specified .year, .era, and .month (iCal code), paired with algebraic .yearsSinceEpoch and .monthNumber
  2. Calendar-specified .year, .era, .month (numeric), and .monthType, paired with the algebraic properties above
  3. Algebraic .year and .month, paired with calendar-specific .eraYear, .era, and .monthCode

In either case, all calendars should accept all properties. For simplicity, we should consider defining an era named "iso8601" to be the only era in the ISO calendar, which can be filled in automatically if not present. For example:

// Option 1 equivalents (with iCal month):
Temporal.Date.from({ era: "iso8601", year: 2020, month: "01", day: 1 });
Temporal.Date.from({ year: 2020, month: "01", day: 1 });  // era optional in single-era calendars
Temporal.Date.from({ year: 2020, month: 1, day: 1 });  // month can be coerced to a string
Temporal.Date.from({ year: 2020, monthNumber: 1, day: 1 });
Temporal.Date.from({ yearsSinceEpoch: 2020, monthNumber: 1, day: 1 });

// Option 2 equivalents (with monthType):
Temporal.Date.from({ era: "iso8601", year: 2020, month: 1, monthType: "standard", day: 1 });
Temporal.Date.from({ year: 2020, month: 1, monthType: "standard", day: 1 });  // calendar makes era optional
Temporal.Date.from({ year: 2020, month: 1, day: 1 });  // calendar makes monthType optional
Temporal.Date.from({ year: 2020, monthNumber: 1, day: 1 });
Temporal.Date.from({ yearsSinceEpoch: 2020, monthNumber: 1, day: 1 });

// Option 3 equivalents (with monthCode):
Temporal.Date.from({ year: 2020, month: 1, day: 1 });
Temporal.Date.from({ year: 2020, monthCode: "01", day: 1 });
Temporal.Date.from({ eraYear: 2020, monthCode: "01", day: 1 });
Temporal.Date.from({ era: "iso8601", eraYear: 2020, monthCode: "01", day: 1 });

I won't yet put a stake in the ground on which option I prefer, except to say that I think we should choose the one that makes it easiest to write trans-calendar code by default.

A separate but related question is how we want to deal with the data .getFields() returns. Options:

  1. .getFields() always returns algebraic year/month
  2. .getFields() always returns calendrical era/year/month[/monthType]
  3. .getFields() picks fields based on what the calendar requests (current behavior)

@justingrant
Copy link
Collaborator Author

justingrant commented Dec 18, 2020

These code samples are great! Thanks @sffc for the comparison.

I think we should choose the one that makes it easiest to write trans-calendar code by default.

I agree, with a caveat: "we should choose the one that makes it easiest to write trans-calendar code by default, as long as it doesn't make it harder to write single-calendar and (especially) ISO-calendar code." Would you be OK with that caveat?

For example, we could choose to make month always a string code, which would make it easier to write trans-calendar code, but at the cost of making it harder to write code for the vast majority of use cases that won't use lunisolar calendars. There's a tradeoff. I'd be OK with making single-calendar code a little harder (e.g. exposing new properties, which as discussed in the meeting might make the API overall harder to understand) but I'd be skeptical about significantly reducing ergonomics of the current ISO API.

A separate but related question is how we want to deal with the data .getFields() returns.

I'd apply the same goal here too: whichever makes it easiest to write trans-calendar code. IMHO, this means that fields have the same (or as similar as possible) names, number, and types across calendars. The current model where some calendars add fields and some don't seems sup-optimal for cross-calendar compatibility.

Also, ideally getFields would avoid field inter-field overlap and dependencies. You should be able to call getFields and make a change to one resulting field without worrying that your change will conflict with another field and/or without having to make a parallel change to 2+ fields (year and eraYear).

Taking these two criteria together, I think it means that the ideal getFields() solution would be that there'd be one primitive-typed year field and one primitive-typed month field, and those two fields (along with day and calendar) would be able to object-serialize and object-initialize any date in any calendar.

Temporal.Date.from({ eraYear: 2020, monthCode: "01", day: 1 });

It may be reasonable to require that era is supplied whenever eraYear is, and vice versa. This might make it really clear that the two are linked, and to push developers looking for a single-field solution to use the field that's appropriate for one-field use cases. Accepting eraYear without era also brings in the complexity of what the default era is, which it'd be good to avoid for Japanese.

@sffc
Copy link
Collaborator

sffc commented Dec 18, 2020

I agree, with a caveat: "we should choose the one that makes it easiest to write trans-calendar code by default, as long as it doesn't make it much harder to write single-calendar and (especially) ISO-calendar code." Would you be OK with that caveat?

I prefer it without the caveat, and there lies the source of many of our polite disagreements. 😉

I'd apply the same goal here too: whichever makes it easiest to write trans-calendar code. IMHO, this means that fields have the same (or as similar as possible) names, number, and types across calendars. The current model where some calendars add fields and some don't seems sup-optimal for cross-calendar compatibility.

Possibly. We should evaluate if we can maintain that invariant while being general enough to support all built-in and userland calendars.

Also, ideally getFields would avoid field inter-field overlap and dependencies. You should be able to call getFields and make a change to one resulting field without worrying that your change will conflict with another field and/or without having to make a parallel change to 2+ fields (year and eraYear).

Not sure about how much value to put here. People are not intended to generally be calling .getFields() and modifying the results; they should use proper arithmetic and type conversion methods.

It may be reasonable to require that era is supplied whenever eraYear is, and vice versa. This might make it really clear that the two are linked, and to push developers looking for a single-field solution to use the field that's appropriate for one-field use cases. Accepting eraYear without era also brings in the complexity of what the default era is, which it'd be good to avoid for Japanese.

That sounds like a reasonable change within the context of option 3.

@Louis-Aime
Copy link

As a "user" of Temporal for defining calendars, I definitely think that:

  • year should be the name for the calendar property, i.e. it requires era
  • epochYear (or yearFromEpoch or whatever similar) should be required from the calendar's author:

a monotonically increasing integer with both positive and negative values, rooted at an epoch.

  • epoch should also be provided: a date (iso8601) that corresponds to the first day of epochYear 0.
  • epochWeekYear should also be provided: like epochYear, but applied to the "week year" that is implicitly expressed with weekOfDay and weekOfYear. epochWeekYear may differ by +/- 1 from epochYear, for the first or the last days of the year, e.g. the last days of 2019 and the first days of 2021 all belong to epochWeekYear 2020.

If these properties are required, I would also specify that in any Temporal expression like Temporal.PlainDate.from() where year is entered without era, the year value shall be analysed as an epochYear value.

Observe that with epochYear, daysOfYear, daysInyear, and also with epochWeekYear, weekOfYear, dayOfWeek, daysInWeek developers can do comfortable and safe computations.

I would also prohibe any "wild calculations" using .month property or assuming that it is a numeric property. I would even define month as being of type any, in order to be sure that no perverse or dumb developer will dare to develop "trans-calendar code" that use months. Please consider that the only invariant abstract object for all calendars is the day, with its integer Julian Day number that is much easier to read as an ISO 8601 string. Years always differ in the more or less long term: 33 years for lunar calendars with respect to solar ones, a few millenia between Julian/Coptic/Ethiopic with respect to Gregorian, etc. As for months, 36 months in Gregorian is 37 in luni-solar and lunar, 39 with coptic (if epagomenal days build up a month), etc. Please show me a serious business case (except for astrology) where you can make computations on dates that way.

@sffc sffc added the calendar Part of the effort for Temporal Calendar API label Jan 7, 2021
@ptomato ptomato added the v2? Candidate for being moved to a Temporal v2 proposal label Jan 15, 2021
@ptomato
Copy link
Collaborator

ptomato commented Jan 15, 2021

I'm not convinced that this meets the high bar of adding new API at this point in the proposal, especially after our discussion today. I would very strongly prefer to defer this to a Temporal v2.

@justingrant
Copy link
Collaborator Author

Is this a new API? Or is it only guidance for calendar authors, including authors of built-in calendars? e.g.:

  • year should be signed and relative to the epoch of an "anchor era"
  • calendars should add era and eraYear getters to the prototypes of Temporal types with year getters

The value of making year epoch-relative is that developers can build for ISO and end up with cross-calendar code. Without signed, epoch-relative years it's pretty much guaranteed that most ISO code won't work for the Japanese calendar or any other calendar where era is required by from.

I'm also concerned about the security implications of year that counts backwards in eras like BC. Code can be tricked into thinking that time is moving backwards, which might unlock unexpected codepaths or cause infinite loops. These seem like avoidable problems if era-relative years are opt-in rather than re-purposing the same field used for the signed ISO year.

The reverse proposal--where year still requires era and we add a separate property for epoch-relative years--IMHO seems like dubious value (now or in V2), because so few developers would actually use it.

@Manishearth
Copy link

Is this a new API? Or is it only guidance for calendar authors, including authors of built-in calendars? e.g.:

  • year should be signed and relative to the epoch of an "anchor era"
  • calendars should add era and eraYear getters to the prototypes of Temporal types with year getters

I like this proposal. I do feel like flipping it to be epochYear (signed) and year (unsigned) might be better, but not strongly.

I will point out that we already have isoYear. year as proposed here is not the same thing, but it is in the common Gregorian/ISO case. So I'm wondering if in the common case we can point people towards isoYear for this

Then again, the whole point of this is for people who may have missed something in the docs, so we can't guarantee they'll use isoYear for this 😄

@justingrant
Copy link
Collaborator Author

For context, the reasons for the proposal in this issue are:

  • Make it more likely that Temporal-using code will support all ICU calendars by making non-ISO calendars act more like ISO. Specifically:
    • Only year is required to set (in from) or change (with with) the year in any calendar. Without this proposal, some calendars like Japanese will require both year and era which almost certainly means that few developers' code will support the Japanese calendar.
    • Ensure that year will not change from the value set in from or with. Without this proposal, code like date.with({year: -10}).year === -10 will fail for backwards-counting eras like BC.
    • date1.year === date2.year always works for dates in the same year. Without this proposal, you also need to compare era or any other field that the calendar may choose to use to modify the year.
    • year monotonically increases as time marches forward. This means you can compare (e.g. date.year > 2000) and increment (e.g. (date.with({year: date.year + 1})) dates in different years using simple primitive operators. And you can easily loop through a large number of years in the same way. Also, without this proposal it's easy to get into an infinite loop if you assume that the year after n is n+1, e.g. when displaying a list of 10 years in a UI.
  • Reduce the potential security issues associated with attackers introducing non-ISO calendars into apps that don't expect them. Specifically:
    • Prevent DoS via infinite loops as noted above
    • Prevent code from being tricked into assuming that the supplied date is later or earlier than it really is by providing an unexpected era.
      This proposal doesn't solve all problems with making ISO code work with non-ISO calendars, but it does lower the bar. This should make it incrementally more likely that more apps and libraries will work (or will require fewer changes) to be handle non-ISO data.

With those goals in mind:

I like this proposal. I do feel like flipping it to be epochYear (signed) and year (unsigned) might be better, but not strongly.

AFAICT, none of the goals above are satisfied unless year is the signed value, because almost all code will be written with ISO in mind so will use year. The main benefit of this proposal is to enable code for ISO years to work as-is for non-ISO calendars without the developer doing extra work.

If we made a non-year property for the signed year, I don't expect it'd get much use because it's not needed by ISO. Therefore, I don't think that the signed year is particularly valuable unless it is exposed by year.

I will point out that we already have isoYear. year as proposed here is not the same thing, but it is in the common Gregorian/ISO case. So I'm wondering if in the common case we can point people towards isoYear for this

Nope, because isoYear differs from the signed calendar year at all in most calendars. If the goal is to enable cross-calendar code, then isoYear helps for Gregorian but probably not for any other calendar. Also, it's hard to discover and requires two method calls: date.getISOFields().isoYear.

BTW, because of the behavior of the BC era, if this proposal isn't adopted then I'd hope that usage of gregory is minimal. Certainly in my own code I'd be very reluctant to use any external component that supplied a gregory date, for exactly the reasons noted above. Especially that I don't want to open space for malicious actors to supply a BC date.

@ptomato
Copy link
Collaborator

ptomato commented Jan 19, 2021

Is this a new API? Or is it only guidance for calendar authors, including authors of built-in calendars?

If it's the case that this is not new API, and it's only guidance for calendar authors, then can we agree to keep the status quo in the polyfill, and close this discussion here, moving it to the https://github.com/tc39/ecma402 repo? I'm very concerned that continuing to discuss on speculative tickets about what the meaning of the year property is, is sending the wrong message to TC39 delegates about the readiness of the proposal for review. We already saw that the editors didn't review it during the review period because the issue tracker looked too busy. If it's in fact the case that this discussion has no bearing on the further advancement of Temporal to Stage 3, then I'd rather not continue it here.

If it is the case that it's new API, because we change the meaning of the year property, then I'm strongly of the opinion that this doesn't meet the high bar for this, and it should be deferred to a follow up proposal.

@ptomato
Copy link
Collaborator

ptomato commented Jan 19, 2021

Consensus from 2021-01-19 meeting:

  • year is the algorithmic (signed) year, in order to make it easy to write code using the ISO calendar that "just works" with non-ISO calendars.
  • eraYear is the numeric year within the era.
  • era is a string identifer for the era.

@ptomato ptomato added documentation Additions to documentation non-prod-polyfill THIS POLYFILL IS NOT FOR PRODUCTION USE! spec-text Specification text involved and removed v2? Candidate for being moved to a Temporal v2 proposal labels Jan 19, 2021
@cjtenny
Copy link
Collaborator

cjtenny commented Jan 26, 2021

The state of this after #1319 is that:

  • year is the algorithmic, signed year as discussed. Its meaning is still calendar-dependent, but it must be an integer. Calendars must return some algorithmic value here, to be used for arithmetic.
  • era and eraYear are optional, though a calendar may choose to require them on input and reject only year. One will not be provided to a calendar without the other. When present, it is enforced that era be a string and eraYear be an integer. eraYear can be negative, for example when referring to dates before the first era.
  • The ISO 8601 calendar does not implement era and eraYear.
  • Update validation, coercion for all built-in fields. #1319 does not address display, nor should it.

@cjtenny
Copy link
Collaborator

cjtenny commented Jan 26, 2021

The slight changes to the polyfill implementation of the Japanese calendar in #1319 use the ISO year as the signed year; I'm wary of this because it seems culturally insensitive at best. For more context, #526. However, since this is only used on output and rejected on input, it is less bad than it could be, and importantly it's not in the spec - just the polyfill.

An improvement would be to select a culturally relevant, non-Gregorian anchor value for the signed year, and to fix the Japanese calendar implementation in the polyfill to recognize inter-ISO-year era+eraYear transitions, but that is also beyond the scope of #1319.

@justingrant
Copy link
Collaborator Author

rejected on input

We should not reject year on input for any built-in calendar. One of the main reasons for year is to enable code to be written that works across all built-in calendars without special cases. If from/with methods using year sometimes throw depending on the calendar, then cross-calendar code won't work.

I'm wary of this because it seems culturally insensitive at best.

Take a look at #526 (comment) which are notes from a Japanese colleague of @sffc about the year choice.

@cjtenny
Copy link
Collaborator

cjtenny commented Jan 26, 2021

Got it. The changes just merged do not specify that anywhere, so let's follow that up in #526 and fix the polyfill for that issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
calendar Part of the effort for Temporal Calendar API documentation Additions to documentation non-prod-polyfill THIS POLYFILL IS NOT FOR PRODUCTION USE! spec-text Specification text involved
Projects
None yet
Development

Successfully merging a pull request may close this issue.

6 participants