Skip to content
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

Arithmetic with out-of-scope duration fields #324

Closed
sffc opened this issue Jan 24, 2020 · 20 comments · Fixed by #418
Closed

Arithmetic with out-of-scope duration fields #324

sffc opened this issue Jan 24, 2020 · 20 comments · Fixed by #418
Assignees

Comments

@sffc
Copy link
Collaborator

sffc commented Jan 24, 2020

See #388 for the original discussion regarding Temporal.Duration and disambiguation = "balance"

@alanhussey

This comment has been minimized.

@ptomato

This comment has been minimized.

@sffc

This comment has been minimized.

@sffc

This comment has been minimized.

@ptomato
Copy link
Collaborator

ptomato commented Feb 16, 2020

I agree it should be valid to have a Duration object with too-high values. However I just realized that means we need to re-think which Duration fields we allow in the various arithmetic methods.

For example we throw a RangeError if you do this:

const wallClockNow = Temporal.now.time()
wallClockNow.plus({days: 2})  // throws if one of days, months, or years ≠ 0

but not this:

wallClockNow.plus({hours: 48})  // doesn't throw

This also goes for MonthDay since you can add or subtract months ≥ 12 or days ≥ 365.

@ljharb
Copy link
Member

ljharb commented Feb 16, 2020

I'm not sure what the rationale would be that would lead the first example to throw. Anything that lets me "plus" anything should let me plus everything, no?

@sffc
Copy link
Collaborator Author

sffc commented Feb 16, 2020

I'm not sure what the rationale would be that would lead the first example to throw. Anything that lets me "plus" anything should let me plus everything, no?

I misread the post too. What @ptomato is saying is that since Temporal.now.time() is a Temporal.Time, which does not have a year/month/day field, it does not make sense to add years, months, or days to it.

In situations like this, we could:

  1. Throw RangeError if there are fields in the duration that aren't in the receiver object
  2. Silently ignore the out-of-range fields
  3. EDIT 2020-03-05: Before performing arithmetic, balance small out-of-range fields up into the smallest valid field, and ignore large out-of-range fields

Note: @pipobscure has pointed out before that since the durations can be data-driven, like reading a string from a database or from user input, it would be undesirable to have .plus/.minus throw RangeError dependent on the fields in the duration.

@younies Thoughts on this?

@gibson042
Copy link
Collaborator

Other cases of particular interest include Temporal.Date.from("2020-02-16").plus({hours: 36}) and especially Temporal.Date.from("2020-02-16").plus(Temporal.Duration.from({hours: 36})), where ignoring the non-overlapping fields will definitely surprise authors.

@ptomato
Copy link
Collaborator

ptomato commented Feb 19, 2020

I'd propose option 2 for units that are too large for the type (e.g. adding years to Time), because they have no effect on the result. However, I'd propose option 3 for units that are too small for the type (e.g. adding hours to Date) because of what @gibson042 mentioned above.

@sffc
Copy link
Collaborator Author

sffc commented Feb 19, 2020

If we do option 3 only in one direction, what happens when you do

let ym = // some Temporal.YearMonth
ym.plus({ days: 30 })

The number of days depends on the month of course.

I guess the semantic could be: expand the type to DateTime (both Date and YearMonth) at midnight on the first of the month; add the fields; disambiguate according to the input option; and then convert back to the input type (which truncates). Does that work?

@ptomato
Copy link
Collaborator

ptomato commented Feb 19, 2020

That would work IMO.

On the other hand maybe the answer is to treat YearMonth differently than Date since date.plus({hours: 24}) is unambiguously the same date while yearMonth.plus({days: 30}) is not.

@sffc
Copy link
Collaborator Author

sffc commented Feb 19, 2020

If you want YearMonth arithmetic to be predictable, then don't give it days. This is an edge case where we're trying to figure out the best behavior.

@sffc sffc changed the title Temporal.Duration and disambiguation = "balance" Arithmetic with out-of-scope duration fields Feb 19, 2020
@sffc
Copy link
Collaborator Author

sffc commented Feb 19, 2020

