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

Temporal: ZonedDateTime::round incorrect results with smallestUnit:day #4021

Merged
merged 2 commits into from
Apr 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@ class TimeZone extends Temporal.TimeZone {
}
}

const units = ["hour", "minute", "second", "millisecond", "microsecond", "nanosecond"];
for (const smallestUnit of units) {
const zdt = new Temporal.ZonedDateTime(0n, new TimeZone("UTC"));
assert.throws(RangeError, () => zdt.round({ smallestUnit, roundingIncrement: 2 }), `zero day-length with smallestUnit ${smallestUnit}`);
}
const zdt = new Temporal.ZonedDateTime(0n, new TimeZone("UTC"));

assert.throws(RangeError, () => zdt.round({ smallestUnit: "day" }), `zero day-length with smallestUnit 'day'`);

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,6 @@ const nonBuiltinISOCalendar = new Temporal.Calendar("iso8601");
const timeZone = new SkippedDateTime();

const instance = new Temporal.ZonedDateTime(0n, timeZone, nonBuiltinISOCalendar);
instance.round({ smallestUnit: "hours" });
instance.round({ smallestUnit: "day" });

assert.sameValue(timeZone.calls, 6, "getPossibleInstantsFor should have been called 6 times");
assert.sameValue(timeZone.calls, 4, "getPossibleInstantsFor should have been called 4 times");
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,6 @@ const expected = [
"get this.timeZone.getPossibleInstantsFor",
// GetPlainDateTimeFor on receiver's instant
"call this.timeZone.getOffsetNanosecondsFor",
// GetInstantFor on preceding midnight
"call this.timeZone.getPossibleInstantsFor",
// AddDaysToZonedDateTime
"call this.timeZone.getPossibleInstantsFor",
// InterpretISODateTimeOffset
"call this.timeZone.getPossibleInstantsFor",
"call this.timeZone.getOffsetNanosecondsFor",
Expand Down Expand Up @@ -83,36 +79,8 @@ beforeFallBackInstance.round(nextHourOptions);
assert.compareArray(actual, expected, "order of operations with rounding result at repeated wall-clock time");
actual.splice(0); // clear

const expectedSkippedDateTime = [
"get options.roundingIncrement",
"get options.roundingIncrement.valueOf",
"call options.roundingIncrement.valueOf",
"get options.roundingMode",
"get options.roundingMode.toString",
"call options.roundingMode.toString",
"get options.smallestUnit",
"get options.smallestUnit.toString",
"call options.smallestUnit.toString",
// lookup
"get this.timeZone.getOffsetNanosecondsFor",
"get this.timeZone.getPossibleInstantsFor",
// GetPlainDateTimeFor on receiver's instant
"call this.timeZone.getOffsetNanosecondsFor",
// GetInstantFor on preceding midnight
"call this.timeZone.getPossibleInstantsFor",
// DisambiguatePossibleInstants
"call this.timeZone.getOffsetNanosecondsFor",
"call this.timeZone.getOffsetNanosecondsFor",
"call this.timeZone.getPossibleInstantsFor",
// AddZonedDateTime
"call this.timeZone.getPossibleInstantsFor",
// InterpretISODateTimeOffset
"call this.timeZone.getPossibleInstantsFor",
"call this.timeZone.getOffsetNanosecondsFor",
];

springForwardInstance.round(options);
assert.compareArray(actual, expectedSkippedDateTime, "order of operations with preceding midnight at skipped wall-clock time");
assert.compareArray(actual, expected, "order of operations with preceding midnight at skipped wall-clock time");
actual.splice(0); // clear

const expectedSkippedResult = [
Expand All @@ -130,14 +98,6 @@ const expectedSkippedResult = [
"get this.timeZone.getPossibleInstantsFor",
// GetPlainDateTimeFor on receiver's instant
"call this.timeZone.getOffsetNanosecondsFor",
// GetInstantFor on preceding midnight
"call this.timeZone.getPossibleInstantsFor",
// AddDaysToZonedDateTime
"call this.timeZone.getPossibleInstantsFor",
// DisambiguatePossibleInstants
"call this.timeZone.getOffsetNanosecondsFor",
"call this.timeZone.getOffsetNanosecondsFor",
"call this.timeZone.getPossibleInstantsFor",
// InterpretISODateTimeOffset
"call this.timeZone.getPossibleInstantsFor",
// DisambiguatePossibleInstants
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,21 +40,22 @@ class TimeZone extends Temporal.TimeZone {
#count = 0;
#nanoseconds;

constructor(nanoseconds) {
constructor(todayEpochNanoseconds, tomorrowEpochNanoseconds) {
super("UTC");
this.#nanoseconds = nanoseconds;
this.#nanoseconds = [todayEpochNanoseconds, tomorrowEpochNanoseconds];
}
getPossibleInstantsFor(dateTime) {
if (++this.#count === 2) {
return [new Temporal.Instant(this.#nanoseconds)];
const nanoseconds = this.#nanoseconds[this.#count++];
if (nanoseconds === undefined) {
return super.getPossibleInstantsFor(dateTime);
}
return super.getPossibleInstantsFor(dateTime);
return [new Temporal.Instant(nanoseconds)];
}
}

function test(epochNanoseconds, tomorrowEpochNanoseconds, testCases) {
function test(epochNanoseconds, todayEpochNanoseconds, tomorrowEpochNanoseconds, testCases) {
for (let [roundingMode, expected] of Object.entries(testCases)) {
let timeZone = new TimeZone(tomorrowEpochNanoseconds);
let timeZone = new TimeZone(todayEpochNanoseconds, tomorrowEpochNanoseconds);
let zoned = new Temporal.ZonedDateTime(epochNanoseconds, timeZone);
let result = zoned.round({ smallestUnit: "days", roundingMode });
assert.sameValue(result.epochNanoseconds, expected);
Expand All @@ -63,31 +64,35 @@ function test(epochNanoseconds, tomorrowEpochNanoseconds, testCases) {

const oneDay = 24n * 60n * 60n * 1000n * 1000n * 1000n;

// Test positive divisor (dayLengthNs).
test(3n, 10n, {
ceil: oneDay,
test(3n, undefined, 10n, {
ceil: 10n, // end-of-day according to TimeZone protocol
floor: 0n,
trunc: 0n,
halfExpand: 0n,
});

test(-3n, 10n, {
ceil: 0n,
test(-3n, undefined, 10n, {
ceil: 10n, // end-of-day according to TimeZone protocol
floor: -oneDay,
trunc: -oneDay,
halfExpand: 0n,
halfExpand: 10n, // end-of-day according to TimeZone protocol
});

test(-3n, -10n, {
ceil: oneDay,
floor: 0n,
trunc: 0n,
halfExpand: 0n,
});
assert.throws(RangeError, () => {
test(-3n, 0n, 10n, { ceil: undefined });
}, "instant is before TimeZone protocol's start-of-day");

assert.throws(RangeError, () => {
test(-3n, undefined, -10n, { ceil: undefined });
}, "instant is after TimeZone protocol's end-of-day");

assert.throws(RangeError, () => {
test(0n, 0n, 0n, { ceil: undefined });
}, "instant is within zero-duration day");

// Test values at int64 boundaries.
test(3n, /*INT64_MAX=*/ 9223372036854775807n, {
ceil: oneDay,
test(3n, undefined, /*INT64_MAX=*/ 9223372036854775807n, {
ceil: /*INT64_MAX=*/ 9223372036854775807n, // end-of-day according to TimeZone protocol
floor: 0n,
trunc: 0n,
halfExpand: 0n,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,6 @@ features: [Temporal]
---*/

const expected = [
"2001-09-09T00:00:00", // called once on midnight of the input datetime
"2001-09-10T00:00:00", // called once on the previous value plus one calendar day
"2001-09-09T02:00:00", // called once on the rounding result
];

Expand Down
10 changes: 6 additions & 4 deletions test/staging/Temporal/ZonedDateTime/old/round.js
Original file line number Diff line number Diff line change
Expand Up @@ -216,17 +216,19 @@ var bal = Temporal.ZonedDateTime.from("1976-11-18T23:59:59.999999999+01:00[+01:0
var timeZone = TemporalHelpers.springForwardFallBackTimeZone();

// rounds correctly to a 25-hour day
// (the 12.5 hour is the halfway point, which is 11:30 local time, since 2:00-2:59 repeats)
var roundTo = { smallestUnit: "day" };
var roundMeDown = Temporal.PlainDateTime.from("2000-10-29T12:29:59").toZonedDateTime(timeZone);
var roundMeDown = Temporal.PlainDateTime.from("2000-10-29T11:29:59").toZonedDateTime(timeZone);
assert.sameValue(`${ roundMeDown.round(roundTo) }`, "2000-10-29T00:00:00-07:00[Custom/Spring_Fall]");
var roundMeUp = Temporal.PlainDateTime.from("2000-10-29T12:30:01").toZonedDateTime(timeZone);
var roundMeUp = Temporal.PlainDateTime.from("2000-10-29T11:30:01").toZonedDateTime(timeZone);
assert.sameValue(`${ roundMeUp.round(roundTo) }`, "2000-10-30T00:00:00-08:00[Custom/Spring_Fall]");

// rounds correctly to a 23-hour day
// (the 11.5 hour is the halfway point, which is 12:30 local time, since 2:00-2:59 skips)
var roundTo = { smallestUnit: "day" };
var roundMeDown = Temporal.PlainDateTime.from("2000-04-02T11:29:59").toZonedDateTime(timeZone);
var roundMeDown = Temporal.PlainDateTime.from("2000-04-02T12:29:59").toZonedDateTime(timeZone);
assert.sameValue(`${ roundMeDown.round(roundTo) }`, "2000-04-02T00:00:00-08:00[Custom/Spring_Fall]");
var roundMeUp = Temporal.PlainDateTime.from("2000-04-02T11:30:01").toZonedDateTime(timeZone);
var roundMeUp = Temporal.PlainDateTime.from("2000-04-02T12:30:01").toZonedDateTime(timeZone);
assert.sameValue(`${ roundMeUp.round(roundTo) }`, "2000-04-03T00:00:00-07:00[Custom/Spring_Fall]");

// rounding up to a nonexistent wall-clock time
Expand Down
Loading