import Calendar from './calendar.js'; import Duration, {DurationLike, toDuration} from './duration.js'; import {ISO8601Result} from './parse.js'; import {PlainDate, PlainDateLike, toPlainDate} from './plain-date.js'; import {PlainTime, PlainTimeLike, toPlainTime} from './plain-time.js'; import {TimeZone} from './time-zone.js'; import type {RoundingMode} from './types.js'; import {PER_UNIT} from './units.js'; import { balanceTime, differencePlainDateTimeWithRounding, isCalendarUnit, parseUnit, roundToIncrement, toTimeDuration, verifyDateComponents, verifyTimeComponents, } from './utilities.js'; import {ZonedDateTime, type ZonedDateTimeComponents, type ZonedDateTimeFromOptions} from './zoned-date-time.js'; export type PlainDateTimeLike = ZonedDateTime | PlainDateTime | PlainDateTimeComponents | string; export interface PlainDateTimeComponents { year: number; month: number; day: number; hour?: number; minute?: number; second?: number; millisecond?: number; } interface PlainDateTimeToStringParameters { calendarName?: 'auto' | 'always' | 'never'; /// The smallest unit of time to round to in the resulting duration, defaults to 'millisecond', which means no rounding. smallestUnit?: 'minute' | 'second' | 'millisecond'; /// Rounding method to use roundingMode?: RoundingMode; /// fractionalSecondDigits?: 'auto' | 0 | 1 | 2 | 3; } interface PlainDateTimeRoundParameters { smallestUnit: 'day' | 'hour' | 'minute' | 'second' | 'millisecond'; roundingIncrement?: number; roundingMode?: RoundingMode; } interface PlainDateTimeDeltaParameters { /// The largest unit of time to round to in the resulting duration, defaults to 'auto'. largestUnit?: 'auto' | 'year' | 'month' | 'week' | 'day' | 'hour' | 'minute' | 'second' | 'millisecond'; /// The smallest unit of time to round to in the resulting duration, defaults to 'millisecond', which means no rounding. smallestUnit?: 'year' | 'month' | 'week' | 'day' | 'hour' | 'minute' | 'second' | 'millisecond'; /// The granularity to round to, of the unit given by smallestUnit. Defaults to `1`. roundingIncrement?: number; /// Rounding method to use roundingMode?: RoundingMode; } export function toPlainDateTime(plainDateTimeLike: PlainDateTimeLike) { return plainDateTimeLike instanceof PlainDateTime ? plainDateTimeLike : PlainDateTime.from(plainDateTimeLike); } export class PlainDateTime { private raw: Date; private calendar: Calendar; constructor( isoYear: number, isoMonth: number, isoDay: number, isoHour: number = 0, isoMinute: number = 0, isoSecond: number = 0, isoMillisecond: number = 0, calendar: string = 'iso8601', ) { verifyDateComponents(isoYear, isoMonth, isoDay, 'reject'); verifyTimeComponents(isoHour, isoMinute, isoSecond, isoMillisecond, 'reject'); this.raw = new Date(Date.UTC(isoYear, isoMonth - 1, isoDay, isoHour, isoMinute, isoSecond, isoMillisecond)); this.calendar = Calendar.from(calendar); } get hour() { return this.raw.getUTCHours(); } get minute() { return this.raw.getUTCMinutes(); } get second() { return this.raw.getUTCSeconds(); } get millisecond() { return this.raw.getUTCMilliseconds(); } get day() { return this.raw.getUTCDate(); } get month() { return this.raw.getUTCMonth() + 1; } get year() { return this.raw.getUTCFullYear(); } 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); } get dayOfWeek(): number { return this.calendar.dayOfWeek(this); } equals(other: PlainDateTimeLike) { return this.raw.valueOf() === toPlainDateTime(other).raw.valueOf(); } valueOf() { throw new Error('Built-in arithmetic operators isn’t supported on ZonedDateTime'); } round(options: PlainDateTimeRoundParameters['smallestUnit'] | PlainDateTimeRoundParameters) { if (typeof options === 'string') { options = { smallestUnit: options, }; } let smallestUnit = parseUnit(options.smallestUnit); let roundingIncrement = options.roundingIncrement ?? 1; let roundingMode = options.roundingMode ?? 'halfExpand'; let epochMilliseconds = this.raw.valueOf(); let remainder = toTimeDuration(this); let startOfDay = epochMilliseconds - remainder; let roundedRemainder = roundToIncrement(remainder, PER_UNIT[smallestUnit] * roundingIncrement, roundingMode); let date = new Date(startOfDay + roundedRemainder); return new PlainDateTime( date.getUTCFullYear(), date.getUTCMonth() + 1, date.getUTCDate(), date.getUTCHours(), date.getUTCMinutes(), date.getUTCSeconds(), date.getUTCMilliseconds(), ); } add(durationLike: DurationLike, options: {overflow?: 'reject' | 'constrain'} = {}): PlainDateTime { let duration = toDuration(durationLike); let timeDuration = toTimeDuration(this); let balanced = balanceTime( duration.hours, duration.minutes, duration.seconds, duration.milliseconds + timeDuration, ); let plainDate = this.toPlainDate(); let date = this.getCalendar().dateAdd( plainDate, new Duration(duration.years, duration.months, duration.weeks, duration.days + balanced.deltaDays), options, ); return date.toPlainDateTime(new PlainTime(balanced.hour, balanced.minute, balanced.second, balanced.millisecond)); } subtract(durationLike: DurationLike, options?: {overflow?: 'reject' | 'constrain'}): PlainDateTime { let duration = toDuration(durationLike); return this.add(duration.negated(), options); } toPlainDate() { return new PlainDate(this.year, this.month, this.day, this.calendarId); } toPlainTime() { return new PlainTime(this.hour, this.minute, this.second, this.millisecond); } toZonedDateTime(timeZone: TimeZone | string, options: ZonedDateTimeFromOptions = {}) { return ZonedDateTime.from( { year: this.year, month: this.month, day: this.day, hour: this.hour, minute: this.minute, second: this.second, millisecond: this.millisecond, timeZone, }, options, ); } toString(options: PlainDateTimeToStringParameters = {}) { let calendarName = options.calendarName ?? 'auto'; let calendarId = this.calendarId; let smallestUnit = parseUnit(options.smallestUnit); let date = smallestUnit || options.roundingMode ? this.round({ smallestUnit: smallestUnit ?? 'second', roundingMode: options.roundingMode ?? 'trunc', }) : this; let plainDate = date.toPlainDate().toString(); let time = date.toPlainTime().toString({ smallestUnit: options.smallestUnit, fractionalSecondDigits: options.fractionalSecondDigits, }); let calendar = calendarName === 'always' || (calendarName === 'auto' && calendarId !== 'iso8601') ? `[u-ca=${this.calendarId}]` : ''; return `${plainDate}T${time}${calendar}`; } toJSON() { return this.toString(); } with(dateComponents: Partial, options?: {overflow?: 'reject' | 'constrain'}) { let overflow = options?.overflow ?? 'constrain'; let {year, month, day} = verifyDateComponents( dateComponents.year ?? this.year, dateComponents.month ?? this.month, dateComponents.day ?? this.day, overflow, ); let {hour, minute, second, millisecond} = verifyTimeComponents( dateComponents.hour ?? this.hour, dateComponents.minute ?? this.minute, dateComponents.second ?? this.second, dateComponents.millisecond ?? this.millisecond, overflow, ); return new PlainDateTime(year, month, day, hour, minute, second, millisecond); } withPlainTime(plainTimeLike: PlainTimeLike) { let plainTime = toPlainTime(plainTimeLike); return this.with({ hour: plainTime.hour, minute: plainTime.minute, second: plainTime.second, millisecond: plainTime.millisecond, }); } withPlainDate(plainDateLike: PlainDateLike) { let plainDate = toPlainDate(plainDateLike); return this.with({ year: plainDate.year, month: plainDate.month, day: plainDate.day, }); } withCalendar(calendar: string) { return new PlainDateTime( this.year, this.month, this.day, this.hour, this.minute, this.second, this.millisecond, calendar, ); } until(otherLike: PlainDateTimeLike, options: PlainDateTimeDeltaParameters) { let other = toPlainDateTime(otherLike); if (this.equals(other)) { return new Duration(); } let smallestUnit = parseUnit(options.smallestUnit, 'millisecond'); let largestUnit = parseUnit(options.largestUnit, 'auto'); let roundingIncrement = options.roundingIncrement ?? 1; let roundingMode = options.roundingMode ?? 'trunc'; if (largestUnit === 'auto') { largestUnit = isCalendarUnit(smallestUnit) ? smallestUnit : 'day'; } let resultRecord = differencePlainDateTimeWithRounding( this, other, smallestUnit, largestUnit, roundingIncrement, roundingMode, ); return resultRecord.duration; } since(other: PlainDateTimeLike, options: PlainDateTimeDeltaParameters) { return toPlainDateTime(other).until(this, options); } static from(item: PlainDateTimeLike, options: {overflow?: 'reject' | 'constrain'} = {}) { if (item instanceof PlainDateTime || item instanceof ZonedDateTime) { return new PlainDateTime(item.year, item.month, item.day, item.hour, item.minute, item.second, item.millisecond); } else if (item && typeof item === 'object' && !Array.isArray(item)) { let {year, month, day} = verifyDateComponents(item.year, item.month, item.day, options.overflow ?? 'constrain'); let {hour, minute, second, millisecond} = verifyTimeComponents( item.hour || 0, item.minute || 0, item.second || 0, item.millisecond || 0, options.overflow ?? 'constrain', ); return new PlainDateTime(year, month, day, hour, minute, second, millisecond); } else if (typeof item === 'string') { let result = ISO8601Result.parseDateTime(item); if (result && result.date) { return new PlainDateTime( result.date.year, result.date.month, result.date.day, result.time?.hour, result.time?.minute, result.time?.second, result.time?.millisecond, ); } } throw new RangeError('Failed to parse PlainDateTime'); } static compare(one: PlainDateTimeLike, two: PlainDateTimeLike): number { return Math.sign(toPlainDateTime(one).raw.valueOf() - toPlainDateTime(two).raw.valueOf()); } }