-
Notifications
You must be signed in to change notification settings - Fork 158
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Remove DateTime arithmetic? #407
Comments
I consider |
Meeting, Mar. 5: A valid use case for DateTime arithmetic is when you have a meeting at a particular DateTime (e.g. March 9, 2020, 3 PM) and you need to postpone it to the same time a week later, regardless of whether there is a DST change in between: We agree it's possible to misuse this but it's not necessarily an anti-pattern. We'll close this issue, and @sffc took an action to open a new issue to make sure to explain carefully in the reference docs when to use DateTime arithmetic and when to use Absolute arithmetic. |
App developer here. I'm building a calendar app so know these problems painfully well. Here's a bunch more use-cases:
I could keep going but you get the idea-- there's lots of reasons why you want to add local days, weeks, months, years to datetime values while trying to keep local time constant. The default rule of thumb that I've used is this:
Also (I'm getting on my knees and begging here) PLEASE don't remove local DateTime arithmetic from the Temporal proposal. I've used date math in every big project I've worked on in the last 10 years. And I've learned painfully that date math is really hard to get right, esp. around DST corner cases. Just this week I fixed two different DST-related date math bugs in date-fns which is a relatively mature library. Having corner cases like these solved "officially" in one place would, IMHO, be a big win for the ecosystem. Also, I really, really like the API y'all are designing. I can't wait to be able to use it in production! |
what if the "official" dst calculation in temporals is inconsistent with dst calculations in mysql/sqlserver/postgres/etc backend? most ppl would typically defer to databases as source-of-truth for dst-corrections, rather than javascript. the only way to create temporally-correct programs in javascript (and by correct i mean having results consistent with backend-databases) is to restrict datetime arithmetic to utc in javascript. |
Thanks @justingrant for your two cents, it's always great to hear use-cases from app developers. Please check out our cookbook at https://tc39.es/proposal-temporal/docs/cookbook.html which has a bunch of example that might interest you. We'd also love to hear back from you if you have any ideas for the cookbook or any general suggestions 😇 |
@kaizhu256, this is not a big concern for me. For a few reasons:
|
@ryzokuken - Thanks! I looked through the docs and filed a few issues and suggestions. Great work on this, BTW. |
Whoa. When I first commented above, I didn't realize that Without DST awareness, offering The underlying problem is that there are two subtly different operations implied with date math. "Add one hour" could mean "advance the wall clock by one hour" (ignore DST) or "one hour later in the real world" (account for DST). In my experience (~10 years of time-series-data reporting apps + 2 years building a calendaring app) the "once hour later in the real world" is used at least 10x (maybe 100x?) more than the "move the clock later by one hour" case. IMHO it's bad if the default API path leads devs to the least common use-case. Especially if discovering that it's the wrong API is only obvious on two days per year. My first-choice recommendation would be to add timezone awareness to DateTime which would enable its arithmetic behavior to match all other major JS libraries' behavior and would solve tricky cases like subtracting one hour from the end of DST which should yield the same clock time but a different underlying instant. My second choice would be to add a required TimeZone parameter to all DateTime arithmetic methods. Devs who want ignore-DST behavior could pass in the UTC timezone. But this would need to be a required parameter. If choosing to ignore DST isn't opt-in, it'll be a bug factory for inexperienced app devs. True, this would make the "ignore DST" API harder to use, but honestly that's a relatively unusual case anyways so adding more hassle seems OK. Also, #568 would make this option a little easier. My third choice would be to move add/subtract/difference APIs from DateTime to TimeZone. This wouldn't be as discoverable, but at least it'd provide a one-line-of-code way to do DST-aware math for devs who could find it. My fourth choice would be to remove add/subtract/difference APIs from DateTime, and instead document how to build these APIs on top of TimeZone and Absolute classes. I think this would be a considerable loss for the usefulness of Temporal because hand-rolling date math is hard! The only option that I think would be a disastrous outcome would be the current state because many devs would probably make the same mistake I did: assume that DST is handled by DateTime when it's not. Sorry again for not having this context when I originally commented above... it's taken me a day to wrap my head around the proposal. |
This is good feedback. Wall clock time is unintuitive to many (most) unversed programmers to whom I've described it. Much (most) of the time, programmers want an Absolute. However, there are some legitimate use cases for wall clock DateTime, and having a first-class type makes those use cases significantly more ergonomic than requiring that you carry a I'll reopen the issue to discuss at the next Temporal Champions call. FYI:
It's not that hard to do. const tz = Temporal.TimeZone.from("America/Chicago");
const newDateTime = oldDateTime.toAbsolute(tz)
.plus(Temporal.Duration.from({ days: 2 }))
.toDateTime(tz); |
The relevant part of our previous discussion is at https://github.com/tc39/proposal-temporal/blob/main/meetings/agenda-minutes-2020-03-05.md |
The current position of the Temporal Champions Group is this one:
|
Great discussion. A few notes:
It's not necessarily hard in lines of code... it's conceptually hard. And therefore easy to introduce bugs. For example, your code above works great for " But " For this reason, the default add/subtract behavior of moment and date-fns acts like |
Last night I realized that
For all hours of the year except one weird two-hour period, So I'd suggest adding |
FWIW, the same DST-unsafe problems are triggered by the |
Thanks @justingrant, I very much appreciate your feedback and especially your use cases. Could I push you a little bit further and ask for some example scenarios in which you anticipate time-zone-ignorant DateTime arithmetic (including |
I think much of the problem here is that the difference between absolute time and clock time is unknown or unintuitive to many programmers. See #569 for other potential solutions. |
@gibson042 - Happy to help. I listed three examples below, with code to illustrate the problem. Caveat: I didn't actually run the code so there may be errors! The general problem that unifies all three examples is that DST-safe code usually needs to perform Time arithmetic in Absolute terms (e.g. "meet me at the bar in 2 hours, not 1 or 3 hours if DST happens tonight") but Date arithmetic using DateTime/Wall-Clock semantics, e.g. "Alexa, reschedule my Friday night dinner reservation for one week later" where local time stays constant as days/weeks/etc are changed. This expectation difference between time math vs. date math is intuitive once it's explained, but it's unknown to most developers. Therefore, having it implemented in a library is important to avoid bugs. For Temporal, this could mean that DateTime (or some class with DateTime's API e.g. Temporal.BikeShed) would modify its Example 1: Date Arithmetic Across Time Zones // correct logic is hard
function getArrivalTimeFromReminder(reminderInstant, departureTz, arrivalTz) {
const localNow = reminderInstant.inTimeZone(departureTz)
// date math in local timezone in case DST starts/ends tonight
const departureLocal = localNow.plus({days:1})
const departureInstant = departureLocal.inTimeZone(departureTz)
// hours math in Absolute because the length of the flight is
// not affected by DST
const arrivalInstant = departureInstant.plus({hours:8})
const arrivalLocal = arrivalInstant.inTimeZone(arrivalTz)
return arrivalLocal.getTime()
}
// Lazy/novice developers can write the code below that works
// almost all the time. But if there's a nearby DST transition, the
// function could return a time that's wrong by 1 or even 2 hours.
function getArrivalTimeFromReminder(reminderInstant, departureTz, arrivalTz) {
const localNow = reminderInstant.inTimeZone(departureTz)
const arrivalLocal = localNow.plus({days:1, hours:8}).inTimeZone(arrivalTz)
return arrivalLocal.getTime()
}
// API changes that would make the code above DST-safe:
// 1. `Absolute.inTimeZone` must return an object that knows its timezone
// 2. By default, `add` should act like DateTime.add for the Date portion,
// but act like Absolute.add for the Time portion. Example 2: Finding Events from 0:00-12:00 today // correct code
function callApi () {
const localTz = Temporal.now.timeZone()
const midnight = Temporal.now.dateTime().withTime({hour: 0})
const noon = today.plus({hours: 12})
return myApi(midnight.inTimeZone(tz), noon.inTimeZone(tz))
}
// will return an extra hour or missing hour of events if DST starts/ends today
function callApi () {
const localTz = Temporal.now.timeZone()
const midnight = Temporal.now.DateTime.withTime({hour: 0}).inTimeZone(localTz)
const noon = today.plus({days: 1}) // bug! Need to use DateTime.add
return myApi(midnight, noon)
}
// API changes that would make the code above DST-safe:
// 1. `Absolute.inTimeZone` returns an object that knows its timezone
// 2. By default, `add` should act like DateTime.add for the Date portion,
// but act like Absolute.add for the Time portion. Example 3: Sorting Today's Events // Correct code: sort timestamps before converting to local time
const localTz = Temporal.now.timeZone()
const sortedEvents = events.sort((r1, r2) => r1.timestamp.compare(r2.timestamp))
const sortedRecords = sortedEvents.map(e => ({
time: e.timestamp.inTimeZone(localTz).getTime(),
id: e.id
}))
// This code may sort out-of-order during the hour before & after DST ends
// where later-timestamped events may have earlier clock times.
const localTz = Temporal.now.timeZone()
const records = events.map(e => ({
time: e.timestamp.inTimeZone(localTz).getTime(),
id: e.id})
}))
const sortedRecords = records.sort((r1, r2) => r1.time.compare(r2.time)) |
To me this is such a hard disagree. DateTime arithmetic is a key functionality that removing it obviates the proposal. |
The idea from here is to ship the polyfill with Temporal.DateTime arithmetic included, see how it's received, and consider revisiting before Stage 3. |
I think this can be closed, since we are addressing the DST-safety problems in another way by adding a zoned type. |
While discussing #307 in the Feb. 27 meeting it was remarked that any arithmetic results you get with DateTime are potentially wrong due to DST changes, so perhaps it's an antipattern to do DateTime arithmetic.
Here are the use cases that we think should be supported, which don't require DateTime.difference:
Additionally, does it make sense to keep DateTime.plus and DateTime.minus without DateTime.difference? Their results may also be incorrect if adding/subtracting across a DST boundary.
The text was updated successfully, but these errors were encountered: