Skip to content

Commit

Permalink
Tests for computing PlainYearMonth addition and subtraction in correc…
Browse files Browse the repository at this point in the history
…t calendar space

tc39/proposal-temporal#2003 is a normative change
that reached consensus at the March 2022 TC39 plenary meeting. This adds
tests that verify the new spec text is implemented correctly, performing
arithmetic on a PlainYearMonth instance that would previously have thrown
an error if it was implemented as written.
  • Loading branch information
ptomato authored and rwaldron committed Apr 4, 2022
1 parent c58ac69 commit 833a784
Show file tree
Hide file tree
Showing 2 changed files with 294 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
// Copyright (C) 2022 Igalia, S.L. All rights reserved.
// This code is governed by the BSD license found in the LICENSE file.

/*---
esid: sec-temporal.plainyearmonth.prototype.add
description: >
Calls calendar's dateFromFields method to obtain a start date for the
operation, based on the sign of the duration
info: |
8. Let _fields_ be ? PrepareTemporalFields(_yearMonth_, _fieldNames_, «»).
9. Let _sign_ be ! DurationSign(_duration_.[[Years]], _duration_.[[Months]], _duration_.[[Weeks]], _balanceResult_.[[Days]], 0, 0, 0, 0, 0, 0).
10. If _sign_ < 0, then
a. Let _dayFromCalendar_ be ? CalendarDaysInMonth(_calendar_, _yearMonth_).
b. Let _day_ be ? ToPositiveInteger(_dayFromCalendar_).
11. Else,
a. Let _day_ be 1.
12. Perform ! CreateDataPropertyOrThrow(_fields_, *"day"*, _day_).
13. Let _date_ be ? DateFromFields(_calendar_, _fields_, *undefined*).
includes: [deepEqual.js, temporalHelpers.js]
features: [Temporal]
---*/

class CustomCalendar extends Temporal.Calendar {
constructor() {
super("iso8601");
this.dateFromFieldsCalls = [];
}
year(date) {
// years in this calendar start and end on the same day as ISO 8601 years
return date.getISOFields().isoYear;
}
month(date) {
// this calendar has 10 months of 36 days each, plus an 11th month of 5 or 6
const { isoYear, isoMonth, isoDay } = date.getISOFields();
const isoDate = new Temporal.PlainDate(isoYear, isoMonth, isoDay);
return Math.floor((isoDate.dayOfYear - 1) / 36) + 1;
}
monthCode(date) {
return "M" + this.month(date).toString().padStart(2, "0");
}
day(date) {
return (date.dayOfYear - 1) % 36 + 1;
}
daysInMonth(date) {
if (this.month(date) < 11) return 36;
return this.daysInYear(date) - 360;
}
_dateFromFieldsImpl({ year, month, monthCode, day }) {
if (year === undefined) throw new TypeError("year required");
if (month === undefined && monthCode === undefined) throw new TypeError("one of month or monthCode required");
if (month !== undefined && month < 1) throw new RangeError("month < 1");
if (day === undefined) throw new TypeError("day required");

if (monthCode !== undefined) {
const numberPart = +(monthCode.slice(1));
if ("M" + `${numberPart}`.padStart(2, "0") !== monthCode) throw new RangeError("invalid monthCode");
if (month === undefined) {
month = numberPart;
} else if (month !== numberPart) {
throw new RangeError("month and monthCode must match");
}
}

const isoDayOfYear = (month - 1) * 36 + day;
return new Temporal.PlainDate(year, 1, 1).add({ days: isoDayOfYear - 1 }).withCalendar(this);
}
dateFromFields(...args) {
this.dateFromFieldsCalls.push(args);
return this._dateFromFieldsImpl(...args);
}
yearMonthFromFields(fields, options) {
const { isoYear, isoMonth, isoDay } = this._dateFromFieldsImpl({ ...fields, day: 1 }, options).getISOFields();
return new Temporal.PlainYearMonth(isoYear, isoMonth, this, isoDay);
}
monthDayFromFields(fields, options) {
const { isoYear, isoMonth, isoDay } = this._dateFromFieldsImpl({ ...fields, year: 2000 }, options).getISOFields();
return new Temporal.PlainMonthDay(isoMonth, isoDay, this, isoYear);
}
dateAdd(date, duration, options) {
if (duration.months) throw new Error("adding months not implemented in this test");
return super.dateAdd(date, duration, options);
}
toString() {
return "thirty-six";
}
}

const calendar = new CustomCalendar();
const month2 = Temporal.PlainYearMonth.from({ year: 2022, month: 2, calendar });
const lessThanOneMonth = new Temporal.Duration(0, 0, 0, 35);
const oneMonth = new Temporal.Duration(0, 0, 0, 36);

