Code Monkey home page Code Monkey logo

proposal-temporal-v2's People

Contributors

ptomato avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Forkers

liviamedeiros

proposal-temporal-v2's Issues

Support for Lunar Calendars Starting New Days at Sunset Instead of Midnight

Lunar Calendars, like the Islamic calendar, begin their new days at sunset rather than midnight. To fully support these calendars, Temporal needs to implement astronomical calculations to determine sunset times, which can vary by location.

Additionally, Temporal should expose astronomical calculation functions and variables, such as sunsetTime and sunriseTime, or any other relevant APIs.

This feature was previously discussed in Temporal V1 and was suggested for inclusion in V2.

Advantages:

Supporting this behaviour in lunar calendars would give more accurate results and help in implementing accurate applications such as calendar applications or in the case of Islamic calendars having some astronomical calculations exposed by Temporal can also help in calculating Muslim Prayer times.

Concerns:

The concerns that i can foresee are:

  • Increase Temporal API surface and code by including astronomical calculations
  • Temporal not having access to the location or having inaccurate location will lead to incorrect or inaccurate behaviour.

Prior art:

I am not aware of any standard library implementing this behaviour however, here are some libraries that implement astronomical calculations for sunset and sunrise.

  • Time4j has an implementation of StartOfDay.
  • sunset is a library that calculate Sunrise and Sunset and the phase of the moon accurately based on date and geographic position (written in C++)
  • EDSunriseSet Objective-C class to calculate Sunrise/Sunset/Twilight times.
  • suncalc A tiny JavaScript library for calculating sun/moon positions and phases.
  • Prayers-call is an example of a library that both demonstrate the use astronomical calculation and would benefit from Temporal behaving the correct way with lunar calendars as well as exposing certain astronomical calculations.

Constraints / corner cases:

  • Calculating sunset and sunrise accurately requires not only latitude and longitude bus altitude as well.

Add resolver option(s) to support ID parsing for custom calendars and time zones

This is a niche use case, but if you create a custom calendar or time zone, there is currently no way to deserialize it, or Temporal objects including it, from a string.

A proposal is described in the closed issue tc39/proposal-temporal#1423 (comment). To summarize that proposal in some code examples:

class CustomCalendar {
  toString() { return 'custom-calendar'; }
  // ... implementation of some custom calendar ...
}
const isoString = '2021-04-28[u-ca=custom-calendar]';

function calendarResolver(id) {
  if (id === 'custom-calendar') return new CustomCalendar();
  return Temporal.Calendar.from(id);
}

// Without calendarResolver, this would throw RangeError: invalid calendar 'custom-calendar'
Temporal.PlainDate.from(isoString, { calendarResolver })

// Same here
time = Temporal.PlainTime.from('12:00');
time.withPlainDate(isoString, { calendarResolver })

Examples for custom time zones not given here, but analogous to the above.

Advantages:

  • The alternative to having a resolver parameter is monkeypatching. This discussion assumes that programmers will want to deserialize strings like 2021-04-28[u-ca=custom-calendar] and will monkeypatch Temporal to achieve that goal if there's no other way to achieve it, even if they are aware that monkeypatching is not a good idea. Adding this parameter gives these programmers an alternative that would hopefully prevent them from publishing monkeypatches in their custom-calendar libraries, that could conflict with each other.
  • If implementing this functionality in userland, then parsing all the different forms of ISO strings like 2021-04-28[u-ca=custom-calendar] is difficult to do correctly, and it would be easy to create discrepancies between how the userland code parses ISO strings and how Temporal does.

Concerns:

  • A resolver parameter would have to be added to over 100 methods. An alternative would be to only patch from() as suggested in tc39/proposal-temporal#1423 (comment) but that still leaves the door open to monkeypatches from programmers who want fuller integration of their custom calendar or time zone.

Prior art:

None so far.

Constraints / corner cases:

  • If custom-calendar libraries supplied their own resolvers, then how would users combine resolvers from more than one library?

Parsing a string with a custom format

Summary of discussion from tc39/proposal-temporal#796:

When working with legacy or third-party systems, you often find yourself receiving date strings in the most unwieldy formats. To remedy this, date libraries often support custom format strings to make it possible to parse such strings.

A Temporal.(type).fromFormat(string, formatString) method was proposed. This format from Unicode TR 35 was identified as a likely microformat to use. It's also used by date-fns with a few additions.

This was the most popular feature that didn't make it into the original Temporal proposal, as determined unscientifically by GitHub emoji reactions and number of times it was mentioned in our feedback survey.

Advantages:

Parsing is difficult to do correctly, and possibly out of reach entirely for inexperienced developers. This is one of the reasons developers have turned to third-party libraries instead of using legacy Date.

Parsing textual month names or era names in a locale-aware way would require including a large bundle of locale data, defeating the purpose of using Temporal rather than Moment or another locale-aware library.

Concerns:

The philosophy in the original Temporal proposal was that parsing anything other than ISO strings is in the domain of business logic because everybody knows their own use case better than Temporal can.

Whatever is enabled here should not repeat the mistakes of Date.parse().

Prior art:

  • Moment.js supports passing a format string as a second argument: moment("07-28-2020", "MM-DD-YYYY")
  • Luxon also supports this through its .fromFormat() method: DateTime.fromFormat("07-28-2020", "MM-DD-YYYY")
  • date-fns: parse('07/28/2020', 'MM/dd/y', new Date())
  • Java's DateTimeFormatter is an entity you construct with your pattern ('YYYY-MM-DD' etc), and use for both parsing and printing/formatting.

Constraints / corner cases:

  • Temporal.PlainDate.fromFormat('2021-02-11', 'yyyy-MM') — format string doesn't include all the required fields
  • Temporal.PlainDate.fromFormat('2021-13-11', 'yyyy-MM-dd') — month is invalid in the ISO calendar but could be valid in another calendar. Need a way to indicate in which calendar the string is parsed.