There are two discussions going on here. Since most of this thread is about the issue @ptomato raised about out-of-scope fields, I renamed this issue to address that particular issue. I moved the original question about disambiguation="balance" behavior to #388.

@gibson042
Copy link
Collaborator

gibson042 commented Feb 19, 2020

For the new question at hand here, I think it's fine to ignore fields that are too large, but methods should throw when the input includes nonzero fields that are too small for their type (because there's no general way to support them).

let date = Temporal.Date.from("2020-02-16");
assert.strictEquals(date.plus({hours: 0}).toString(), "2020-02-16");
Array(60).fill().map((_, i) => i + 1).forEach(hours =>
  assert.throws(() => date.plus({hours}));
);

let yearMonth = Temporal.YearMonth.from("2020-02");
assert.strictEquals(yearMonth.plus({days: 0}).toString(), "2020-02");
Array(60).fill().map((_, i) => i + 1).forEach(days =>
  assert.throws(() => yearMonth.plus({days}));
);

If you want to do arithmetic with smaller units, then first map the receiver to a type that understands them (e.g., date.constructor.from(date.withTime("00:00").plus({hours: 24})) or yearMonth.constructor.from(yearMonth.withDay(1).plus({days: 30}))).

@sffc
Copy link
Collaborator Author

sffc commented Feb 19, 2020

I'm slightly confused by your comment, because 3 days ago, you said:

Other cases of particular interest include Temporal.Date.from("2020-02-16").plus({hours: 36}) and especially Temporal.Date.from("2020-02-16").plus(Temporal.Duration.from({hours: 36})), where ignoring the non-overlapping fields will definitely surprise authors.

Now you are suggesting that we throw when the fields are too small.

@gibson042
Copy link
Collaborator

gibson042 commented Feb 19, 2020

Ignoring the non-overlapping fields will definitely surprise authors, so they should not be ignored. And because too-small fields cannot be processed in the general case (e.g. adding 36 hours to a Temporal.Date), the proper alternative to ignoring them is rejection.

Too-large fields that can't affect the calculation (e.g., time.plus({days: 1})) are safe to ignore because that is identical to processing them, but if they can affect the calculation then they too should be rejected.

@sffc
Copy link
Collaborator Author

sffc commented Mar 5, 2020

Consensus is to go with Option 3 from #324 (comment)

@gibson042
Copy link
Collaborator

So what happens for Temporal.Date.from("2020-02-16").plus({hours: 36}), and why is that different from either Temporal.Date.from("2020-02-16").plus({hours: 24}) or Temporal.Date.from("2020-02-16").plus({hours: 48})?

@sffc
Copy link
Collaborator Author

sffc commented Mar 6, 2020

So what happens for Temporal.Date.from("2020-02-16").plus({hours: 36})

The duration would be balanced into {days: 1, hours: 12}, and you would add 1 day to the Date, ignoring the small units.

and why is that different from either Temporal.Date.from("2020-02-16").plus({hours: 24})

The answer would be the same for all hours between 24 and 47 inclusive.

or Temporal.Date.from("2020-02-16").plus({hours: 48})?

Now you're adding 2 days.

@gibson042
Copy link
Collaborator

I object to that treatment, but am not prepared to fight consensus at this time.

ryzokuken pushed a commit that referenced this issue Mar 30, 2020
* Arithmetic with out-of-scope units for Date, Time, YearMonth

Out-of-scope units are ignored if they are higher than the type's units
and wouldn't have any effect (e.g. adding years to a Time).

Out-of-scope units are balanced up to the type's lowest unit if they are
lower than the type's units, and any remaining fractions of the type's
lowest unit are ignored (e.g. adding 36 hours to a Date adds 1 day.) In
the case of YearMonth, days are balanced into months based on the first
day of the month.

Closes: #324 

Co-Authored-By: Ms2ger <Ms2ger@gmail.com>

Co-authored-by: Ms2ger <Ms2ger@gmail.com>
ptomato added a commit that referenced this issue Mar 26, 2024
Ms2ger pushed a commit that referenced this issue Mar 28, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants