Skip to content

Commit

Permalink
Fix lunisolar calendars' {months, years} addition
Browse files Browse the repository at this point in the history
Fixes #149
  • Loading branch information
justingrant committed May 20, 2022
1 parent f3d0ca9 commit 4f8b04c
Show file tree
Hide file tree
Showing 2 changed files with 73 additions and 7 deletions.
11 changes: 6 additions & 5 deletions lib/calendar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1106,13 +1106,14 @@ abstract class HelperBase {
return calendarDate;
}
addCalendar(
calendarDate: CalendarYMD,
calendarDate: CalendarYMD & { monthCode: string },
{ years = 0, months = 0, weeks = 0, days = 0 },
overflow: Overflow,
cache: OneObjectCache
): FullCalendarDate {
const { year, month, day } = calendarDate;
const addedMonths = this.addMonthsCalendar({ year: year + years, month, day }, months, overflow, cache);
const { year, day, monthCode } = calendarDate;
const addedYears = this.adjustCalendarDate({ year: year + years, monthCode, day }, cache);
const addedMonths = this.addMonthsCalendar(addedYears, months, overflow, cache);
const initialDays = days + weeks * 7;
const addedDays = this.addDaysCalendar(addedMonths, initialDays, cache);
return addedDays;
Expand Down Expand Up @@ -1213,8 +1214,8 @@ abstract class HelperBase {
const lastDayOfPreviousMonthCalendar = this.isoToCalendarDate(lastDayOfPreviousMonthIso, cache);
return lastDayOfPreviousMonthCalendar.day;
}
startOfCalendarYear(calendarDate: CalendarYearOnly): CalendarYMD {
return { year: calendarDate.year, month: 1, day: 1 };
startOfCalendarYear(calendarDate: CalendarYearOnly): CalendarYMD & { monthCode: string } {
return { year: calendarDate.year, month: 1, monthCode: 'M01', day: 1 };
}
startOfCalendarMonth(calendarDate: CalendarYM): CalendarYMD {
return { year: calendarDate.year, month: calendarDate.month, day: 1 };
Expand Down
69 changes: 67 additions & 2 deletions test/intl.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -745,7 +745,7 @@ describe('Intl', () => {
years: {
duration: { years: 3, months: 6, days: 17 },
results: addYearsMonthsDaysCases,
startDate: { year: 1997, month: 12, day: 1 }
startDate: { year: 1997, monthCode: 'M12', day: 1 }
}
};
const calendars = Object.keys(addMonthsCases);
Expand All @@ -768,7 +768,33 @@ describe('Intl', () => {
equal(`add ${unit} ${id} month: ${end.month}`, `add ${unit} ${id} month: ${values.month}`);
equal(`add ${unit} ${id} monthCode: ${end.monthCode}`, `add ${unit} ${id} monthCode: ${values.monthCode}`);
const calculatedStart = end.subtract(duration);
equal(`start ${calculatedStart.toString()}`, `start ${start.toString()}`);
// For lunisolar calendars, adding/subtracting years and months in the
// same duration may not be reversible because the number of months in
// a year can vary. To see why this is the case, let's use Chinese
// year 2001, which is a leap year with a leap month after the 4th
// normal month. Adding P1Y6M to the first day of Chinese year 2000
// will first add one year and then add six months, yielding a date
// that's the first day of the 7th ordinal month of 2001. But because
// that year is a leap year, the result's month code will be M06.
//
// Now let's look at subtracting the same duration from the result.
// First subtract one year, yielding the M06 month of the non-leap
// year 2000. Because M06 (unlike in the following leap year) is the
// 6th month ordinally, subtracting 6 months yields the M12 month of
// the Chinese year 1999. One month earlier than the original date!
//
// This is not a bug; it's an expected consequence of Temporal's
// largest-units-first order of operations that is aligned to the
// behavior standardized by RFC 5545.
//
// Note that this behavior is similar to all calendars' behavior when
// adding or subtracting months and days together. In those cases,
// addition and subtraction may not be reversible because months are
// different lengths.
const isLunisolar = ['chinese', 'dangi', 'hebrew'].includes(id);
const expectedCalculatedStart =
isLunisolar && duration.years !== 0 && !end.monthCode.endsWith('L') ? start.subtract({ months: 1 }) : start;
equal(`start ${calculatedStart.toString()}`, `start ${expectedCalculatedStart.toString()}`);
const diff = start.until(end, { largestUnit: unit });
equal(`diff ${unit} ${id}: ${diff}`, `diff ${unit} ${id}: ${duration}`);

Expand Down Expand Up @@ -1197,6 +1223,45 @@ describe('Intl', () => {
});
});

describe('Addition across lunisolar leap months', () => {
it('Adding years across Hebrew leap month', () => {
const date = Temporal.PlainDate.from({ year: 5783, monthCode: 'M08', day: 2, calendar: 'hebrew' });
const added = date.add({ years: 1 });
equal(added.monthCode, date.monthCode);
equal(added.year, date.year + 1);
});
it('Adding months across Hebrew leap month', () => {
const date = Temporal.PlainDate.from({ year: 5783, monthCode: 'M08', day: 2, calendar: 'hebrew' });
const added = date.add({ months: 13 });
equal(added.monthCode, date.monthCode);
equal(added.year, date.year + 1);
});
it('Adding months and years across Hebrew leap month', () => {
const date = Temporal.PlainDate.from({ year: 5783, monthCode: 'M08', day: 2, calendar: 'hebrew' });
const added = date.add({ years: 1, months: 12 });
equal(added.monthCode, date.monthCode);
equal(added.year, date.year + 2);
});
it('Adding years across Chinese leap month', () => {
const date = Temporal.PlainDate.from({ year: 2000, monthCode: 'M08', day: 2, calendar: 'chinese' });
const added = date.add({ years: 1 });
equal(added.monthCode, date.monthCode);
equal(added.year, date.year + 1);
});
it('Adding months across Chinese leap month', () => {
const date = Temporal.PlainDate.from({ year: 2000, monthCode: 'M08', day: 2, calendar: 'chinese' });
const added = date.add({ months: 13 });
equal(added.monthCode, date.monthCode);
equal(added.year, date.year + 1);
});
it('Adding months and years across Chinese leap month', () => {
const date = Temporal.PlainDate.from({ year: 2001, monthCode: 'M08', day: 2, calendar: 'chinese' });
const added = date.add({ years: 1, months: 12 });
equal(added.monthCode, date.monthCode);
equal(added.year, date.year + 2);
});
});

describe('DateTimeFormat', () => {
describe('supportedLocalesOf', () => {
it('should return an Array', () => assert(Array.isArray(Intl.DateTimeFormat.supportedLocalesOf())));
Expand Down

0 comments on commit 4f8b04c

Please sign in to comment.