Static string validation of PlainDate, PlainTime and PlainDateTime

String validation of date, time or date with time is currently error prone and/or memory hungry. With this proposal, String validation will have native performance and be correct.

String is very useful for serialization and communication between services but require validation when expressing date, time or date with time.

By adding a static .test method to Temporal.PlainDate, Temporal.PlainTime and Temporal.PlainDateTime, validation would be performant, easy and correct.

API:

  • Temporal.PlainDate.test (String) Where String = YYYY-MM-DD or ±YYYY-MM-DD
  • Temporal.PlainTime.test (String) Where String = hh:mm:ss
  • Temporal.PlainDateTime.test (String) Where String = YYYY-MM-DDThh:mm:ss

YYYY refers to a year between 0001 and 9999, both included.

MM refers to a a zero-padded month between 01 and 12, both included.

DD refers to a a zero-padded month day between 01 and 31, both included, in the Gregorian calendar.

hh refers to a zero-padded hour between 00 and 23, both included.

mm refers to a zero-padded minute between 00 and 59, both included.

ss refers to a zero-padded second between 00 and 60 (where 60 is only used to denote an added leap second), both included.

Ref: https://en.wikipedia.org/wiki/ISO_8601

Advantages:

One use-case is to send a valid date as a String to a database and while the database will validate the date, we would want to check the date before opening a database connection and wait for the response. This can obviously be done both client-side and server-side before communicating with the database.

With Temporal proposal 1, it can be implemented like so:

const validateDate = x => {
    try {
        Temporal.PlainDate.from (x)
    } catch (e) {
        return false
    }
    return true
}

validateDate ('2022-02-29') // <- false, because there is only 28 days in February 2022.

An optimization would be to have Temporal.PlainDate.test so that the static .test method can bypass the instantiation process and return a Boolean. Perhaps bringing it on par with the performance of the regex solution.

const validateDate = x => /^\d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[1-2]\d|3[0-1])$/.test (x)

validateDate ('2022-02-29') // <- true, because regex has no concept of dates

Concerns:

Communicating between services sometimes also means different formats. This is covered in #2 which in contrast to this suggestion, creates an object instance. Similarly to #2, it might be useful to be able to validate other serializable formats than the ones Temporal's from method accept.

For outputting different serializable formats from Temporal, there is #5.

I have not gone much into Temporal.PlainTime.test and Temporal.PlainDateTime.test. Further examination of use-cases might reveal that it's more complicated than stated here.

Prior art:

There does not seem to be any static date parsing libraries in the current JS ecosystem (as of 2021).
All four libraries below will create instance of Date.

Lib Name Static Syntax URL
Moment.js No moment("2022-02-29").isValid() https://momentjs.com/docs/#/parsing/is-valid/
luxon No DateTime.fromISO("2022-02-29").isValid https://moment.github.io/luxon/#/validity
date-fns No isValid (parseISO('2022-02-29')) https://date-fns.org/v2.27.0/docs/isValid#examples
date-fp No isValid (parse ('YYYY-MM-DD', '2022-02-29')) https://cullophid.github.io/date-fp/docs/functions/is-valid.html

Constraints / corner cases:

  • Temporal.PlainDate.test ('2020-01-01T00:00Z') MUST return false. See tc39/proposal-temporal#1751 for a discussion about why that is important.

Convert Instant/ZonedDateTime -> Date

Add Date.fromTemporalInstant and Date.fromTemporalZonedDateTime function to easily create Dates from Temporal values.

Advantages:

Converting to Date currently requires:

new Date(instant.epochMilliseconds)
new Date(zonedDateTime.epochMilliseconds)

This is awkward, and requires remembering the unit for Dates. (seconds? millis? nanos?) For adoption, it's important that converting to/from Dates is easy and rebost.

This proposal offers convenience functions:

Date.fromTemporalInstant(instant)
Date.fromTemporalZonedDateTime(zonedDateTime)

Concerns:

See:

Converting from ZonedDateTime and Date suggests that Date has time zone (not just offset)

Downcasting is common. No one would think that ZonedDateTIme -> Instant suggests that Instant has time zone.

Usage of Date should be discouraged

This sentiment vastly underestimates the stickiness of current Date usage and the necessity of easy conversion between the two.

new Date(instant.epochMilliseconds) is easy enough

With all due respect, not at all. For example MDN list the following constructor calls for Date:

new Date()
new Date(value)
new Date(dateString)
new Date(dateObject)

new Date(year, monthIndex)
new Date(year, monthIndex, day)
new Date(year, monthIndex, day, hours)
new Date(year, monthIndex, day, hours, minutes)
new Date(year, monthIndex, day, hours, minutes, seconds)
new Date(year, monthIndex, day, hours, minutes, seconds, milliseconds)

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/Date

I'm supposed to know that I should pass one arg, and that arg is milliseconds? Come on, man.

Prior art:

N/A

Constraints / corner cases:

N/A

PlainYear to complement PlainYearMonth and PlainDate

