Proposal-temporal: Write cookbook of practical scenarios using Temporal

Created on 30 Oct 2019  ·  39Comments  ·  Source: tc39/proposal-temporal

@gibson042 suggested doing this, and I think it's a great idea that shouldn't be lost. A couple purposes:

  • Validate the design, to know that these things can be done ergonomically
  • Serve as documentation for JS developers who want to use Temporal
  • Good material to copy/paste into slide presentations about Temporal!

Resources for Moment and date-fns, including documentation and stack overflow questions, could be good seed material

documentation

Most helpful comment

I don't know if anyone has looked yet, but looking at a list of questions tagged date and javascript on Stack Overflow and sorting by votes will show you the most-often asked questions about dates. Some of them have nothing to do with Temporal (formatting, getting the name of the month, etc.), but some of them do (get the number of milliseconds since unix epoch, get the current date [in local time, in UTC, in some other time zone], compare two dates (number of units of time between two dates, also is it before or after), add/subtract x units of time to/from a date, etc.).

All 39 comments

As promised earlier today, this is _finally_ ready. It remains my hope that filling in the bodies of the below functions is used to validate the Temporal API, and in particular that any discomfort identified is used to _change_ that API before it is finalized. This should have happened months ago, but I still believe that it is better late than never.

Construction

Instant from legacy Date

Map a legacy ECMAScript Date instance into a Temporal.Absolute instance corresponding to the same instant in absolute time.

function getInstantAtEs2019DateInstance( es2019Date ) {
    // ???
}

Zoned instant from instant and time zone

Map a Temporal.Absolute instance and a time zone name or object into a string serialization of the local time in that zone corresponding to the instant in absolute time.

function getParseableIanaZonedStringAtInstant( absolute, ianaTimeZoneName ) {
    let timeZoneObject = getTimeZoneObjectFromIanaName( ianaTimeZoneName );
    return getParseableZonedStringAtInstant(absolute, timeZoneObject);
}
function getParseableZonedStringAtInstant( absolute, timeZoneObject ) {
    // ???
}

getParseableIanaZonedStringAtInstant(
    Temporal.Absolute.from("2020-01-09T00:00Z"),
    "Europe/Paris"
).replace(/(T\d+:\d+).*?([+-])/, "$1$2") ===
"2020-01-09T01:00+01:00[Europe/Paris]";

Fixed-offset instant from instant and UTC offset

Map a Temporal.Absolute instance and a UTC offset into a string serialization of the local time in that offset corresponding to the instant in absolute time.

function getParseableFixedOffsetStringAtInstant( absolute, utcOffset ) {
    // ???
}

getParseableFixedOffsetStringAtInstant(
    Temporal.Absolute.from("2020-01-09T00:00Z"),
    "+09:00"
).replace(/(T\d+:\d+).*?([+-])/, "$1$2") ===
"2020-01-09T09:00+09:00";

TimeZone instance

Map an IANA time zone name into an object encapsulating all corresponding offsets and transition rules.

function getTimeZoneObjectFromIanaName( ianaTimeZoneName ) {
    // ???
}

Construct such an object directly from tzdata-compatible rules of arbitrary complexity (e.g., for use in testing).

function getTimeZoneObjectFromRules( rules ) {
    // ???
}

Sorting

Zoneless datetimes

Sort an array of zoneless Temporal.DateTime instances by the corresponding local date and time of day (e.g., for building a conference schedule).

function getSortedLocalDateTimes( datetimes, direction ) {
    // ???
}

Absolute instants

Sort an array of strings (each of which is parseable as a Temporal.Absolute and may or may not include an IANA time zone name) by the corresponding absolute time (e.g., for presenting global log events sequentially).

function getSortedInstants( parseableAbsoluteStrings, direction ) {
    // ???
}

Time Zone Conversion

Preserving local time

Map a zoneless date and time of day into a Temporal.Absolute instance at which the local date and time of day in a specified time zone matches it, with user-specifiable results for inputs that correspond with either zero or multiple local values in the target time zone (fail vs. clip-to-earlier [e.g., 01:30 to 00:59:59.999999999 when skipped or to the first repeated 01:30] vs. approximate-to-first [e.g., 01:30 to 02:30 when skipped or to the first repeated 01:30] vs. approximate-to-last [e.g., 01:30 to 02:30 when skipped or to the _last_ repeated 01:30]). This could be used when updating the time zone for an appointment or scheduled reminder, or a local-time schedule.

function getInstantWithLocalTimeInZone(
        dateTime,
        timeZoneObject,
        disambiguationPolicy = "constrain" ) {
    // ???
}

Preserving absolute instant

Map a zoned date and time of day into a string serialization of the local time in a target zone at the corresponding instant in absolute time. This could be used when converting user-input date-time values between time zones.

function getParseableZonedStringAtInstantWithLocalTimeInOtherZone(
        sourceDateTime,
        sourceTimeZoneObject,
        targetTimeZoneObject,
        sourceDisambiguationPolicy = "constrain" ) {
    let instant = getInstantWithLocalTimeInZone(
        sourceDateTime,
        sourceTimeZoneObject,
        sourceDisambiguationPolicy
    );
    return getParseableZonedStringAtInstant(instant, targetTimeZoneObject);
}

getParseableZonedStringAtInstantWithLocalTimeInOtherZone(
    Temporal.DateTime.from("2020-01-09T00:00"),
    getTimeZoneObjectFromIanaName("America/Chicago"),
    getTimeZoneObjectFromIanaName("America/Los_Angeles")
).replace(/(T\d+:\d+).*?([+-])/, "$1$2") ===
"2020-01-08T22:00-08:00[America/Los_Angeles]";

