Skip to content

Commit 5dd0b0d

Browse files
catamorphismptomato
authored andcommitted
Normative: Change NudgeToCalendarUnit to use relative date when comparing durations
See #3168
1 parent 0df570c commit 5dd0b0d

File tree

2 files changed

+148
-32
lines changed

2 files changed

+148
-32
lines changed

polyfill/lib/ecmascript.mjs

Lines changed: 88 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3169,40 +3169,34 @@ export function DifferenceZonedDateTime(ns1, ns2, timeZone, calendar, largestUni
31693169
return CombineDateAndTimeDuration(dateDifference, timeDuration);
31703170
}
31713171

3172-
// Epoch-nanosecond bounding technique where the start/end of the calendar-unit
3173-
// interval are converted to epoch-nanosecond times and destEpochNs is nudged to
3174-
// either one.
3175-
function NudgeToCalendarUnit(
3172+
// Returns: { r1, r2, startEpochNs, endEpochNs, startDuration, endDuration }
3173+
function ComputeNudgeWindow(
31763174
sign,
31773175
duration,
31783176
originEpochNs,
3179-
destEpochNs,
31803177
isoDateTime,
31813178
timeZone,
31823179
calendar,
31833180
increment,
31843181
unit,
3185-
roundingMode
3182+
additionalShift
31863183
) {
3187-
// unit must be day, week, month, or year
3188-
// timeZone may be undefined
3189-
31903184
// Create a duration with smallestUnit trunc'd towards zero
31913185
// Create a separate duration that incorporates roundingIncrement
31923186
let r1, r2, startDuration, endDuration;
31933187
switch (unit) {
31943188
case 'year': {
31953189
const years = RoundNumberToIncrement(duration.date.years, increment, 'trunc');
3196-
r1 = years;
3197-
r2 = years + increment * sign;
3190+
r1 = !additionalShift ? years : years + increment * sign;
3191+
r2 = r1 + increment * sign;
31983192
startDuration = { years: r1, months: 0, weeks: 0, days: 0 };
31993193
endDuration = { ...startDuration, years: r2 };
32003194
break;
32013195
}
32023196
case 'month': {
32033197
const months = RoundNumberToIncrement(duration.date.months, increment, 'trunc');
3204-
r1 = months;
3205-
r2 = months + increment * sign;
3198+
r1 = !additionalShift ? months : months + increment * sign;
3199+
r2 = r1 + increment * sign;
32063200
startDuration = AdjustDateDurationRecord(duration.date, 0, 0, r1);
32073201
endDuration = AdjustDateDurationRecord(duration.date, 0, 0, r2);
32083202
break;
@@ -3240,7 +3234,7 @@ function NudgeToCalendarUnit(
32403234
// If the start of the bound is the same as the "origin" (aka relativeTo),
32413235
// use the origin's epoch-nanoseconds as-is instead of relying on isoDateTime,
32423236
// which then gets zoned and converted back to epoch-nanoseconds,
3243-
// which looses precision and creates a distorted bounding window.
3237+
// which loses precision and creates a distorted bounding window.
32443238
startEpochNs = originEpochNs;
32453239
} else {
32463240
const start = CalendarDateAdd(calendar, isoDateTime.isoDate, startDuration, 'constrain');
@@ -3257,9 +3251,85 @@ function NudgeToCalendarUnit(
32573251
? GetEpochNanosecondsFor(timeZone, endDateTime, 'compatible')
32583252
: GetUTCEpochNanoseconds(endDateTime);
32593253

3254+
return { r1, r2, startEpochNs, endEpochNs, startDuration, endDuration };
3255+
}
3256+
3257+
// Epoch-nanosecond bounding technique where the start/end of the calendar-unit
3258+
// interval are converted to epoch-nanosecond times and destEpochNs is nudged to
3259+
// either one.
3260+
function NudgeToCalendarUnit(
3261+
sign,
3262+
duration,
3263+
originEpochNs,
3264+
destEpochNs,
3265+
isoDateTime,
3266+
timeZone,
3267+
calendar,
3268+
increment,
3269+
unit,
3270+
roundingMode
3271+
) {
3272+
// unit must be day, week, month, or year
3273+
// timeZone may be undefined
3274+
3275+
var didExpandCalendarUnit = false;
3276+
let nudgeWindow = ComputeNudgeWindow(
3277+
sign,
3278+
duration,
3279+
originEpochNs,
3280+
isoDateTime,
3281+
timeZone,
3282+
calendar,
3283+
increment,
3284+
unit,
3285+
false
3286+
);
3287+
var { r1, r2, startEpochNs, endEpochNs, startDuration, endDuration } = nudgeWindow;
3288+
32603289
// Round the smallestUnit within the epoch-nanosecond span
3261-
if (sign === 1) assert(startEpochNs.leq(destEpochNs) && destEpochNs.leq(endEpochNs), `${unit} was 0 days long`);
3262-
if (sign === -1) assert(endEpochNs.leq(destEpochNs) && destEpochNs.leq(startEpochNs), `${unit} was 0 days long`);
3290+
if (sign === 1) {
3291+
if (!(nudgeWindow.startEpochNs.leq(destEpochNs) && destEpochNs.leq(nudgeWindow.endEpochNs))) {
3292+
// Retry nudge window if it's out of bounds
3293+
nudgeWindow = ComputeNudgeWindow(
3294+
sign,
3295+
duration,
3296+
originEpochNs,
3297+
isoDateTime,
3298+
timeZone,
3299+
calendar,
3300+
increment,
3301+
unit,
3302+
true
3303+
);
3304+
assert(
3305+
nudgeWindow.startEpochNs.leq(destEpochNs) && destEpochNs.leq(nudgeWindow.endEpochNs),
3306+
`${unit} was 0 days long`
3307+
);
3308+
didExpandCalendarUnit = true;
3309+
}
3310+
} else if (sign == -1) {
3311+
if (!(nudgeWindow.endEpochNs.leq(destEpochNs) && destEpochNs.leq(nudgeWindow.startEpochNs))) {
3312+
// Retry nudge window if it's out of bounds
3313+
nudgeWindow = ComputeNudgeWindow(
3314+
sign,
3315+
duration,
3316+
originEpochNs,
3317+
isoDateTime,
3318+
timeZone,
3319+
calendar,
3320+
increment,
3321+
unit,
3322+
true
3323+
);
3324+
assert(
3325+
nudgeWindow.endEpochNs.leq(destEpochNs) && destEpochNs.leq(nudgeWindow.startEpochNs),
3326+
`${unit} was 0 days long`
3327+
);
3328+
didExpandCalendarUnit = true;
3329+
}
3330+
}
3331+
({ r1, r2, startEpochNs, endEpochNs, startDuration, endDuration } = nudgeWindow);
3332+
32633333
assert(!endEpochNs.equals(startEpochNs), 'startEpochNs must ≠ endEpochNs');
32643334
const numerator = TimeDuration.fromEpochNsDiff(destEpochNs, startEpochNs);
32653335
const denominator = TimeDuration.fromEpochNsDiff(endEpochNs, startEpochNs);
@@ -3278,8 +3348,8 @@ function NudgeToCalendarUnit(
32783348
assert(MathAbs(r1) <= MathAbs(total) && MathAbs(total) <= MathAbs(r2), 'r1 ≤ total ≤ r2');
32793349

32803350
// Determine whether expanded or contracted
3281-
const didExpandCalendarUnit = roundedUnit === MathAbs(r2);
3282-
duration = { date: didExpandCalendarUnit ? endDuration : startDuration, time: TimeDuration.ZERO };
3351+
didExpandCalendarUnit ||= roundedUnit === MathAbs(r2);
3352+
duration = { date: roundedUnit == MathAbs(r2) ? endDuration : startDuration, time: TimeDuration.ZERO };
32833353

32843354
const nudgeResult = {
32853355
duration,

spec/duration.html

Lines changed: 60 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1564,38 +1564,43 @@ <h1>Duration Nudge Result Records</h1>
15641564
</emu-table>
15651565
</emu-clause>
15661566

1567-
<emu-clause id="sec-temporal-nudgetocalendarunit" type="abstract operation">
1567+
<emu-clause id="sec-temporal-computenudgewindow" type="abstract operation">
15681568
<h1>
1569-
NudgeToCalendarUnit (
1569+
ComputeNudgeWindow (
15701570
_sign_: -1 or 1,
15711571
_duration_: an Internal Duration Record,
15721572
_originEpochNs_: a BigInt,
1573-
_destEpochNs_: a BigInt,
15741573
_isoDateTime_: an ISO Date-Time Record,
15751574
_timeZone_: an available time zone identifier or ~unset~,
15761575
_calendar_: a calendar type,
15771576
_increment_: a positive integer,
15781577
_unit_: a date unit,
1579-
_roundingMode_: a rounding mode,
1580-
): either a normal completion containing a Record with fields [[NudgeResult]] (a Duration Nudge Result Record) and [[Total]] (a mathematical value), or a throw completion
1578+
_additionalShift_: a Boolean,
1579+
): either a normal completion containing a Record with fields [[R1]] (a mathematical value), [[R2]] (a mathematical value), [[StartEpochNs]] (a BigInt), [[EndEpochNs]] (a BigInt), [[StartDuration]] (an Internal Duration Record), and [[EndDuration]] (an Internal Duration Record), or a throw completion
15811580
</h1>
15821581
<dl class="header">
15831582
<dt>description</dt>
15841583
<dd>
1585-
It implements rounding a duration to an increment of a calendar unit, relative to a starting point, by calculating the upper and lower bounds of the starting point added to the duration in epoch nanoseconds, and rounding according to which one is closer to _destEpochNs_.
1584+
It implements calculating the upper and lower bounds of the starting point added to the duration in epoch nanoseconds.
15861585
</dd>
15871586
</dl>
15881587
<emu-alg>
15891588
1. If _unit_ is ~year~, then
15901589
1. Let _years_ be RoundNumberToIncrement(_duration_.[[Date]].[[Years]], _increment_, ~trunc~).
1591-
1. Let _r1_ be _years_.
1592-
1. Let _r2_ be _years_ + _increment_ × _sign_.
1590+
1. If _additionalShift_ is *false*, then
1591+
1. Let _r1_ be _years_.
1592+
1. Else,
1593+
1. Let _r1_ be _years_ + _increment_ × _sign_.
1594+
1. Let _r2_ be _r1_ + _increment_ × _sign_.
15931595
1. Let _startDuration_ be ? CreateDateDurationRecord(_r1_, 0, 0, 0).
15941596
1. Let _endDuration_ be ? CreateDateDurationRecord(_r2_, 0, 0, 0).
15951597
1. Else if _unit_ is ~month~, then
15961598
1. Let _months_ be RoundNumberToIncrement(_duration_.[[Date]].[[Months]], _increment_, ~trunc~).
1597-
1. Let _r1_ be _months_.
1598-
1. Let _r2_ be _months_ + _increment_ × _sign_.
1599+
1. If _additionalShift_ is *false*, then
1600+
1. Let _r1_ be _months_.
1601+
1. Else,
1602+
1. Let _r1_ be _months_ + _increment_ × _sign_.
1603+
1. Let _r2_ be _r1_ + _increment_ × _sign_.
15991604
1. Let _startDuration_ be ? AdjustDateDurationRecord(_duration_.[[Date]], 0, 0, _r1_).
16001605
1. Let _endDuration_ be ? AdjustDateDurationRecord(_duration_.[[Date]], 0, 0, _r2_).
16011606
1. Else if _unit_ is ~week~, then
@@ -1632,10 +1637,52 @@ <h1>
16321637
1. Let _endEpochNs_ be GetUTCEpochNanoseconds(_endDateTime_).
16331638
1. Else,
16341639
1. Let _endEpochNs_ be ? GetEpochNanosecondsFor(_timeZone_, _endDateTime_, ~compatible~).
1640+
1. Return the Record { [[R1]]: _r1_, [[R2]]: _r2_, [[StartEpochNs]]: _startEpochNs_, [[EndEpochNs]]: _endEpochNs_, [[StartDuration]]: _startDuration_, [[EndDuration]]: _endDuration_ }.
1641+
</emu-alg>
1642+
</emu-clause>
1643+
1644+
<emu-clause id="sec-temporal-nudgetocalendarunit" type="abstract operation">
1645+
<h1>
1646+
NudgeToCalendarUnit (
1647+
_sign_: -1 or 1,
1648+
_duration_: an Internal Duration Record,
1649+
_originEpochNs_: a BigInt,
1650+
_destEpochNs_: a BigInt,
1651+
_isoDateTime_: an ISO Date-Time Record,
1652+
_timeZone_: an available time zone identifier or ~unset~,
1653+
_calendar_: a calendar type,
1654+
_increment_: a positive integer,
1655+
_unit_: a date unit,
1656+
_roundingMode_: a rounding mode,
1657+
): either a normal completion containing a Record with fields [[NudgeResult]] (a Duration Nudge Result Record) and [[Total]] (a mathematical value), or a throw completion
1658+
</h1>
1659+
<dl class="header">
1660+
<dt>description</dt>
1661+
<dd>
1662+
It implements rounding a duration to an increment of a calendar unit, relative to a starting point, by calculating the upper and lower bounds of the starting point added to the duration in epoch nanoseconds, and rounding according to which one is closer to _destEpochNs_.
1663+
</dd>
1664+
</dl>
1665+
<emu-alg>
1666+
1. Let _didExpandCalendarUnit_ be *false*.
1667+
1. Let _nudgeWindow_ be ? ComputeNudgeWindow(_sign_, _duration_, _originEpochNs_, _isoDateTime_, _timeZone_, _calendar_, _increment_, _unit_, *false*).
1668+
1. Let _startEpochNs_ be _nudgeWindow_.[[StartEpochNs]].
1669+
1. Let _endEpochNs_ be _nudgeWindow_.[[EndEpochNs]].
16351670
1. If _sign_ is 1, then
1636-
1. Assert: _startEpochNs__destEpochNs__endEpochNs_.
1671+
1. If _startEpochNs__destEpochNs__endEpochNs_ is *false*, then
1672+
1. Set _nudgeWindow_ to ? ComputeNudgeWindow(_sign_, _duration_, _originEpochNs_, _isoDateTime_, _timeZone_, _calendar_, _increment_, _unit_, *true*).
1673+
1. Assert: _nudgeWindow_.[[StartEpochNs]] ≤ _destEpochNs__nudgeWindow_.[[EndEpochNs]].
1674+
1. Set _didExpandCalendarUnit_ to *true*.
16371675
1. Else,
1638-
1. Assert: _endEpochNs__destEpochNs__startEpochNs_.
1676+
1. If _endEpochNs__destEpochNs__startEpochNs_ is *false*, then
1677+
1. Set _nudgeWindow_ to ? ComputeNudgeWindow(_sign_, _duration_, _originEpochNs_, _isoDateTime_, _timeZone_, _calendar_, _increment_, _unit_, *true*).
1678+
1. Assert: _nudgeWindow_.[[EndEpochNs]] ≤ _destEpochNs__nudgeWindow_.[[StartEpochNs]].
1679+
1. Set _didExpandCalendarUnit_ to *true*.
1680+
1. Let _r1_ be _nudgeWindow_.[[R1]].
1681+
1. Let _r2_ be _nudgeWindow_.[[R2]].
1682+
1. Set _startEpochNs_ to _nudgeWindow_.[[StartEpochNs]].
1683+
1. Set _endEpochNs_ to _nudgeWindow_.[[StartEpochNs]].
1684+
1. Let _startDuration_ be _nudgeWindow_.[[StartDuration]].
1685+
1. Let _endDuration_ be _nudgeWindow_.[[EndDuration]].
16391686
1. Assert: _startEpochNs__endEpochNs_.
16401687
1. Let _progress_ be (_destEpochNs_ - _startEpochNs_) / (_endEpochNs_ - _startEpochNs_).
16411688
1. Let _total_ be _r1_ + _progress_ × _increment_ × _sign_.
@@ -1649,11 +1696,10 @@ <h1>
16491696
1. Assert: abs(_r1_) ≤ abs(_total_) &lt; abs(_r2_).
16501697
1. Let _roundedUnit_ be ApplyUnsignedRoundingMode(abs(_total_), abs(_r1_), abs(_r2_), _unsignedRoundingMode_).
16511698
1. If _roundedUnit_ is abs(_r2_), then
1652-
1. Let _didExpandCalendarUnit_ be *true*.
1699+
1. Set _didExpandCalendarUnit_ to *true*.
16531700
1. Let _resultDuration_ be _endDuration_.
16541701
1. Let _nudgedEpochNs_ be _endEpochNs_.
16551702
1. Else,
1656-
1. Let _didExpandCalendarUnit_ be *false*.
16571703
1. Let _resultDuration_ be _startDuration_.
16581704
1. Let _nudgedEpochNs_ be _startEpochNs_.
16591705
1. Set _resultDuration_ to CombineDateAndTimeDuration(_resultDuration_, 0).

0 commit comments

Comments
 (0)