(Migrating from tc39/proposal-temporal#2359.)

I'm currently working with an HTTP API that might return a date, a year–month combination or just a year by itself.

I'm turning them into the type:

type PartialDate = number | PlainYearMonth | PlainDate

(https://github.com/thewilkybarkid/crossref-ts/blob/2ae08e578e344e14864a0dbdce73ae6834c58bef/src/index.ts#L56)

This isn't ideal as a year by itself isn't strongly typed nor provides a toLocaleString function.

(I can work around the latter by turning it into a PlainYearMonth and then using the { year: 'numeric' } option to ignore the fake month.)

An equivalent object exists in other languages, e.g. java.time.Year.

I can only find one reference to a PlainYear object, but that seems to have been referenced in a different context (tc39/proposal-temporal#1203).

Disambiguation options for PlainDate.toZonedDateTime, PlainTime.toZonedDateTime

Temporal.PlainDateTime.prototype.toZonedDateTime() has an options argument with which you can choose the disambiguation strategy if the PlainDateTime doesn't exist, or exists twice, in the given time zone.

Temporal.PlainDate and Temporal.PlainTime's toZonedDateTime() methods don't have this options argument. The disambiguation strategy is always "compatible" when calling these methods.

Advantages:

Here's an example of when this might be needed:

const timeZone = Temporal.TimeZone.from('America/Vancouver');
const plainDate = Temporal.PlainDate.from('2021-03-14');
const plainTime = Temporal.PlainTime.from('02:30');

plainDate.toZonedDateTime({ timeZone, plainTime })
plainTime.toZonedDateTime({ timeZone, plainDate })

Both of these return a ZonedDateTime of 2021-03-14T03:30:00-07:00[America/Vancouver]. It's not possible to get 01:30 as it would be with PlainDateTime.

Of course, this is easily worked around by creating a PlainDateTime first:

plainDate.toPlainDateTime(plainTime).toZonedDateTime(timeZone, { disambiguation: 'earlier' })

This returns a ZonedDateTime of 2021-03-14T01:30:00-08:00[America/Vancouver] as expected.

Concerns:

TBD

Prior art:

  • PlainDateTime.toZonedDateTime()

Constraints / corner cases:

TBD

Constants for the minimum and maximum values of each Temporal type

Several Temporal types have an allowed range of values (tc39/proposal-temporal#24):

  • Instant, ZonedDateTime: ±1e8 24-hour days around the Unix epoch (±86400_0000_0000_000_000_000n nanoseconds), same as legacy Date
    • min value: -271821-04-20T00:00:00Z
    • max value: +275760-09-13T00:00:00Z
  • PlainDateTime: such that any valid Instant can be converted to a PlainDateTime at any UTC offset in the range of ±23:59:59.999999999
    • min value: -271821-04-19T00:00:00.000000001
    • max value: +275760-09-13T23:59:59.999999999
  • PlainDate: such that any valid PlainDateTime can be converted to a PlainDate
    • min value: -271821-04-20
    • max value: +275760-09-13
  • PlainYearMonth: such that any valid PlainDate can be converted to a PlainYearMonth
    • min value: -271821-04
    • max value: +275760-09

It's not currently possible to get these values from JS, except by knowing what they are through out-of-band documentation.

One recommendation would be to include constants on each type's constructor (e.g. Temporal.Instant.MAX_VALUE). This would avoid polluting top-level autocomplete with a large number of rarely used options. Also it would match other types' behavior like Number.

Advantages:

These are useful for input validation, expanded polyfills, and tests.

Concerns:

None, this seems useful to have.

Prior art:

  • Legacy Date has a Date object where the internal date value is NaN, but no min or max value constants
  • Python: datetime.datetime.max, etc.
  • Rust: chrono::MAX_DATE, etc.

Constraints / corner cases:

  • What calendar would the min and max values for ZonedDateTime, PlainDate, PlainDateTime, and PlainYearMonth have? iso8601 as the "machine" calendar might be the answer here.
  • What time zone would the min and max values for ZonedDateTime have? Unlike the calendar, it seems like it could be harmful to choose UTC as the time zone.
  • Would we have max and min values for PlainTime (00:00–23:59:59.999999999)?
  • Would we have max and min values for PlainMonthDay (01-01–??-??)? The max value depends on the calendar. For example, the Gregorian calendar would have 12-31 as the max value, and the Hebrew calendar would have a max value of PlainMonthDay.from({ monthCode: 'M12', day: 29 })(?)

load time zone from RFC 8536 TZif data

I'd like an easy way to get a Temporal.TimeZone object from a serialized time zone in the RFC 8536 TZif format (application/tzif or application/tzif-leap) used by the IANA time zone database.

Maybe const tz = Temporal.TimeZone.fromTzif(tzif_blob); (so from a Blob or maybe from an ArrayBuffer).

Advantages:

In an application I'm working on, the server maintains an (in-memory) index of data by calendar day in a particular time zone. For the application to work as expected, the Javascript client should perform date math with exactly the same definition of the time zone. If there's any mismatch, data may appear in a different day than specified by the index, causing confusion. It'd be most convenient if the server could just send its zone file in the standard format and the client could easily load it to a TimeZone object.

It should be possible with the current proposal to write a custom TimeZone protocol implementation that parses this format, but it's not completely trivial. I'd expect a Temporal implementation to have a built-in parser for zones stored in this format. Exposing it would avoid redundant effort, reduce application size, and possibly make operations more efficient.

Concerns:

Bloat, if the Temporal implementation keeps its internal zones in a different format or delegates to some system library that doesn't support this loading. I'm not sure if either of these are a concern in practice.

Prior art:

Go's standard library exposes this exact mechanism: https://golang.org/pkg/time/#LoadLocationFromTZData

Python has zoneinfo for explicitly loading IANA-format time zones from the filesystem (or a compiled-in list). It doesn't quite do loading them from a passed-in bytes object. edit: it can load "file-like" blobs via ZoneInfo.from_file.

Many libraries use this format internally. The IANA has an incomplete list of Other TZif readers.

Constraints / corner cases:

Temporal.Calendar.toLocaleString method

From @FrankYFTang at tc39/proposal-temporal#2367:

Currently, Temporal.Calendar only has toString ( ) method but no toLocaleString ( ) method as other Temporal objects ( PlainDate, PlainTime, PlainDateTime, PlainYearMonth, PlainMonthDay, ZonedDateTime, Instant, Duration)

Temporal spec should consider adding toLocaleString for Temporal.Calendar

Intl.DisplayNames in ECMA402 already support "calendar", Temporal.Calendar.prototype.toLocaleString() could just delegate to Intl.DisplayNames in the same way as how it delegate to Intl.DateTimeFormat in the current spec.

Advantages:

A more intuitive way to get the localized name of a calendar:

calendar.toLocaleString('en-CA', { style: 'short' });

vs

const name = new Intl.DisplayNames('en-CA', { type: 'calendar', style: 'short' });
name.of(calendar);

Concerns:

Currently calling calendar.toLocaleString() resolves to Object.prototype.toLocaleString() which calls calendar.toString(). So existing behaviour could change. I believe that changing the output of toLocaleString() would be web-compatible, though.

Prior art:

  • Prior art is actually within JS already: Intl.DisplayNames and the precedent of toLocaleString().

Constraints / corner cases:

  • None known

Sortability of Temporal.PlainMonthDay

Should there be a Temporal.PlainMonthDay.compare() static method? Follow up from tc39/proposal-temporal#523.

Advantages:

The main use case is sorting a list of birthdays or anniversaries, holidays that fall on the same date each year, etc. This is easy enough when using the ISO calendar, and human calendars based on it. For example, you can do it in several ways if your instances are guaranteed to have the ISO calendar:

// Rely on the default behaviour of converting to a string and sorting lexicographically:
birthdays.sort();

// Sort by monthCode, then day
birthdays.sort((a, b) => {
  if (a.monthCode === b.monthCode) return a.day - b.day;
  return a.monthCode < b.monthCode ? -1 : 1;
});

// Sort by projecting into Temporal.PlainDate in a leap year
birthdays.sort((a, b) => {
  const year = 2000;
  return Temporal.PlainDate.compare(a.toPlainDate({ year }), b.toPlainDate({ year }));
});

However, if sorting in a calendar with leap months, this is not easy at all and requires making choices about how intercalary months are ordered.

Concerns:

  • PlainMonthDay is cyclical, i.e. is December 31 really after January 1 or before? However, this applies to PlainTime as well, and Temporal has a comparison function for that. In the original Temporal proposal, this was justified because:
    • comparing PlainTime was a more common operation than comparing PlainMonthDay;
    • PlainTime comparison is unambiguous (see corner cases below);
    • PlainTime comparison, like all the other types, does not have to consult the calendar.

Prior art:

Constraints / corner cases:

  • Cultural expectations for what happens in intercalary months might differ. For example, does Temporal.PlainMonthDay.from({ monthCode: 'M05L', day: 15, calendar: 'chinese' }) come after Temporal.PlainMonthDay.from({ monthCode: 'M04L', day: 15, calendar: 'chinese' }), or is it undefined because these two months never occur in the same year?

Temporal.PlainYearMonth.atEndOfMonth method

Summary of discussion at tc39/proposal-temporal#729:

The proposal is a Temporal.PlainYearMonth.prototype.atEndOfMonth() method that returns a PlainDate on the last day of the given month.

Advantages:

The current way to do this is a bit roundabout:

yearMonth.toPlainDate({ day: yearMonth.daysInMonth })

The use case is building a date picker with a PlainYearMonth as input parameter.

Concerns:

TBD

Prior art:

Constraints / corner cases:

TBD

Better API for PlainYearMonth/PlainMonthDay toLocaleString

The toLocaleString() method of PlainYearMonth and PlainMonthDay usually requires some surprising extra manipulation in order for it to work. See tc39/proposal-temporal#262 (comment) for the rationale. A future change might make these toLocaleString() methods easier to use.

A sampler of the copious feedback we've gotten on this part of the proposal:

Advantages:

Currently, to format a PlainYearMonth as human-readable, you have to do one of three things:

  1. Ensure the PlainYearMonth object is created with the locale's calendar
const localeCalendar = new Intl.DateTimeFormat().resolvedOptions().calendar;
const pym = Temporal.PlainYearMonth.from({ ...data, calendar: localeCalendar });
console.log(pym.toLocaleString());
  1. Format into a locale whose calendar is the PlainYearMonth's calendar
console.log(pym.toLocaleString(undefined, { calendar: pym.calendarId });
  1. Use a PlainDate instead
console.log(date.toLocaleString(undefined, { year: 'numeric', month: 'long' });

These workarounds are surprising because they aren't necessary for other Temporal types (PlainDate, etc.) and unpleasantly verbose for what they do.

Concerns:

The original reasons for this decision still apply:

  • It's not possible to convert a PlainYearMonth or PlainMonthDay 1:1 from one calendar to another directly, for this reason they don't have a withCalendar() method.
  • If the conversion were allowed in cases where the calendars align, that would produce "data-driven exceptions" where programs would work in development (notably when converting 1:1 from the ISO 8601 calendar to Gregorian) and fail in production in other locales.

Prior art:

None yet

Constraints / corner cases:

None yet

Provide a means to get the clock resolution

Previous discussion at tc39/proposal-temporal#45 and tc39/proposal-temporal#977.

It could be useful for developers to know when they can rely on nanoseconds in Temporal.now, since not all environments may support that, or artificially limit it.

Advantages:

Some use cases are:

  • Setting expectations for how accurate the provided timestamp is when animating on very high refresh rate screens (120 / 144 / 240 Hz)
  • Estimating jitter in network latency calculations
  • Improving logic in other time-based routine work like audio or games
  • Branching shared logic that runs both client-side and server-side (Node.js would provide the system timestamp accuracy while a browser with heightened privacy may only provide precision up to 2 ms)
  • Notifying users when precision is too low for a good experience (eg. VR)

Concerns:

It's not clear whether Temporal is the right place to expose this information, since it would also affect other APIs available in each environment (such as performance.now and requestAnimationFrame in the DOM).

Exposing this information may be a browser fingerprinting concern.

The clock resolution must be injectable in environments that patch out Temporal.now.

Prior art:

Constraints / corner cases:

TBD

Temporal.Calendar.weeksInYear method

Previous discussion at tc39/proposal-temporal#634 and tc39/proposal-temporal#1058.

Similar to Temporal.Calendar.monthsInYear() and the associated monthsInYear properties on types that have a year (PlainDate, PlainDateTime, PlainYearMonth, ZonedDateTime), the proposal is to add a weeksInYear() method to Temporal.Calendar and weeksInYear properties to the types that have a year.

Proposed semantics would be to return the integer number of the last week of the year. Alternatively, the number of weeks in the "week year" that the date falls in. (For example, 2021-01-01 is in week 53 of 2020 in the ISO calendar week-numbering rules.)

Also needed would be a yearOfWeek() method which returns the week year, or a weekYearOffset() method which returns the difference between the week year and the calendar year.

Advantages:

It's currently not possible to get this information in Temporal. This means that it's not possible to correctly output an ISO year-week-day date, since the year would need to be the week year.

Numbered weeks of the year are fairly commonly used in business contexts in Europe.

Concerns:

See "corner cases" below. This would have to be defined very carefully.

Prior art:

Constraints / corner cases:

  • In the ISO calendar and others based on it, there isn't an integer number of weeks in any particular year.
  • If dealing with ISO week years, it would be ill-defined which week year a Temporal.PlainYearMonth would fall in.

Shorthand for current Unix epoch time in ms

Follow up from tc39/proposal-temporal#745.

You can get the current Unix time in ms with Temporal.Now.instant().epochMilliseconds which is quite a lot longer than Date.now(). In Temporal we had some user feedback that this was objectionably long.

Advantages:

It might hinder adoption of Temporal if developers are tempted to continue using Date.now() for this purpose because it's conveniently short.

Concerns:

It remains to be seen whether there's a big need for Unix timestamps in ms when fully porting legacy Date code to Temporal. It might be better to use Temporal.Instant, and Temporal.Now.instant(), while longer than Date.now(), is not inconveniently so.

Allow `until`/`since` across calendars if `largestUnit` is 'days' or smaller

In Temporal V1, we restricted until and since to same-calendar arguments, even if largestUnit was a calendar-safe unit like 'days' or smaller. The reason for this restriction was to allow for future Temporal versions to support calendars that had different conceptions of measuring time, e.g. calendar days that began at sundown instead of 00:00.

If we don't want to support those kinds of custom time calendars, then we should relax this restriction and allow until and since across calendars as long as largestUnit is 'days' or smaller.

Note that we removed time calendars from Temporal V1 because we couldn't find any real-world use cases where calendar-specific time was used in software.

Advantages:

Fewer unnecessary exceptions when writing cross-calendar code

Concerns:

If we still want to support time calendars in the future, we either shouldn't make this change OR we could specify the meaning of until and since as being relative to the calendar of this, which would support future time calendars.

Prior art:

Don't know.

Constraints / corner cases:

See discussion of time calendars above.

Interval type, iterating through a range

Summary of discussions from tc39/proposal-temporal#205 and tc39/proposal-temporal#682.

From user feedback, it seems that the main use cases are iterating through a range between two points on the time line (whether exact time or calendar/wall-clock time), and having an interval object which boxes up two orderable Temporal objects with methods for checking whether another Temporal object or interval lies within the range.

Possible APIs proposed:

const start = new Temporal.PlainDate(2021, 1, 1);
const end = new Temporal.PlainDate(2020, 12, 31);
const step = Temporal.Duration.from({ days: 1 });
for (const date of Temporal.range(start, end, step)) { ... }
class Interval {
  constructor(startInstant, endInstant) { ... }
  static from(isoStringOrIntervalLike) { ... }
  contains(instant) { ... }
  intersects(interval) { ... }
  encloses(interval) { ... }
  equals(interval) { ... }
  *iterate(duration) { ... }
  get start() { ... }
  get end() { ... }
  get duration() { ... }
  with(intervalLike) { ... }
  toString(startTimeZone, endTimeZone) { ... }
  ...
}

Advantages:

Intervals are not difficult to implement in userland, but it could be a benefit for Temporal to have a type with a consistent set of operations, and to avoid off-by-one errors.

Concerns:

An Interval object doesn't quite fit with the strict typing philosophy of Temporal. It's possible there would have to be different Interval types for the different Temporal types: at least Instant and ZonedDateTime, probably PlainDateTime and PlainDate, and possibly PlainYearMonth and PlainTime. Probably business logic would use at most one of these types at a time.

Prior art:

Constraints / corner cases:

  • Is it correct to have an interval with non-exact types, since iterating through it might mean that the steps aren't always equal in exact time?
  • ISO 8601 interval strings (at least before the 2019 edition) don't take negative durations into account, but Temporal allows negative durations.

Increased fractional precision in Duration strings

(Moved from tc39/proposal-temporal#1712)

Temporal.Duration strings are currently allowed to specify fractional hours, minutes, and seconds, up to 9 digits.

Advantages:

Example strings that are currently rejected by Temporal.Duration.from(), that nonetheless represent exact numbers of nanoseconds:

  • PT0.00000000001H → 36 nanoseconds
  • PT0.0000000001M → 6 nanoseconds

Concerns:

Increasing the allowed precision beyond 9 digits for fractional seconds, 10 digits for fractional minutes, or 11 digits for fractional hours, may pose implementation difficulties since implementations would have to calculate with fractional nanoseconds.

Prior art:

  • TBD

Constraints / corner cases:

  • TBD

`round` on date-having types should support month and year rounding

round() with smallestUnit: 'month' and smallestUnit: 'year' should be allowed on PlainDate, PlainDateTime, ZonedDateTime, and (year only) PlainYearMonth.

In V1, PlainDate lacks a round method, and PlainDateTime and ZonedDateTime are both limited to smallestUnit: 'day' or smaller.

Rounding up would advance to the first day of the next month or year, respectively. (Not the last day of the month or year.)

roundingIncrement would be limited to 1 or undefined, because the number of months in a year can vary in lunisolar calendars like Hebrew or Chinese that have 12 months in non-leap years but 13 months in leap years.

Advantages:

  • Better consistency across Temporal types
  • Easier support for use cases like 'closest start of year'
/** Are we halfway done with the year yet? */
function isHalfwayDoneWithYear(date) {
  return date.round({ smallestUnit: 'year' }).year === date.year;
}
// current workaround
function isHalfwayDoneWithYear(date) {
  const daysSoFar = date.since(date.with({month: 1, day: 1}));
  return daysSoFar >= date.daysInYear / 2;
}
  • Avoids bugs in non-ISO calendars, e.g.
// proposed
date = date.round({smallestUnit: 'month', roundingMode: 'ceil'});
// current, which breaks in the chinese or hebrew calendar where years can have 12 or 13 months
if (date.day > 1) {
  date = date.month === 12 ? date.with({ year: date.year + 1, month: 1 }) : date.with({ month: date.month + 1 });
}
// BTW, here's a less obvious alternative to buggy code above
if (date.day > 1) date = date.add({month: 1}).with({day: 1})

Concerns:

  • Workaround is pretty easy today, so not sure this change is necessarily needed. Workaround is:
function roundToMonthsOrYears(temporalInstance, roundOptions) { 
  const baseValues = { 
    year: temporalInstance.year, month: 1, monthCode: 'M01', day: 1,
    hour: 0, minute: 0, second: 0, millisecond: 0, microsecond: 0, nanosecond: 0
  };
  const base = temporalInstance.with(baseValues)
  const duration = base.until(temporalInstance, roundOptions);
  return base.add(duration);
}
roundToMonthsOrYears(Temporal.PlainDate.from('2021-02-15'), { smallestUnit: 'month', roundingMode: 'trunc' });
// => 2021-02-01
roundToMonthsOrYears(Temporal.PlainDate.from('2021-02-15'), { smallestUnit: 'month', roundingMode: 'ceil' });
// => 2021-03-01
roundToMonthsOrYears(Temporal.PlainDate.from('2021-02-15'), { smallestUnit: 'month', roundingMode: 'halfExpand' });
// => 2021-03-01
roundToMonthsOrYears(Temporal.PlainDate.from('2021-02-14'), { smallestUnit: 'month', roundingMode: 'halfExpand' });
// => 2021-02-01

Prior art:

Year and month rounding already exists on until and since and Duration.prototype.round, just not on the round methods of PlainDate, PlainDateTime, ZonedDateTime.

See tc39/proposal-temporal#1785 for previous discussion.

Constraints / corner cases:

None that I know of, but I can add if they're raised in comments.

Format a Temporal object into a user-supplied string format

Similar to #2, but for string formatting instead of string parsing.

If #2 is adopted, an API using the same microformat could be proposed.

Advantages:

Sometimes it's necessary to obtain a string that fits a particular format that is neither ISO 8601 nor locale-sensitive. For these cases, third-party date libraries usually provide a formatting API.

Concerns:

In general, for human-readable strings, developers should not customize the format themselves, and use only the locale-specific formats provided by Intl.DateTimeFormat. While this functionality might be useful for formatting strings that must fit a certain format for machine-readability, providing such an API might result in worse user experiences because developers are tempted to use it for human-readable strings instead of Intl.DateTimeFormat.

Unlike parsing, this kind of functionality is easy to build in business logic.

Prior art:

TBD

Constraints / corner cases:

TBD

Multiplying and dividing Temporal.Duration

Summary of previous discussion at tc39/proposal-temporal#585:

It could be useful to be able to multiply or divide a duration by a scalar number.

Advantages:

The main use cases involve averaging a list of durations, and making an event happen when a certain percentage of a duration has elapsed.

The current way to average durations is uninituitive and not easily discoverable: decide what unit you want you want the resolution of the average to be, total the durations to that unit, sum and divide, and then balance:

const durations = ['PT3H', 'PT2H30M', 'PT3H15M']
  .map(str => Temporal.Duration.from(str));
const sumSeconds = durations
  .reduce((total, d) => total + d.total({ unit: 'seconds' }), 0);
const average = Temporal.Duration.from({
  seconds: Math.round(sumSeconds / durations.length)
}).round({ largestUnit: 'hours' });

Concerns:

TBD

Prior art:

Constraints / corner cases:

  • This will need options to control what to do with the remainder when dividing.

fractionalSecond(s) property

Summary of tc39/proposal-temporal#888:

It could be convenient to offer a fractionalSecond or fractionalSeconds property on PlainTime, PlainDateTime, ZonedDateTime, and Duration. The value would be equal to millisecond / 1e3 + microsecond / 1e6 + nanosecond / 1e9.

Advantages:

millisecond / 1e3 + microsecond / 1e6 + nanosecond / 1e9 is a reasonably common operation, and providing it in Temporal would reduce the chances of carelessly getting the exponents wrong.

It could be useful for customized formatting, or for validation (e.g. pdt.fractionalSecond === 0).

Concerns:

The returned value wouldn't be exactly representable as a JS number, which could lead to unwanted effects. An alternative might be to give an integer number of nanoseconds, equal to millisecond * 1e6 + microsecond * 1e3 + nanosecond.

Prior art:

TBD

Constraints / corner cases:

TBD

More idiomatic comparison methods

Summary of discussion from tc39/proposal-temporal#1086 and meeting minutes:

The Temporal.(type).compare() static methods have confusing parameter order, in order for them to be able to be used as the comparator function for Array.prototype.sort().

A suggestion that's come up several times is that Temporal types could have more idiomatic methods named something like lessThan(), earlierThan(), isBefore(), isSameOrAfter(). Although these would duplicate the functionality of compare(), they would make user code much easier to read.

For example:

// proposed
function afterKidsBedtime(time) {
  return time.greaterThan('20:30');
}
// current
function afterKidsBedtime(time) {
  return Temporal.PlainTime.compare(time, '20:30') > 0;
}

There was a work-in-progress at tc39/proposal-temporal#1074 on which a proposal in this repository can be based.

Sets of names that were proposed so far:

  1. greaterThan(), lessThan(), greaterEquals(), lessEquals()
  2. greater(), lesser(), greaterEquals(), lesserEquals()
  3. greater(), lesser(), greaterOrEquals(), lesserOrEquals()
  4. greater(), less(), greaterEquals(), lessEquals()
  5. greater(), less(), greaterOrEquals(), lessOrEquals()
  6. gt(), lt(), ge(), le()
  7. earlier(), later(), earlierEquals(), laterEquals()

Advantages:

The meaning of the return value of compare(a, b) has been confusing since it was first introduced in C, because you have to think carefully about whether 1 means a > b or b < a. It can't be read left-to-right.

An isSame() method could, in addition, implement equality based on the internal ISO-calendar slots of the Temporal object, without taking the calendar into account, which makes it fulfill a different function from equals().

Concerns:

Such an API might become useless if operator overloading is ever added to the language, at which point the comparison operators could simply be used.

Names like greaterThan() are closely tied to English grammar and so may only improve the issue for English speakers.

A problem with names implying "before" or "after" is that due to time zones, a wall-clock time that is "greater than" another wall-clock time may not actually be "after" it. These names also wouldn't be consistent if similar methods were added to Temporal.Duration.

Prior art:

  • Moment (including the ability to specify the granularity: isSame('day')
  • ThreeTen / java-time / js-joda: time1.isAfter(time2)
  • TBD

Constraints / corner cases:

TBD

Provide a means to get the time zone database version

Summary of discussion at tc39/proposal-temporal#1317:

Technically, if you store a ZonedDateTime that's in the future, it doesn't translate to a unique PlainDateTime (or Instant, if you store it as an ISO string); you need to add the version of the time zone database that was current when it was stored.

Advantages:

An example use case would be to store a ZonedDateTime, with the associated PlainDateTime cached, and the tzdata version. If the tzdata version changes, the cached PlainDateTime would be updated.

Another use case would be to compare the versions of the time zone data between client and server, to make sure that date operations are consistent if both client and server perform them.

Concerns:

There is no requirement for implementations to provide time zone data based on "the" time zone database, or any time zone data at all other than UTC, so a meaningful version is not always possible.

Exposing this information is a browser fingerprinting concern.

Prior art:

Constraints / corner cases:

TBD

Formatting and parsing support for RFC 7231 HTTP-dates

Add formatting and parsing support for RFC 7231 HTTP-dates, as used in HTTP headers (Expires, Last-Modified, etc) and cookies.

Advantages:

There's clear utility, as it's a ubiquitous format on the web, due to being used in many common HTTP headers and cookies. Currently the best way of handling in JS is to use the Date methods, but that's not ideal as Date is already being referred to as "legacy" in the Temporal documentation due to its many drawbacks. One could reasonably expect "no Date" to become a common linter rule once Temporal achieves widespread platform support.

Date also doesn't implement validation for the string parsing — arbitrary strings will never throw and may or may not return valid dates that may or may not be what was expected.

In addition, Date parsing fails to fully implement HTTP-date as specified in RFC 7231, as the two obsolete (pre-1995) formats aren't accounted for.

Concerns:

None that I can think of

Prior art:

Formatting:

Parsing:

  • Date.parse, or by extension the Date constructor, which uses the same parsing logic
    • Implementations should correctly handle the string values generated by toUTCString
    • The two obsolete (pre-1995) formats mentioned by RFC 7231 are unspecified

Constraints / corner cases:

  • How strict should parsing be — for example, should Mon, 06 Nov 1994 08:49:37 GMT throw due to that date being a Sunday not a Monday?
  • Should the two obsolete (pre-1995) formats mentioned by RFC 7231 be included in the parsing logic? I have no idea how common these are in-the-wild as of 2023.
  • Names/locations of the new methods — maybe something like Temporal.Instant.toHttpDateString and Temporal.Instant.fromHttpDateString? Logically, Instant seems to make more sense than ZonedDateTime, despite the IMF-fixdate and rfc850-date formats specifying "GMT" (it's always GMT).

First-class support for year-week-day dates

Prior discussion at tc39/proposal-temporal#255, and a bit at tc39/proposal-temporal#819.

Going even farther than #8, to add first-class support for year-week-day dates, Temporal.PlainDate, Temporal.PlainDateTime, and Temporal.ZonedDateTime should accept the ISO string format 2020-W06-5 (2020-02-12, of which the weekOfYear property is 6 and dayOfWeek is 5). (Note that 2020 is the "week year", i.e. 2020-W53-5 is 2021-01-01.) Additionally, add a Temporal.PlainYearWeek type, and methods to convert to and from Temporal.PlainDate. Temporal.PlainDate.toString would also need to gain an option to output itself in the ISO year-week-day format.

Advantages:

Numbered weeks of the year are fairly commonly used in business contexts in Europe. Although not a common request during the development of Temporal, it has been requested several times independently.

Concerns:

TBD

Prior art:

  • ISO 8601 has string formats that are intended to handle this scheme.

Constraints / corner cases:

  • Year-week-day is a not-uncommonly used scheme in the ISO calendar and calendars derived from it. Check whether this only makes sense for the ISO calendar, or similar year-week-day schemes exist in other calendars.

Make calendar ID optional in withCalendar() methods

Previously proposed at tc39/proposal-temporal#1324.

The proposal is that the withCalendar() methods of PlainDate, PlainDateTime, and ZonedDateTime should allow omitting the argument, or undefined, to mean the ISO calendar.

Advantages:

The ISO calendar acts like a default calendar elsewhere in Temporal, except where a calendar is explicitly needed from the programmer. But if you have a maybe-non-ISO date and want to convert it to ISO (which will be a very common operation when accepting external data), the developer needs to spell out 'iso8601' in every call site. This seems unnecessary, since it's not a call site where omitting the calendar might lead to unexpected results when encountering non-ISO calendars.

Concerns:

TBD

Prior art:

TBD

Constraints / corner cases:

TBD

ISO 8601 string parser

A string parsing API accessible to the programmer (not just through Temporal.___.from()) was considered almost from the very beginning as a potential part of the Temporal (V1) proposal. Ultimately it was decided to be out of scope.

This issue captures the use cases and discussion in case a parser API is proposed in the future.

This feature would probably subsume #22.

Advantages:

These are the use cases that we became aware of during the development of the Temporal V1 proposal:

  • Impose stricter control on string inputs. e.g. require minutes to be specified in ISO time strings, or require the time to be included to be specified in a date/time string
  • Clarify user intent. e.g. differentiating Z strings vs. local+offset strings in ZonedDateTime.from
  • Process information not part of Temporal's data model. e.g., custom annotations.
  • Handle partially-invalid/partially-valid ISO strings. Temporal doesn't support this case today, because invalid ISO strings throw even if some parts are valid.
  • Handle ISO strings extended by mutual agreement of communicating parties. There are several of these extensions in ISO 8601, e.g., extended years longer than 6 digits, or more than 9 decimal places.
  • Generically parse ISO strings for use in non-Temporal code. In this case, the user wants a parser to chop up an ISO string into parts so they can do whatever they want with those parts (e.g. custom parsing, display to users, etc.) Temporal doesn't support this case today.
  • Validate a string without the overhead of creating a Temporal object. (#22).

Previous discussions:

Concerns:

TBD

Prior art:

Many programming languages have an ISO 8601 parser in their standard library, but they all either return the equivalent of a Temporal instance in that language, or an epoch time, or a normalized ISO 8601 string. Some third-party libraries provide a parser functionality:

Constraints / corner cases:

TBD

Strict ("all fields match") comparisons that take calendar and timezone into account

The current compare methods in Temporal ignore calendar. For ZonedDateTime.compare, time zone is also ignored. When sorting, this method can result in different results depending on the order of the input array. It also means that equals (which only returns true if all fields including calendar and timezone are equivalent) behaves differently than compare(t1, t2) === 0.

A use case that's challenging in the V1 API is "Are these arrays of dates equivalent?", because a custom comparison function is required:

function calendarAwarePlainDateComparer(d1, d2) {
  let result = Temporal.PlainDate.compare(d1, d2);
  if (result === 0) {
    result = d1.calendar.id === d2.calendar.id 
      ? 0
      : d1.calendar.id > d2.calendar.id
      ? 1
      : -1;
  }
  return result;
}

function areDateArraysEquivalent(arr1, arr2) {
  if (arr1.length !== arr2.length) return false;
  const sorted1 = [...arr1].sort(calendarAwarePlainDateComparer);
  const sorted2 = [...arr2].sort(calendarAwarePlainDateComparer); 
  return sorted1.every((d, i) => d.equals(sorted2[i]));
}

One solution could be to add an option to the existing compare method. This would make the solution above somewhat more ergonomic by avoiding the custom comparison function. In ZonedDateTime.compare, this would also allow using different strictness for time zones vs. calendars.

function areDateArraysEquivalent(arr1, arr2) {
  if (arr1.length !== arr2.length) return false;
  const sorted1 = [...arr1].sort((a, b) => Temporal.PlainDate.compare(a, b, { ignoreCalendar: false }));
  const sorted2 = [...arr2].sort((a, b) => Temporal.PlainDate.compare(a, b, { ignoreCalendar: false })); 
  return sorted1.every((d, i) => d.equals(sorted2[i]));
}

The most ergonomic solution (albeit at the cost of not being able to have different ZDT.compare behavior for calendar vs. timezone differences) would be a new static method that could be used interchangeably with the existing compare method:

function areDateArraysEquivalent(arr1, arr2) {
  if (arr1.length !== arr2.length) return false;
  const sorted1 = [...arr1].sort(Temporal.PlainDate.compareStrict);
  const sorted2 = [...arr2].sort(Temporal.PlainDate.compareStrict);
  return sorted1.every((d, i) => d.equals(sorted2[i]));
}

Advantages:

  • More ergonomic comparisons and sorting where "all fields match" comparison semantics is desired
  • Easier to avoid bugs caused by "holes" between equals and compare:
if (!d1.equals(d2)) {
  if (Temporal.PlainDate.compare(d1, d2) < 0) {
    doSomethingWhenEarlier();
  } else {
    // BUG!  This code will also execute if d1 and d2 are the same date but different calendars
    doSomethingWhenLater();
  }
}
  • If greater-than/less-than semantics proposed in #6 are also strict, then this proposed feature would make #6 easier to document and learn, e.g. "date1.lessThan(date2) is a shorthand for Temporal.PlainDate.compareStrict(date1, date2) < 0"

Concerns:

  • Adding more surface area to an already large API
  • Potential confusion between the comparison methods (this IMHO depends a lot on the name chosen)

Prior art:

  • Java's equivalents to PlainDate and PlainDateTime have a compareTo method with the same strict semantics proposed here: ("chronology" below means a calendar in Temporal)

The comparison is based first on the underlying time-line date, then on the chronology.

  • Java's ZonedDateTime also has a compareTo method but with different semantics than what's proposed here:

The comparison is based first on the instant, then on the local date-time, then on the zone ID, then on the chronology.
(AFAIK, "chrononlogy" is a calendar)

The different semantics of Java's ZonedDateTime comparisons, which sort using the local time as a tiebreaker if the instants match, is interesting. It implies that more Westernly time zones would sort first. I'm not sure why this behavior is valuable. Note that if we adopted that behavior it'd make comparisons slower (because implementations would need to look up the time zone offset on every comparison) so I'd be hesitant to match Java's semantics unless we had a really good use case that was improved by those semantics.

Constraints / corner cases:

Not aware of any

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.