Add Months With End-Of-Month Clamping

Add or subtract months in UTC while clamping the day to the target month's last day, avoiding date rollover bugs.

TypescriptData Integrity

Code

snippet
export function daysInMonth(year: number, monthIndex: number): number {
  if (!Number.isInteger(year) || !Number.isInteger(monthIndex)) {
    throw new TypeError("year and monthIndex must be integers");
  }
  return new Date(Date.UTC(year, monthIndex + 1, 0)).getUTCDate();
}

// Add months with end-of-month clamping.
export function addMonthsClamped(date: Date, months: number): Date {
  if (!(date instanceof Date) || Number.isNaN(date.getTime())) {
    throw new RangeError("date must be a valid Date");
  }
  if (!Number.isInteger(months)) throw new TypeError("months must be an integer");
  const target = new Date(Date.UTC(
    date.getUTCFullYear(),
    date.getUTCMonth() + months,
    1,
    date.getUTCHours(),
    date.getUTCMinutes(),
    date.getUTCSeconds(),
    date.getUTCMilliseconds(),
  ));
  const day = Math.min(date.getUTCDate(), daysInMonth(target.getUTCFullYear(), target.getUTCMonth()));
  target.setUTCDate(day);
  return target;
}

export function addYearsClamped(date: Date, years: number): Date {
  if (!Number.isInteger(years)) throw new TypeError("years must be an integer");
  return addMonthsClamped(date, years * 12);
}