UTC offset for a zoned event, as a string

Map a Temporal.Absolute instance and a time zone into the UTC offset at that instant in that time zone, as a string.

function getUtcOffsetStringAtInstant( absolute, timeZoneObject ) {
    // ???
}

getUtcOffsetStringAtInstant(
    Temporal.Absolute.from("2020-01-09T00:00Z"),
    getTimeZoneObjectFromIanaName("America/New_York")
) === "-05:00";

UTC offset for a zoned event, as a number of seconds

Map a Temporal.Absolute instance and a time zone into the UTC offset at that instant in that time zone, as a number of seconds.

function getUtcOffsetSecondsAtInstant( absolute, timeZoneObject ) {
    // ???
}

getUtcOffsetSecondsAtInstant(
    Temporal.Absolute.from("2020-01-09T00:00Z"),
    getTimeZoneObjectFromIanaName("America/New_York")
) === -18000;

Offset between two time zones at an instant

Map a Temporal.Absolute instance and two time zones into the signed difference of UTC offsets between those time zones at that instant, as a number of seconds.

function getUtcOffsetDifferenceSecondsAtInstant(
        absolute,
        sourceTimeZoneObject,
        targetTimeZoneObject ) {
    // ???
}

getUtcOffsetDifferenceSecondsAtInstant(
    Temporal.Absolute.from("2020-01-09T00:00Z"),
    getTimeZoneObjectFromIanaName("Etc/UTC"),
    getTimeZoneObjectFromIanaName("America/Chicago")
) === -21600;

Arithmetic

Unit-constrained duration between now and a past/future zoned event

Map two Temporal.Absolute instances into an ascending/descending order indicator and a Temporal.Duration instance representing the duration between the two instants without using units coarser than specified (e.g., for presenting a meaningful countdown with vs. without using months or days).

function getElapsedDurationSinceInstant(
        absoluteThen,
        absoluteNow,
        largestTimeElementInResult = "year" ) {
    // ???
}

(result => `${result.sign}${result.duration}`)(
    getElapsedDurationSinceInstant(
        Temporal.Absolute.from("2020-01-09T00:00Z"),
        Temporal.Absolute.from("2020-01-09T04:00Z")
    )
) === "+PT4H";

Nearest offset transition in a time zone

Map a Temporal.Absolute instance and a time zone object into a Temporal.Absolute instance representing the nearest instant (at-or-after vs. after vs. at-or-before vs. before) at which there is an offset transition in the time zone (e.g., for setting reminders).

function getInstantOfNearestOffsetTransitionToInstant(
        absolute,
        timeZoneObject,
        directionAndClusivity ) {
    // ???
}

Comparison of an instant to business hours

Courtesy @gilmoreorless at https://github.com/tc39/proposal-temporal/issues/26#issuecomment-513208398 , map a localized date and time of day into a time-sensitive state indicator ("opening soon" vs. "open" vs. "closing soon" vs. "closed").

function getBusinessOpenStateText(
        absoluteNow,
        timeZoneObject,
        businessHours,
        soonWindowDuration ) {
    // ???
}

Flight arrival/departure/duration

Courtesy @kaizhu256 at https://github.com/tc39/proposal-temporal/issues/139#issuecomment-510925843 , map localized trip departure and arrival times into trip duration in units no larger than hours.

function getTripDurationInHrMinSec( parseableDeparture, parseableArrival ) {
    // ???
}

Map localized departure time and duration into localized arrival time.

function getLocalizedArrival( departureAbsolute, duration, destinationTimeZoneObject ) {
    // ???
}

"A month from now"

Map a Temporal.Date instance into a new Temporal.Date instance that follows it by a possibly negative integer number of months, with user-specifiable policy of how to deal with results that don't exist because of variable month duration (fail vs. clip-to-fit vs. spill-into-following-month vs. any other identified strategies). This could be used for billing date calculation or anniversary reminders.

function plusMonths( date, months, disambiguationPolicy = "constrain" ) {
    // ???
}

Push back a launch date

Add the number of days it took to get an approval, and advance to the start of the following month.

function plusAndRoundToMonthStart( date, delayDays ) {
    // ???
}

Schedule a reminder ahead of matching a record-setting duration

Map a Temporal.Absolute instance, a previous-record Temporal.Duration, and an advance-notice Temporal.Duration into a Temporal.Absolute instance corresponding with an absolute instant ahead of the instant at which the previous record will be matched by the specified window. This could be used for workout tracking, racing (including _long_ and potentially time-zone-crossing races like the Bullrun Rally, Idatarod, Self-Transcendence 3100, and Clipper Round The World), or even open-ended analogs like event-every-day "streaks".

function getInstantBeforeOldRecord(
        startAbsolute,
        previousRecordDuration,
        noticeWindowDuration ) {
    // ???
}

Nth weekday of the month

https://github.com/tc39/proposal-temporal/issues/240#issuecomment-591078109
Given a Temporal.YearMonth instance and an ISO 8601 ordinal calendar day of the week ranging from 1 (Monday) to 7 (Sunday), return a chronologically ordered array of Temporal.Date instances corresponding with every day in the month that is the specified day of the week (of which there will always be either four or five).

function getWeeklyDaysInMonth( yearMonth, dayNumberOfTheWeek ) {
    // ???
}

getWeeklyDaysInMonth(new Temporal.YearMonth(2020, 2), 1).join(" ") ===
"2020-02-03 2020-02-10 2020-02-17 2020-02-24";
getWeeklyDaysInMonth(new Temporal.YearMonth(2020, 2), 6).join(" ") ===
"2020-02-01 2020-02-08 2020-02-15 2020-02-22 2020-02-29";

Given a Temporal.Date instance, return the count of preceding days in its month that share its day of the week.

function countPrecedingWeeklyDaysInMonth( date ) {
    // ???
}

countPrecedingWeeklyDaysInMonth(Temporal.Date.from("2020-02-28")) === 3;
countPrecedingWeeklyDaysInMonth(Temporal.Date.from("2020-02-29")) === 4;

Isolation

Attenuation

Create an object that supports exactly the same interface as Temporal and is indistinguishable from it (even though side-door means such as Function.prototype.toString) except that it provides no mechanism by which code could use it to determine that the host environment is not executing within a date, time, time zone, and tzdata edition under the control of the creator, without costing the creator any capabilities provided by the native Temporal. This has use for secure environments like SES, but also for purely functional environments like Elm (cf. #103) and for testing.
NOTE: If Temporal were 100% pure and deterministic (like e.g. Array), then the unmodified (except for perhaps being deeply frozen) Temporal object itself would serve this purpose.
cc @erights

function getAttenuatedTemporal( Temporal, attenuations ) {
    // ???
}

Extension

Extra-expanded years

Create a Temporal derivative that supports arbitrarily-large years (e.g., +635427810-02-02) for astronomical purposes, ideally without requiring modifications to year-agnostic interfaces such as Time (but still supporting e.g. UnlimitedTemporal.Time.from("10:23").withDate("+635427810-02-02")).

function makeExpandedTemporal( Temporal ) {
    // ???
}

Thanks for the cookbook! FYI, of the 18 examples here, I believe only 3 require calendar information:

  • Unit-constrained duration between now and a past/future zoned event
  • "A month from now"
  • Push back a launch date

This matches with my instinct that we should support calendar systems but we don't need to reshape the whole API around them. Rather, we can introduce a calendar field in places it is needed, but without adding any radically new types.

Just posting here as FYI: we have an internal client who wants to do something that is difficult with the current JavaScript Date class. The client wants to display midnight in an arbitrary IANA timezone in the user's local timezone. For example, initialize a datetime as midnight in America/Chicago, and then display it in America/Los_Angeles. This is difficult in JavaScript Date because it doesn't support IANA timezones. This could be another cookbook example.

Good news, this is composable from the existing recipes! I've added it as an explicit scenario with implementation: Preserving absolute instant

I think this was accidentally closed by the "Addition of cookbook fixes #240" commit message, in any case there are still more cookbook examples to add, so let's keep this open!

Another use case from an internal client. They want to:

  1. Get the IANA timezone for the current OS

    • Yes: Temporal.now.timeZone()

  2. Get the GMT offset of the timezone

    • Yes: timeZone.getOffsetFor(Temporal.now.absolute())

  3. Know whether the timezone is daylight time or not

    • We have methods for finding the next summer time transitions, but do we have a way to determine whether or not the timezone is currently in summer time?

I will continue posting use cases here as we get them. They come organically from internal email lists and question boards.

How would they define whether a time zone is "in summer time"? I suppose it depends on the location of the time zone (northern/southern hemisphere) as well as whether that time zone does a daylight saving time transition at all? My understanding is also that time zones can have offset transitions for other reasons than daylight saving.

DST is not a universal concept. There are places that don’t have anything like it; there are those that have multiples. Also it’s not even the case that one offset correlates to astronomical time (noon = highest sun) in any way. So whether a time is DST or not is actually pretty meaningless.
The IANA database has that info in it in a rather arbitrary way. So we could just go with that. But I don’t think it’s actually used anywhere. So I question the utility.

TL;DR “Is this summertime” is a meaningless question

Know whether the timezone is daylight time or not

I agree with @pipobscure that the question becomes meaningless when it's dug into.

1. _Why_ do they need to know if it's in "daylight time"?

  • Is it to do calculations with offsets? In that case, use other methods to get the offsets without bothering about DST.
  • Is it to display the correct time zone abbreviation? Then that's a formatting issue better handled by Intl.DateTimeFormat.
  • Is it to know the "base offset" of a time zone? That could be a good use case, until you dig into the history of time zones.

    • What exactly defines a "base offset" anyway? Many places have equal parts of the year with DST and without it (i.e. 6 months between transitions).

    • What happens when the "base offset" _itself_ is changed? There have been several instances in the past few years of regions moving to what they call "permanent DST". For example, moving from +00 in winter and +01 in summer, to +01 all year round with no DST.

2. "Daylight saving time" is NOT the same as "summer time".

This discussion has been circling on the tzdb mailing list for a few years now (where there's a suggestion to redefine "DST" as "daylight shifted time").

The real catch comes from countries that define their timekeeping laws to say that "standard time" is in summer, with "alternative time" (or some other name) in winter. That is, they have _negative_ DST. The main examples of this are Ireland (+01 "standard" in summer, +00 in winter), Nigeria, and Morocco. Morocco is a particularly interesting case because they recently changed from +00/+01 to a "permanent" +01... except they still need to switch back to +00 during the month of Ramadan.

See also these threads in tzdb list archives from the past year or so:

The most relevant quote comes from the last thread listed above (specifically from this reply):

I preferred whichever reference used the terms "standard time" and "alternative time"

That was POSIX, before it was changed (not yet published, but applied,
"alternative time" will no longer appear when the next edition is pubished)
based upon some input from this direction.

But, while I agree it was better, it's not good enough either - first
because there's not just two, and second, because there's no point giving
these things a name - whatever time it is (commonly agreed, whether
legislatively backed or not) is "standard time" - that's what it means
to be "standard". The only other thing we can (at least currently) rely
upon is that there is a current offset from UTC (ie: that UTC is stable,
and the time anywhere is, for a short while anyway, a constant offset
from UTC - positive or negative (or 0)).

Some juristictions provide names for the various times, mostly for convenience
around the transitions, but none of them are very useful, and here we really
cannot rely upon any such thing existing.

It appears the client is trying to interop with the win32 function SetDynamicTimeZoneInformation, which requires a struct containing the following fields, which all appear to be required:

typedef struct _TIME_DYNAMIC_ZONE_INFORMATION {
  LONG       Bias;
  WCHAR      StandardName[32];
  SYSTEMTIME StandardDate;
  LONG       StandardBias;
  WCHAR      DaylightName[32];
  SYSTEMTIME DaylightDate;
  LONG       DaylightBias;
  WCHAR      TimeZoneKeyName[128];
  BOOLEAN    DynamicDaylightTimeDisabled;
} DYNAMIC_TIME_ZONE_INFORMATION, *PDYNAMIC_TIME_ZONE_INFORMATION;

So in order to interop with this API, you need to know which offset ("bias") is for daylight time and which offset is for standard time.

Probably not super important for ECMAScript, but worth pointing out. The existence of this win32 API suggests that this distinction between which offset is daylight or not may also appear in other places in the tech ecosystem.

Taken from @gilmoreorless

I’ve followed this proposal from the start, but I had to ignore it for a while (for reasons unimportant to the issue at hand). I’ve now caught up on everything that’s happened in the past few months, and I have some concerns.

It’s great that the proposal is gaining traction and going through the TC39 process. However, there are still barely any actual use cases listed. This proposal was designed to replace the shortcomings of Date, because Date doesn’t easily handle many real-world scenarios without a fair amount of hackery (such as mis-using the UTC timeline). But if we don’t explicitly list out these scenarios and use cases, how can we be sure that the proposed API is actually covering them?

I notice that this particular issue is still unresolved after 2 years. It’s listed in the “finalize documentation” project, but I’m not sure if that comes before or after “finalize spec text”. I very strongly feel that locking in the API _before_ documenting the use cases is the wrong way around. The README currently (commit 4a0f079 at time of writing) links to examples.md, but that only contains a single scenario, with the rest being API documentation.

Rather than just complaining, I’ll briefly list some scenarios I’ve personally had to deal with in production systems. I can provide more details/clarification if needed, but I want to make sure the expanded information would be useful first. Some of these cases are well-known problems with many library solutions available, but it’s still worth listing them for completeness.

1. Abstract date logic which is not tied to any specific location

_(I’m avoiding the “Local”/“Civil” prefix naming debate here)_

  1. Showing a generic calendar control, with any day beyond “today” disabled and not selectable.
  2. Simple calculations of “how many days until {future date}”.
  3. Integration with wearable devices.

2. Dealing with dates and times in a time zone that differs from the user’s browser

  1. A graph showing activity for a storage tank in a fixed location. The requirement was to always start the graph at midnight for the tank’s location, regardless of the viewer’s time zone.
  2. Calculation of recurring schedules for a specific location.

    • “Email this generated report at 4pm daily, Brisbane time.”
  3. Calculation of how recurring schedules will be affected in other locations.

    • “This meeting is always at 2pm in Sydney. What time will that be for our remote team in Ho Chi Minh City before and after Sydney’s daylight saving shifts?”
  4. Trying to book a meeting or event that works for 4 different time zones, with a display of the relative times in each zone (_à la_ World Time Buddy).

3. Combinations of both

  1. Show opening hours for a chain of stores spread across multiple time zones.

    • e.g. The default is for all stores to be open from 8am to 6pm in their respective locations.
    • Viewing the website for a particular store needs to show if it’s currently open/closed, or closing soon. This means taking the abstract shared closing time and applying it to the time zone of the specific store.

I don’t want to be the _only_ person providing these, though, as it’s just one person’s perspective. There have been some other examples scattered among various issues that I’ve seen so far:

  • @kaizhu256 has provided a use-case in #139 (comment) regarding flight durations and take-off/landing times.
  • @ljharb recounted a bug with scheduling a future event in #78 (comment)
  • I’m also hopeful that @ljharb could provide some more scenarios based on the things Airbnb have had to deal with.

We could specify a list of chosen scenarios, with examples of how to do them with Temporal vs currently-existing methods (some of which might only be possible with third-party libraries). But first we have to decide on what that list contains.

Taken from @apaprocki

One common scenario you need the timezone for is for future events. The next TC39 meeting is 2017-07-25 10:00:00 America/Los_Angeles. That is stored in your calendar app. It can't be resolved to an absolute point in UTC and stored because between now and 7/25, the US could decide to abolish DST -- but your meeting will still need to be 10am in local time on that date. So resolution to an absolute point in UTC is done at the presentation layer or when needed "as-of-now", but isn't used to store the actual event (or at least the timezone string must be stored as well). So I think a good scenario to write up is retrieving the next few future TC39 meetings from a hypothetical request and displaying them to a user as absolute as-of-now local datetimes in an arbitrary timezone (say, Asia/Tokyo since there are no meetings there).

Another cookbook case from a client:

First Tuesday of Month

Get the "first Tuesday", "second Tuesday", or "last Tuesday" of a month, and also, given a day, identify whether it's the first, second, etc. of that day in the month. Note that the "last Tuesday" could be the 4th or the 5th Tuesday, and also, the nth Tuesday does not necessarily fall on the nth week of the month.

I don't know if anyone has looked yet, but looking at a list of questions tagged date and javascript on Stack Overflow and sorting by votes will show you the most-often asked questions about dates. Some of them have nothing to do with Temporal (formatting, getting the name of the month, etc.), but some of them do (get the number of milliseconds since unix epoch, get the current date [in local time, in UTC, in some other time zone], compare two dates (number of units of time between two dates, also is it before or after), add/subtract x units of time to/from a date, etc.).

Another cookbook case from a client:

First Tuesday of Month

Get the "first Tuesday", "second Tuesday", or "last Tuesday" of a month, and also, given a day, identify whether it's the first, second, etc. of that day in the month. Note that the "last Tuesday" could be the 4th or the 5th Tuesday, and also, the nth Tuesday does not necessarily fall on the nth week of the month.

Lets start with this
https://github.com/tc39/proposal-temporal/pull/409

a list of questions tagged date and javascript on Stack Overflow

Thanks @mikemccaughan ill take a look through these

Let's knock some of these out.

N/A - In Temporal we're more strict on input.
https://stackoverflow.com/questions/1353684/detecting-an-invalid-date-date-instance-in-javascript

N/A - comparing dates in Temporal is already well documented, and one of the cookbook examples uses it.
https://stackoverflow.com/questions/492994/compare-two-dates-with-javascript

N/A - Not sure about this one, the intl.DateTimeFormat covers a lot of this already
https://stackoverflow.com/questions/3552461/how-to-format-a-javascript-date

This one keeps getting autoclosed because the cookbook PRs link to it.

I've made a quick cut of most of the mentioned examples in https://github.com/tc39/proposal-temporal/commits/240-more-cookbook

Over the coming days I will continue refining the prose in these, and opening pull requests one by one for review.

To see them rendered (some of them integrate with HTML elements in the cookbook page), check out the branch, run npm build:docs, and open out/docs/cookbook.html in your browser.

Here are notes on some individual examples which would still need to be addressed:

  • getInstantWithLocalTimeInZone - Absolute.prototype.inTimeZone() covers this, but the text mentions several more disambiguation policies than we support. @gibson042, did you intend for all of these to be included ?
  • getInstantOfNearestOffsetTransitionToInstant — This one isn't possible as described with the current API, as TimeZone.getTransitions() is buggy
  • plusMonths — This is identical to Temporal.Date.plus or Temporal.Date.minus. We recently removed disambiguation: 'balance' from arithmetic methods. @gibson042, are "spill-into-following-month" semantics still considered useful enough that we should bring them back in this cookbook example? Otherwise I think it's obvious enough that you should use plus() or minus() here.
  • Integration with wearable devices@gilmoreorless, could you go into more detail about the kind of data obtained from FitBit API requests and how it was to be displayed? Alternatively, maybe we should stick this in the DateTime documentation instead, as an illustration of when you would _not_ use Absolute?
  • getAttenuatedTemporal — Needs to be done after calendars and custom time zones have landed.
  • getTimeZoneObjectFromRules — Needs to be done after custom time zones have landed.
  • makeExpandedTemporal — This is a bit larger project than the other cookbook examples. It should probably be attempted after calendars have landed.
  • Interoperating with SetDynamicTimeZoneInformation — I suggest not doing anything with this. As per the discussion, it's ill-defined which offset is "standard" and which is "daylight" and any attempt at a cookbook recipe would probably be wrong for half the users who might use it.

Since the original goal of this issue was to validate the API design and take note of things which are not ergonomic, here are some points that occurred to me while writing these:

  • There's a lot of Temporal.Something.from() involved, making for long lines. For example, to take a DateTime and get midnight on that day, it's either dateTime.getDate().withTime(Temporal.Time.from('00:00')) (or, even longer if you want to avoid the from(), dateTime.with({ hour: 0, minute: 0, second: 0, microsecond: 0, millisecond: 0, nanosecond: 0 })). It might be more readable to reverse our decision not to take strings in methods like withTime(), plus(), etc.
  • ZonedDateTime was removed because it can easily be replaced by a { dateTime, timeZone } box. I'm not necessarily suggesting we reconsider that, but I did note that it's tempting to use the string returned by absolute.toString(timeZone) instead of the box. This is not necessarily correct, because even though Temporal will still deserialize the string correctly if the time zone changes its offset rules, other programs might not.
  • Likewise, another box that seems to be commonly needed is { sign, duration }. I don't know if it was ever considered to have signed Durations but sign < 0 ? foo.minus(duration) : foo.plus(duration) and sign = Temporal.Foo.compare(one, two) < 0 ? -1 : 1 are pretty cumbersome.

Not allowing negative durations was a conscious decision; there's some discussion here. However, in light of new evidence that this might not be a smart decision from an ergonomics point of view, perhaps that decision should be reconsidered. Not here; in a new thread.

@ptomato I've finally had some time to look at your request (spare time is harder to attain for me in the current global situation 😉). Thank you so much for including all those examples, it's great work!

Integration with wearable devices — @gilmoreorless, could you go into more detail about the kind of data obtained from FitBit API requests and how it was to be displayed? Alternatively, maybe we should stick this in the DateTime documentation instead, as an illustration of when you would _not_ use Absolute?

A good sample of FitBit data is in their API documentation: https://dev.fitbit.com/build/reference/web-api/sleep/

I've edited down an example for clarity (and posterity, if they change their docs). Note the lack of time zone indicator in the ISO strings.

{
  "dateOfSleep": "2017-04-02",
  "levels": {
    "data": [
      {
        "datetime": "2017-04-01T23:58:30.000",
        "level": "wake",
        "seconds": <value>
      },
      {
        "datetime": "2017-04-02T00:16:30.000",
        "level": "rem",
        "seconds": <value>
      },
      <...>
    ],
  },
  "startTime": "2017-04-01T23:58:30.000",
  "timeInBed": <value in minutes>,
  "type": "stages"
}

I don't have a screenshot available of how it was displayed, unfortunately. To be honest, I'm not sure it's worth making a stand-alone cookbook example for this case (although a note in the documentation is a good idea). But if you want an example, I wouldn't mind trying to put up a PR based on your storage tank example (fantastic work BTW, you showed exactly the kind of thing I had to deal with).

I also have some general feedback after looking at all the examples, but I'll put that in a separate comment to keep this one from blowing out.

@ptomato Some general feedback on the cookbook examples.

There's a lot of Temporal.Something.from() involved, making for long lines. For example, to take a DateTime and get midnight on that day, it's either dateTime.getDate().withTime(Temporal.Time.from('00:00')) (or, even longer if you want to avoid the from(), dateTime.with({ hour: 0, minute: 0, second: 0, microsecond: 0, millisecond: 0, nanosecond: 0 })). It might be more readable to reverse our decision not to take strings in methods like withTime(), plus(), etc.

I definitely agree with this. Reading all the .from() code makes the API look very cumbersome to work with, which _may_ hinder adoption. Playing around with some use cases in the console, I found myself automatically writing things like date.withTime('10:00') and getting annoyed that it doesn't work.

Some other things I've noticed:

  • Sort ISO date/time strings — as it's currently written, this isn't a great example of sorting by instants. Because all the instants are on different dates, you'd get the exact same result by sorting the strings alphabetically without using Temporal. Making the strings with named zones happen on the same date would be a better example.
  • UTC offset for a zoned event, as a number of seconds — I'm really surprised this isn't part of the Temporal API, especially given the complexity of getting the value. I mainly say that because the legacy Date returns the offset as a numeric value via .getTimezoneOffset() (albeit as minutes in the "wrong" direction). It would be odd to say that Temporal is a replacement for Date while not providing equivalent functionality for the offset.
  • How many days until a future date — does it need to submit a GET request and reload the page? I thought it was broken at first, until I scrolled all the way back down to the example and realised it had filled in the data.
  • Schedule a reminder ahead of matching a record-setting duration — the opening explanation of this example was quite confusing until I read it a few times. It could do with some rewording for clarity.
  • The formatting of assert.equal statements is inconsistent:
    js // some use this... assert.equal(`${thing}`, ...) // ...and some use this... assert.equal(thing.toString(), ...)
  • I wonder if it's worth adding an example that uses MonthDay? I think it's the only Temporal object not represented so far. Off the top of my head, it could be calculating which day of the week an annual event falls on for a range of years (e.g. a birthday, public holiday).

Thanks for the comprehensive feedback, @gilmoreorless! I found it really helpful.

  • Sort ISO date/time strings — as it's currently written, this isn't a great example of sorting by instants. Because all the instants are on different dates, you'd get the exact same result by sorting the strings alphabetically without using Temporal. Making the strings with named zones happen on the same date would be a better example.

  • How many days until a future date — does it need to submit a GET request and reload the page? I thought it was broken at first, until I scrolled all the way back down to the example and realised it had filled in the data.

  • Schedule a reminder ahead of matching a record-setting duration — the opening explanation of this example was quite confusing until I read it a few times. It could do with some rewording for clarity.

  • The formatting of assert.equal statements is inconsistent: [...]

Thanks, agreed with all of these points. These are hopefully addressed in #529.

  • UTC offset for a zoned event, as a number of seconds — I'm really surprised this isn't part of the Temporal API, especially given the complexity of getting the value. I mainly say that because the legacy Date returns the offset as a numeric value via .getTimezoneOffset() (albeit as minutes in the "wrong" direction). It would be odd to say that Temporal is a replacement for Date while not providing equivalent functionality for the offset.

Good point. In #498 I have proposed an API that does this, so this is another mark in favour of it.

  • I wonder if it's worth adding an example that uses MonthDay? I think it's the only Temporal object not represented so far. Off the top of my head, it could be calculating which day of the week an annual event falls on for a range of years (e.g. a birthday, public holiday).

Good catch. There are some usage examples in the docs but not really any realistic use-cases. I've added one to the in-progress branch, hopefully a bit more realistic: calculating extra "bridge" public holidays to form a long weekend when a yearly holiday falls on a Tuesday or Thursday.


In #529 I've also added a paragraph about wearable devices to the DateTime documentation. If you feel like making a FitBit example based on the storage tank example, go right ahead, I'll be happy to review it! But I think I'd probably not do one myself without having a better idea of what I'd be aiming for.

monthday should be dropped if cookbook example cannot be found. there's no realistic-scenario i can think of where the user would use month-and-day w/o a year-context.

The rent is due on the first of every month, or on the 15th of every month.

My birthday is on the same MonthDay every year.

Many public holidays in every country in the world are on the same MonthDay every year.

Is it really that hard, for example, to think of "Christmas" or "Valentine's Day" or "your birthday" as realistic scenarios?

that's an input-issue that can be elegantly solved with isostring "12-25" (e.g. christmas).

you don't need over-engineered MonthDay if its only purpose is as input for Date.from().

As we've gone over many times, nothing is elegantly solved with a string. MonthDay is a first-class object that can be passed around, with helpful methods attached and with an intention-conveying identity, and that's what's required here.

a MonthDay object is more headache to message-pass between iframes, and ui <-> sql-databases than a simple isostring MM-DD.

So is every other kind of object; that isn't a reason strings are acceptable, it's a reason a reasonable serialization is needed - for MonthDay, it's clearly MM-DD, problem solved.

but that serializtion is unnecessary. just use isostring Date.from(year + "-" + "12-25") for the common input use-case. isostrings are easier for me to inspect/debug during product-integration.

monthday should be dropped if cookbook example cannot be found

This discussion is beside the point, as I mentioned in my comment above I did find a realistic cookbook example and added it to the in-progress branch.

Thanks @ptomato!

Thanks, agreed with all of these points. These are hopefully addressed in #529.

Yep, that looks good. The record-setting example is much easier to comprehend now.

In #529 I've also added a paragraph about wearable devices to the DateTime documentation. If you feel like making a FitBit example based on the storage tank example, go right ahead, I'll be happy to review it! But I think I'd probably not do one myself without having a better idea of what I'd be aiming for.

Cheers, I'll see if I can come up with a good example.

the one-and-only cookbook example showcasing MonthDay (as input-parameter to construct Date) is weak and less elegant than following example using direct MM-DD isostring from web-inputs/databases:

/**
 * Calculates the days that need to be taken off work in order to have a long
 * weekend around a public holiday, "bridging" the holiday if it falls on a
 * Tuesday or Thursday.
 *
- * @param {Temporal.MonthDay} holiday - Yearly date on the calendar
+ * @param {MM-DD} holiday - Yearly date on the calendar
+ * efficiently passed directly from isostring web-inputs and databases
 * @param {number} year - Year in which to calculate the bridge days
 * @returns {Temporal.Date[]} List of dates to be taken off work
 */
function bridgePublicHolidays(holiday, year) {
function bridgePublicHolidays(holiday, year) {
-  const date = holiday.withYear(year);
+  // simple MM-DD regex validation-check
+  if (!(/^\d\d-\d\d$/).test(holiday)) {
+    throw new Error("invalid MM-DD web-input");
+  }
+  const date = Temporal.Date.from(year + "-" + holiday);
  switch (date.dayOfWeek) {
    case 1: // Mon
    case 3: // Wed
    case 5: // Fri
      return [date];
    case 2: // Tue; take Monday off
      return [date.minus({ days: 1 }), date];
    case 4: // Thu; take Friday off
      return [date, date.plus({ days: 1 })];
    case 6: // Sat
    case 7: // Sun
      return [];
  }
}

-const labourDay = Temporal.MonthDay.from('05-01');
+// efficiently pass MM-DD isostring from web-input/database
+// avoiding unnecessary class-instantiation (and garbage-collection)
+const labourDay = '05-01';

// No bridge day
assert.deepEqual(
  bridgePublicHolidays(labourDay, 2020).map((d) => d.toString()),
  ['2020-05-01']
);

// Bridge day
assert.deepEqual(
  bridgePublicHolidays(labourDay, 2018).map((d) => d.toString()),
  ['2018-04-30', '2018-05-01']
);

// Bad luck, the holiday is already on a weekend
assert.deepEqual(
  bridgePublicHolidays(labourDay, 2021).map((d) => d.toString()),
  []
);

why was the previous comment marked off-topic?

why was the previous comment marked off-topic?

Because once again you are spamming an issue with “oh strings are just so much better and the solution to everything”. If you want to actively engage in the discussion, you are more than welcome, but complaining that life with strings is just so much better, is not helpful.

Issues have now been opened for all the discussions raised about changing the API based on writing the cookbook examples.

Here are the cookbook examples that haven't been written yet, that would be good to have. In particular for exercising the custom time zone and custom calendar APIs when they are merged into main.

Once those are written I think we can close this issue, or else close it now and open separate issues for those three.

Isolation

Attenuation

Create an object that supports exactly the same interface as Temporal and is indistinguishable from it (even though side-door means such as Function.prototype.toString) except that it provides no mechanism by which code could use it to determine that the host environment is not executing within a date, time, time zone, and tzdata edition under the control of the creator, without costing the creator any capabilities provided by the native Temporal. This has use for secure environments like SES, but also for purely functional environments like Elm (cf. #103) and for testing.
NOTE: If Temporal were 100% pure and deterministic (like e.g. Array), then the unmodified (except for perhaps being deeply frozen) Temporal object itself would serve this purpose.
cc @erights

function getAttenuatedTemporal( Temporal, attenuations ) {
    // ???
}

Extension

Extra-expanded years

Create a Temporal derivative that supports arbitrarily-large years (e.g., +635427810-02-02) for astronomical purposes, ideally without requiring modifications to year-agnostic interfaces such as Time (but still supporting e.g. UnlimitedTemporal.Time.from("10:23").withDate("+635427810-02-02")).

function makeExpandedTemporal( Temporal ) {
    // ???
}

TimeZone instance

Construct such an object directly from tzdata-compatible rules of arbitrary complexity (e.g., for use in testing).

function getTimeZoneObjectFromRules( rules ) {
    // ???
}

Hooray! +1 for opening a new issue for the remaining cases, and close this issue as fixed. We (you) deserve it!

I apologize for not having time for Temporal lately, but I wanted to address the big comment here in advance of reviewing the full cookbook.

  • getInstantWithLocalTimeInZone - Absolute.prototype.inTimeZone() covers this, but the text mentions several more disambiguation policies than we support. @gibson042, did you intend for all of these to be included ?

Yes, the cookbook getInstantWithLocalTimeInZone needs sufficient configurability for skipped wall-clock times to reject vs. clip to latest preceding instant vs. replace with equivalent post-transition instant and for repeated wall-clock times to reject vs. use first vs. use last. And if that's too difficult with the existing API surface area, then we have a signal that the existing surface area is insufficient.

I don't think polyfill implementation issues should block cookbook recipes coded against the documented API, but presumably this was fixed by #513 anyway.

  • plusMonths — This is identical to Temporal.Date.plus or Temporal.Date.minus. We recently removed disambiguation: 'balance' from arithmetic methods. @gibson042, are "spill-into-following-month" semantics still considered useful enough that we should bring them back in this cookbook example? Otherwise I think it's obvious enough that you should use plus() or minus() here.

Yes, the interesting aspects of this recipe are the disambiguation. plusMonths needs sufficient configurability for addition to reject vs. clip vs. overflow-into-following and for subtraction to reject vs. clip. It should be possible to select among at least the following behaviors by changing only the configured policy:

  • 2021-03-31 plus one month yields 2021-04-30, and 2021-03-31 plus −1 months yields 2021-02-28
  • 2021-03-31 plus one month yields 2021-05-01, and 2021-03-31 plus −1 months yields 2021-02-28
  • 2021-03-31 plus one month yields an error, and 2021-03-31 plus −1 months yields 2021-02-28
  • 2021-03-31 plus one month yields an error, and 2021-03-31 plus −1 months yields an error
  • There's a lot of Temporal.Something.from() involved, making for long lines. For example, to take a DateTime and get midnight on that day, it's either dateTime.getDate().withTime(Temporal.Time.from('00:00')) (or, even longer if you want to avoid the from(), dateTime.with({ hour: 0, minute: 0, second: 0, microsecond: 0, millisecond: 0, nanosecond: 0 })). It might be more readable to reverse our decision not to take strings in methods like withTime(), plus(), etc.

I agree, .withTime("00:00") seems both friendly and unambiguous. That should be the case for every with* method, which can thus follow a common algorithm pattern of initial input type-casting (no-op when a brand-check against valid types passes, otherwise lookup and calling of <TemporalType>.from).

  • ZonedDateTime was removed because it can easily be replaced by a { dateTime, timeZone } box. I'm not necessarily suggesting we reconsider that, but I did note that it's tempting to use the string returned by absolute.toString(timeZone) instead of the box. This is not necessarily correct, because even though Temporal will still deserialize the string correctly if the time zone changes its offset rules, other programs might not.
  • Likewise, another box that seems to be commonly needed is { sign, duration }. I don't know if it was ever considered to have signed Durations but sign < 0 ? foo.minus(duration) : foo.plus(duration) and sign = Temporal.Foo.compare(one, two) < 0 ? -1 : 1 are pretty cumbersome.

Excellent; this is precisely the kind of insight that we were hoping would be provided by having the cookbook. I'm currently inclined towards both Temporal.ZonedDateTime and signed Temporal.Duration, although the latter comes with serialization/deserialization concerns because ISO 8601 durations are unsigned (resulting in some libraries supporting/preferring negative-valued time elements such as a) PT1H-210M while others support/preferring only a single leading negation such as b) -PT2H30M or conceivably even c) P-T2H30—I prefer the latter model, c specifically if there's a gun to my head, but all of the syntax changes make me uncomfortable because 8601 is very particular about designators).

Yes, the cookbook getInstantWithLocalTimeInZone needs sufficient configurability for skipped wall-clock times to reject vs. clip to latest preceding instant vs. replace with equivalent post-transition instant and for repeated wall-clock times to reject vs. use first vs. use last. And if that's too difficult with the existing API surface area, then we have a signal that the existing surface area is insufficient.

In the meantime I did write a cookbook example that does this: https://github.com/tc39/proposal-temporal/blob/main/docs/cookbook/getInstantWithLocalTimeInZone.mjs (It became possible after adding Temporal.TimeZone.prototype.getPossibleAbsolutesFor().)

I don't think polyfill implementation issues should block cookbook recipes coded against the documented API, but presumably this was fixed by #513 anyway.

Yes, this was fixed.

Yes, the interesting aspects of this recipe are the disambiguation. plusMonths needs sufficient configurability for addition to reject vs. clip vs. overflow-into-following and for subtraction to reject vs. clip. It should be possible to select among at least the following behaviors by changing only the configured policy:

  • 2021-03-31 plus one month yields 2021-04-30, and 2021-03-31 plus −1 months yields 2021-02-28

  • 2021-03-31 plus one month yields 2021-05-01, and 2021-03-31 plus −1 months yields 2021-02-28

  • 2021-03-31 plus one month yields an error, and 2021-03-31 plus −1 months yields 2021-02-28

  • 2021-03-31 plus one month yields an error, and 2021-03-31 plus −1 months yields an error

These are covered by, respectively:

  • plus({months: 1}), minus({months: 1}) (i.e. constrain, the default)
  • plus({months: 1}, {disambiguation: 'balance'}) (which we removed, discussion in #344), minus({months: 1})
  • plus({months: 1}, {disambiguation: 'reject'}), minus({months: 1})
  • plus({months: 1}, {disambiguation: 'reject'}), minus({months: 1}, {disambiguation: 'reject'})

I'm not sure I agree that it's critical to have a cookbook recipe for something we couldn't figure out a use case for. But let's continue that discussion in a new issue?

I agree, .withTime("00:00") seems both friendly and unambiguous. That should be the case for every with* method, which can thus follow a common algorithm pattern of initial input type-casting (no-op when a brand-check against valid types passes, otherwise lookup and calling of <TemporalType>.from).

Prior discussion in #237, current feedback thread in #592.

Temporal.ZonedDateTime and signed Temporal.Duration

Current feedback threads in #569 and #558.

Was this page helpful?
0 / 5 - 0 ratings