Supporting Calendar Systems in Temporal

ECMAScript Temporal is the new date and time API for ECMAScript. Because ECMAScript is designed for global audiences, high-quality internationalization is a design goal.

This document discusses why and how calendar systems are included in the Temporal API, explains problems we expect developers to encounter with calendars, and outlines how to avoid those problems. A final section explains alternative designs for using calendar systems in Temporal, along with an explanation of why the current approach was chosen instead.

Background and Motivation

Calendar systems provide a way to assign human-readable numbers, such as year, month, and day, to a particular period of time.

Much of the world uses the Gregorian calendar system, which uses solar years, months, and days: over time, it remains synchronized with the cycle of the seasons. However, there are other calendar systems used throughout much of the world. Some popular calendar systems include Hebrew, Islamic, Buddhist, Hindu, Chinese, Japanese, and Ethiopic.

Today's date is March 4, 2021 in the Gregorian calendar. Here are several other representations of that date:

Non-Gregorian calendar systems are used in day-to-day life in several countries, particularly those with weaker cultural and economic ties to Gregorian-dominant Western countries, as well as in religious, cultural, and academic applications. For example, several countries including Japan, China, Iran, Afghanistan, Saudi Arabia, and Taiwan use non-Gregorian calendars for at least some official government purposes. Beyond those official uses, a majority of the world's population lives in countries that use non-Gregorian calendars to determine the dates of religious or cultural holidays.

Calendars in Business Logic (Not Only String Localization)

Calendar systems play an important role in localization (l10n). They enable a human-friendly display representation of timestamps, dates, and times.

However, calendar systems also play an important role in the business logic of an application. Applications involving dates intended for human consumption must consider the calendar system when performing arithmetic or any other business-logic operation that uses months and years. Examples of calendar-sensitive operations include:

In other words, units of time greater than a solar day are inherently dependent on the calendar system. The only universal solution to date arithmetic would be to omit the concept of months and years, but a date API without months and years would be insufficient for many use cases.

Why Are Calendars a Higher Priority for ECMAScript vs. Other Platforms?

Problems integrating calendar systems into Temporal are obvious, especially the "Unexpected Calendar Problem" (see below), where code breaks when presented with a calendar it wasn’t designed to handle. What's less clear are the benefits of integrating calendars in Temporal that can outweigh those problems. One way to understand these benefits is to highlight differences between the needs of the ECMAScript ecosystem compared to other platforms like Java and .NET which have taken a different approach to handling calendars. Some of these differences include:

Choosing the current design was not easy, because it will cause more bugs in ISO-only software than other possible solutions. But, given global trends in economic growth, population growth, and tech adoption, the consensus of this proposal's champions is that the current design is a reasonable tradeoff for long-term success in an increasingly global ECMAScript developer community.

Intl-First Design

ECMAScript supports internationalization (i18n) as a primary feature. The i18n subcommittee, TC39-TG2, evangelizes "Intl-first design." Two principles of Intl-first design are (1) language- and region-dependent operations should be data-driven, and (2) user preferences should be an explicit input to APIs. These principles help ensure that APIs work well for developers targeting a wide range of end-users around the world.

Temporal applies these design principles by abstracting calendar-specific logic and by making the calendar an explicit choice when constructing objects. The result is an API that supports use cases like the ones listed above for regions that use only Gregorian and for those that have other calendar systems in common use.

Using Calendars in Temporal Code

The following section shows how calendar systems are exposed in Temporal code. For a more thorough discussion, see the documentation.

In Temporal, every object that includes date fields also has a calendar field. For example, both of the following lines result in a Temporal.PlainDate instance that represents 23 Adar I 5779 in the Hebrew calendar:

// Create a Hebrew date from a bag of fields
Temporal.PlainDate.from({
  year: 5779,
  monthCode: 'M05L',
  day: 23,
  calendar: 'hebrew'
});

// Create an ISO date and convert it to Hebrew
Temporal.PlainDate.from('2019-02-28').withCalendar('hebrew');

Internally, the day is always represented in the ISO calendar, meaning that if the calendar annotation gets dropped, the day remains the same. This is true for both the internal slots of Temporal types and the string representation. Thus, the following two lines also result in the same 23 Adar I 5779 instance:

// Parse from an annotated ISO string (IETF RFC pending)
Temporal.PlainDate.from('2019-02-28[u-ca=hebrew]');

// Feed the internal data model via the constructor
new Temporal.PlainDate(2019, 2, 28, 'hebrew');

Operations on Temporal instances interpret all month, day, and year values in context of the calendar system. For example:

date = Temporal.PlainDate.from('2019-02-28[u-ca=hebrew]');
date.with({ day: 1 }); // => 2019-02-06[u-ca=hebrew]
date.with({ day: 1 }).toLocaleString('en-US', { calendar: 'hebrew' }); // => '1 Adar I 5779'
date.year; // => 5779
date.monthCode; // => 'M05L'
date.month; // => 6
date.day; // => 23
date.inLeapYear; // => true
date.calendar.id; // => 'hebrew'
inFourMonths = date.add({ months: 4 });
inFourMonths.toLocaleString('en-US', { calendar: 'hebrew' }); // => '23 Sivan 5779'
inFourMonths.withCalendar('iso8601'); // => 2019-06-26
date.until(inFourMonths, { largestUnit: 'month' }); // => P4M

By definition, creating a bag of fields with year, month, and day requires choosing a calendar system. We elide the ISO calendar if no other calendar system is specified in this situation, and it is the only place Temporal performs calendar elision:

Temporal.PlainDate.from({ year: 2019, month: 2, day: 28 });

The following Temporal types all carry a calendar: Temporal.ZonedDateTime, Temporal.PlainDateTime, Temporal.PlainDate, Temporal.PlainYearMonth, Temporal.PlainMonthDay

The Unexpected Calendar Problem

Integrating calendar awareness into many Temporal operations is essential for a global audience, but it introduces a new problem: if code is written to assume one calendar system, bugs may result if a different calendar system is introduced instead. Introducing unexpected calendars could also be a vector for malicious activity.

This section describes this problem, which we call the "Unexpected Calendar Problem," and how to mitigate it in userland Temporal code.

Problem Definition

Below is a canonical example of this problem: a function which tests whether a particular date is the last day of the year. It assumes a calendar with 12 months and whose last month is 31 days long.

function isLastDayOfYear(date) {
  // Note: This is a BAD example!
  return date.month === 12 && date.day === 31;
}

// works for ISO calendar
isLastDayOfYear(Temporal.PlainDate.from('2019-12-31')); // => true
isLastDayOfYear(Temporal.PlainDate.from('2020-01-01')); // => false

// fails for some non-ISO calendars
hebrewNewYearsEve = Temporal.PlainDate.from({
  year: 5780,
  monthCode: 'M12',
  day: 29,
  calendar: 'hebrew'
});
isLastDayOfYear(hebrewNewYearsEve); // => false
// (desired: true)

Mitigating the Unexpected Calendar Problem

There are two ways to avoid the Unexpected Calendar Problem: the preferred solution of writing calendar-safe code, or a fallback option to validate or coerce inputs to a known calendar.

Writing Calendar-Safe Code

The best way to mitigate the Unexpected Calendar Problem is to write "calendar-safe" code that works for all built-in calendars. For example, the function above can easily be written to be calendar-safe by using the monthsInYear and daysInMonth properties instead of hard-coding ISO constants. Developers who follow documented best practices for writing calendar-safe Temporal code will be able to use the same code for all built-in calendars.

Beyond documentation, the Temporal API itself has been designed to make it easier to write calendar-safe code. For example:

Validating or Coercing Inputs

Even though Temporal makes it straightforward to write calendar-safe code, doing so still requires work from developers. Some developers won't do this work. Sometimes this will be because they don't know about or don't understand non-ISO calendars. In other cases, developers won't bother to follow best practices because they expect to only be dealing with ISO-calendar data. Even if a developer conscientiously follows best practices in their own code, most ECMAScript apps have many library dependencies that may not do so. Or they may have to interoperate with existing code that cannot be changed due to time constraints.

In these situations, the developer should ensure that inputs to that code are either (a) validated to ensure the input is in the expected calendar system, or (b) coerced to that system. Whether to validate or coerce depends on the use case.

To coerce:

date = date.withCalendar('iso8601');

To validate:

if (date.calendar.id !== 'iso8601') throw new Error('invalid calendar');

Note that "inputs" are not only external data. Inputs could include values returned from library dependencies, e.g. date pickers. Unless the library or input source guarantees a certain calendar system, defensive code should assume Temporal objects (or strings or objects that will be converted into a Temporal object) to be "external" and subject to validation or coercion to the ISO calendar if the code that will use that data is not calendar-safe.

Alternatives Considered

Integrating calendars into Temporal was one of the most challenging parts of designing the API. Below is a quick discussion about alternative calendar APIs we considered before adopting the current design. Note that none of the options considered (including the current Temporal design) are ideal; they all have known flaws that require workarounds for some cases. We believe that the current design is the best option overall, relative to the alternatives below.

Alternative 1: No Calendar in the Data Model

An alternative approach to the current Temporal design would have been to store calendar information separately from Temporal instances. This approach would have the advantage of avoiding the Unexpected Calendar Problem, but at significant ergonomic cost:

The Temporal champions believe that the ergonomic challenges of this alternative are worse than the work required to mitigate the Unexpected Calendar Problem.

Alternative 2: Separate ISO and Calendar Properties

It would be possible to retain the calendar in Temporal objects' data model without being vulnerable to the Unexpected Calendar Problem, but doing so would involve a significant increase in Temporal surface area. Below is one way it could be done.

After considering this and similar options, the champions decided that the additional complexity wasn't worth it. In particular, we were concerned that the dual-property approach would be more confusing and would result in more bugs than teaching users to validate or coerce inputs.

Alternative 3: Subclasses

Instead of having the calendar system be a slot of the PlainDate type, we considered keeping PlainDate as an ISO-only type, with subclasses for all other calendar systems, such as PlainHebrewDate, PlainChineseDate, and so on. Read more in the discussion on calendar subclassing.

We decided against the calendar subclassing approach because:

In the end, the champions decided that the subclassing approach brought a high cost with no clear benefit over the other approaches, and that keeping all calendar-sensitive operations neatly organized into the Temporal.Calendar object resulted in a cleaner model.

Alternative 4: Separate Types for Calendared Dates

This approach would mean introducing a new type that carries a date without a calendar system, to supplement the type with the calendar system. Most documentation would recommend the calendar-agnostic type except when calendar-aware logic is necessary. We discuss this approach in more detail as Option 5 in calendar-draft.md.

We decided against the dual classes approach because:

  1. If the new type were truly calendar-agnostic, it could not support month and year arithmetic, because doing so would introduce an ISO bias.
  2. If the new type had ISO-like behavior for months and years, then it is no different from PlainDate with an ISO calendar.

Note that (2) is true for ECMAScript because it is dynamically typed. Statically typed languages could enforce compile-time type checking for (2) that is not possible in ECMAScript. Note also that clients such as TypeScript can treat PlainDate as having a type parameter corresponding to the calendar system in order to enforce more compile-time type checking of calendar systems. Separate data types are not necessary for that validation.