Skip to content

Commit 6a79293

Browse files
committed
Normative: Prevent indefinite loops in NormalizedTimeDurationToDays
It's possible to make at least the second loop continue indefinitely with a contrived calendar and time zone. DRAFT: Still to be determined if this precludes any non-contrived use cases. If so, we will keep the loops, but still put an upper limit on the number of iterations. Includes a few more tests in the NYSE time zone cookbook example to make sure that a time zone transition of >24h continues to work.
1 parent 8b6cbab commit 6a79293

File tree

3 files changed

+66
-35
lines changed

3 files changed

+66
-35
lines changed

docs/cookbook/stockExchangeTimeZone.mjs

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,30 @@ assert.equal(monday.hoursInDay, 24);
222222
const friday = monday.add({ days: 4 });
223223
assert.equal(friday.hoursInDay, 72);
224224

225-
// Adding 1 day to Friday gets you the next Monday
225+
// Adding 1 day to Friday gets you the next Monday (disambiguates forward)
226226
assert.equal(friday.add({ days: 1 }).toString(), '2022-08-29T09:30:00-04:00[NYSE]');
227227
// Adding 3 days to Friday also gets you the next Monday
228228
assert.equal(friday.add({ days: 3 }).toString(), '2022-08-29T09:30:00-04:00[NYSE]');
229+
230+
const nextMonday = monday.add({ weeks: 1 });
231+
232+
// Subtracting 1 day from Monday gets you the same day (disambiguates forward)
233+
assert.equal(nextMonday.subtract({ days: 1 }).toString(), '2022-08-29T09:30:00-04:00[NYSE]');
234+
// Subtracting 3 days from Monday gets you the previous Friday
235+
assert.equal(nextMonday.subtract({ days: 3 }).toString(), '2022-08-26T09:30:00-04:00[NYSE]');
236+
237+
// Difference between Friday and Monday is 72 hours or 3 days
238+
const fridayUntilMonday = friday.until(nextMonday);
239+
assert.equal(fridayUntilMonday.toString(), 'PT72H');
240+
assert.equal(fridayUntilMonday.total('hours'), 72);
241+
assert.equal(fridayUntilMonday.total('days'), 3);
242+
243+
const mondaySinceFriday = nextMonday.since(friday);
244+
assert.equal(mondaySinceFriday.toString(), 'PT72H');
245+
assert.equal(mondaySinceFriday.total('hours'), 72);
246+
assert.equal(mondaySinceFriday.total('days'), 3);
247+
248+
// One week is still 7 days
249+
const oneWeek = Temporal.Duration.from({ weeks: 1 });
250+
assert.equal(oneWeek.total({ unit: 'days', relativeTo: monday }), 7);
251+
assert.equal(oneWeek.total({ unit: 'days', relativeTo: friday }), 7);

polyfill/lib/ecmascript.mjs

