diff --git a/lib/calendar.ts b/lib/calendar.ts index c80efcb5..1a17a478 100644 --- a/lib/calendar.ts +++ b/lib/calendar.ts @@ -23,9 +23,11 @@ import { JSONStringify, MathAbs, MathFloor, + MathTrunc, MathMax, NumberIsNaN, MathSign, + ObjectAssign, ObjectEntries, ReflectApply, RegExpPrototypeExec, @@ -81,8 +83,11 @@ function calendarDateWeekOfYear( } const calendar = impl[id]; let yow = isoDate.year; - const dayOfWeek = calendar.dayOfWeek(isoDate); - const dayOfYear = calendar.dayOfYear(isoDate); + const { dayOfWeek, dayOfYear, daysInYear } = calendar.isoToDate(isoDate, { + dayOfWeek: true, + dayOfYear: true, + daysInYear: true + }); const fdow = calendar.getFirstDayOfWeek(); const mdow = calendar.getMinimalDaysInFirstWeek(); ES.uncheckedAssertNarrowedType(fdow, 'guaranteed to exist for iso8601/gregory'); @@ -103,7 +108,10 @@ function calendarDateWeekOfYear( if (woy == 0) { // Check for last week of previous year; if true, handle the case for // first week of next year - let prevDoy = dayOfYear + calendar.daysInYear(calendar.dateAdd(isoDate, { years: -1 }, 'constrain')); + const prevYearCalendar = calendar.isoToDate(calendar.dateAdd(isoDate, { years: -1 }, 'constrain'), { + daysInYear: true + }); + let prevDoy = dayOfYear + prevYearCalendar.daysInYear; woy = weekNumber(fdow, mdow, prevDoy, dayOfWeek); yow--; } else { @@ -111,7 +119,7 @@ function calendarDateWeekOfYear( // L-5 L // doy: 359 360 361 362 363 364 365 001 // dow: 1 2 3 4 5 6 7 - let lastDoy = calendar.daysInYear(isoDate); + let lastDoy = daysInYear; if (dayOfYear >= lastDoy - 5) { let lastRelDow = (relDow + lastDoy - dayOfYear) % 7; if (lastRelDow < 0) { @@ -126,6 +134,37 @@ function calendarDateWeekOfYear( return { week: woy, year: yow }; } +function ISODateSurpasses(sign: -1 | 0 | 1, y1: number, m1: number, d1: number, y2: number, m2: number, d2: number) { + const cmp = ES.CompareISODate(y1, m1, d1, y2, m2, d2); + return sign * cmp === 1; +} + +interface CalendarDateRecord { + era: string | undefined; + eraYear: number | undefined; + year: number; + month: number; + monthCode: string; + day: number; + dayOfWeek: number; + dayOfYear: number; + weekOfYear: { week: number; year: number } | { week: undefined; year: undefined }; + daysInWeek: number; + daysInMonth: number; + daysInYear: number; + monthsInYear: number; + inLeapYear: boolean; +} + +type ResolveFieldsReturn = Resolve< + CalendarFieldsRecord & { + year: Type extends 'date' ? number : never; + month: number; + monthCode: string; + day: number; + } +>; + /** * Shape of internal implementation of each built-in calendar. Note that * parameter types are simpler than CalendarProtocol because the `Calendar` @@ -137,24 +176,23 @@ function calendarDateWeekOfYear( * latter is cloned for each non-ISO calendar at the end of this file. */ export interface CalendarImpl { - year(isoDate: ISODate): number; - month(isoDate: ISODate): number; - monthCode(isoDate: ISODate): string; - day(isoDate: ISODate): number; - era(isoDate: ISODate): string | undefined; - eraYear(isoDate: ISODate): number | undefined; - dayOfWeek(isoDate: ISODate): number; - dayOfYear(isoDate: ISODate): number; + isoToDate< + Request extends Partial>, + T extends { + [Field in keyof CalendarDateRecord]: Request extends { [K in Field]: true } ? CalendarDateRecord[Field] : never; + } + >( + isoDate: ISODate, + requestedFields: Request + ): T; getFirstDayOfWeek(): number | undefined; getMinimalDaysInFirstWeek(): number | undefined; - daysInWeek(isoDate: ISODate): number; - daysInMonth(isoDate: Omit): number; - daysInYear(isoDate: ISODate): number; - monthsInYear(isoDate: ISODate): number; - inLeapYear(isoDate: ISODate): boolean; - dateFromFields(fields: CalendarFieldsRecord, overflow: 'constrain' | 'reject'): ISODate; - yearMonthFromFields(fields: CalendarFieldsRecord, overflow: 'constrain' | 'reject'): ISODate; - monthDayFromFields(fields: MonthDayFromFieldsObject, overflow: 'constrain' | 'reject'): ISODate; + resolveFields( + fields: CalendarFieldsRecord, + type: Type + ): asserts fields is ResolveFieldsReturn; + dateToISO(fields: ResolveFieldsReturn<'date'>, overflow: Overflow): ISODate; + monthDayToISOReferenceDate(fields: ResolveFieldsReturn<'month-day'>, overflow: Overflow): ISODate; dateAdd(date: ISODate, duration: Partial, overflow: Overflow): ISODate; dateUntil(one: ISODate, two: ISODate, largestUnit: 'year' | 'month' | 'week' | 'day'): DateDuration; extraFields(): FieldKey[]; @@ -179,25 +217,21 @@ const impl: CalendarImplementations = {} as unknown as CalendarImplementations; * without Intl (ECMA-402) support. */ impl['iso8601'] = { - dateFromFields(fields, overflow) { - requireFields(fields, ['year', 'day']); - let { year, month, day } = resolveNonLunisolarMonth(fields); - ({ year, month, day } = ES.RegulateISODate(year, month, day, overflow)); - ES.RejectDateRange(year, month, day); - return { year, month, day }; + resolveFields(fields, type) { + if ((type === 'date' || type === 'year-month') && fields.year === undefined) { + throw new TypeErrorCtor('year is required'); + } + if ((type === 'date' || type === 'month-day') && fields.day === undefined) { + throw new TypeErrorCtor('day is required'); + } + ObjectAssign(fields, resolveNonLunisolarMonth(fields)); }, - yearMonthFromFields(fields, overflow) { - requireFields(fields, ['year']); - let { year, month } = resolveNonLunisolarMonth(fields); - ({ year, month } = ES.RegulateISOYearMonth(year, month, overflow)); - ES.RejectYearMonthRange(year, month); - return { year, month, /* reference */ day: 1 }; + dateToISO(fields, overflow) { + return ES.RegulateISODate(fields.year, fields.month, fields.day, overflow); }, - monthDayFromFields(fields, overflow) { - requireFields(fields, ['day']); + monthDayToISOReferenceDate(fields, overflow) { const referenceISOYear = 1972; - let { month, day, year } = resolveNonLunisolarMonth(fields); - ({ month, day } = ES.RegulateISODate(year !== undefined ? year : referenceISOYear, month, day, overflow)); + const { month, day } = ES.RegulateISODate(fields.year ?? referenceISOYear, fields.month, fields.day, overflow); return { month, day, year: referenceISOYear }; }, extraFields() { @@ -223,69 +257,106 @@ impl['iso8601'] = { ({ year, month } = ES.BalanceISOYearMonth(year, month)); ({ year, month, day } = ES.RegulateISODate(year, month, day, overflow)); day += days + 7 * weeks; - ({ year, month, day } = ES.BalanceISODate(year, month, day)); - ES.RejectDateRange(year, month, day); - return { year, month, day }; + return ES.BalanceISODate(year, month, day); }, dateUntil(one, two, largestUnit) { - return ES.DifferenceISODate(one.year, one.month, one.day, two.year, two.month, two.day, largestUnit); - }, - year({ year }) { - return year; - }, - era() { - return undefined; - }, - eraYear() { - return undefined; - }, - month({ month }) { - return month; - }, - monthCode({ month }) { - return buildMonthCode(month); - }, - day({ day }) { - return day; - }, - dayOfWeek({ year, month, day }) { - const m = month + (month < 3 ? 10 : -2); - const Y = year - (month < 3 ? 1 : 0); + const sign = -ES.CompareISODate(one.year, one.month, one.day, two.year, two.month, two.day); + if (sign === 0) return { years: 0, months: 0, weeks: 0, days: 0 }; + ES.uncheckedAssertNarrowedType<-1 | 1>(sign, "the - operator's return type is number"); - const c = MathFloor(Y / 100); - const y = Y - c * 100; - const d = day; + let years = 0; + let months = 0; + let intermediate; + if (largestUnit === 'year' || largestUnit === 'month') { + // We can skip right to the neighbourhood of the correct number of years, + // it'll be at least one less than two.year - one.year (unless it's zero) + let candidateYears = two.year - one.year; + if (candidateYears !== 0) candidateYears -= sign; + // loops at most twice + while (!ISODateSurpasses(sign, one.year + candidateYears, one.month, one.day, two.year, two.month, two.day)) { + years = candidateYears; + candidateYears += sign; + } - const pD = d; - const pM = MathFloor(2.6 * m - 0.2); - const pY = y + MathFloor(y / 4); - const pC = MathFloor(c / 4) - 2 * c; + let candidateMonths = sign; + intermediate = ES.BalanceISOYearMonth(one.year + years, one.month + candidateMonths); + // loops at most 12 times + while (!ISODateSurpasses(sign, intermediate.year, intermediate.month, one.day, two.year, two.month, two.day)) { + months = candidateMonths; + candidateMonths += sign; + intermediate = ES.BalanceISOYearMonth(intermediate.year, intermediate.month + sign); + } - const dow = (pD + pM + pY + pC) % 7; + if (largestUnit === 'month') { + months += years * 12; + years = 0; + } + } - return dow + (dow <= 0 ? 7 : 0); - }, - dayOfYear({ year, month, day }) { - let days = day; - for (let m = month - 1; m > 0; m--) { - days += this.daysInMonth({ year, month: m }); + intermediate = ES.BalanceISOYearMonth(one.year + years, one.month + months); + const constrained = ES.ConstrainISODate(intermediate.year, intermediate.month, one.day); + + let weeks = 0; + let days = + ES.ISODateToEpochDays(two.year, two.month - 1, two.day) - + ES.ISODateToEpochDays(constrained.year, constrained.month - 1, constrained.day); + + if (largestUnit === 'week') { + weeks = MathTrunc(days / 7); + days %= 7; } - return days; - }, - daysInWeek() { - return 7; - }, - daysInMonth({ year, month }) { - return ES.ISODaysInMonth(year, month); - }, - daysInYear(isoDate) { - return this.inLeapYear(isoDate) ? 366 : 365; - }, - monthsInYear() { - return 12; + + return { years, months, weeks, days }; }, - inLeapYear({ year }) { - return ES.LeapYear(year); + isoToDate< + Request extends Partial>, + T extends { + [Field in keyof CalendarDateRecord]: Request extends { [K in Field]: true } ? CalendarDateRecord[Field] : never; + } + >({ year, month, day }: ISODate, requestedFields: Request): T { + // requestedFields parameter is not part of the spec text. It's an + // illustration of one way implementations may choose to optimize this + // operation. + const date: Partial = { + era: undefined, + eraYear: undefined, + year, + month, + day, + daysInWeek: 7, + monthsInYear: 12 + }; + if (requestedFields.monthCode) date.monthCode = buildMonthCode(month); + if (requestedFields.dayOfWeek) { + // https://en.wikipedia.org/wiki/Determination_of_the_day_of_the_week#Disparate_variation + const shiftedMonth = month + (month < 3 ? 10 : -2); + const shiftedYear = year - (month < 3 ? 1 : 0); + + const century = MathFloor(shiftedYear / 100); + const yearInCentury = shiftedYear - century * 100; + + const monthTerm = MathFloor(2.6 * shiftedMonth - 0.2); + const yearTerm = yearInCentury + MathFloor(yearInCentury / 4); + const centuryTerm = MathFloor(century / 4) - 2 * century; + + const dow = (day + monthTerm + yearTerm + centuryTerm) % 7; + + date.dayOfWeek = dow + (dow <= 0 ? 7 : 0); + } + if (requestedFields.dayOfYear) { + let days = day; + for (let m = month - 1; m > 0; m--) { + days += ES.ISODaysInMonth(year, m); + } + date.dayOfYear = days; + } + if (requestedFields.weekOfYear) date.weekOfYear = calendarDateWeekOfYear('iso8601', { year, month, day }); + if (requestedFields.daysInMonth) date.daysInMonth = ES.ISODaysInMonth(year, month); + if (requestedFields.daysInYear || requestedFields.inLeapYear) { + date.inLeapYear = ES.LeapYear(year); + date.daysInYear = date.inLeapYear ? 366 : 365; + } + return date as T; }, getFirstDayOfWeek() { return 1; @@ -346,28 +417,10 @@ function monthCodeNumberPart(monthCode: string) { return month; } -function buildMonthCode(month: number | string, leap = false) { - return `M${Call(StringPrototypePadStart, `${month}`, [2, '0'])}${leap ? 'L' : ''}`; -} - -type CalendarKey = 'era' | 'eraYear' | 'year' | 'month' | 'monthCode' | 'day'; - -type WithRequired = T & { [P in K]-?: T[P] }; - -function requireFields>( - fields: T, - // Constrains the Required keys to be a subset of the given field keys - // This could have been written directly into the parameter type, but that - // causes an unintended effect where the required fields are added to the list - // of field keys, even if that key isn't present in 'fields'. - requiredFieldNames: RequiredFields -): asserts fields is Resolve> { - for (let index = 0; index < requiredFieldNames.length; index++) { - const fieldName = requiredFieldNames[index]; - if (fields[fieldName] === undefined) { - throw new TypeErrorCtor(`${fieldName} is required`); - } - } +function buildMonthCode(month: number, leap = false) { + const digitPart = Call(StringPrototypePadStart, `${month}`, [2, '0']); + const leapMarker = leap ? 'L' : ''; + return `M${digitPart}${leapMarker}`; } /** @@ -1027,19 +1080,10 @@ abstract class HelperBase { calendarDaysUntil(calendarOne: CalendarYMD, calendarTwo: CalendarYMD, cache: OneObjectCache): number { const oneIso = this.calendarToIsoDate(calendarOne, 'constrain', cache); const twoIso = this.calendarToIsoDate(calendarTwo, 'constrain', cache); - return this.isoDaysUntil(oneIso, twoIso); - } - isoDaysUntil(oneIso: ISODate, twoIso: ISODate): number { - const duration = ES.DifferenceISODate( - oneIso.year, - oneIso.month, - oneIso.day, - twoIso.year, - twoIso.month, - twoIso.day, - 'day' + return ( + ES.ISODateToEpochDays(twoIso.year, twoIso.month - 1, twoIso.day) - + ES.ISODateToEpochDays(oneIso.year, oneIso.month - 1, oneIso.day) ); - return duration.days; } // Override if calendar uses eras hasEra = false; @@ -1773,9 +1817,6 @@ abstract class GregorianBaseHelper extends HelperBase { ({ code, names = [] }) => code === era || ES.Call(ArrayPrototypeIncludes, names, [era]) ]); if (!matchingEra) throw new RangeErrorCtor(`Era ${era} (ISO year ${eraYear}) was not matched by any era`); - if (eraYear < 1 && matchingEra.reverseOf) { - throw new RangeErrorCtor(`Years in ${era} era must be positive, not ${year}`); - } if (matchingEra.reverseOf) { year = matchingEra.anchorEpoch.year - eraYear; } else { @@ -2224,32 +2265,21 @@ class NonIsoCalendar implements CalendarImpl { if (type === 'month-day') return []; return ['era', 'eraYear']; } - dateFromFields(fields: CalendarFieldsRecord, overflow: Overflow): ISODate { - const cache = new OneObjectCache(); + resolveFields(fields: CalendarFieldsRecord /* , type */) { if (this.helper.calendarType !== 'lunisolar') { + const cache = new OneObjectCache(); const largestMonth = this.helper.monthsInYear({ year: fields.year ?? 1972 }, cache); resolveNonLunisolarMonth(fields, undefined, largestMonth); } - const result = this.helper.calendarToIsoDate(fields, overflow, cache); - cache.setObject(result); - return result; } - yearMonthFromFields(fields: CalendarFieldsRecord, overflow: Overflow): ISODate { + dateToISO(fields: CalendarDateFields, overflow: Overflow) { const cache = new OneObjectCache(); - if (this.helper.calendarType !== 'lunisolar') { - const largestMonth = this.helper.monthsInYear({ year: fields.year ?? 1972 }, cache); - resolveNonLunisolarMonth(fields, undefined, largestMonth); - } - const result = this.helper.calendarToIsoDate({ ...fields, day: 1 }, overflow, cache); + const result = this.helper.calendarToIsoDate(fields, overflow, cache); cache.setObject(result); return result; } - monthDayFromFields(fields: MonthDayFromFieldsObject, overflow: Overflow): ISODate { + monthDayToISOReferenceDate(fields: MonthDayFromFieldsObject, overflow: Overflow) { const cache = new OneObjectCache(); - if (this.helper.calendarType !== 'lunisolar') { - const largestMonth = this.helper.monthsInYear({ year: fields.year ?? 1972 }, cache); - resolveNonLunisolarMonth(fields, undefined, largestMonth); - } const result = this.helper.monthDayFromFields(fields, overflow, cache); // result.year is a reference year where this month/day exists in this calendar cache.setObject(result); @@ -2318,87 +2348,33 @@ class NonIsoCalendar implements CalendarImpl { const result = this.helper.untilCalendar(calendarOne, calendarTwo, largestUnit, cacheOne); return result; } - year(date: ISODate): number { - const cache = OneObjectCache.getCacheForObject(date); - const calendarDate = this.helper.isoToCalendarDate(date, cache); - return calendarDate.year; - } - month(date: ISODate): number { - const cache = OneObjectCache.getCacheForObject(date); - const calendarDate = this.helper.isoToCalendarDate(date, cache); - return calendarDate.month; - } - day(date: ISODate): number { - const cache = OneObjectCache.getCacheForObject(date); - const calendarDate = this.helper.isoToCalendarDate(date, cache); - return calendarDate.day; - } - era(date: ISODate): string | undefined { - if (!this.helper.hasEra) return undefined; - const cache = OneObjectCache.getCacheForObject(date); - const calendarDate = this.helper.isoToCalendarDate(date, cache); - return calendarDate.era; - } - eraYear(date: ISODate): number | undefined { - if (!this.helper.hasEra) return undefined; - const cache = OneObjectCache.getCacheForObject(date); - const calendarDate = this.helper.isoToCalendarDate(date, cache); - return calendarDate.eraYear; - } - monthCode(date: ISODate): string { - const cache = OneObjectCache.getCacheForObject(date); - const calendarDate = this.helper.isoToCalendarDate(date, cache); - return calendarDate.monthCode; - } - dayOfWeek(isoDate: ISODate) { - return impl['iso8601'].dayOfWeek(isoDate); - } - dayOfYear(isoDate: ISODate) { - const cache = OneObjectCache.getCacheForObject(isoDate); - const calendarDate = this.helper.isoToCalendarDate(isoDate, cache); - const startOfYear = this.helper.startOfCalendarYear(calendarDate); - const diffDays = this.helper.calendarDaysUntil(startOfYear, calendarDate, cache); - return diffDays + 1; - } - daysInWeek(date: ISODate): number { - return impl['iso8601'].daysInWeek(date); - } - daysInMonth(date: ISODate): number { - const cache = OneObjectCache.getCacheForObject(date); - const calendarDate = this.helper.isoToCalendarDate(date, cache); - - // Easy case: if the helper knows the length without any heavy calculation. - const max = this.helper.maximumMonthLength(calendarDate); - const min = this.helper.minimumMonthLength(calendarDate); - if (max === min) return max; - - // The harder case is where months vary every year, e.g. islamic calendars. - // Find the answer by calculating the difference in days between the first - // day of the current month and the first day of the next month. - const startOfMonthCalendar = this.helper.startOfCalendarMonth(calendarDate); - const startOfNextMonthCalendar = this.helper.addMonthsCalendar(startOfMonthCalendar, 1, 'constrain', cache); - const result = this.helper.calendarDaysUntil(startOfMonthCalendar, startOfNextMonthCalendar, cache); - return result; - } - daysInYear(isoDate: ISODate) { + isoToDate< + Request extends Partial>, + T extends { + [Field in keyof CalendarDateRecord]: Request extends { [K in Field]: true } ? CalendarDateRecord[Field] : never; + } + >(isoDate: ISODate, requestedFields: Request): T { const cache = OneObjectCache.getCacheForObject(isoDate); - const calendarDate = this.helper.isoToCalendarDate(isoDate, cache); - const startOfYearCalendar = this.helper.startOfCalendarYear(calendarDate); - const startOfNextYearCalendar = this.helper.addCalendar(startOfYearCalendar, { years: 1 }, 'constrain', cache); - const result = this.helper.calendarDaysUntil(startOfYearCalendar, startOfNextYearCalendar, cache); - return result; - } - monthsInYear(date: ISODate): number { - const cache = OneObjectCache.getCacheForObject(date); - const calendarDate = this.helper.isoToCalendarDate(date, cache); - const result = this.helper.monthsInYear(calendarDate, cache); - return result; - } - inLeapYear(date: ISODate) { - const cache = OneObjectCache.getCacheForObject(date); - const calendarDate = this.helper.isoToCalendarDate(date, cache); - const result = this.helper.inLeapYear(calendarDate, cache); - return result; + const calendarDate: Partial & FullCalendarDate = this.helper.isoToCalendarDate(isoDate, cache); + if (requestedFields.dayOfWeek) { + calendarDate.dayOfWeek = impl['iso8601'].isoToDate(isoDate, { dayOfWeek: true }).dayOfWeek; + } + if (requestedFields.dayOfYear) { + const startOfYear = this.helper.startOfCalendarYear(calendarDate); + const diffDays = this.helper.calendarDaysUntil(startOfYear, calendarDate, cache); + calendarDate.dayOfYear = diffDays + 1; + } + if (requestedFields.weekOfYear) calendarDate.weekOfYear = calendarDateWeekOfYear(this.helper.id, isoDate); + calendarDate.daysInWeek = 7; + if (requestedFields.daysInMonth) calendarDate.daysInMonth = this.helper.daysInMonth(calendarDate, cache); + if (requestedFields.daysInYear) { + const startOfYearCalendar = this.helper.startOfCalendarYear(calendarDate); + const startOfNextYearCalendar = this.helper.addCalendar(startOfYearCalendar, { years: 1 }, 'constrain', cache); + calendarDate.daysInYear = this.helper.calendarDaysUntil(startOfYearCalendar, startOfNextYearCalendar, cache); + } + if (requestedFields.monthsInYear) calendarDate.monthsInYear = this.helper.monthsInYear(calendarDate, cache); + if (requestedFields.inLeapYear) calendarDate.inLeapYear = this.helper.inLeapYear(calendarDate, cache); + return calendarDate as T; } getFirstDayOfWeek(): number | undefined { return this.helper.getFirstDayOfWeek(); @@ -2440,4 +2416,3 @@ function calendarImpl(calendar: BuiltinCalendarId) { // Probably not what the intrinsics mechanism was intended for, but view this as // an export of calendarImpl while avoiding circular dependencies DefineIntrinsic('calendarImpl', calendarImpl); -DefineIntrinsic('calendarDateWeekOfYear', calendarDateWeekOfYear); diff --git a/lib/duration.ts b/lib/duration.ts index 593a1460..3af220f2 100644 --- a/lib/duration.ts +++ b/lib/duration.ts @@ -8,7 +8,6 @@ import { // class static functions and methods MathAbs, - NumberIsNaN, ObjectDefineProperty, // miscellaneous @@ -235,7 +234,7 @@ export class Duration implements Temporal.Duration { const calendar = GetSlot(zonedRelativeTo, CALENDAR); const relativeEpochNs = GetSlot(zonedRelativeTo, EPOCHNANOSECONDS); const targetEpochNs = ES.AddZonedDateTime(relativeEpochNs, timeZone, calendar, duration); - ({ duration } = ES.DifferenceZonedDateTimeWithRounding( + duration = ES.DifferenceZonedDateTimeWithRounding( relativeEpochNs, targetEpochNs, timeZone, @@ -244,7 +243,7 @@ export class Duration implements Temporal.Duration { roundingIncrement, smallestUnit, roundingMode - )); + ); if (ES.TemporalUnitCategory(largestUnit) === 'date') largestUnit = 'hour'; return ES.UnnormalizeDuration(duration, largestUnit); } @@ -259,7 +258,7 @@ export class Duration implements Temporal.Duration { const dateDuration = ES.AdjustDateDurationRecord(duration.date, targetTime.deltaDays); const targetDate = ES.CalendarDateAdd(calendar, isoRelativeToDate, dateDuration, 'constrain'); - ({ duration } = ES.DifferencePlainDateTimeWithRounding( + duration = ES.DifferencePlainDateTimeWithRounding( isoRelativeToDate.year, isoRelativeToDate.month, isoRelativeToDate.day, @@ -283,7 +282,7 @@ export class Duration implements Temporal.Duration { roundingIncrement, smallestUnit, roundingMode - )); + ); return ES.UnnormalizeDuration(duration, largestUnit); } @@ -296,7 +295,7 @@ export class Duration implements Temporal.Duration { } assert(!ES.IsCalendarUnit(smallestUnit), 'smallestUnit was larger than largestUnit'); let duration = ES.NormalizeDurationWith24HourDays(this); - ({ duration } = ES.RoundTimeDuration(duration, roundingIncrement, smallestUnit, roundingMode)); + duration = ES.RoundTimeDuration(duration, roundingIncrement, smallestUnit, roundingMode); return ES.UnnormalizeDuration(duration, largestUnit); } total(optionsParam: Params['total'][0]): Return['total'] { @@ -316,18 +315,7 @@ export class Duration implements Temporal.Duration { const calendar = GetSlot(zonedRelativeTo, CALENDAR); const relativeEpochNs = GetSlot(zonedRelativeTo, EPOCHNANOSECONDS); const targetEpochNs = ES.AddZonedDateTime(relativeEpochNs, timeZone, calendar, duration); - const { total } = ES.DifferenceZonedDateTimeWithRounding( - relativeEpochNs, - targetEpochNs, - timeZone, - calendar, - unit, - 1, - unit, - 'trunc' - ); - assert(!NumberIsNaN(total), 'total went through NudgeToZonedTime code path'); - return total; + return ES.DifferenceZonedDateTimeWithTotal(relativeEpochNs, targetEpochNs, timeZone, calendar, unit); } if (plainRelativeTo) { @@ -340,7 +328,7 @@ export class Duration implements Temporal.Duration { const dateDuration = ES.AdjustDateDurationRecord(duration.date, targetTime.deltaDays); const targetDate = ES.CalendarDateAdd(calendar, isoRelativeToDate, dateDuration, 'constrain'); - const { total } = ES.DifferencePlainDateTimeWithRounding( + return ES.DifferencePlainDateTimeWithTotal( isoRelativeToDate.year, isoRelativeToDate.month, isoRelativeToDate.day, @@ -360,13 +348,8 @@ export class Duration implements Temporal.Duration { targetTime.microsecond, targetTime.nanosecond, calendar, - unit, - 1, - unit, - 'trunc' + unit ); - assert(!NumberIsNaN(total), 'total went through NudgeToZonedTime code path'); - return total; } // No reference date to calculate difference relative to @@ -378,8 +361,7 @@ export class Duration implements Temporal.Duration { throw new RangeErrorCtor(`a starting point is required for ${unit}s total`); } const duration = ES.NormalizeDurationWith24HourDays(this); - const { total } = ES.RoundTimeDuration(duration, 1, unit, 'trunc'); - return total; + return ES.TotalTimeDuration(duration.norm, unit); } toString(options: Params['toString'][0] = undefined): string { if (!ES.IsTemporalDuration(this)) throw new TypeErrorCtor('invalid receiver'); @@ -400,7 +382,7 @@ export class Duration implements Temporal.Duration { const largestUnit = ES.DefaultTemporalLargestUnit(this); let duration = ES.NormalizeDuration(this); - ({ duration } = ES.RoundTimeDuration(duration, increment, unit, roundingMode)); + duration = ES.RoundTimeDuration(duration, increment, unit, roundingMode); const roundedDuration = ES.UnnormalizeDuration(duration, ES.LargerOfTwoTemporalUnits(largestUnit, 'second')); return ES.TemporalDurationToString(roundedDuration, precision); } diff --git a/lib/ecmascript.ts b/lib/ecmascript.ts index c90dd0a2..b5858fea 100644 --- a/lib/ecmascript.ts +++ b/lib/ecmascript.ts @@ -76,6 +76,7 @@ import JSBI from 'jsbi'; import type { Temporal } from '..'; import { assert, assertNotReached } from './assert'; import { abs, compare, DAY_NANOS_JSBI, divmod, ensureJSBI, isEven, MILLION, ONE, TWO, ZERO } from './bigintmath'; +import type { CalendarImpl } from './calendar'; import type { AnyTemporalLikeType, UnitSmallerThanOrEqualTo, @@ -917,21 +918,6 @@ export function RegulateTime( return { hour, minute, second, millisecond, microsecond, nanosecond }; } -export function RegulateISOYearMonth(yearParam: number, monthParam: number, overflow: Overflow) { - let year = yearParam; - let month = monthParam; - const referenceISODay = 1; - switch (overflow) { - case 'reject': - RejectISODate(year, month, referenceISODay); - break; - case 'constrain': - ({ year, month } = ConstrainISODate(year, month)); - break; - } - return { year, month }; -} - export function ToTemporalPartialDurationRecord(temporalDurationLike: Temporal.DurationLike | string) { if (!IsObject(temporalDurationLike)) { throw new TypeErrorCtor('invalid duration-like'); @@ -1005,7 +991,7 @@ export function PlainDateTimeToISODateTimeRecord(plainDateTime: Temporal.PlainDa }; } -function ISODateTimeToDateRecord({ year, month, day }: ISODateTime) { +export function ISODateTimeToDateRecord({ year, month, day }: ISODateTime) { return { year, month, day }; } @@ -1361,6 +1347,21 @@ export function TemporalObjectToFields( return ISODateToFields(calendar, isoDate, type); } +function calendarImplForID(calendar: BuiltinCalendarId) { + return GetIntrinsic('%calendarImpl%')(calendar); +} + +export function calendarImplForObj( + temporalObj: + | Temporal.PlainDate + | Temporal.PlainDateTime + | Temporal.PlainMonthDay + | Temporal.PlainYearMonth + | Temporal.ZonedDateTime +) { + return GetIntrinsic('%calendarImpl%')(GetSlot(temporalObj, CALENDAR)); +} + type ISODateToFieldsReturn = Resolve<{ year: Type extends 'date' | 'year-month' ? number : never; monthCode: string; @@ -1375,12 +1376,15 @@ export function ISODateToFields( ): ISODateToFieldsReturn; export function ISODateToFields(calendar: BuiltinCalendarId, isoDate: ISODate, type = 'date') { const fields = ObjectCreate(null); - fields.monthCode = CalendarMonthCode(calendar, isoDate); + const calendarImpl = calendarImplForID(calendar); + const calendarDate = calendarImpl.isoToDate(isoDate, { year: true, monthCode: true, day: true }); + + fields.monthCode = calendarDate.monthCode; if (type === 'month-day' || type === 'date') { - fields.day = CalendarDay(calendar, isoDate); + fields.day = calendarDate.day; } if (type === 'year-month' || type === 'date') { - fields.year = CalendarYear(calendar, isoDate); + fields.year = calendarDate.year; } return fields; } @@ -1416,7 +1420,7 @@ export function PrepareCalendarFields< nonCalendarFieldNames: Array, requiredFields: RequiredFields ): PrepareCalendarFieldsReturn { - const extraFieldNames = GetIntrinsic('%calendarImpl%')(calendar).extraFields(); + const extraFieldNames = calendarImplForID(calendar).extraFields(); const fields: FieldKeys[] = Call(ArrayPrototypeConcat, calendarFieldNames, [nonCalendarFieldNames, extraFieldNames]); const result: Partial> = ObjectCreate(null); let any = false; @@ -1696,10 +1700,31 @@ export function ToTemporalInstant(itemParam: InstantParams['from'][0]) { } = time === 'start-of-day' ? {} : time; // ParseTemporalInstantString ensures that either `z` is true or or `offset` is non-undefined - const offsetNanoseconds = z ? 0 : ParseDateTimeUTCOffset((assertExists(offset), offset)); - const epochNanoseconds = JSBI.subtract( - GetUTCEpochNanoseconds(year, month, day, hour, minute, second, millisecond, microsecond, nanosecond), - JSBI.BigInt(offsetNanoseconds) + const offsetNanoseconds = z ? 0 : ParseDateTimeUTCOffset(castExists(offset)); + const balanced = BalanceISODateTime( + year, + month, + day, + hour, + minute, + second, + millisecond, + microsecond, + nanosecond - offsetNanoseconds + ); + if (MathAbs(ISODateToEpochDays(balanced.year, balanced.month - 1, balanced.day)) > 1e8) { + throw new RangeErrorCtor('date/time value is outside the supported range'); + } + const epochNanoseconds = GetUTCEpochNanoseconds( + balanced.year, + balanced.month, + balanced.day, + balanced.hour, + balanced.minute, + balanced.second, + balanced.millisecond, + balanced.microsecond, + balanced.nanosecond ); ValidateEpochNanoseconds(epochNanoseconds); return new TemporalInstant(epochNanoseconds); @@ -1861,6 +1886,43 @@ export function InterpretISODateTimeOffset( return GetEpochNanosecondsFor(timeZone, dt, disambiguation); } + // The caller wants the offset to always win ('use') OR the caller is OK + // with the offset winning ('prefer' or 'reject') as long as it's valid + // for this timezone and date/time. + if (offsetBehaviour === 'exact' || offsetOpt === 'use') { + // Calculate the instant for the input's date/time and offset + const balanced = BalanceISODateTime( + year, + month, + day, + time.hour, + time.minute, + time.second, + time.millisecond, + time.microsecond, + time.nanosecond - offsetNs + ); + if (MathAbs(ISODateToEpochDays(balanced.year, balanced.month - 1, balanced.day)) > 1e8) { + throw new RangeErrorCtor('date/time outside of supported range'); + } + const epochNs = GetUTCEpochNanoseconds( + balanced.year, + balanced.month, + balanced.day, + balanced.hour, + balanced.minute, + balanced.second, + balanced.millisecond, + balanced.microsecond, + balanced.nanosecond + ); + ValidateEpochNanoseconds(epochNs); + return epochNs; + } + + if (MathAbs(ISODateToEpochDays(year, month - 1, day)) > 1e8) { + throw new RangeErrorCtor('date/time outside of supported range'); + } const utcEpochNs = GetUTCEpochNanoseconds( year, month, @@ -1873,16 +1935,6 @@ export function InterpretISODateTimeOffset( time.nanosecond ); - // The caller wants the offset to always win ('use') OR the caller is OK - // with the offset winning ('prefer' or 'reject') as long as it's valid - // for this timezone and date/time. - if (offsetBehaviour === 'exact' || offsetOpt === 'use') { - // Calculate the instant for the input's date/time and offset - const epochNs = JSBI.subtract(utcEpochNs, JSBI.BigInt(offsetNs)); - ValidateEpochNanoseconds(epochNs); - return epochNs; - } - // "prefer" or "reject" const possibleEpochNs = GetPossibleEpochNanoseconds(timeZone, dt); for (let index = 0; index < possibleEpochNs.length; index++) { @@ -2207,7 +2259,7 @@ export function CalendarMergeFields, ToAdd additionalFields: ToAdd ) { const additionalKeys = CalendarFieldKeysPresent(additionalFields); - const overriddenKeys = GetIntrinsic('%calendarImpl%')(calendar).fieldKeysToIgnore(additionalKeys); + const overriddenKeys = calendarImplForID(calendar).fieldKeysToIgnore(additionalKeys); const merged = ObjectCreate(null); const fieldsKeys = CalendarFieldKeysPresent(fields); for (let ix = 0; ix < CALENDAR_FIELD_KEYS.length; ix++) { @@ -2230,7 +2282,9 @@ export function CalendarDateAdd( dateDuration: Partial, overflow: Overflow ) { - return GetIntrinsic('%calendarImpl%')(calendar).dateAdd(isoDate, dateDuration, overflow); + const result = calendarImplForID(calendar).dateAdd(isoDate, dateDuration, overflow); + RejectDateRange(result.year, result.month, result.day); + return result; } function CalendarDateUntil( @@ -2239,67 +2293,7 @@ function CalendarDateUntil( isoOtherDate: ISODate, largestUnit: Temporal.DateUnit ) { - return GetIntrinsic('%calendarImpl%')(calendar).dateUntil(isoDate, isoOtherDate, largestUnit); -} - -export function CalendarYear(calendar: BuiltinCalendarId, isoDate: ISODate) { - return GetIntrinsic('%calendarImpl%')(calendar).year(isoDate); -} - -export function CalendarMonth(calendar: BuiltinCalendarId, isoDate: ISODate) { - return GetIntrinsic('%calendarImpl%')(calendar).month(isoDate); -} - -export function CalendarMonthCode(calendar: BuiltinCalendarId, isoDate: ISODate) { - return GetIntrinsic('%calendarImpl%')(calendar).monthCode(isoDate); -} - -export function CalendarDay(calendar: BuiltinCalendarId, isoDate: ISODate) { - return GetIntrinsic('%calendarImpl%')(calendar).day(isoDate); -} - -export function CalendarEra(calendar: BuiltinCalendarId, isoDate: ISODate) { - return GetIntrinsic('%calendarImpl%')(calendar).era(isoDate); -} - -export function CalendarEraYear(calendar: BuiltinCalendarId, isoDate: ISODate) { - return GetIntrinsic('%calendarImpl%')(calendar).eraYear(isoDate); -} - -export function CalendarDayOfWeek(calendar: BuiltinCalendarId, isoDate: ISODate) { - return GetIntrinsic('%calendarImpl%')(calendar).dayOfWeek(isoDate); -} - -export function CalendarDayOfYear(calendar: BuiltinCalendarId, isoDate: ISODate) { - return GetIntrinsic('%calendarImpl%')(calendar).dayOfYear(isoDate); -} - -export function CalendarWeekOfYear(calendar: BuiltinCalendarId, isoDate: ISODate) { - return GetIntrinsic('%calendarDateWeekOfYear%')(calendar, isoDate).week; -} - -export function CalendarYearOfWeek(calendar: BuiltinCalendarId, isoDate: ISODate) { - return GetIntrinsic('%calendarDateWeekOfYear%')(calendar, isoDate).year; -} - -export function CalendarDaysInWeek(calendar: BuiltinCalendarId, isoDate: ISODate) { - return GetIntrinsic('%calendarImpl%')(calendar).daysInWeek(isoDate); -} - -export function CalendarDaysInMonth(calendar: BuiltinCalendarId, isoDate: ISODate) { - return GetIntrinsic('%calendarImpl%')(calendar).daysInMonth(isoDate); -} - -export function CalendarDaysInYear(calendar: BuiltinCalendarId, isoDate: ISODate) { - return GetIntrinsic('%calendarImpl%')(calendar).daysInYear(isoDate); -} - -export function CalendarMonthsInYear(calendar: BuiltinCalendarId, isoDate: ISODate) { - return GetIntrinsic('%calendarImpl%')(calendar).monthsInYear(isoDate); -} - -export function CalendarInLeapYear(calendar: BuiltinCalendarId, isoDate: ISODate) { - return GetIntrinsic('%calendarImpl%')(calendar).inLeapYear(isoDate); + return calendarImplForID(calendar).dateUntil(isoDate, isoOtherDate, largestUnit); } export function ToTemporalCalendarIdentifier(calendarLike: Temporal.CalendarLike): BuiltinCalendarId { @@ -2339,7 +2333,11 @@ export function CalendarEquals(one: BuiltinCalendarId, two: BuiltinCalendarId) { } export function CalendarDateFromFields(calendar: BuiltinCalendarId, fields: CalendarFieldsRecord, overflow: Overflow) { - return GetIntrinsic('%calendarImpl%')(calendar).dateFromFields(fields, overflow); + const calendarImpl: CalendarImpl = calendarImplForID(calendar); + calendarImpl.resolveFields(fields, 'date'); + const result = calendarImpl.dateToISO(fields, overflow); + RejectDateRange(result.year, result.month, result.day); + return result; } export function CalendarYearMonthFromFields( @@ -2347,7 +2345,12 @@ export function CalendarYearMonthFromFields( fields: CalendarFieldsRecord, overflow: Overflow ) { - return GetIntrinsic('%calendarImpl%')(calendar).yearMonthFromFields(fields, overflow); + const calendarImpl: CalendarImpl = calendarImplForID(calendar); + calendarImpl.resolveFields(fields, 'year-month'); + fields.day = 1; + const result = calendarImpl.dateToISO(fields, overflow); + RejectYearMonthRange(result.year, result.month); + return result; } export function CalendarMonthDayFromFields( @@ -2355,7 +2358,9 @@ export function CalendarMonthDayFromFields( fields: MonthDayFromFieldsObject, overflow: Overflow ) { - return GetIntrinsic('%calendarImpl%')(calendar).monthDayFromFields(fields, overflow); + const calendarImpl: CalendarImpl = calendarImplForID(calendar); + calendarImpl.resolveFields(fields, 'month-day'); + return calendarImpl.monthDayToISOReferenceDate(fields, overflow); } export function ToTemporalTimeZoneIdentifier(temporalTimeZoneLike: unknown): string { @@ -2509,14 +2514,38 @@ function GetPossibleEpochNanoseconds(timeZone: string, isoDateTime: ISODateTime) const { year, month, day, hour, minute, second, millisecond, microsecond, nanosecond } = isoDateTime; const offsetMinutes = ParseTimeZoneIdentifier(timeZone).offsetMinutes; if (offsetMinutes !== undefined) { - return [ - JSBI.subtract( - GetUTCEpochNanoseconds(year, month, day, hour, minute, second, millisecond, microsecond, nanosecond), - JSBI.BigInt(offsetMinutes * 60e9) - ) - ]; + const balanced = BalanceISODateTime( + year, + month, + day, + hour, + minute - offsetMinutes, + second, + millisecond, + microsecond, + nanosecond + ); + if (MathAbs(ISODateToEpochDays(balanced.year, balanced.month - 1, balanced.day)) > 1e8) { + throw new RangeErrorCtor('date/time value is outside the supported range'); + } + const epochNs = GetUTCEpochNanoseconds( + balanced.year, + balanced.month, + balanced.day, + balanced.hour, + balanced.minute, + balanced.second, + balanced.millisecond, + balanced.microsecond, + balanced.nanosecond + ); + ValidateEpochNanoseconds(epochNs); + return [epochNs]; } + if (MathAbs(ISODateToEpochDays(year, month - 1, day)) > 1e8) { + throw new RangeErrorCtor('date/time value is outside the supported range'); + } return GetNamedTimeZoneEpochNanoseconds( timeZone, year, @@ -3184,6 +3213,7 @@ function GetNamedTimeZoneEpochNanoseconds( ) { return undefined; } + ValidateEpochNanoseconds(epochNanoseconds); return epochNanoseconds; } ]); @@ -3391,8 +3421,8 @@ export function UnbalanceDateDurationRelative(dateDuration: DateDuration, plainR // balance years, months, and weeks down to days const isoDate = TemporalObjectToISODateRecord(plainRelativeTo); const later = CalendarDateAdd(GetSlot(plainRelativeTo, CALENDAR), isoDate, yearsMonthsWeeksDuration, 'constrain'); - const epochDaysEarlier = ISODateToEpochDays(isoDate.year, isoDate.month, isoDate.day); - const epochDaysLater = ISODateToEpochDays(later.year, later.month, later.day); + const epochDaysEarlier = ISODateToEpochDays(isoDate.year, isoDate.month - 1, isoDate.day); + const epochDaysLater = ISODateToEpochDays(later.year, later.month - 1, later.day); const yearsMonthsWeeksInDays = epochDaysLater - epochDaysEarlier; return dateDuration.days + yearsMonthsWeeksInDays; } @@ -3418,7 +3448,7 @@ export function ConstrainToRange(value: number | undefined, min: number, max: nu // used for optional params in the method below. return MathMin(max, MathMax(min, value as number)); } -function ConstrainISODate(year: number, monthParam: number, dayParam?: number) { +export function ConstrainISODate(year: number, monthParam: number, dayParam?: number) { const month = ConstrainToRange(monthParam, 1, 12); const day = ConstrainToRange(dayParam, 1, ISODaysInMonth(year, month)); return { year, month, day }; @@ -3450,7 +3480,7 @@ function RejectISODate(year: number, month: number, day: number) { RejectToRange(day, 1, ISODaysInMonth(year, month)); } -export function RejectDateRange(year: number, month: number, day: number) { +function RejectDateRange(year: number, month: number, day: number) { // Noon avoids trouble at edges of DateTime range (excludes midnight) RejectDateTimeRange(year, month, day, 12, 0, 0, 0, 0, 0); } @@ -3515,7 +3545,7 @@ export function ValidateEpochNanoseconds(epochNanoseconds: JSBI) { } } -export function RejectYearMonthRange(year: number, month: number) { +function RejectYearMonthRange(year: number, month: number) { RejectToRange(year, YEAR_MIN, YEAR_MAX); if (year === YEAR_MIN) { RejectToRange(month, 4, 12); @@ -3560,11 +3590,6 @@ export function RejectDuration( } } -function ISODateSurpasses(sign: -1 | 1, y1: number, m1: number, d1: number, y2: number, m2: number, d2: number) { - const cmp = CompareISODate(y1, m1, d1, y2, m2, d2); - return sign * cmp === 1; -} - export function NormalizeDuration(duration: Temporal.Duration) { const date = { years: GetSlot(duration, YEARS), @@ -3721,66 +3746,9 @@ function CombineDateAndNormalizedTimeDuration(dateDuration: DateDuration, norm: return { date: dateDuration, norm }; } -function ISODateToEpochDays(y: number, m: number, d: number) { - // This is inefficient, but we use GetUTCEpochNanoseconds to avoid duplicating - // the workarounds for legacy Date. (see that function for explanation) - return JSBI.toNumber(JSBI.divide(GetUTCEpochNanoseconds(y, m, d, 0, 0, 0, 0, 0, 0), DAY_NANOS_JSBI)); -} - -export function DifferenceISODate( - y1: number, - m1: number, - d1: number, - y2: number, - m2: number, - d2: number, - largestUnit: Allowed -) { - const sign = -CompareISODate(y1, m1, d1, y2, m2, d2); - if (sign === 0) return ZeroDateDuration(); - uncheckedAssertNarrowedType<-1 | 1>(sign, "the - operator's return type is number"); - - let years = 0; - let months = 0; - let intermediate; - if (largestUnit === 'year' || largestUnit === 'month') { - // We can skip right to the neighbourhood of the correct number of years, - // it'll be at least one less than y2 - y1 (unless it's zero) - let candidateYears = y2 - y1; - if (candidateYears !== 0) candidateYears -= sign; - // loops at most twice - while (!ISODateSurpasses(sign, y1 + candidateYears, m1, d1, y2, m2, d2)) { - years = candidateYears; - candidateYears += sign; - } - - let candidateMonths = sign; - intermediate = BalanceISOYearMonth(y1 + years, m1 + candidateMonths); - // loops at most 12 times - while (!ISODateSurpasses(sign, intermediate.year, intermediate.month, d1, y2, m2, d2)) { - months = candidateMonths; - candidateMonths += sign; - intermediate = BalanceISOYearMonth(intermediate.year, intermediate.month + sign); - } - - if (largestUnit === 'month') { - months += years * 12; - years = 0; - } - } - - intermediate = BalanceISOYearMonth(y1 + years, m1 + months); - const constrained = ConstrainISODate(intermediate.year, intermediate.month, d1); - - let weeks = 0; - let days = ISODateToEpochDays(y2, m2, d2) - ISODateToEpochDays(constrained.year, constrained.month, constrained.day); - - if (largestUnit === 'week') { - weeks = MathTrunc(days / 7); - days %= 7; - } - - return { years, months, weeks, days }; +// Caution: month is 0-based +export function ISODateToEpochDays(y: number, m: number, d: number) { + return GetUTCEpochMilliseconds(y, m + 1, d, 0, 0, 0, 0) / DAY_MS; } function DifferenceTime( @@ -3962,7 +3930,9 @@ function DifferenceZonedDateTime( // Similar to what happens in DifferenceISODateTime with date parts only: const dateLargestUnit = LargerOfTwoTemporalUnits('day', largestUnit) as Temporal.DateUnit; - const dateDifference = CalendarDateUntil(calendar, isoDtStart, intermediateDateTime, dateLargestUnit); + const isoStartDate = ISODateTimeToDateRecord(isoDtStart); + const isoIntermediateDate = ISODateTimeToDateRecord(intermediateDateTime); + const dateDifference = CalendarDateUntil(calendar, isoStartDate, isoIntermediateDate, dateLargestUnit); return CombineDateAndNormalizedTimeDuration(dateDifference, norm); } @@ -3987,6 +3957,7 @@ function NudgeToCalendarUnit( // Create a duration with smallestUnit trunc'd towards zero // Create a separate duration that incorporates roundingIncrement let r1, r2, startDuration, endDuration; + const startDate = ISODateTimeToDateRecord(dateTime); switch (unit) { case 'year': { const years = RoundNumberToIncrement(duration.date.years, increment, 'trunc'); @@ -4006,7 +3977,7 @@ function NudgeToCalendarUnit( } case 'week': { const yearsMonths = AdjustDateDurationRecord(duration.date, 0, 0); - const weeksStart = CalendarDateAdd(calendar, dateTime, yearsMonths, 'constrain'); + const weeksStart = CalendarDateAdd(calendar, startDate, yearsMonths, 'constrain'); const weeksEnd = BalanceISODate(weeksStart.year, weeksStart.month, weeksStart.day + duration.date.days); const untilResult = CalendarDateUntil(calendar, weeksStart, weeksEnd, 'week'); const weeks = RoundNumberToIncrement(duration.date.weeks + untilResult.weeks, increment, 'trunc'); @@ -4032,7 +4003,6 @@ function NudgeToCalendarUnit( if (sign === -1) assert(r1 <= 0 && r1 > r2, `negative ordering of r1, r2: 0 ≥ ${r1} > ${r2}`); // Apply to origin, output PlainDateTimes - const startDate = ISODateTimeToDateRecord(dateTime); const start = CalendarDateAdd(calendar, startDate, startDuration, 'constrain'); const end = CalendarDateAdd(calendar, startDate, endDuration, 'constrain'); @@ -4108,12 +4078,12 @@ function NudgeToCalendarUnit( const didExpandCalendarUnit = roundedUnit === MathAbs(r2); duration = { date: didExpandCalendarUnit ? endDuration : startDuration, norm: TimeDuration.ZERO }; - return { + const nudgeResult = { duration, - total, nudgedEpochNs: didExpandCalendarUnit ? endEpochNs : startEpochNs, didExpandCalendarUnit }; + return { nudgeResult, total }; } // Attempts rounding of time units within a time zone's day, but if the rounding @@ -4177,7 +4147,6 @@ function NudgeToZonedTime( const resultDuration = CombineDateAndNormalizedTimeDuration(dateDuration, roundedNorm); return { duration: resultDuration, - total: NaN, // Not computed in this path, so we assert that it is not NaN later on nudgedEpochNs, didExpandCalendarUnit: didRoundBeyondDay }; @@ -4198,7 +4167,6 @@ function NudgeToDayOrTime( const norm = duration.norm.add24HourDays(duration.date.days); // Convert to nanoseconds and round const unitLength = Call(MapPrototypeGet, NS_PER_TIME_UNIT, [smallestUnit]); - const total = norm.fdiv(JSBI.BigInt(unitLength)); const roundedNorm = norm.round(JSBI.BigInt(increment * unitLength), roundingMode); const diffNorm = roundedNorm.subtract(norm); @@ -4219,7 +4187,6 @@ function NudgeToDayOrTime( const dateDuration = AdjustDateDurationRecord(duration.date, days); return { duration: { date: dateDuration, norm: remainder }, - total, nudgedEpochNs, didExpandCalendarUnit: didExpandDays }; @@ -4341,7 +4308,7 @@ function RoundRelativeDuration( let nudgeResult; if (irregularLengthUnit) { // Rounding an irregular-length unit? Use epoch-nanosecond-bounding technique - nudgeResult = NudgeToCalendarUnit( + ({ nudgeResult } = NudgeToCalendarUnit( sign, duration, destEpochNs, @@ -4351,11 +4318,9 @@ function RoundRelativeDuration( increment, smallestUnit, roundingMode - ); + )); } else if (timeZone) { - // Special-case for rounding time units within a zoned day. total() never - // takes this path because largestUnit is then also a time unit, so - // DifferenceZonedDateTimeWithRounding uses Instant math + // Special-case for rounding time units within a zoned day uncheckedAssertNarrowedType( smallestUnit, 'other values handled in irregularLengthUnit clause above' @@ -4388,7 +4353,32 @@ function RoundRelativeDuration( ); } - return { duration, total: nudgeResult.total }; + return duration; +} + +function TotalRelativeDuration( + duration: InternalDuration, + destEpochNs: JSBI, + dateTime: ISODateTime, + timeZone: string | null, + calendar: BuiltinCalendarId, + unit: Temporal.DateTimeUnit +) { + // The duration must already be balanced. This should be achieved by calling + // one of the non-rounding since/until internal methods prior. It's okay to + // have a bottom-heavy weeks because weeks don't bubble-up into months. It's + // okay to have >24 hour day assuming the final day of relativeTo+duration has + // >24 hours in its timezone. (should automatically end up like this if using + // non-rounding since/until internal methods prior) + if (IsCalendarUnit(unit) || (timeZone && unit === 'day')) { + // Rounding an irregular-length unit? Use epoch-nanosecond-bounding technique + const sign = NormalizedDurationSign(duration) < 0 ? -1 : 1; + return NudgeToCalendarUnit(sign, duration, destEpochNs, dateTime, timeZone, calendar, 1, unit, 'trunc').total; + } + // Rounding uniform-length days/hours/minutes/etc units. Simple nanosecond + // math. years/months/weeks unchanged + const norm = duration.norm.add24HourDays(duration.date.days); + return TotalTimeDuration(norm, unit); } export function DifferencePlainDateTimeWithRounding( @@ -4417,10 +4407,7 @@ export function DifferencePlainDateTimeWithRounding( roundingMode: Temporal.RoundingMode ) { if (CompareISODateTime(y1, mon1, d1, h1, min1, s1, ms1, µs1, ns1, y2, mon2, d2, h2, min2, s2, ms2, µs2, ns2) == 0) { - return { - duration: { date: ZeroDateDuration(), norm: TimeDuration.ZERO }, - total: 0 - }; + return { date: ZeroDateDuration(), norm: TimeDuration.ZERO }; } const duration = DifferenceISODateTime( @@ -4446,9 +4433,7 @@ export function DifferencePlainDateTimeWithRounding( largestUnit ); - if (smallestUnit === 'nanosecond' && roundingIncrement === 1) { - return { duration, total: JSBI.toNumber(duration.norm.totalNs) }; - } + if (smallestUnit === 'nanosecond' && roundingIncrement === 1) return duration; const dateTime = { year: y1, @@ -4475,6 +4460,72 @@ export function DifferencePlainDateTimeWithRounding( ); } +export function DifferencePlainDateTimeWithTotal( + y1: number, + mon1: number, + d1: number, + h1: number, + min1: number, + s1: number, + ms1: number, + µs1: number, + ns1: number, + y2: number, + mon2: number, + d2: number, + h2: number, + min2: number, + s2: number, + ms2: number, + µs2: number, + ns2: number, + calendar: BuiltinCalendarId, + unit: Temporal.DateTimeUnit +) { + if (CompareISODateTime(y1, mon1, d1, h1, min1, s1, ms1, µs1, ns1, y2, mon2, d2, h2, min2, s2, ms2, µs2, ns2) == 0) { + return 0; + } + + const duration = DifferenceISODateTime( + y1, + mon1, + d1, + h1, + min1, + s1, + ms1, + µs1, + ns1, + y2, + mon2, + d2, + h2, + min2, + s2, + ms2, + µs2, + ns2, + calendar, + unit + ); + + if (unit === 'nanosecond') return JSBI.toNumber(duration.norm.totalNs); + + const dateTime = { + year: y1, + month: mon1, + day: d1, + hour: h1, + minute: min1, + second: s1, + millisecond: ms1, + microsecond: µs1, + nanosecond: ns1 + }; + const destEpochNs = GetUTCEpochNanoseconds(y2, mon2, d2, h2, min2, s2, ms2, µs2, ns2); + return TotalRelativeDuration(duration, destEpochNs, dateTime, null, calendar, unit); +} + export function DifferenceZonedDateTimeWithRounding( ns1: JSBI, ns2: JSBI, @@ -4492,9 +4543,7 @@ export function DifferenceZonedDateTimeWithRounding( const duration = DifferenceZonedDateTime(ns1, ns2, timeZone, calendar, largestUnit); - if (smallestUnit === 'nanosecond' && roundingIncrement === 1) { - return { duration, total: JSBI.toNumber(duration.norm.totalNs) }; - } + if (smallestUnit === 'nanosecond' && roundingIncrement === 1) return duration; const dateTime = GetISODateTimeFor(timeZone, ns1); return RoundRelativeDuration( @@ -4510,6 +4559,23 @@ export function DifferenceZonedDateTimeWithRounding( ); } +export function DifferenceZonedDateTimeWithTotal( + ns1: JSBI, + ns2: JSBI, + timeZone: string, + calendar: BuiltinCalendarId, + unit: Temporal.DateTimeUnit +) { + if (TemporalUnitCategory(unit) === 'time') { + // The user is only asking for a time difference, so return difference of instants. + return TotalTimeDuration(TimeDuration.fromEpochNsDiff(ns2, ns1), unit as Temporal.TimeUnit); + } + + const duration = DifferenceZonedDateTime(ns1, ns2, timeZone, calendar, unit); + const dateTime = GetISODateTimeFor(timeZone, ns1); + return TotalRelativeDuration(duration, ns2, dateTime, timeZone, calendar, unit); +} + type DifferenceOperation = 'since' | 'until'; function GetDifferenceSettings( @@ -4584,7 +4650,7 @@ export function DifferenceTemporalInstant( const onens = GetSlot(instant, EPOCHNANOSECONDS); const twons = GetSlot(other, EPOCHNANOSECONDS); - const { duration } = DifferenceInstant( + const duration = DifferenceInstant( onens, twons, settings.roundingIncrement, @@ -4648,7 +4714,7 @@ export function DifferenceTemporalPlainDate( settings.roundingIncrement, settings.smallestUnit, settings.roundingMode - ).duration; + ); } let result = UnnormalizeDuration(duration, 'day'); @@ -4687,7 +4753,7 @@ export function DifferenceTemporalPlainDateTime( return new Duration(); } - const { duration } = DifferencePlainDateTimeWithRounding( + const duration = DifferencePlainDateTimeWithRounding( GetSlot(plainDateTime, ISO_YEAR), GetSlot(plainDateTime, ISO_MONTH), GetSlot(plainDateTime, ISO_DAY), @@ -4745,12 +4811,7 @@ export function DifferenceTemporalPlainTime( ); let duration = { date: ZeroDateDuration(), norm }; if (settings.smallestUnit !== 'nanosecond' || settings.roundingIncrement !== 1) { - ({ duration } = RoundTimeDuration( - duration, - settings.roundingIncrement, - settings.smallestUnit, - settings.roundingMode - )); + duration = RoundTimeDuration(duration, settings.roundingIncrement, settings.smallestUnit, settings.roundingMode); } let result = UnnormalizeDuration(duration, settings.largestUnit); @@ -4815,7 +4876,7 @@ export function DifferenceTemporalPlainYearMonth( settings.roundingIncrement, settings.smallestUnit, settings.roundingMode - ).duration; + ); } let result = UnnormalizeDuration(duration, 'day'); @@ -4847,7 +4908,7 @@ export function DifferenceTemporalZonedDateTime( let result; if (TemporalUnitCategory(settings.largestUnit) !== 'date') { // The user is only asking for a time difference, so return difference of instants. - const { duration } = DifferenceInstant( + const duration = DifferenceInstant( ns1, ns2, settings.roundingIncrement, @@ -4866,7 +4927,7 @@ export function DifferenceTemporalZonedDateTime( if (JSBI.equal(ns1, ns2)) return new Duration(); - const { duration } = DifferenceZonedDateTimeWithRounding( + const duration = DifferenceZonedDateTimeWithRounding( ns1, ns2, timeZone, @@ -4926,7 +4987,8 @@ export function AddZonedDateTime( // RFC 5545 requires the date portion to be added in calendar days and the // time portion to be added in exact time. const dt = GetISODateTimeFor(timeZone, epochNs); - const addedDate = CalendarDateAdd(calendar, dt, duration.date, overflow); + const datePart = ISODateTimeToDateRecord(dt); + const addedDate = CalendarDateAdd(calendar, datePart, duration.date, overflow); const dtIntermediate = CombineISODateAndTimeRecord(addedDate, dt); // Note that 'compatible' is used below because this disambiguation behavior @@ -5266,24 +5328,23 @@ export function RoundTimeDuration( roundingMode: Temporal.RoundingMode ) { // unit must not be a calendar unit - - let days = duration.date.days; - let norm = duration.norm; - let total; if (unit === 'day') { // First convert time units up to days - const { quotient, remainder } = norm.divmod(DAY_NANOS); - days += quotient; - total = days + remainder.fdiv(DAY_NANOS_JSBI); - days = RoundNumberToIncrement(total, increment, roundingMode); - norm = TimeDuration.ZERO; - } else { - const divisor = JSBI.BigInt(Call(MapPrototypeGet, NS_PER_TIME_UNIT, [unit])); - total = norm.fdiv(divisor); - norm = norm.round(JSBI.multiply(divisor, JSBI.BigInt(increment)), roundingMode); + const { quotient, remainder } = duration.norm.divmod(DAY_NANOS); + let days = duration.date.days + quotient + remainder.fdiv(DAY_NANOS_JSBI); + days = RoundNumberToIncrement(days, increment, roundingMode); + const dateDuration = AdjustDateDurationRecord(duration.date, days); + return CombineDateAndNormalizedTimeDuration(dateDuration, TimeDuration.ZERO); } - const dateDuration = AdjustDateDurationRecord(duration.date, days); - return { duration: CombineDateAndNormalizedTimeDuration(dateDuration, norm), total }; + + const divisor = Call(MapPrototypeGet, NS_PER_TIME_UNIT, [unit]); + const norm = duration.norm.round(JSBI.BigInt(divisor * increment), roundingMode); + return CombineDateAndNormalizedTimeDuration(duration.date, norm); +} + +export function TotalTimeDuration(norm: TimeDuration, unit: TimeUnitOrDay) { + const divisor: number = Call(MapPrototypeGet, NS_PER_TIME_UNIT, [unit]); + return norm.fdiv(JSBI.BigInt(divisor)); } export function CompareISODate(y1: number, m1: number, d1: number, y2: number, m2: number, d2: number) { diff --git a/lib/intrinsicclass.ts b/lib/intrinsicclass.ts index 7daab280..b808fe5c 100644 --- a/lib/intrinsicclass.ts +++ b/lib/intrinsicclass.ts @@ -12,7 +12,7 @@ import { import type JSBI from 'jsbi'; import type { Temporal } from '..'; import type { CalendarImpl } from './calendar'; -import type { BuiltinCalendarId, ISODate } from './internaltypes'; +import type { BuiltinCalendarId } from './internaltypes'; import { DEBUG } from './debug'; @@ -50,7 +50,6 @@ type TemporalIntrinsicPrototypeRegisteredKeys = { type OtherIntrinsics = { calendarImpl: (id: BuiltinCalendarId) => CalendarImpl; - calendarDateWeekOfYear: (id: BuiltinCalendarId, isoDate: ISODate) => { week?: number; year?: number }; }; type OtherIntrinsicKeys = { [key in keyof OtherIntrinsics as `%${key}%`]: OtherIntrinsics[key] }; diff --git a/lib/plaindate.ts b/lib/plaindate.ts index 269379f9..7288e451 100644 --- a/lib/plaindate.ts +++ b/lib/plaindate.ts @@ -40,77 +40,77 @@ export class PlainDate implements Temporal.PlainDate { get era(): Return['era'] { if (!ES.IsTemporalDate(this)) throw new TypeErrorCtor('invalid receiver'); const isoDate = ES.TemporalObjectToISODateRecord(this); - return ES.CalendarEra(GetSlot(this, CALENDAR), isoDate); + return ES.calendarImplForObj(this).isoToDate(isoDate, { era: true }).era; } get eraYear(): Return['eraYear'] { if (!ES.IsTemporalDate(this)) throw new TypeErrorCtor('invalid receiver'); const isoDate = ES.TemporalObjectToISODateRecord(this); - return ES.CalendarEraYear(GetSlot(this, CALENDAR), isoDate); + return ES.calendarImplForObj(this).isoToDate(isoDate, { eraYear: true }).eraYear; } get year(): Return['year'] { if (!ES.IsTemporalDate(this)) throw new TypeErrorCtor('invalid receiver'); const isoDate = ES.TemporalObjectToISODateRecord(this); - return ES.CalendarYear(GetSlot(this, CALENDAR), isoDate); + return ES.calendarImplForObj(this).isoToDate(isoDate, { year: true }).year; } get month(): Return['month'] { if (!ES.IsTemporalDate(this)) throw new TypeErrorCtor('invalid receiver'); const isoDate = ES.TemporalObjectToISODateRecord(this); - return ES.CalendarMonth(GetSlot(this, CALENDAR), isoDate); + return ES.calendarImplForObj(this).isoToDate(isoDate, { month: true }).month; } get monthCode(): Return['monthCode'] { if (!ES.IsTemporalDate(this)) throw new TypeErrorCtor('invalid receiver'); const isoDate = ES.TemporalObjectToISODateRecord(this); - return ES.CalendarMonthCode(GetSlot(this, CALENDAR), isoDate); + return ES.calendarImplForObj(this).isoToDate(isoDate, { monthCode: true }).monthCode; } get day(): Return['day'] { if (!ES.IsTemporalDate(this)) throw new TypeErrorCtor('invalid receiver'); const isoDate = ES.TemporalObjectToISODateRecord(this); - return ES.CalendarDay(GetSlot(this, CALENDAR), isoDate); + return ES.calendarImplForObj(this).isoToDate(isoDate, { day: true }).day; } get dayOfWeek(): Return['dayOfWeek'] { if (!ES.IsTemporalDate(this)) throw new TypeErrorCtor('invalid receiver'); const isoDate = ES.TemporalObjectToISODateRecord(this); - return ES.CalendarDayOfWeek(GetSlot(this, CALENDAR), isoDate); + return ES.calendarImplForObj(this).isoToDate(isoDate, { dayOfWeek: true }).dayOfWeek; } get dayOfYear(): Return['dayOfYear'] { if (!ES.IsTemporalDate(this)) throw new TypeErrorCtor('invalid receiver'); const isoDate = ES.TemporalObjectToISODateRecord(this); - return ES.CalendarDayOfYear(GetSlot(this, CALENDAR), isoDate); + return ES.calendarImplForObj(this).isoToDate(isoDate, { dayOfYear: true }).dayOfYear; } get weekOfYear(): Return['weekOfYear'] { if (!ES.IsTemporalDate(this)) throw new TypeErrorCtor('invalid receiver'); const isoDate = ES.TemporalObjectToISODateRecord(this); - return ES.CalendarWeekOfYear(GetSlot(this, CALENDAR), isoDate); + return ES.calendarImplForObj(this).isoToDate(isoDate, { weekOfYear: true }).weekOfYear.week; } get yearOfWeek(): Return['weekOfYear'] { if (!ES.IsTemporalDate(this)) throw new TypeErrorCtor('invalid receiver'); const isoDate = ES.TemporalObjectToISODateRecord(this); - return ES.CalendarYearOfWeek(GetSlot(this, CALENDAR), isoDate); + return ES.calendarImplForObj(this).isoToDate(isoDate, { weekOfYear: true }).weekOfYear.year; } get daysInWeek(): Return['daysInWeek'] { if (!ES.IsTemporalDate(this)) throw new TypeErrorCtor('invalid receiver'); const isoDate = ES.TemporalObjectToISODateRecord(this); - return ES.CalendarDaysInWeek(GetSlot(this, CALENDAR), isoDate); + return ES.calendarImplForObj(this).isoToDate(isoDate, { daysInWeek: true }).daysInWeek; } get daysInMonth(): Return['daysInMonth'] { if (!ES.IsTemporalDate(this)) throw new TypeErrorCtor('invalid receiver'); const isoDate = ES.TemporalObjectToISODateRecord(this); - return ES.CalendarDaysInMonth(GetSlot(this, CALENDAR), isoDate); + return ES.calendarImplForObj(this).isoToDate(isoDate, { daysInMonth: true }).daysInMonth; } get daysInYear(): Return['daysInYear'] { if (!ES.IsTemporalDate(this)) throw new TypeErrorCtor('invalid receiver'); const isoDate = ES.TemporalObjectToISODateRecord(this); - return ES.CalendarDaysInYear(GetSlot(this, CALENDAR), isoDate); + return ES.calendarImplForObj(this).isoToDate(isoDate, { daysInYear: true }).daysInYear; } get monthsInYear(): Return['monthsInYear'] { if (!ES.IsTemporalDate(this)) throw new TypeErrorCtor('invalid receiver'); const isoDate = ES.TemporalObjectToISODateRecord(this); - return ES.CalendarMonthsInYear(GetSlot(this, CALENDAR), isoDate); + return ES.calendarImplForObj(this).isoToDate(isoDate, { monthsInYear: true }).monthsInYear; } get inLeapYear(): Return['inLeapYear'] { if (!ES.IsTemporalDate(this)) throw new TypeErrorCtor('invalid receiver'); const isoDate = ES.TemporalObjectToISODateRecord(this); - return ES.CalendarInLeapYear(GetSlot(this, CALENDAR), isoDate); + return ES.calendarImplForObj(this).isoToDate(isoDate, { inLeapYear: true }).inLeapYear; } with(temporalDateLike: Params['with'][0], options: Params['with'][1] = undefined): Return['with'] { if (!ES.IsTemporalDate(this)) throw new TypeErrorCtor('invalid receiver'); @@ -218,29 +218,23 @@ export class PlainDate implements Temporal.PlainDate { timeZone = ES.ToTemporalTimeZoneIdentifier(item); } - const calendar = GetSlot(this, CALENDAR); - const year = GetSlot(this, ISO_YEAR); - const month = GetSlot(this, ISO_MONTH); - const day = GetSlot(this, ISO_DAY); + const isoDate = ES.TemporalObjectToISODateRecord(this); let epochNs; if (temporalTime === undefined) { - epochNs = ES.GetStartOfDay(timeZone, { year, month, day }); + epochNs = ES.GetStartOfDay(timeZone, isoDate); } else { temporalTime = ES.ToTemporalTime(temporalTime); - const dt = { - year, - month, - day, + const dt = ES.CombineISODateAndTimeRecord(isoDate, { hour: GetSlot(temporalTime, ISO_HOUR), minute: GetSlot(temporalTime, ISO_MINUTE), second: GetSlot(temporalTime, ISO_SECOND), millisecond: GetSlot(temporalTime, ISO_MILLISECOND), microsecond: GetSlot(temporalTime, ISO_MICROSECOND), nanosecond: GetSlot(temporalTime, ISO_NANOSECOND) - }; + }); epochNs = ES.GetEpochNanosecondsFor(timeZone, dt, 'compatible'); } - return ES.CreateTemporalZonedDateTime(epochNs, timeZone, calendar); + return ES.CreateTemporalZonedDateTime(epochNs, timeZone, GetSlot(this, CALENDAR)); } toPlainYearMonth(): Return['toPlainYearMonth'] { if (!ES.IsTemporalDate(this)) throw new TypeErrorCtor('invalid receiver'); diff --git a/lib/plaindatetime.ts b/lib/plaindatetime.ts index 58bedbe7..89d58bdb 100644 --- a/lib/plaindatetime.ts +++ b/lib/plaindatetime.ts @@ -66,22 +66,22 @@ export class PlainDateTime implements Temporal.PlainDateTime { get year(): Return['year'] { if (!ES.IsTemporalDateTime(this)) throw new TypeErrorCtor('invalid receiver'); const isoDate = ES.TemporalObjectToISODateRecord(this); - return ES.CalendarYear(GetSlot(this, CALENDAR), isoDate); + return ES.calendarImplForObj(this).isoToDate(isoDate, { year: true }).year; } get month(): Return['month'] { if (!ES.IsTemporalDateTime(this)) throw new TypeErrorCtor('invalid receiver'); const isoDate = ES.TemporalObjectToISODateRecord(this); - return ES.CalendarMonth(GetSlot(this, CALENDAR), isoDate); + return ES.calendarImplForObj(this).isoToDate(isoDate, { month: true }).month; } get monthCode(): Return['monthCode'] { if (!ES.IsTemporalDateTime(this)) throw new TypeErrorCtor('invalid receiver'); const isoDate = ES.TemporalObjectToISODateRecord(this); - return ES.CalendarMonthCode(GetSlot(this, CALENDAR), isoDate); + return ES.calendarImplForObj(this).isoToDate(isoDate, { monthCode: true }).monthCode; } get day(): Return['day'] { if (!ES.IsTemporalDateTime(this)) throw new TypeErrorCtor('invalid receiver'); const isoDate = ES.TemporalObjectToISODateRecord(this); - return ES.CalendarDay(GetSlot(this, CALENDAR), isoDate); + return ES.calendarImplForObj(this).isoToDate(isoDate, { day: true }).day; } get hour(): Return['hour'] { if (!ES.IsTemporalDateTime(this)) throw new TypeErrorCtor('invalid receiver'); @@ -110,57 +110,57 @@ export class PlainDateTime implements Temporal.PlainDateTime { get era(): Return['era'] { if (!ES.IsTemporalDateTime(this)) throw new TypeErrorCtor('invalid receiver'); const isoDate = ES.TemporalObjectToISODateRecord(this); - return ES.CalendarEra(GetSlot(this, CALENDAR), isoDate); + return ES.calendarImplForObj(this).isoToDate(isoDate, { era: true }).era; } get eraYear(): Return['eraYear'] { if (!ES.IsTemporalDateTime(this)) throw new TypeErrorCtor('invalid receiver'); const isoDate = ES.TemporalObjectToISODateRecord(this); - return ES.CalendarEraYear(GetSlot(this, CALENDAR), isoDate); + return ES.calendarImplForObj(this).isoToDate(isoDate, { eraYear: true }).eraYear; } get dayOfWeek(): Return['dayOfWeek'] { if (!ES.IsTemporalDateTime(this)) throw new TypeErrorCtor('invalid receiver'); const isoDate = ES.TemporalObjectToISODateRecord(this); - return ES.CalendarDayOfWeek(GetSlot(this, CALENDAR), isoDate); + return ES.calendarImplForObj(this).isoToDate(isoDate, { dayOfWeek: true }).dayOfWeek; } get dayOfYear(): Return['dayOfYear'] { if (!ES.IsTemporalDateTime(this)) throw new TypeErrorCtor('invalid receiver'); const isoDate = ES.TemporalObjectToISODateRecord(this); - return ES.CalendarDayOfYear(GetSlot(this, CALENDAR), isoDate); + return ES.calendarImplForObj(this).isoToDate(isoDate, { dayOfYear: true }).dayOfYear; } get weekOfYear(): Return['weekOfYear'] { if (!ES.IsTemporalDateTime(this)) throw new TypeErrorCtor('invalid receiver'); const isoDate = ES.TemporalObjectToISODateRecord(this); - return ES.CalendarWeekOfYear(GetSlot(this, CALENDAR), isoDate); + return ES.calendarImplForObj(this).isoToDate(isoDate, { weekOfYear: true }).weekOfYear.week; } get yearOfWeek(): Return['yearOfWeek'] { if (!ES.IsTemporalDateTime(this)) throw new TypeErrorCtor('invalid receiver'); const isoDate = ES.TemporalObjectToISODateRecord(this); - return ES.CalendarYearOfWeek(GetSlot(this, CALENDAR), isoDate); + return ES.calendarImplForObj(this).isoToDate(isoDate, { weekOfYear: true }).weekOfYear.year; } get daysInWeek(): Return['daysInWeek'] { if (!ES.IsTemporalDateTime(this)) throw new TypeErrorCtor('invalid receiver'); const isoDate = ES.TemporalObjectToISODateRecord(this); - return ES.CalendarDaysInWeek(GetSlot(this, CALENDAR), isoDate); + return ES.calendarImplForObj(this).isoToDate(isoDate, { daysInWeek: true }).daysInWeek; } get daysInYear(): Return['daysInYear'] { if (!ES.IsTemporalDateTime(this)) throw new TypeErrorCtor('invalid receiver'); const isoDate = ES.TemporalObjectToISODateRecord(this); - return ES.CalendarDaysInYear(GetSlot(this, CALENDAR), isoDate); + return ES.calendarImplForObj(this).isoToDate(isoDate, { daysInYear: true }).daysInYear; } get daysInMonth(): Return['daysInMonth'] { if (!ES.IsTemporalDateTime(this)) throw new TypeErrorCtor('invalid receiver'); const isoDate = ES.TemporalObjectToISODateRecord(this); - return ES.CalendarDaysInMonth(GetSlot(this, CALENDAR), isoDate); + return ES.calendarImplForObj(this).isoToDate(isoDate, { daysInMonth: true }).daysInMonth; } get monthsInYear(): Return['monthsInYear'] { if (!ES.IsTemporalDateTime(this)) throw new TypeErrorCtor('invalid receiver'); const isoDate = ES.TemporalObjectToISODateRecord(this); - return ES.CalendarMonthsInYear(GetSlot(this, CALENDAR), isoDate); + return ES.calendarImplForObj(this).isoToDate(isoDate, { monthsInYear: true }).monthsInYear; } get inLeapYear(): Return['inLeapYear'] { if (!ES.IsTemporalDateTime(this)) throw new TypeErrorCtor('invalid receiver'); const isoDate = ES.TemporalObjectToISODateRecord(this); - return ES.CalendarInLeapYear(GetSlot(this, CALENDAR), isoDate); + return ES.calendarImplForObj(this).isoToDate(isoDate, { inLeapYear: true }).inLeapYear; } with(temporalDateTimeLike: Params['with'][0], options: Params['with'][1] = undefined): Return['with'] { if (!ES.IsTemporalDateTime(this)) throw new TypeErrorCtor('invalid receiver'); diff --git a/lib/plainmonthday.ts b/lib/plainmonthday.ts index c417cef5..c08acbef 100644 --- a/lib/plainmonthday.ts +++ b/lib/plainmonthday.ts @@ -25,12 +25,12 @@ export class PlainMonthDay implements Temporal.PlainMonthDay { get monthCode(): Return['monthCode'] { if (!ES.IsTemporalMonthDay(this)) throw new TypeErrorCtor('invalid receiver'); const isoDate = ES.TemporalObjectToISODateRecord(this); - return ES.CalendarMonthCode(GetSlot(this, CALENDAR), isoDate); + return ES.calendarImplForObj(this).isoToDate(isoDate, { monthCode: true }).monthCode; } get day(): Return['day'] { if (!ES.IsTemporalMonthDay(this)) throw new TypeErrorCtor('invalid receiver'); const isoDate = ES.TemporalObjectToISODateRecord(this); - return ES.CalendarDay(GetSlot(this, CALENDAR), isoDate); + return ES.calendarImplForObj(this).isoToDate(isoDate, { day: true }).day; } get calendarId(): Return['calendarId'] { if (!ES.IsTemporalMonthDay(this)) throw new TypeErrorCtor('invalid receiver'); diff --git a/lib/plainyearmonth.ts b/lib/plainyearmonth.ts index 0806ee54..9f7e67e2 100644 --- a/lib/plainyearmonth.ts +++ b/lib/plainyearmonth.ts @@ -24,17 +24,17 @@ export class PlainYearMonth implements Temporal.PlainYearMonth { get year(): Return['year'] { if (!ES.IsTemporalYearMonth(this)) throw new TypeErrorCtor('invalid receiver'); const isoDate = ES.TemporalObjectToISODateRecord(this); - return ES.CalendarYear(GetSlot(this, CALENDAR), isoDate); + return ES.calendarImplForObj(this).isoToDate(isoDate, { year: true }).year; } get month(): Return['month'] { if (!ES.IsTemporalYearMonth(this)) throw new TypeErrorCtor('invalid receiver'); const isoDate = ES.TemporalObjectToISODateRecord(this); - return ES.CalendarMonth(GetSlot(this, CALENDAR), isoDate); + return ES.calendarImplForObj(this).isoToDate(isoDate, { month: true }).month; } get monthCode(): Return['monthCode'] { if (!ES.IsTemporalYearMonth(this)) throw new TypeErrorCtor('invalid receiver'); const isoDate = ES.TemporalObjectToISODateRecord(this); - return ES.CalendarMonthCode(GetSlot(this, CALENDAR), isoDate); + return ES.calendarImplForObj(this).isoToDate(isoDate, { monthCode: true }).monthCode; } get calendarId(): Return['calendarId'] { if (!ES.IsTemporalYearMonth(this)) throw new TypeErrorCtor('invalid receiver'); @@ -43,32 +43,32 @@ export class PlainYearMonth implements Temporal.PlainYearMonth { get era(): Return['era'] { if (!ES.IsTemporalYearMonth(this)) throw new TypeErrorCtor('invalid receiver'); const isoDate = ES.TemporalObjectToISODateRecord(this); - return ES.CalendarEra(GetSlot(this, CALENDAR), isoDate); + return ES.calendarImplForObj(this).isoToDate(isoDate, { era: true }).era; } get eraYear(): Return['eraYear'] { if (!ES.IsTemporalYearMonth(this)) throw new TypeErrorCtor('invalid receiver'); const isoDate = ES.TemporalObjectToISODateRecord(this); - return ES.CalendarEraYear(GetSlot(this, CALENDAR), isoDate); + return ES.calendarImplForObj(this).isoToDate(isoDate, { eraYear: true }).eraYear; } get daysInMonth(): Return['daysInMonth'] { if (!ES.IsTemporalYearMonth(this)) throw new TypeErrorCtor('invalid receiver'); const isoDate = ES.TemporalObjectToISODateRecord(this); - return ES.CalendarDaysInMonth(GetSlot(this, CALENDAR), isoDate); + return ES.calendarImplForObj(this).isoToDate(isoDate, { daysInMonth: true }).daysInMonth; } get daysInYear(): Return['daysInYear'] { if (!ES.IsTemporalYearMonth(this)) throw new TypeErrorCtor('invalid receiver'); const isoDate = ES.TemporalObjectToISODateRecord(this); - return ES.CalendarDaysInYear(GetSlot(this, CALENDAR), isoDate); + return ES.calendarImplForObj(this).isoToDate(isoDate, { daysInYear: true }).daysInYear; } get monthsInYear(): Return['monthsInYear'] { if (!ES.IsTemporalYearMonth(this)) throw new TypeErrorCtor('invalid receiver'); const isoDate = ES.TemporalObjectToISODateRecord(this); - return ES.CalendarMonthsInYear(GetSlot(this, CALENDAR), isoDate); + return ES.calendarImplForObj(this).isoToDate(isoDate, { monthsInYear: true }).monthsInYear; } get inLeapYear(): Return['inLeapYear'] { if (!ES.IsTemporalYearMonth(this)) throw new TypeErrorCtor('invalid receiver'); const isoDate = ES.TemporalObjectToISODateRecord(this); - return ES.CalendarInLeapYear(GetSlot(this, CALENDAR), isoDate); + return ES.calendarImplForObj(this).isoToDate(isoDate, { inLeapYear: true }).inLeapYear; } with(temporalYearMonthLike: Params['with'][0], options: Params['with'][1] = undefined): Return['with'] { if (!ES.IsTemporalYearMonth(this)) throw new TypeErrorCtor('invalid receiver'); diff --git a/lib/zoneddatetime.ts b/lib/zoneddatetime.ts index 559bc1ed..80eaf299 100644 --- a/lib/zoneddatetime.ts +++ b/lib/zoneddatetime.ts @@ -64,19 +64,19 @@ export class ZonedDateTime implements Temporal.ZonedDateTime { } get year(): Return['year'] { if (!ES.IsTemporalZonedDateTime(this)) throw new TypeErrorCtor('invalid receiver'); - return ES.CalendarYear(GetSlot(this, CALENDAR), dateTime(this)); + return ES.calendarImplForObj(this).isoToDate(date(this), { year: true }).year; } get month(): Return['month'] { if (!ES.IsTemporalZonedDateTime(this)) throw new TypeErrorCtor('invalid receiver'); - return ES.CalendarMonth(GetSlot(this, CALENDAR), dateTime(this)); + return ES.calendarImplForObj(this).isoToDate(date(this), { month: true }).month; } get monthCode(): Return['monthCode'] { if (!ES.IsTemporalZonedDateTime(this)) throw new TypeErrorCtor('invalid receiver'); - return ES.CalendarMonthCode(GetSlot(this, CALENDAR), dateTime(this)); + return ES.calendarImplForObj(this).isoToDate(date(this), { monthCode: true }).monthCode; } get day(): Return['day'] { if (!ES.IsTemporalZonedDateTime(this)) throw new TypeErrorCtor('invalid receiver'); - return ES.CalendarDay(GetSlot(this, CALENDAR), dateTime(this)); + return ES.calendarImplForObj(this).isoToDate(date(this), { day: true }).day; } get hour(): Return['hour'] { if (!ES.IsTemporalZonedDateTime(this)) throw new TypeErrorCtor('invalid receiver'); @@ -104,11 +104,11 @@ export class ZonedDateTime implements Temporal.ZonedDateTime { } get era(): Return['era'] { if (!ES.IsTemporalZonedDateTime(this)) throw new TypeErrorCtor('invalid receiver'); - return ES.CalendarEra(GetSlot(this, CALENDAR), dateTime(this)); + return ES.calendarImplForObj(this).isoToDate(date(this), { era: true }).era; } get eraYear(): Return['eraYear'] { if (!ES.IsTemporalZonedDateTime(this)) throw new TypeErrorCtor('invalid receiver'); - return ES.CalendarEraYear(GetSlot(this, CALENDAR), dateTime(this)); + return ES.calendarImplForObj(this).isoToDate(date(this), { eraYear: true }).eraYear; } get epochMilliseconds(): Return['epochMilliseconds'] { if (!ES.IsTemporalZonedDateTime(this)) throw new TypeErrorCtor('invalid receiver'); @@ -121,26 +121,25 @@ export class ZonedDateTime implements Temporal.ZonedDateTime { } get dayOfWeek(): Return['dayOfWeek'] { if (!ES.IsTemporalZonedDateTime(this)) throw new TypeErrorCtor('invalid receiver'); - return ES.CalendarDayOfWeek(GetSlot(this, CALENDAR), dateTime(this)); + return ES.calendarImplForObj(this).isoToDate(date(this), { dayOfWeek: true }).dayOfWeek; } get dayOfYear(): Return['dayOfYear'] { if (!ES.IsTemporalZonedDateTime(this)) throw new TypeErrorCtor('invalid receiver'); - return ES.CalendarDayOfYear(GetSlot(this, CALENDAR), dateTime(this)); + return ES.calendarImplForObj(this).isoToDate(date(this), { dayOfYear: true }).dayOfYear; } get weekOfYear(): Return['weekOfYear'] { if (!ES.IsTemporalZonedDateTime(this)) throw new TypeErrorCtor('invalid receiver'); - return ES.CalendarWeekOfYear(GetSlot(this, CALENDAR), dateTime(this)); + return ES.calendarImplForObj(this).isoToDate(date(this), { weekOfYear: true }).weekOfYear.week; } get yearOfWeek(): Return['yearOfWeek'] { if (!ES.IsTemporalZonedDateTime(this)) throw new TypeErrorCtor('invalid receiver'); - return ES.CalendarYearOfWeek(GetSlot(this, CALENDAR), dateTime(this)); + return ES.calendarImplForObj(this).isoToDate(date(this), { weekOfYear: true }).weekOfYear.year; } get hoursInDay(): Return['hoursInDay'] { if (!ES.IsTemporalZonedDateTime(this)) throw new TypeErrorCtor('invalid receiver'); const timeZone = GetSlot(this, TIME_ZONE); - const { year, month, day } = ES.GetISODateTimeFor(timeZone, GetSlot(this, EPOCHNANOSECONDS)); - const today = { year, month, day }; - const tomorrow = ES.BalanceISODate(year, month, day + 1); + const today = date(this); + const tomorrow = ES.BalanceISODate(today.year, today.month, today.day + 1); const todayNs = ES.GetStartOfDay(timeZone, today); const tomorrowNs = ES.GetStartOfDay(timeZone, tomorrow); const diff = TimeDuration.fromEpochNsDiff(tomorrowNs, todayNs); @@ -148,23 +147,23 @@ export class ZonedDateTime implements Temporal.ZonedDateTime { } get daysInWeek(): Return['daysInWeek'] { if (!ES.IsTemporalZonedDateTime(this)) throw new TypeErrorCtor('invalid receiver'); - return ES.CalendarDaysInWeek(GetSlot(this, CALENDAR), dateTime(this)); + return ES.calendarImplForObj(this).isoToDate(date(this), { daysInWeek: true }).daysInWeek; } get daysInMonth(): Return['daysInMonth'] { if (!ES.IsTemporalZonedDateTime(this)) throw new TypeErrorCtor('invalid receiver'); - return ES.CalendarDaysInMonth(GetSlot(this, CALENDAR), dateTime(this)); + return ES.calendarImplForObj(this).isoToDate(date(this), { daysInMonth: true }).daysInMonth; } get daysInYear(): Return['daysInYear'] { if (!ES.IsTemporalZonedDateTime(this)) throw new TypeErrorCtor('invalid receiver'); - return ES.CalendarDaysInYear(GetSlot(this, CALENDAR), dateTime(this)); + return ES.calendarImplForObj(this).isoToDate(date(this), { daysInYear: true }).daysInYear; } get monthsInYear(): Return['monthsInYear'] { if (!ES.IsTemporalZonedDateTime(this)) throw new TypeErrorCtor('invalid receiver'); - return ES.CalendarMonthsInYear(GetSlot(this, CALENDAR), dateTime(this)); + return ES.calendarImplForObj(this).isoToDate(date(this), { monthsInYear: true }).monthsInYear; } get inLeapYear(): Return['inLeapYear'] { if (!ES.IsTemporalZonedDateTime(this)) throw new TypeErrorCtor('invalid receiver'); - return ES.CalendarInLeapYear(GetSlot(this, CALENDAR), dateTime(this)); + return ES.calendarImplForObj(this).isoToDate(date(this), { inLeapYear: true }).inLeapYear; } get offset(): Return['offset'] { if (!ES.IsTemporalZonedDateTime(this)) throw new TypeErrorCtor('invalid receiver'); @@ -186,9 +185,10 @@ export class ZonedDateTime implements Temporal.ZonedDateTime { const timeZone = GetSlot(this, TIME_ZONE); const epochNs = GetSlot(this, EPOCHNANOSECONDS); const offsetNs = ES.GetOffsetNanosecondsFor(timeZone, epochNs); - const isoDateTime = ES.GetISODateTimeFor(timeZone, epochNs); + const isoDateTime = dateTime(this); + const isoDate = ES.ISODateTimeToDateRecord(isoDateTime); let fields = { - ...ES.ISODateToFields(calendar, isoDateTime), + ...ES.ISODateToFields(calendar, isoDate), hour: isoDateTime.hour, minute: isoDateTime.minute, second: isoDateTime.second, @@ -233,7 +233,7 @@ export class ZonedDateTime implements Temporal.ZonedDateTime { const timeZone = GetSlot(this, TIME_ZONE); const calendar = GetSlot(this, CALENDAR); - const iso = ES.GetISODateTimeFor(timeZone, GetSlot(this, EPOCHNANOSECONDS)); + const iso = date(this); let epochNs; if (temporalTimeParam === undefined) { @@ -316,23 +316,22 @@ export class ZonedDateTime implements Temporal.ZonedDateTime { // first, round the underlying DateTime fields const timeZone = GetSlot(this, TIME_ZONE); const thisNs = GetSlot(this, EPOCHNANOSECONDS); - const iso = ES.GetISODateTimeFor(timeZone, thisNs); + const iso = dateTime(this); let epochNanoseconds; if (smallestUnit === 'day') { // Compute Instants for start-of-day and end-of-day // Determine how far the current instant has progressed through this span. - const { year, month, day } = iso; - const dtStart = { year, month, day }; - const dtEnd = ES.BalanceISODate(year, month, day + 1); + const dateStart = ES.ISODateTimeToDateRecord(iso); + const dateEnd = ES.BalanceISODate(dateStart.year, dateStart.month, dateStart.day + 1); - const startNs = ES.GetStartOfDay(timeZone, dtStart); + const startNs = ES.GetStartOfDay(timeZone, dateStart); assert( JSBI.greaterThanOrEqual(thisNs, startNs), 'cannot produce an instant during a day that occurs before start-of-day instant' ); - const endNs = ES.GetStartOfDay(timeZone, dtEnd); + const endNs = ES.GetStartOfDay(timeZone, dateEnd); assert( JSBI.lessThan(thisNs, endNs), 'cannot produce an instant during a day that occurs on or after end-of-day instant' @@ -480,10 +479,9 @@ export class ZonedDateTime implements Temporal.ZonedDateTime { startOfDay(): Return['startOfDay'] { if (!ES.IsTemporalZonedDateTime(this)) throw new TypeErrorCtor('invalid receiver'); const timeZone = GetSlot(this, TIME_ZONE); - const calendar = GetSlot(this, CALENDAR); - const { year, month, day } = ES.GetISODateTimeFor(timeZone, GetSlot(this, EPOCHNANOSECONDS)); - const epochNanoseconds = ES.GetStartOfDay(timeZone, { year, month, day }); - return ES.CreateTemporalZonedDateTime(epochNanoseconds, timeZone, calendar); + const isoDate = date(this); + const epochNanoseconds = ES.GetStartOfDay(timeZone, isoDate); + return ES.CreateTemporalZonedDateTime(epochNanoseconds, timeZone, GetSlot(this, CALENDAR)); } getTimeZoneTransition(directionParam: Params['getTimeZoneTransition'][0]): Return['getTimeZoneTransition'] { if (!ES.IsTemporalZonedDateTime(this)) throw new TypeErrorCtor('invalid receiver'); @@ -562,3 +560,7 @@ MakeIntrinsicClass(ZonedDateTime, 'Temporal.ZonedDateTime'); function dateTime(zdt: Temporal.ZonedDateTime) { return ES.GetISODateTimeFor(GetSlot(zdt, TIME_ZONE), GetSlot(zdt, EPOCHNANOSECONDS)); } + +function date(zdt: Temporal.ZonedDateTime) { + return ES.ISODateTimeToDateRecord(dateTime(zdt)); +}