-
Notifications
You must be signed in to change notification settings - Fork 149
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
Algorithm for balance-constrain #857
Comments
This comment has been minimized.
This comment has been minimized.
Here's one other problematic problematic case: (copying from the open issues section of #856)
|
@sffc sorry for the late reply! In ISO 8601-2 the "composite duration form" was meant to delay the resolution of date time duration until application because some time scale units require contextual information. Specifically, the following time scale unit conversions are contextual (the "conversion boundaries"):
The key point is that there is always a minimally factored form if you really wish to. As with arithmetic factorization, there is a limit on how much we can factor down to. These limits are the conversion boundaries. If we do not care about being particularly accurate, one could simplify to omit some boundaries to adopt "nominal duration", such as ignoring the leap second (i.e. ignore rule 3) and treat 1 minute = 60 seconds. All other conversions can occur interchangeably. Therefore from the 8601 perspective:
Due to rule 3, we cannot convert between M and S until there is contextual information on whether the actual time span includes the last minute of the year (which is when the negative or positive leap second can apply). In the 8601 perspective this should remain in composite form, i.e. intermediate form:
This is possible because the conversion between M <> H is deterministic, i.e. 1H = 60M always stays true (even when the last minute may contain a leap second -- this is the definition of minute by BIPM).
Since S <> M crosses conversion boundary we can't factor down the intermediate form (in the 8601 understanding).
Since H <> M is deterministic, one can freely convert H and M. This means that 1H60S = 60M60S - 122M => -62M60S.
Again this crosses the M <> S boundary, so the intermediate form is the best we can do.
|
Note that in Temporal we take the POSIX route of ignoring leap seconds, so we do indeed omit the minute/second boundary. On the other hand, we add other boundaries because durations can be considered relative to dates in particular time zones with DST changes, or dates in other calendar systems than ISO 8601:
Currently the following results come out of the Temporal polyfill and Temporal spec. Note that the current algorithm is to start by considering the smallest unit, and steal from the next-largest unit ("up first") if its sign is different from the sign of the largest unit in the duration. This does not work if the overall sign flips due to balancing, so that's why case 3 is buggy. So we do need to special-case this. (That means that almost everything in my comment #857 (comment) was wrong; apparently I didn't think hard enough about it, which is not surprising; writing this comment has taken me several hours. The one thing I do think is correct from that comment is that I think this is too detailed for the docs, and belongs in the spec.) Case 1: Temporal.Duration.from({ hours: 5, seconds: 120 }).subtract({ minutes: 1 }) // => PT4H59M120S
Temporal.Duration.from({ hours: 5, seconds: 120 }).subtract({ minutes: 1 }, { overflow: 'balance' }) // => PT5H1M This seems correct given the up-first algorithm. Case 2: Temporal.Duration.from({ seconds: 60 }).subtract({ minutes: 2 }) // => -PT1M Ditto. Case 3: Temporal.Duration.from({ hours: 1, seconds: 60 }).subtract({ minutes: 122 }) This throws, which I believe is definitely a bug, see above. One way to resolve this bug that still seems consistent with the up-first algorithm, would be to check if the sign of the largest unit was flipped as a result of balancing, and perform the algorithm a second time if so. That would give -PT1H1M as the preferred answer to this case. It would have to be guaranteed that performing the algorithm again would not keep flipping the sign, leading to an infinite loop, but I believe that is the case; after one pass of the algorithm, no units should have overflows, so I don't believe it's possible for the sign to flip again. Case 4: Temporal.Duration.from({ seconds: 15 }).subtract({ minutes: 2 }) // => -PT1M45S This one again seems correct given the up-first algorithm we have. Justin's rounding case: Temporal.Duration.from({ hours: 2, minutes: 59, seconds: 55 }).round({ smallestUnit: 'minutes' }) // => PT3H I think we did take care in the API design of round() to make sure that balanced durations don't become unbalanced due to a round operation, by making the largestUnit option default to the largest nonzero unit. So I think this case is sufficiently addressed. |
Another possibility for Case 3: what if we were to adopt #980 as the algorithm to resolve balance-constrain?
The algorithm for #980-style Balance Constrain is simple: use the Balance algorithm, but don't bubble up beyond the highest unit in the input. |
That seems to give identical results to the algorithm I proposed, for at least all the cases that we have in our test suite (including the extra ones I wrote for this issue), and is simpler, so I'm all for it. (Provided that we only apply it when there are mixed signs to be resolved; if you apply it indiscriminately, then it does change the results.) |
I thought we'd already agreed on this, per #856. Specifically, from #856 (comment):
In other words, there is no such thing as "balance constrain" anymore in Temporal. There's just "no balancing" (Duration's Does this match what you guys are thinking? More details about expected behavior are in OP (edited) of #856. Here's some relevant sections:
EDIT: balance-constrain is already removed from balancing.md in the docs. Here's what the docs say now:
|
If we use two different algorithms, it might produce unexpected results. For example, |
Why would we use different algorithms? Why not the simpler "always balance up to largest unit"? |
I suppose that makes this whole issue out of date, then. I had left off #856 until ZonedDateTime lands and hadn't realized that it obsoleted this one. |
Yep, agreed. So sorry @ptomato that you ended up spending a bunch of time on this. I was offline for a few hours or I wouId have responded sooner that this issue is probably moot. It does make me wonder if there are other issues like this one that have been superseded by later decisions. I'll try to take a look through open issues to see if there are any obvious others. |
The current algorithm produces correct results for all the test cases we have, but it will produce wrong results when we fix the bug where addition or subtraction of a duration flips the overall sign of the receiving duration (in cases like, 1 hour, -61 minutes; we cannot test these directly.) See: #857
Removes the 'overflow' option from Duration.add() and Duration.subtract(). This also removes the concept of "balance-constrain." Durations are balanced up to the largest nonzero unit in the input duration, and a different largest unit can be selected with the largestUnit option. The options object in Duration.add() and Duration.subtract() remains for the time being, even though it accepts no valid options, because rounding options will be added in #856. See: #856 Closes: #857
Silver lining is that it wasn't all a waste; the code to implement the proposed balance-constrain algorithm was quite similar to the code to always balance. |
OK cool. BTW I went through all 93 open Temporal issues and didn't find any others that were obviously superseded by recent decisions. There were a few feedback issues where we'd recently improved things and I posed code samples in those. |
Thanks! Once we deliver the proposal to reviewers I intend to spend time updating all the feedback issues. |
The current algorithm produces correct results for all the test cases we have, but it will produce wrong results when we fix the bug where addition or subtraction of a duration flips the overall sign of the receiving duration (in cases like, 1 hour, -61 minutes; we cannot test these directly.) See: #857
Removes the 'overflow' option from Duration.add() and Duration.subtract(). This also removes the concept of "balance-constrain." Durations are balanced up to the largest nonzero unit in the input duration, and a different largest unit can be selected with the largestUnit option. The options object in Duration.add() and Duration.subtract() remains for the time being, even though it accepts no valid options, because rounding options will be added in #856. See: #856 Closes: #857
The current algorithm produces correct results for all the test cases we have, but it will produce wrong results when we fix the bug where addition or subtraction of a duration flips the overall sign of the receiving duration (in cases like, 1 hour, -61 minutes; we cannot test these directly.) See: #857
Removes the 'overflow' option from Duration.add() and Duration.subtract(). This also removes the concept of "balance-constrain." Durations are balanced up to the largest nonzero unit in the input duration, and a different largest unit can be selected with the largestUnit option. The options object in Duration.add() and Duration.subtract() remains for the time being, even though it accepts no valid options, because rounding options will be added in #856. See: #856 Closes: #857
I was reading balancing.md as well as the logic in ecmascript.mjs. I think there are some open questions in the algorithm, in particular, how to resolve mixed-sign durations (either when mixed signs are given in
.from()
or a the result of.plus()
/.minus()
). The doc and the polyfill do not properly support negative durations. I assume this change is being tracked by the negative duration issues, so I won't discuss it here.I think balance is easier to specify, because the answer is unambiguous. Given a mixed-sign duration as input, convert all fields to nanoseconds, add them together, and then bubble the results up through all the fields.
However, with balanceConstrain (or whatever we end up calling it), the contract is that we perform the "minimum necessary balancing" to resolve the mixed signs. This means that there are potentially ambiguous results. Here are some examples:
PT5H120S
minusPT1M
(intermediate form:PT5H-1M120S
). Is the answer:PT5H60S
because we steal 60 seconds from the smaller unit?PT4H59M120S
because we steal 1 minute from the larger unit?PT60S
minusPT2M
(intermediate form:PT-2M60S
). Is the answer:-PT1M
because we resolve seconds into minutes?-PT60S
because we resolve the minutes into seconds?PT1H60S
minusPT122M
(intermediate formPT1H-122M60S
). Is the answer:-PT61M
(minutes only)?-PT3660S
(seconds only)??-PT1H1M
(hours and minutes)??-PT60M60S
(minutes and seconds)??-PT1H60S
(hours and seconds)???PT15S
minusPT2M
(intermediate formPT-2M15S
). Is the answer:-PT1M45S
(minutes and seconds)?-PT105S
(seconds only)?I looked for an algorithm to handle this case in ISO-8601-2, but could not find one (see #819).
I think it would be useful to specify a step-by-step algorithm in balancing.md. Consider thinking about whether we start by balance-constraining "up first" (steal from bigger units) or "down first" (steal from smaller units). Also consider whether we want/need to special-case balance-constrain when the overall sign of the duration changes (
PT1H-75M
), versus when the overall sign will end up staying the same (PT1H-15M
).@justingrant @ptomato
The text was updated successfully, but these errors were encountered: