@gibson042 suggested doing this, and I think it's a great idea that shouldn't be lost. A couple purposes:
Resources for Moment and date-fns, including documentation and stack overflow questions, could be good seed material
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.
Map a legacy ECMAScript Date instance into a Temporal.Absolute instance corresponding to the same instant in absolute time.
function getInstantAtEs2019DateInstance( es2019Date ) {
// ???
}
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]";
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";
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 ) {
// ???
}
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 ) {
// ???
}
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 ) {
// ???
}
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" ) {
// ???
}
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]";
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";
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;
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;
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";
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 ) {
// ???
}
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 ) {
// ???
}
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 ) {
// ???
}
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" ) {
// ???
}
Add the number of days it took to get an approval, and advance to the start of the following month.
function plusAndRoundToMonthStart( date, delayDays ) {
// ???
}
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 ) {
// ???
}
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;
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 ) {
// ???
}
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:
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:
Temporal.now.timeZone()timeZone.getOffsetFor(Temporal.now.absolute())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.
Intl.DateTimeFormat.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, becauseDatedoesn’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)_
- Showing a generic calendar control, with any day beyond “today” disabled and not selectable.
- Simple calculations of “how many days until {future date}”.
Integration with wearable devices.
- e.g. Fitbit always records sleep data in the user’s “local” time, wherever they are in the world. Thus their API returns zoneless datetimes: https://dev.fitbit.com/build/reference/web-api/basics/#time-zones
2. Dealing with dates and times in a time zone that differs from the user’s browser
- 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.
Calculation of recurring schedules for a specific location.
- “Email this generated report at 4pm daily, Brisbane time.”
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?”
- 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
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:
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
dateandjavascripton 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 buggyplusMonths — 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.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.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:
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.{ 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.{ 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 eitherdateTime.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:
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.assert.equal statements is inconsistent:js
// some use this...
assert.equal(`${thing}`, ...)
// ...and some use this...
assert.equal(thing.toString(), ...)
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.equalstatements 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
Datereturns 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 forDatewhile 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
Temporaland is indistinguishable from it (even though side-door means such asFunction.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 nativeTemporal. This has use for secure environments like SES, but also for purely functional environments like Elm (cf. #103) and for testing.
NOTE: IfTemporalwere 100% pure and deterministic (like e.g.Array), then the unmodified (except for perhaps being deeply frozen)Temporalobject itself would serve this purpose.
cc @erightsfunction 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.
- ~
getInstantOfNearestOffsetTransitionToInstant— This one isn't possible as described with the current API, as TimeZone.getTransitions() is buggy~
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 removeddisambiguation: '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:
- 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 eitherdateTime.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 byabsolute.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 butsign < 0 ? foo.minus(duration) : foo.plus(duration)andsign = Temporal.Foo.compare(one, two) < 0 ? -1 : 1are 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
getInstantWithLocalTimeInZoneneeds 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.
plusMonthsneeds 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 everywith*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.
Most helpful comment
I don't know if anyone has looked yet, but looking at a list of questions tagged
dateandjavascripton 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.).