Lines changed: 27 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3255,20 +3255,34 @@ export function NormalizedTimeDurationToDays(norm, zonedRelativeTo, timeZoneRec,
32553255
// back inside the period where it belongs. Note that this case only can
32563256
// happen for positive durations because the only direction that
32573257
// `disambiguation: 'compatible'` can change clock time is forwards.
3258-
if (sign === 1) {
3259-
while (days > 0 && relativeResult.epochNs.greater(endNs)) {
3260-
days--;
3261-
relativeResult = AddDaysToZonedDateTime(start, dtStart, timeZoneRec, calendar, days);
3262-
// may do disambiguation
3258+
if (sign === 1 && days > 0 && relativeResult.epochNs.greater(endNs)) {
3259+
days--;
3260+
relativeResult = AddDaysToZonedDateTime(start, dtStart, timeZoneRec, calendar, days);
3261+
// may do disambiguation
3262+
if (days > 0 && relativeResult.epochNs.greater(endNs)) {
3263+
throw new RangeError('inconsistent result from custom time zone getInstantFor()');
32633264
}
32643265
}
32653266
norm = TimeDuration.fromEpochNsDiff(endNs, relativeResult.epochNs);
32663267

3267-
let isOverflow = false;
3268-
let dayLengthNs;
3269-
do {
3270-
// calculate length of the next day (day that contains the time remainder)
3271-
const oneDayFarther = AddDaysToZonedDateTime(
3268+
// calculate length of the next day (day that contains the time remainder)
3269+
let oneDayFarther = AddDaysToZonedDateTime(
3270+
relativeResult.instant,
3271+
relativeResult.dateTime,
3272+
timeZoneRec,
3273+
calendar,
3274+
sign
3275+
);
3276+
let dayLengthNs = TimeDuration.fromEpochNsDiff(oneDayFarther.epochNs, relativeResult.epochNs);
3277+
const oneDayLess = norm.subtract(dayLengthNs);
3278+
let isOverflow = oneDayLess.sign() * sign >= 0;
3279+
if (isOverflow) {
3280+
norm = oneDayLess;
3281+
relativeResult = oneDayFarther;
3282+
days += sign;
3283+
3284+
// ensure there was no more overflow
3285+
oneDayFarther = AddDaysToZonedDateTime(
32723286
relativeResult.instant,
32733287
relativeResult.dateTime,
32743288
timeZoneRec,
@@ -3277,14 +3291,9 @@ export function NormalizedTimeDurationToDays(norm, zonedRelativeTo, timeZoneRec,
32773291
);
32783292

32793293
dayLengthNs = TimeDuration.fromEpochNsDiff(oneDayFarther.epochNs, relativeResult.epochNs);
3280-
const oneDayLess = norm.subtract(dayLengthNs);
3281-
isOverflow = oneDayLess.sign() * sign >= 0;
3282-
if (isOverflow) {
3283-
norm = oneDayLess;
3284-
relativeResult = oneDayFarther;
3285-
days += sign;
3286-
}
3287-
} while (isOverflow);
3294+
isOverflow = norm.subtract(dayLengthNs).sign() * sign >= 0;
3295+
if (isOverflow) throw new RangeError('inconsistent result from custom time zone getPossibleInstantsFor()');
3296+
}
32883297
if (days !== 0 && MathSign(days) != sign) {
32893298
throw new RangeError('Time zone or calendar converted nanoseconds into a number of days with the opposite sign');
32903299
}

spec/zoneddatetime.html

Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1446,23 +1446,22 @@ <h1>
14461446
1. Else if _days_ &lt; 0 and _timeSign_ &lt; 0, then
14471447
1. Set _days_ to _days_ + 1.
14481448
1. Let _relativeResult_ be ? AddDaysToZonedDateTime(_startInstant_, _startDateTime_, _timeZoneRec_, _zonedRelativeTo_.[[Calendar]], _days_).
1449-
1. If _sign_ is 1, then
1450-
1. Repeat, while _days_ &gt; 0 and ℝ(_relativeResult_.[[EpochNanoseconds]]) &gt; _endNs_,
1451-
1. Set _days_ to _days_ - 1.
1452-
1. Set _relativeResult_ to ? AddDaysToZonedDateTime(_startInstant_, _startDateTime_, _timeZoneRec_, _zonedRelativeTo_.[[Calendar]], _days_).
1449+
1. If _sign_ = 1, and _days_ &gt; 0, and ℝ(_relativeResult_.[[EpochNanoseconds]]) &gt; _endNs_, then
1450+
1. Set _days_ to _days_ - 1.
1451+
1. Set _relativeResult_ to ? AddDaysToZonedDateTime(_startInstant_, _startDateTime_, _timeZoneRec_, _zonedRelativeTo_.[[Calendar]], _days_).
1452+
1. If _days_ &gt; 0 and ℝ(_relativeResult_.[[EpochNanoseconds]]) &gt; _endNs_, throw a *RangeError* exception.
14531453
1. Set _norm_ to NormalizedTimeDurationFromEpochNanosecondsDifference(_endNs_, _relativeResult_.[[EpochNanoseconds]]).
1454-
1. Let _done_ be *false*.
1455-
1. Let _dayLengthNs_ be ~unset~.
1456-
1. Repeat, while _done_ is *false*,
1457-
1. Let _oneDayFarther_ be ? AddDaysToZonedDateTime(_relativeResult_.[[Instant]], _relativeResult_.[[DateTime]], _timeZoneRec_, _zonedRelativeTo_.[[Calendar]], _sign_).
1458-
1. Set _dayLengthNs_ to NormalizedTimeDurationFromEpochNanosecondsDifference(_oneDayFarther_.[[EpochNanoseconds]], _relativeResult_.[[EpochNanoseconds]]).
1459-
1. Let _oneDayLess_ be ? SubtractNormalizedTimeDuration(_norm_, _dayLengthNs_).
1460-
1. If NormalizedTimeDurationSign(_oneDayLess_) &times; _sign_ &ge; 0, then
1461-
1. Set _norm_ to _oneDayLess_.
1462-
1. Set _relativeResult_ to _oneDayFarther_.
1463-
1. Set _days_ to _days_ + _sign_.
1464-
1. Else,
1465-
1. Set _done_ to *true*.
1454+
1. Let _oneDayFarther_ be ? AddDaysToZonedDateTime(_relativeResult_.[[Instant]], _relativeResult_.[[DateTime]], _timeZoneRec_, _zonedRelativeTo_.[[Calendar]], _sign_).
1455+
1. Let _dayLengthNs_ be NormalizedTimeDurationFromEpochNanosecondsDifference(_oneDayFarther.[[EpochNanoseconds]], _relativeResult_.[[EpochNanoseconds]]).
1456+
1. Let _oneDayLess_ be ? SubtractNormalizedTimeDuration(_norm_, _dayLengthNs_).
1457+
1. If NormalizedTimeDurationSign(_oneDayLess_) &times; _sign_ &ge; 0, then
1458+
1. Set _norm_ to _oneDayLess_.
1459+
1. Set _relativeResult_ to _oneDayFarther_.
1460+
1. Set _days_ to _days_ + _sign_.
1461+
1. Set _oneDayFarther_ to ? AddDaysToZonedDateTime(_relativeResult_.[[Instant]], _relativeResult_.[[DateTime]], _timeZoneRec_, _zonedRelativeTo_.[[Calendar]], _sign_).
1462+
1. Set _dayLengthNs_ to NormalizedTimeDurationFromEpochNanosecondsDifference(_oneDayFarther.[[EpochNanoseconds]], _relativeResult_.[[EpochNanoseconds]]).
1463+
1. If NormalizedTimeDurationSign(? SubtractNormalizedTimeDuration(_norm_, _dayLengthNs_)) &times; _sign_ &ge; 0, then
1464+
1. Throw a *RangeError* exception.
14661465
1. If _days_ &lt; 0 and _sign_ = 1, throw a *RangeError* exception.
14671466
1. If _days_ &gt; 0 and _sign_ = -1, throw a *RangeError* exception.
14681467
1. If NormalizedTimeDurationSign(_norm_) = -1, then

0 commit comments

Comments
 (0)