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:
.add-ing one day until date.year > originalDate.year. For BC years, that's an infinite loop.So far, the best solution I can come up with is:
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.
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.
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 alsohebrew,persian, allislamicand alsoiso8601as 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 thecoptic,roc, andgregory(IMHOiso8601should 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.
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:
signedYear and an orderedMonth, or whatever you call them. These are the "computations-oriented" versions of year and month.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.
Temporal documentation should clearly state to calendar's author whether the
era,year,monthanddaythey use should be "computations-oriented" or "display-oriented".
This is very well stated! I agree.
The first option means clearly:
yearis a signed integer with no gap. It also probably means the same formonth. 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 writeyear++ insteadofdate.Add ({ year : 1}).
Agreed also.
In the second option,
yearmust be associated withera. And I would say:monthmust be associated withmonthType, 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:
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.
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.
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.
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:
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)".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 #1203.)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.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:
eraYear property.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.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.
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
.yearreturn calendar year and.yearsFromEpochreturn the signed year, or.yearcould return the signed year and.yearOfEracould 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.
`
.yearor.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.
So, pending further research (and I do expect further research to be coming), I see three choices emerging:
.year, .era, and .month (iCal code), paired with algebraic .yearsSinceEpoch and .monthNumber.year, .era, .month (numeric), and .monthType, paired with the algebraic properties above.year and .month, paired with calendar-specific .eraYear, .era, and .monthCodeIn 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:
.getFields() always returns algebraic year/month.getFields() always returns calendrical era/year/month[/monthType].getFields() picks fields based on what the calendar requests (current behavior)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.
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. :wink:
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
getFieldswould avoid field inter-field overlap and dependencies. You should be able to callgetFieldsand 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 (yearanderaYear).
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
erais supplied whenevereraYearis, 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. AcceptingeraYearwithouteraalso 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.
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.
epochshould also be provided: a date (iso8601) that corresponds to the first day of epochYear 0.epochWeekYearshould also be provided: likeepochYear, but applied to the "week year" that is implicitly expressed withweekOfDayandweekOfYear.epochWeekYearmay differ by +/- 1 fromepochYear, 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.
Most helpful comment
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
.yearreturn calendar year and.yearsFromEpochreturn the signed year, or.yearcould return the signed year and.yearOfEracould return the calendar year.Also, here's a little table comparing this discussion with #1203:
| Property Type | Arithmetic Property | Calendar Property |
|---|---|---|
| Year |
.yearor.yearsFromEpoch|.yearor.yearOfEra|| Month |
.monthor.monthNumber|.monthor.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.