import {type ZonedDateTime} from './zoned-date-time.js'; import Duration, {toDuration} from './duration.js'; import {PlainDate} from './plain-date.js'; import {normalizeDateAdd, toISOWeekOfYear} from './utilities.js'; import {PlainDateTime} from './plain-date-time.js'; import {DAY} from './units.js'; export function inLeapYear(year: number): boolean { return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0; } export function daysInMonth(year: number, month: number) { switch (month) { case 2: return inLeapYear(year) ? 29 : 28; case 4: case 6: case 9: case 11: return 30; default: return 31; } } // Gregorian calendar export default class Calendar { get id() { return 'iso8601'; } toString() { return this.id; } toJSON() { return this.id; } monthsInYear(_date: ZonedDateTime | PlainDate | PlainDateTime) { return 12; } daysInWeek(_date: ZonedDateTime | PlainDate | PlainDateTime) { return 7; } weekOfYear(date: ZonedDateTime | PlainDate | PlainDateTime) { return toISOWeekOfYear(date).week; } yearOfWeek(date: ZonedDateTime | PlainDate | PlainDateTime) { return toISOWeekOfYear(date).year; } daysInMonth(date: ZonedDateTime | PlainDate | PlainDateTime): number { return daysInMonth(date.year, date.month); } inLeapYear(date: ZonedDateTime | PlainDate | PlainDateTime): boolean { return inLeapYear(date.year); } daysInYear(date: ZonedDateTime | PlainDate | PlainDateTime): number { return this.inLeapYear(date) ? 366 : 365; } dayOfWeek(date: ZonedDateTime | PlainDate | PlainDateTime): number { let utcDate = new Date(Date.UTC(date.year, date.month - 1, date.day)); let dayOfWeek = utcDate.getUTCDay(); return dayOfWeek === 0 ? 7 : dayOfWeek; } /** * Returns the ordinal day of the year that the * date falls on. For the ISO 8601 calendar, this is a value between 1 and 365, * or 366 in a leap year. */ dayOfYear(date: ZonedDateTime | PlainDate | PlainDateTime) { return (Date.UTC(date.year, date.month - 1, date.day) - Date.UTC(date.year, 0, 0)) / DAY; } dateUntil( one: PlainDate, two: PlainDate, options?: {largestUnit: 'auto' | 'year' | 'month' | 'week' | 'day'}, ): Duration { let sign = PlainDate.compare(two, one); if (sign === 0) { return new Duration(); } let largestUnit = options?.largestUnit || 'auto'; if (largestUnit === 'auto') { largestUnit = 'day'; } if (largestUnit === 'day') { let a = Date.UTC(one.year, one.month - 1, one.day); let b = Date.UTC(two.year, two.month - 1, two.day); return Duration.from({ days: (b - a) / DAY, }); } let yearDelta = two.year - one.year; let monthDelta = two.month - one.month; let monthSign = Math.sign(monthDelta); let durationDeltaYear = yearDelta; let durationDeltaMonth = monthDelta; if (monthSign !== 0 && monthSign !== sign) { durationDeltaYear -= sign; durationDeltaMonth += 12 * sign; } let twoDayOfYear = this.dayOfYear(two); let oneExtended = one.add({years: yearDelta}).add({ months: monthDelta, }); let oneExtendedDayOfYear = this.dayOfYear(oneExtended); let daySign = Math.sign(twoDayOfYear - oneExtendedDayOfYear); if (daySign !== 0 && daySign !== sign) { oneExtended = one.add({years: yearDelta}).add({ months: monthDelta - sign, }); durationDeltaMonth -= sign; if (durationDeltaMonth === -sign) { durationDeltaMonth = 11 * sign; durationDeltaYear -= sign; } } let a = Date.UTC(oneExtended.year, oneExtended.month - 1, oneExtended.day); let b = Date.UTC(two.year, two.month - 1, two.day); let days = (b - a) / DAY; if (largestUnit === 'month') { return Duration.from({ months: durationDeltaMonth + durationDeltaYear * 12, days, }); } return Duration.from({ years: durationDeltaYear, months: durationDeltaMonth, days, }); } dateAdd(date: PlainDate, durationLike: Duration | string, options?: {overflow?: 'reject' | 'constrain'}) { let duration = toDuration(durationLike); let raw: Date; if (duration.years || duration.months) { let {year, month, day} = normalizeDateAdd(date.year, date.month, date.day, duration, options?.overflow); raw = new Date(Date.UTC(year, month - 1, day)); } else { raw = new Date(Date.UTC(date.year, date.month - 1, date.day)); } if (duration.days || duration.weeks) { let days = duration.days + duration.weeks * 7; raw.setUTCMilliseconds(raw.getMilliseconds() + DAY * days); } return new PlainDate(raw.getUTCFullYear(), raw.getUTCMonth() + 1, raw.getUTCDate()); } static from(type: string) { switch (type) { case 'iso8601': return isoCalendar; default: throw new RangeError('Unsupported calendar'); } } } const isoCalendar = new Calendar();