import Calendar from './calendar.js'; import {DurationLike, toDuration} from './duration.js'; import {ISO8601Result} from './parse.js'; import {PlainDateTime} from './plain-date-time.js'; import {PlainTimeLike, toPlainTime} from './plain-time.js'; import {RoundingMode} from './types.js'; import {maxTemporalUnit, parseUnit, verifyDateComponents} from './utilities.js'; import {ZonedDateTime, pad, padYear} from './zoned-date-time.js'; export type PlainDateLike = | PlainDate | (PlainDateComponents & {calendar?: string}) | string | ZonedDateTime | PlainDateTime; interface PlainDateComponents { year: number; month: number; day: number; } interface PlainDateDeltaParameters { /// The largest unit of time to round to in the resulting duration, defaults to 'auto'. largestUnit?: 'auto' | 'year' | 'month' | 'week' | 'day'; /// The smallest unit of time to round to in the resulting duration, defaults to 'millisecond', which means no rounding. smallestUnit?: 'year' | 'month' | 'day'; /// The granularity to round to, of the unit given by smallestUnit. Defaults to `1`. roundingIncrement?: number; /// Rounding method to use roundingMode?: RoundingMode; } export function toPlainDate(plainDateLike: PlainDateLike) { return plainDateLike instanceof PlainDate ? plainDateLike : PlainDate.from(plainDateLike); } export class PlainDate { private calendar: Calendar; readonly year: number; readonly month: number; readonly day: number; constructor(isoYear: number, isoMonth: number, isoDay: number, calendar: string = 'iso8601') { this.calendar = Calendar.from(calendar); let {year, month, day} = verifyDateComponents(isoYear, isoMonth, isoDay, 'reject'); this.year = year; this.month = month; this.day = day; } getCalendar() { return this.calendar; } get calendarId() { return this.calendar.id; } get monthsInYear() { return this.calendar.monthsInYear(this); } get inLeapYear() { return this.calendar.inLeapYear(this); } get weekOfYear() { return this.calendar.weekOfYear(this); } get yearOfWeek() { return this.calendar.yearOfWeek(this); } get dayOfYear() { return this.calendar.dayOfYear(this); } get daysInYear() { return this.calendar.daysInYear(this); } get daysInMonth() { return this.calendar.daysInMonth(this); } get daysInWeek() { return this.calendar.daysInWeek(this); } /** * The dayOfWeek read-only property gives the weekday number that the date falls * on. For the ISO 8601 calendar, the weekday number is defined as in the ISO 8601 * standard: a value between 1 and 7, inclusive, with Monday being 1, and Sunday 7 */ get dayOfWeek(): number { return this.calendar.dayOfWeek(this); } with(dateLike: Partial, options?: {overflow: 'reject' | 'constrain'}) { let {year, month, day} = verifyDateComponents( dateLike.year ?? this.year, dateLike.month ?? this.month, dateLike.day ?? this.day, options?.overflow ?? 'constrain', ); return new PlainDate(year, month, day); } add(durationLike: DurationLike, options?: {overflow: 'reject' | 'constrain'}) { return this.calendar.dateAdd(this, toDuration(durationLike), options); } subtract(durationLike: DurationLike, options?: {overflow: 'reject' | 'constrain'}) { return this.calendar.dateAdd(this, toDuration(durationLike).negated(), options); } until(other: PlainDateLike, options: PlainDateDeltaParameters) { let smallestUnit = parseUnit(options.smallestUnit, 'day'); let largestUnit = parseUnit(options.largestUnit, 'auto'); let roundingIncrement = options.roundingIncrement ?? 1; let roundingMode = options.roundingMode ?? 'trunc'; if (largestUnit === 'auto') { largestUnit = maxTemporalUnit('day', smallestUnit); } let duration = this.calendar.dateUntil(this, toPlainDate(other), { largestUnit: 'year', }); return duration.round({ largestUnit, smallestUnit, roundingIncrement, roundingMode, relativeTo: this, }); } since(other: PlainDateLike, options: PlainDateDeltaParameters) { return this.until(other, options).negated(); } equals(other: PlainDateLike) { let otherDate = toPlainDate(other); return this.year === otherDate.year && this.month === otherDate.month && this.day === otherDate.day; } toJSON() { return this.toString(); } toString(options: {calendarName?: 'auto' | 'always' | 'never'} = {}) { let calendarName = options.calendarName ?? 'auto'; let calendar = calendarName === 'always' || (calendarName === 'auto' && this.calendarId !== 'iso8601') ? `[u-ca=${this.calendarId}]` : ''; return `${padYear(this.year)}-${pad(this.month)}-${pad(this.day)}${calendar}`; } toLocaleString(locales: string | string[], options: Intl.DateTimeFormatOptions = {}) { let formatter = new Intl.DateTimeFormat(locales, { ...options, timeZone: 'UTC', }); return formatter.format(Date.UTC(this.year, this.month - 1, this.day)); } toPlainDateTime(timeLike: PlainTimeLike) { let time = toPlainTime(timeLike); return new PlainDateTime(this.year, this.month, this.day, time.hour, time.minute, time.second, time.millisecond); } valueOf() { throw new Error('Built-in arithmetic operators isn’t supported on PlainDate'); } static from(item: PlainDateLike, options: {overflow?: 'reject' | 'constrain'} = {}) { if (item instanceof PlainDate || item instanceof ZonedDateTime || item instanceof PlainDateTime) { return new PlainDate(item.year, item.month, item.day, item.calendarId); } else if (item && typeof item === 'object' && !Array.isArray(item)) { let {year, month, day} = verifyDateComponents(item.year, item.month, item.day, options.overflow ?? 'constrain'); return new PlainDate(year, month, day, item.calendar); } else if (typeof item === 'string') { let result = ISO8601Result.parseDateTime(item); if (result && result.date) { return new PlainDate(result.date.year, result.date.month, result.date.day); } } throw new RangeError('Unknown type'); } static compare(one: PlainDateLike, two: PlainDateLike) { let oneDate = toPlainDate(one); let twoDate = toPlainDate(two); return ( [ Math.sign(oneDate.year - twoDate.year), Math.sign(oneDate.month - twoDate.month), Math.sign(oneDate.day - twoDate.day), ].find(value => value !== 0) ?? 0 ); } }