calendar.dateFromFieldsCalls = [];
TemporalHelpers.assertPlainYearMonth(
month2.add(lessThanOneMonth),
2022, 2, "M02",
"adding positive less than one month's worth of days yields the same month"
);
assert.sameValue(calendar.dateFromFieldsCalls.length, 1, "dateFromFields was called");
assert.deepEqual(
calendar.dateFromFieldsCalls[0][0],
{ year: 2022, month: 2, monthCode: "M02", day: 1 },
"first day of month 2 passed to dateFromFields when adding positive duration"
);
assert.sameValue(calendar.dateFromFieldsCalls[0][1], undefined, "undefined options passed");

calendar.dateFromFieldsCalls = [];
TemporalHelpers.assertPlainYearMonth(
month2.add(oneMonth),
2022, 3, "M03",
"adding positive one month's worth of days yields the following month"
);
assert.sameValue(calendar.dateFromFieldsCalls.length, 1, "dateFromFields was called");
assert.deepEqual(
calendar.dateFromFieldsCalls[0][0],
{ year: 2022, month: 2, monthCode: "M02", day: 1 },
"first day of month 2 passed to dateFromFields when adding positive duration"
);
assert.sameValue(calendar.dateFromFieldsCalls[0][1], undefined, "undefined options passed");

calendar.dateFromFieldsCalls = [];
TemporalHelpers.assertPlainYearMonth(
month2.add(lessThanOneMonth.negated()),
2022, 2, "M02",
"adding negative less than one month's worth of days yields the same month"
);
assert.sameValue(calendar.dateFromFieldsCalls.length, 1, "dateFromFields was called");
assert.deepEqual(
calendar.dateFromFieldsCalls[0][0],
{ year: 2022, month: 2, monthCode: "M02", day: 36 },
"last day of month 2 passed to dateFromFields when adding negative duration"
);
assert.sameValue(calendar.dateFromFieldsCalls[0][1], undefined, "undefined options passed");

calendar.dateFromFieldsCalls = [];
TemporalHelpers.assertPlainYearMonth(
month2.add(oneMonth.negated()),
2022, 1, "M01",
"adding negative one month's worth of days yields the previous month"
);
assert.sameValue(calendar.dateFromFieldsCalls.length, 1, "dateFromFields was called");
assert.deepEqual(
calendar.dateFromFieldsCalls[0][0],
{ year: 2022, month: 2, monthCode: "M02", day: 36 },
"last day of month 2 passed to dateFromFields when adding negative duration"
);
assert.sameValue(calendar.dateFromFieldsCalls[0][1], undefined, "undefined options passed");
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
// Copyright (C) 2022 Igalia, S.L. All rights reserved.
// This code is governed by the BSD license found in the LICENSE file.

/*---
esid: sec-temporal.plainyearmonth.prototype.subtract
description: >
Calls calendar's dateFromFields method to obtain a start date for the
operation, based on the sign of the duration
info: |
9. Let _fields_ be ? PrepareTemporalFields(_yearMonth_, _fieldNames_, «»).
10. Let _sign_ be ! DurationSign(_duration_.[[Years]], _duration_.[[Months]], _duration_.[[Weeks]], _balanceResult_.[[Days]], 0, 0, 0, 0, 0, 0).
11. If _sign_ < 0, then
a. Let _dayFromCalendar_ be ? CalendarDaysInMonth(_calendar_, _yearMonth_).
b. Let _day_ be ? ToPositiveInteger(_dayFromCalendar_).
12. Else,
a. Let _day_ be 1.
13. Perform ! CreateDataPropertyOrThrow(_fields_, *"day"*, _day_).
14. Let _date_ be ? DateFromFields(_calendar_, _fields_, *undefined*).
includes: [deepEqual.js, temporalHelpers.js]
features: [Temporal]
---*/

class CustomCalendar extends Temporal.Calendar {
constructor() {
super("iso8601");
this.dateFromFieldsCalls = [];
}
year(date) {
// years in this calendar start and end on the same day as ISO 8601 years
return date.getISOFields().isoYear;
}
month(date) {
// this calendar has 10 months of 36 days each, plus an 11th month of 5 or 6
const { isoYear, isoMonth, isoDay } = date.getISOFields();
const isoDate = new Temporal.PlainDate(isoYear, isoMonth, isoDay);
return Math.floor((isoDate.dayOfYear - 1) / 36) + 1;
}
monthCode(date) {
return "M" + this.month(date).toString().padStart(2, "0");
}
day(date) {
return (date.dayOfYear - 1) % 36 + 1;
}
daysInMonth(date) {
if (this.month(date) < 11) return 36;
return this.daysInYear(date) - 360;
}
_dateFromFieldsImpl({ year, month, monthCode, day }) {
if (year === undefined) throw new TypeError("year required");
if (month === undefined && monthCode === undefined) throw new TypeError("one of month or monthCode required");
if (month !== undefined && month < 1) throw new RangeError("month < 1");
if (day === undefined) throw new TypeError("day required");

if (monthCode !== undefined) {
const numberPart = +(monthCode.slice(1));
if ("M" + `${numberPart}`.padStart(2, "0") !== monthCode) throw new RangeError("invalid monthCode");
if (month === undefined) {
month = numberPart;
} else if (month !== numberPart) {
throw new RangeError("month and monthCode must match");
}
}

const isoDayOfYear = (month - 1) * 36 + day;
return new Temporal.PlainDate(year, 1, 1).add({ days: isoDayOfYear - 1 }).withCalendar(this);
}
dateFromFields(...args) {
this.dateFromFieldsCalls.push(args);
return this._dateFromFieldsImpl(...args);
}
yearMonthFromFields(fields, options) {
const { isoYear, isoMonth, isoDay } = this._dateFromFieldsImpl({ ...fields, day: 1 }, options).getISOFields();
return new Temporal.PlainYearMonth(isoYear, isoMonth, this, isoDay);
}
monthDayFromFields(fields, options) {
const { isoYear, isoMonth, isoDay } = this._dateFromFieldsImpl({ ...fields, year: 2000 }, options).getISOFields();
return new Temporal.PlainMonthDay(isoMonth, isoDay, this, isoYear);
}
dateAdd(date, duration, options) {
if (duration.months) throw new Error("adding months not implemented in this test");
return super.dateAdd(date, duration, options);
}
toString() {
return "thirty-six";
}
}

const calendar = new CustomCalendar();
const month2 = Temporal.PlainYearMonth.from({ year: 2022, month: 2, calendar });
const lessThanOneMonth = new Temporal.Duration(0, 0, 0, 35);
const oneMonth = new Temporal.Duration(0, 0, 0, 36);

calendar.dateFromFieldsCalls = [];
TemporalHelpers.assertPlainYearMonth(
month2.subtract(lessThanOneMonth),
2022, 2, "M02",
"subtracting positive less than one month's worth of days yields the same month"
);
assert.sameValue(calendar.dateFromFieldsCalls.length, 1, "dateFromFields was called");
assert.deepEqual(
calendar.dateFromFieldsCalls[0][0],
{ year: 2022, month: 2, monthCode: "M02", day: 36 },
"last day of month 2 passed to dateFromFields when subtracting positive duration"
);
assert.sameValue(calendar.dateFromFieldsCalls[0][1], undefined, "undefined options passed");

calendar.dateFromFieldsCalls = [];
TemporalHelpers.assertPlainYearMonth(
month2.subtract(oneMonth),
2022, 1, "M01",
"subtracting positive one month's worth of days yields the previous month"
);
assert.sameValue(calendar.dateFromFieldsCalls.length, 1, "dateFromFields was called");
assert.deepEqual(
calendar.dateFromFieldsCalls[0][0],
{ year: 2022, month: 2, monthCode: "M02", day: 36 },
"last day of month 2 passed to dateFromFields when subtracting positive duration"
);
assert.sameValue(calendar.dateFromFieldsCalls[0][1], undefined, "undefined options passed");

calendar.dateFromFieldsCalls = [];
TemporalHelpers.assertPlainYearMonth(
month2.subtract(lessThanOneMonth.negated()),
2022, 2, "M02",
"subtracting negative less than one month's worth of days yields the same month"
);
assert.sameValue(calendar.dateFromFieldsCalls.length, 1, "dateFromFields was called");
assert.deepEqual(
calendar.dateFromFieldsCalls[0][0],
{ year: 2022, month: 2, monthCode: "M02", day: 1 },
"first day of month 2 passed to dateFromFields when subtracting negative duration"
);
assert.sameValue(calendar.dateFromFieldsCalls[0][1], undefined, "undefined options passed");

calendar.dateFromFieldsCalls = [];
TemporalHelpers.assertPlainYearMonth(
month2.subtract(oneMonth.negated()),
2022, 3, "M03",
"subtracting negative one month's worth of days yields the following month"
);
assert.sameValue(calendar.dateFromFieldsCalls.length, 1, "dateFromFields was called");
assert.deepEqual(
calendar.dateFromFieldsCalls[0][0],
{ year: 2022, month: 2, monthCode: "M02", day: 1 },
"first day of month 2 passed to dateFromFields when subtracting negative duration"
);
assert.sameValue(calendar.dateFromFieldsCalls[0][1], undefined, "undefined options passed");

0 comments on commit 833a784

Please sign in to comment.