From 0c92944fb9e51d9d64e69bb44a3f9237c44ac36e Mon Sep 17 00:00:00 2001 From: Justin Grant Date: Fri, 31 Jul 2020 17:06:54 -0700 Subject: [PATCH] Remove `durationKind` options in math methods We decided in 2020-07-31 Champions' meeting to simplify this type by removing the ability to choose a particular duration kind. Instead, developers who want to use non-hybrid durations can convert to DateTime or Absolute and perform the math there. * Remove `durationKind`-related TS types * Remove `durationKind` options from `plus`, `minus`, and `difference`. * Remove `calculation` option from `compare` * Remove disambiguation from math methods, because hybrid durations use `compatible` disambiguation per RFC5545. * Simplify math implementation considerably! --- docs/cookbook/getLocalizedArrival.mjs | 4 +- docs/cookbook/getTripDurationInHrMinSec.mjs | 2 +- poc.d.ts | 1211 ----------------- polyfill/lib/localdatetime.mjs | 353 ++--- polyfill/lib/poc/LocalDateTime.d.ts | 199 +-- .../lib/poc/LocalDateTime.nocomments.d.ts | 20 +- polyfill/lib/poc/LocalDateTime.ts | 435 ++---- polyfill/poc.d.ts | 201 +-- 8 files changed, 387 insertions(+), 2038 deletions(-) delete mode 100644 poc.d.ts diff --git a/docs/cookbook/getLocalizedArrival.mjs b/docs/cookbook/getLocalizedArrival.mjs index eb0d323876..dcc40968f7 100644 --- a/docs/cookbook/getLocalizedArrival.mjs +++ b/docs/cookbook/getLocalizedArrival.mjs @@ -17,8 +17,8 @@ */ function getLocalizedArrival(parseableDeparture, flightTime, destinationTimeZone) { const departure = Temporal.LocalDateTime.from(parseableDeparture); - const arrival = departure.plus(flightTime, { durationKind: 'absolute' }); - return arrival.with({ timeZone: destinationTimeZone }); + const arrival = departure.absolute.plus(flightTime); + return arrival.toLocalDateTime(destinationTimeZone); } const arrival = getLocalizedArrival( diff --git a/docs/cookbook/getTripDurationInHrMinSec.mjs b/docs/cookbook/getTripDurationInHrMinSec.mjs index cf01d6af3e..38d93d40c3 100644 --- a/docs/cookbook/getTripDurationInHrMinSec.mjs +++ b/docs/cookbook/getTripDurationInHrMinSec.mjs @@ -9,7 +9,7 @@ function getTripDurationInHrMinSec(parseableDeparture, parseableArrival) { const departure = Temporal.LocalDateTime.from(parseableDeparture); const arrival = Temporal.LocalDateTime.from(parseableArrival); - return arrival.difference(departure, { largestUnit: 'hours', durationKind: 'absolute' }); + return arrival.difference(departure, { largestUnit: 'hours' }); } const flightTime = getTripDurationInHrMinSec( diff --git a/poc.d.ts b/poc.d.ts deleted file mode 100644 index 6074423677..0000000000 --- a/poc.d.ts +++ /dev/null @@ -1,1211 +0,0 @@ -export namespace Temporal { - export type ComparisonResult = -1 | 0 | 1; - type ConstructorOf = new (...args: unknown[]) => T; - - /** - * Options for assigning fields using `with()` or entire objects with - * `from()`. - * */ - export type AssignmentOptions = { - /** - * How to deal with out-of-range values - * - * - In `'constrain'` mode, out-of-range values are clamped to the nearest - * in-range value. - * - In `'reject'` mode, out-of-range values will cause the function to - * throw a RangeError. - * - * The default is `'constrain'`. - */ - disambiguation: 'constrain' | 'reject'; - }; - - /** - * Options for assigning fields using `Duration.prototype.with()` or entire - * objects with `Duration.prototype.from()`. - * */ - export type DurationAssignmentOptions = { - /** - * How to deal with out-of-range values - * - * - In `'constrain'` mode, out-of-range values are clamped to the nearest - * in-range value. - * - In `'balance'` mode, out-of-range values are resolved by balancing them - * with the next highest unit. - * - In `'reject'` mode, out-of-range values will cause the function to - * throw a RangeError. - * - * The default is `'constrain'`. - */ - disambiguation: 'constrain' | 'balance' | 'reject'; - }; - - /** - * Options for conversions of `Temporal.DateTime` to `Temporal.Absolute` - * */ - export type ToAbsoluteOptions = { - /** - * Controls handling of invalid or ambiguous times caused by time zone - * offset changes like Daylight Saving time (DST) transitions. - * - * This option is only relevant if a `DateTime` value does not exist in the - * destination time zone (e.g. near "Spring Forward" DST transitions), or - * exists more than once (e.g. near "Fall Back" DST transitions). - * - * In case of ambiguous or non-existent times, this option controls what - * absolute time to return: - * - `'earlier'`: The earlier time of two possible times - * - `'later'`: The later of two possible times - * - `'reject'`: Throw a RangeError instead. - * - * The default is `'earlier'`. - * - * Compatibility Note: the legacy `Date` object (and libraries like - * moment.js and Luxon) gives the same result as `earlier` when turning the - * clock back, and `later` when setting the clock forward. - * - * */ - disambiguation: 'earlier' | 'later' | 'reject'; - }; - - /** - * Options for arithmetic operations like `plus()` and `minus()` - * */ - export type ArithmeticOptions = { - /** - * Controls handling of out-of-range arithmetic results. - * - * If a result is out of range, then `'constrain'` will clamp the result to - * the allowed range, while `'reject'` will throw a RangeError. - * - * The default is `'constrain'`. - */ - disambiguation: 'constrain' | 'reject'; - }; - - /** - * Options to control `Duration.prototype.minus()` behavior - * */ - export type DurationMinusOptions = { - /** - * Controls how to deal with subtractions that result in negative overflows - * - * In `'balanceConstrain'` mode, negative fields are balanced with the next - * highest field so that none of the fields are negative in the result. If - * this is not possible, a `RangeError` is thrown. - * - * In `'balance'` mode, all fields are balanced with the next highest field, - * no matter if they are negative or not. - * - * The default is `'balanceConstrain'`. - */ - disambiguation: 'balanceConstrain' | 'balance'; - }; - - export interface DifferenceOptions { - /** - * The largest unit to allow in the resulting `Temporal.Duration` object. - * - * Valid values may include `'years'`, `'months'`, `'days'`, `'hours'`, - * `'minutes'`, and `'seconds'`, although some types may throw an exception - * if a value is used that would produce an invalid result. For example, - * `hours` is not accepted by `Date.prototype.difference()`. - * - * The default depends on the type being used. - */ - largestUnit: T; - } - - export type DurationLike = { - years?: number; - months?: number; - weeks?: number; - days?: number; - hours?: number; - minutes?: number; - seconds?: number; - milliseconds?: number; - microseconds?: number; - nanoseconds?: number; - }; - - type DurationFields = Required; - - /** - * - * A `Temporal.Duration` represents an immutable duration of time which can be - * used in date/time arithmetic. - * - * See https://tc39.es/proposal-temporal/docs/duration.html for more details. - */ - export class Duration implements DurationFields { - static from( - item: Temporal.Duration | DurationLike | string, - options?: DurationAssignmentOptions - ): Temporal.Duration; - constructor( - years?: number, - months?: number, - weeks?: number, - days?: number, - hours?: number, - minutes?: number, - seconds?: number, - milliseconds?: number, - microseconds?: number, - nanoseconds?: number - ); - readonly years: number; - readonly months: number; - readonly weeks: number; - readonly days: number; - readonly hours: number; - readonly minutes: number; - readonly seconds: number; - readonly milliseconds: number; - readonly microseconds: number; - readonly nanoseconds: number; - with(durationLike: DurationLike, options?: DurationAssignmentOptions): Temporal.Duration; - plus(other: Temporal.Duration | DurationLike, options?: ArithmeticOptions): Temporal.Duration; - minus(other: Temporal.Duration | DurationLike, options?: DurationMinusOptions): Temporal.Duration; - getFields(): DurationFields; - toLocaleString(locales?: string | string[], options?: Intl.DateTimeFormatOptions): string; - toJSON(): string; - toString(): string; - } - - /** - * A `Temporal.Absolute` is an absolute point in time, with a precision in - * nanoseconds. No time zone or calendar information is present. Therefore, - * `Temporal.Absolute` has no concept of days, months, or even hours. - * - * For convenience of interoperability, it internally uses nanoseconds since - * the {@link https://en.wikipedia.org/wiki/Unix_time|Unix epoch} (midnight - * UTC on January 1, 1970). However, a `Temporal.Absolute` can be created from - * any of several expressions that refer to a single point in time, including - * an {@link https://en.wikipedia.org/wiki/ISO_8601|ISO 8601 string} with a - * time zone offset such as '2020-01-23T17:04:36.491865121-08:00'. - * - * See https://tc39.es/proposal-temporal/docs/absolute.html for more details. - */ - export class Absolute { - static fromEpochSeconds(epochSeconds: number): Temporal.Absolute; - static fromEpochMilliseconds(epochMilliseconds: number): Temporal.Absolute; - static fromEpochMicroseconds(epochMicroseconds: bigint): Temporal.Absolute; - static fromEpochNanoseconds(epochNanoseconds: bigint): Temporal.Absolute; - static from(item: Temporal.Absolute | string): Temporal.Absolute; - static compare(one: Temporal.Absolute, two: Temporal.Absolute): ComparisonResult; - constructor(epochNanoseconds: bigint); - getEpochSeconds(): number; - getEpochMilliseconds(): number; - getEpochMicroseconds(): bigint; - getEpochNanoseconds(): bigint; - equals(other: Temporal.Absolute): boolean; - plus(durationLike: Temporal.Duration | DurationLike): Temporal.Absolute; - minus(durationLike: Temporal.Duration | DurationLike): Temporal.Absolute; - difference( - other: Temporal.Absolute, - options?: DifferenceOptions<'days' | 'hours' | 'minutes' | 'seconds'> - ): Temporal.Duration; - inTimeZone(tzLike?: TimeZoneProtocol | string, calendar?: CalendarProtocol | string): Temporal.DateTime; - toLocaleString(locales?: string | string[], options?: Intl.DateTimeFormatOptions): string; - toJSON(): string; - toString(tzLike?: TimeZoneProtocol | string): string; - } - - export interface CalendarProtocol { - id: string; - year(date: Temporal.Date): number; - month(date: Temporal.Date): number; - day(date: Temporal.Date): number; - era(date: Temporal.Date): string | undefined; - dayOfWeek?(date: Temporal.Date): number; - dayOfYear?(date: Temporal.Date): number; - weekOfYear?(date: Temporal.Date): number; - daysInMonth?(date: Temporal.Date): number; - daysInYear?(date: Temporal.Date): number; - isLeapYear?(date: Temporal.Date): boolean; - dateFromFields( - fields: DateLike, - options: AssignmentOptions, - constructor: ConstructorOf - ): Temporal.Date; - yearMonthFromFields( - fields: YearMonthLike, - options: AssignmentOptions, - constructor: ConstructorOf - ): Temporal.YearMonth; - monthDayFromFields( - fields: MonthDayLike, - options: AssignmentOptions, - constructor: ConstructorOf - ): Temporal.MonthDay; - plus?( - date: Temporal.Date, - duration: Temporal.Duration, - options: ArithmeticOptions, - constructor: ConstructorOf - ): Temporal.Date; - minus?( - date: Temporal.Date, - duration: Temporal.Duration, - options: ArithmeticOptions, - constructor: ConstructorOf - ): Temporal.Date; - difference?( - smaller: Temporal.Date, - larger: Temporal.Date, - options: DifferenceOptions<'years' | 'months' | 'weeks' | 'days'> - ): Temporal.Duration; - } - - /** - * A `Temporal.Calendar` is a representation of a calendar system. It includes - * information about how many days are in each year, how many months are in - * each year, how many days are in each month, and how to do arithmetic in\ - * that calendar system. - * - * See https://tc39.es/proposal-temporal/docs/calendar.html for more details. - */ - export class Calendar implements Required { - static from(item: CalendarProtocol | string): Temporal.Calendar; - constructor(calendarIdentifier: string); - readonly id: string; - year(date: Temporal.Date): number; - month(date: Temporal.Date): number; - day(date: Temporal.Date): number; - era(date: Temporal.Date): string | undefined; - dayOfWeek(date: Temporal.Date): number; - dayOfYear(date: Temporal.Date): number; - weekOfYear(date: Temporal.Date): number; - daysInMonth(date: Temporal.Date): number; - daysInYear(date: Temporal.Date): number; - isLeapYear(date: Temporal.Date): boolean; - dateFromFields( - fields: DateLike, - options: AssignmentOptions, - constructor: ConstructorOf - ): Temporal.Date; - yearMonthFromFields( - fields: YearMonthLike, - options: AssignmentOptions, - constructor: ConstructorOf - ): Temporal.YearMonth; - monthDayFromFields( - fields: MonthDayLike, - options: AssignmentOptions, - constructor: ConstructorOf - ): Temporal.MonthDay; - plus( - date: Temporal.Date, - duration: Temporal.Duration, - options: ArithmeticOptions, - constructor: ConstructorOf - ): Temporal.Date; - minus( - date: Temporal.Date, - duration: Temporal.Duration, - options: ArithmeticOptions, - constructor: ConstructorOf - ): Temporal.Date; - difference( - smaller: Temporal.Date, - larger: Temporal.Date, - options?: DifferenceOptions<'years' | 'months' | 'weeks' | 'days'> - ): Temporal.Duration; - toString(): string; - } - - export type DateLike = { - era?: string | undefined; - year?: number; - month?: number; - day?: number; - calendar?: CalendarProtocol | string; - }; - - type DateFields = { - era: string | undefined; - year: number; - month: number; - day: number; - calendar: CalendarProtocol; - }; - - type DateISOCalendarFields = { - year: number; - month: number; - day: number; - }; - - /** - * A `Temporal.Date` represents a calendar date. "Calendar date" refers to the - * concept of a date as expressed in everyday usage, independent of any time - * zone. For example, it could be used to represent an event on a calendar - * which happens during the whole day no matter which time zone it's happening - * in. - * - * See https://tc39.es/proposal-temporal/docs/date.html for more details. - */ - export class Date implements DateFields { - static from(item: Temporal.Date | DateLike | string, options?: AssignmentOptions): Temporal.Date; - static compare(one: Temporal.Date, two: Temporal.Date): ComparisonResult; - constructor(isoYear: number, isoMonth: number, isoDay: number, calendar?: CalendarProtocol); - readonly year: number; - readonly month: number; - readonly day: number; - readonly calendar: CalendarProtocol; - readonly era: string | undefined; - readonly dayOfWeek: number; - readonly dayOfYear: number; - readonly weekOfYear: number; - readonly daysInYear: number; - readonly daysInMonth: number; - readonly isLeapYear: boolean; - equals(other: Temporal.Date): boolean; - with(dateLike: DateLike, options?: AssignmentOptions): Temporal.Date; - withCalendar(calendar: CalendarProtocol | string): Temporal.Date; - plus(durationLike: Temporal.Duration | DurationLike, options?: ArithmeticOptions): Temporal.Date; - minus(durationLike: Temporal.Duration | DurationLike, options?: ArithmeticOptions): Temporal.Date; - difference( - other: Temporal.Date, - options?: DifferenceOptions<'years' | 'months' | 'weeks' | 'days'> - ): Temporal.Duration; - withTime(temporalTime: Temporal.Time): Temporal.DateTime; - getYearMonth(): Temporal.YearMonth; - getMonthDay(): Temporal.MonthDay; - getFields(): DateFields; - getISOCalendarFields(): DateISOCalendarFields; - toLocaleString(locales?: string | string[], options?: Intl.DateTimeFormatOptions): string; - toJSON(): string; - toString(): string; - } - - export type DateTimeLike = { - era?: string | undefined; - year?: number; - month?: number; - day?: number; - hour?: number; - minute?: number; - second?: number; - millisecond?: number; - microsecond?: number; - nanosecond?: number; - calendar?: CalendarProtocol | string; - }; - - type DateTimeFields = { - era: string | undefined; - year: number; - month: number; - day: number; - hour: number; - minute: number; - second: number; - millisecond: number; - microsecond: number; - nanosecond: number; - calendar: CalendarProtocol; - }; - - type DateTimeISOCalendarFields = { - year: number; - month: number; - day: number; - hour: number; - minute: number; - second: number; - millisecond: number; - microsecond: number; - nanosecond: number; - }; - - /** - * A `Temporal.DateTime` represents a calendar date and wall-clock time, with - * a precision in nanoseconds, and without any time zone. Of the Temporal - * classes carrying human-readable time information, it is the most general - * and complete one. `Temporal.Date`, `Temporal.Time`, `Temporal.YearMonth`, - * and `Temporal.MonthDay` all carry less information and should be used when - * complete information is not required. - * - * See https://tc39.es/proposal-temporal/docs/datetime.html for more details. - */ - export class DateTime implements DateTimeFields { - static from(item: Temporal.DateTime | DateTimeLike | string, options?: AssignmentOptions): Temporal.DateTime; - static compare(one: Temporal.DateTime, two: Temporal.DateTime): ComparisonResult; - constructor( - isoYear: number, - isoMonth: number, - isoDay: number, - hour?: number, - minute?: number, - second?: number, - millisecond?: number, - microsecond?: number, - nanosecond?: number, - calendar?: CalendarProtocol - ); - readonly year: number; - readonly month: number; - readonly day: number; - readonly hour: number; - readonly minute: number; - readonly second: number; - readonly millisecond: number; - readonly microsecond: number; - readonly nanosecond: number; - readonly calendar: CalendarProtocol; - readonly era: string | undefined; - readonly dayOfWeek: number; - readonly dayOfYear: number; - readonly weekOfYear: number; - readonly daysInYear: number; - readonly daysInMonth: number; - readonly isLeapYear: boolean; - equals(other: Temporal.DateTime): boolean; - with(dateTimeLike: DateTimeLike, options?: AssignmentOptions): Temporal.DateTime; - withCalendar(calendar: CalendarProtocol | string): Temporal.DateTime; - plus(durationLike: Temporal.Duration | DurationLike, options?: ArithmeticOptions): Temporal.DateTime; - minus(durationLike: Temporal.Duration | DurationLike, options?: ArithmeticOptions): Temporal.DateTime; - difference( - other: Temporal.DateTime, - options?: DifferenceOptions<'years' | 'months' | 'weeks' | 'days' | 'hours' | 'minutes' | 'seconds'> - ): Temporal.Duration; - inTimeZone(tzLike: TimeZoneProtocol | string, options?: ToAbsoluteOptions): Temporal.Absolute; - getDate(): Temporal.Date; - getYearMonth(): Temporal.YearMonth; - getMonthDay(): Temporal.MonthDay; - getTime(): Temporal.Time; - getFields(): DateTimeFields; - getISOCalendarFields(): DateTimeISOCalendarFields; - toLocaleString(locales?: string | string[], options?: Intl.DateTimeFormatOptions): string; - toJSON(): string; - toString(): string; - } - - export type MonthDayLike = { - month?: number; - day?: number; - }; - - type MonthDayFields = { - month: number; - day: number; - calendar: CalendarProtocol; - }; - - /** - * A `Temporal.MonthDay` represents a particular day on the calendar, but - * without a year. For example, it could be used to represent a yearly - * recurring event, like "Bastille Day is on the 14th of July." - * - * See https://tc39.es/proposal-temporal/docs/monthday.html for more details. - */ - export class MonthDay implements MonthDayFields { - static from(item: Temporal.MonthDay | MonthDayLike | string, options?: AssignmentOptions): Temporal.MonthDay; - constructor(isoMonth: number, isoDay: number, calendar?: CalendarProtocol, refISOYear?: number); - readonly month: number; - readonly day: number; - readonly calendar: CalendarProtocol; - equals(other: Temporal.MonthDay): boolean; - with(monthDayLike: MonthDayLike, options?: AssignmentOptions): Temporal.MonthDay; - withYear(year: number | { era?: string | undefined; year: number }): Temporal.Date; - getFields(): MonthDayFields; - getISOCalendarFields(): DateISOCalendarFields; - toLocaleString(locales?: string | string[], options?: Intl.DateTimeFormatOptions): string; - toJSON(): string; - toString(): string; - } - - export type TimeLike = { - hour?: number; - minute?: number; - second?: number; - millisecond?: number; - microsecond?: number; - nanosecond?: number; - }; - - type TimeFields = Required; - - /** - * A `Temporal.Time` represents a wall-clock time, with a precision in - * nanoseconds, and without any time zone. "Wall-clock time" refers to the - * concept of a time as expressed in everyday usage — the time that you read - * off the clock on the wall. For example, it could be used to represent an - * event that happens daily at a certain time, no matter what time zone. - * - * `Temporal.Time` refers to a time with no associated calendar date; if you - * need to refer to a specific time on a specific day, use - * `Temporal.DateTime`. A `Temporal.Time` can be converted into a - * `Temporal.DateTime` by combining it with a `Temporal.Date` using the - * `withDate()` method. - * - * See https://tc39.es/proposal-temporal/docs/time.html for more details. - */ - export class Time implements TimeFields { - static from(item: Temporal.Time | TimeLike | string, options?: AssignmentOptions): Temporal.Time; - static compare(one: Temporal.Time, two: Temporal.Time): ComparisonResult; - constructor( - hour?: number, - minute?: number, - second?: number, - millisecond?: number, - microsecond?: number, - nanosecond?: number - ); - readonly hour: number; - readonly minute: number; - readonly second: number; - readonly millisecond: number; - readonly microsecond: number; - readonly nanosecond: number; - equals(other: Temporal.Time): boolean; - with(timeLike: Temporal.Time | TimeLike, options?: AssignmentOptions): Temporal.Time; - plus(durationLike: Temporal.Duration | DurationLike, options?: ArithmeticOptions): Temporal.Time; - minus(durationLike: Temporal.Duration | DurationLike, options?: ArithmeticOptions): Temporal.Time; - difference(other: Temporal.Time, options?: DifferenceOptions<'hours' | 'minutes' | 'seconds'>): Temporal.Duration; - withDate(temporalDate: Temporal.Date): Temporal.DateTime; - getFields(): TimeFields; - toLocaleString(locales?: string | string[], options?: Intl.DateTimeFormatOptions): string; - toJSON(): string; - toString(): string; - } - - /** - * A plain object implementing the protocol for a custom time zone. - */ - export interface TimeZoneProtocol { - name?: string; - getOffsetNanosecondsFor(absolute: Temporal.Absolute): number; - getOffsetStringFor?(absolute: Temporal.Absolute): string; - getDateTimeFor(absolute: Temporal.Absolute, calendar?: CalendarProtocol | string): Temporal.DateTime; - getAbsoluteFor?(dateTime: Temporal.DateTime, options?: ToAbsoluteOptions): Temporal.Absolute; - getNextTransition?(startingPoint: Temporal.Absolute): Temporal.Absolute | null; - getPreviousTransition?(startingPoint: Temporal.Absolute): Temporal.Absolute | null; - getPossibleAbsolutesFor(dateTime: Temporal.DateTime): Temporal.Absolute[]; - toString(): string; - toJSON?(): string; - } - - /** - * A `Temporal.TimeZone` is a representation of a time zone: either an - * {@link https://www.iana.org/time-zones|IANA time zone}, including - * information about the time zone such as the offset between the local time - * and UTC at a particular time, and daylight saving time (DST) changes; or - * simply a particular UTC offset with no DST. - * - * Since `Temporal.Absolute` and `Temporal.DateTime` do not contain any time - * zone information, a `Temporal.TimeZone` object is required to convert - * between the two. - * - * See https://tc39.es/proposal-temporal/docs/timezone.html for more details. - */ - export class TimeZone implements Required { - static from(timeZone: Temporal.TimeZone | string): Temporal.TimeZone; - constructor(timeZoneIdentifier: string); - readonly name: string; - getOffsetNanosecondsFor(absolute: Temporal.Absolute): number; - getOffsetStringFor(absolute: Temporal.Absolute): string; - getDateTimeFor(absolute: Temporal.Absolute, calendar?: CalendarProtocol | string): Temporal.DateTime; - getAbsoluteFor(dateTime: Temporal.DateTime, options?: ToAbsoluteOptions): Temporal.Absolute; - getNextTransition(startingPoint: Temporal.Absolute): Temporal.Absolute | null; - getPreviousTransition(startingPoint: Temporal.Absolute): Temporal.Absolute | null; - getPossibleAbsolutesFor(dateTime: Temporal.DateTime): Temporal.Absolute[]; - toString(): string; - toJSON(): string; - } - - export type YearMonthLike = { - era?: string | undefined; - year?: number; - month?: number; - }; - - type YearMonthFields = { - era: string | undefined; - year: number; - month: number; - calendar: CalendarProtocol; - }; - - /** - * A `Temporal.YearMonth` represents a particular month on the calendar. For - * example, it could be used to represent a particular instance of a monthly - * recurring event, like "the June 2019 meeting". - * - * See https://tc39.es/proposal-temporal/docs/yearmonth.html for more details. - */ - export class YearMonth implements YearMonthFields { - static from(item: Temporal.YearMonth | YearMonthLike | string, options?: AssignmentOptions): Temporal.YearMonth; - static compare(one: Temporal.YearMonth, two: Temporal.YearMonth): ComparisonResult; - constructor(isoYear: number, isoMonth: number, calendar?: CalendarProtocol, refISODay?: number); - readonly year: number; - readonly month: number; - readonly calendar: CalendarProtocol; - readonly era: string | undefined; - readonly daysInMonth: number; - readonly daysInYear: number; - readonly isLeapYear: boolean; - equals(other: Temporal.YearMonth): boolean; - with(yearMonthLike: YearMonthLike, options?: AssignmentOptions): Temporal.YearMonth; - plus(durationLike: Temporal.Duration | DurationLike, options?: ArithmeticOptions): Temporal.YearMonth; - minus(durationLike: Temporal.Duration | DurationLike, options?: ArithmeticOptions): Temporal.YearMonth; - difference(other: Temporal.YearMonth, options?: DifferenceOptions<'years' | 'months'>): Temporal.Duration; - withDay(day: number): Temporal.Date; - getFields(): YearMonthFields; - getISOCalendarFields(): DateISOCalendarFields; - toLocaleString(locales?: string | string[], options?: Intl.DateTimeFormatOptions): string; - toJSON(): string; - toString(): string; - } - - /** - * The `Temporal.now` object has several methods which give information about - * the current date, time, and time zone. - * - * See https://tc39.es/proposal-temporal/docs/now.html for more details. - */ - export namespace now { - /** - * Get the system date and time as a `Temporal.Absolute`. - * - * This method gets the current absolute system time, without regard to - * calendar or time zone. This is a good way to get a timestamp for an - * event, for example. It works like the old-style JavaScript `Date.now()`, - * but with nanosecond precision instead of milliseconds. - * */ - export function absolute(): Temporal.Absolute; - - /** - * Get the current calendar date and clock time in a specific time zone. - * - * @param {TimeZoneProtocol | string} [tzLike] - - * {@link https://en.wikipedia.org/wiki/List_of_tz_database_time_zones|IANA time zone identifier} - * string (e.g. `'Europe/London'`), `Temporal.TimeZone` instance, or an - * object implementing the time zone protocol. If omitted, - * the environment's current time zone will be used. - * @param {Temporal.Calendar | string} [calendar] - calendar identifier, or - * a `Temporal.Calendar` instance, or an object implementing the calendar - * protocol. If omitted, the ISO 8601 calendar is used. - */ - export function dateTime( - tzLike?: TimeZoneProtocol | string, - calendar?: CalendarProtocol | string - ): Temporal.DateTime; - - /** - * Get the current calendar date in a specific time zone. - * - * @param {TimeZoneProtocol | string} [tzLike] - - * {@link https://en.wikipedia.org/wiki/List_of_tz_database_time_zones|IANA time zone identifier} - * string (e.g. `'Europe/London'`), `Temporal.TimeZone` instance, or an - * object implementing the time zone protocol. If omitted, - * the environment's current time zone will be used. - * @param {Temporal.Calendar | string} [calendar] - calendar identifier, or - * a `Temporal.Calendar` instance, or an object implementing the calendar - * protocol. If omitted, the ISO 8601 calendar is used. - */ - export function date(tzLike?: TimeZoneProtocol | string, calendar?: CalendarProtocol | string): Temporal.Date; - - /** - * Get the current clock time in a specific time zone. - * - * @param {TimeZoneProtocol | string} [tzLike] - - * {@link https://en.wikipedia.org/wiki/List_of_tz_database_time_zones|IANA time zone identifier} - * string (e.g. `'Europe/London'`), `Temporal.TimeZone` instance, or an - * object implementing the time zone protocol. If omitted, the environment's - * current time zone will be used. - */ - export function time(tzLike?: TimeZoneProtocol | string): Temporal.Time; - - /** - * Get the environment's current time zone. - * - * This method gets the current system time zone. This will usually be a - * named - * {@link https://en.wikipedia.org/wiki/List_of_tz_database_time_zones|IANA time zone}. - */ - export function timeZone(): Temporal.TimeZone; - } - - // ========== POC Types=========== - export type LocalDateTimeLike = Temporal.DateTimeLike & { - /**`Temporal.TimeZone`, IANA time zone identifier, or offset string */ - timeZone?: Temporal.TimeZone | string; - /**`Temporal.Absolute` or ISO "Z" string */ - absolute?: Temporal.Absolute | string; - /** Enables `from` using only local time values */ - timeZoneOffsetNanoseconds?: number; - }; - type LocalDateTimeFields = ReturnType & { - timeZone: Temporal.TimeZone; - absolute: Temporal.Absolute; - timeZoneOffsetNanoseconds: number; - }; - type LocalDateTimeISOCalendarFields = ReturnType & { - timeZone: Temporal.TimeZone; - absolute: Temporal.Absolute; - timeZoneOffsetNanoseconds: number; - }; - /** - * The `durationKind` option allows users to customize how calculations behave - * when days aren't exactly 24 hours long. This occurs on days when Daylight - * Savings Time (DST) starts or ends, or when a country or region legally - * changes its time zone offset. - * - * Choices are: - * - `'absolute'` - Days are treated as 24 hours long, even if there's a - * change in local timezone offset. Math is performed on the underlying - * Absolute timestamp and then local time fields are refreshed to match the - * updated timestamp. - * - `'dateTime'` - Day length will vary according to time zone offset changes - * like DST transitions. Math is performed on the calendar date and clock - * time, and then the Absolute timestamp is refreshed to match the new - * calendar date and clock time. - * - `'hybrid'` - Math is performed by using `'absolute'` math on the time - * portion, and `'dateTime'` math on the date portion. - * - * Days are almost always 24 hours long, these options produce identical - * results if the time zone offset of the endpoint matches the time zone offset - * of the original LocalDateTime. But they may return different results if - * there's a time zone offset change like a DST transition. - * - * For `plus` and `minus` operations the default is `'hybrid'` which matches - * most users' expectations: - * - Adding or subtracting whole days should keep clock time unchanged, even - * if a DST transition happens. For example: "Postpone my 6:00PM dinner by 7 - * days, but make sure that the time stays 6:00PM, not 5:00PM or 7:00PM if - * DST starts over the weekend." - * - Adding or removing time should ignore DST changes. For example: "Meet me - * at the party in 2 hours, not 1 hour or 3 hours if DST starts tonight". - * - * The default is also `'hybrid'` for `difference` operations. In this case, - * typical users expect that short durations that span a DST boundary - * are measured using real-world durations, while durations of one day or longer - * are measured by default using calendar days and clock time. For example: - * - 1:30AM -> 4:30AM on the day that DST starts is "2 hours", because that's - * how much time elapsed in the real word despite a 3-hour difference on the - * wall clock. - * - 1:30AM on the day DST starts -> next day 1:30AM is "1 day" even though only - * 23 hours have elapsed in the real world. - * - * To support these expectations, `'hybrid'` for `difference` works as follows: - * - If `hours` in clock time is identical at start and end, then an integer - * number of days is reported with no `hours` remainder, even if there was a - * DST transition in between. - * - Otherwise, periods of 24 or more real-world hours are reported using clock - * time, while periods less than 24 hours are reported using elapsed time. - */ - export interface DurationKindOptions { - durationKind: 'absolute' | 'dateTime' | 'hybrid'; - } - /** - * For `compare` operations, the default is `'absolute'` because sorting - * almost always is based on the actual instant that something happened in the - * real world, even during unusual periods like the hour before and after DST - * ends where the same clock hour is replayed twice in the real world. During - * that period, an earlier clock time like "2:30AM Pacific Standard Time" is - * actually later in the real world than "2:15AM Pacific Daylight Time" which - * was 45 minutes earlier in the real world but 15 minutes later according to - * a wall clock. To sort by wall clock times instead, use `'dateTime'`. (`'hybrid'` - * is not needed nor available for `compare` operations.) - */ - export interface CompareCalculationOptions { - calculation: 'absolute' | 'dateTime'; - } - export interface OverflowOptions { - /** - * How to deal with out-of-range values - * - * - In `'constrain'` mode, out-of-range values are clamped to the nearest - * in-range value. - * - In `'reject mode'`, out-of-range values will cause the function to throw - * a RangeError. - * - * The default is `'constrain'`. - */ - overflow: 'constrain' | 'reject'; - } - /** - * Time zone definitions can change. If an application stores data about events - * in the future, then stored data about future events may become ambiguous, - * for example if a country permanently abolishes DST. The `prefer` option - * controls this unusual case. - * - * - The default is `'offset'` which will keep the real-world time constant for - * these future events, even if their local times change. - * - The `'dateTime'` option will instead try to keep the local time constant, - * even if that results in a different real-world instant. - * - The `'reject'` option will throw an exception if the the time zone offset - * and the time zone identifier result in different real-world instants. - * - * If a time zone offset is not present in the input, then this option is - * ignored. - */ - export interface TimeZoneOffsetDisambiguationOptions { - prefer?: 'offset' | 'dateTime' | 'reject'; - } - export type LocalDateTimeAssignmentOptions = Partial & - Partial & - Partial; - export type LocalDateTimeMathOptions = Partial & - Partial & - Partial; - export type LocalDateTimeDifferenceOptions = Partial< - Temporal.DifferenceOptions<'years' | 'months' | 'weeks' | 'days' | 'hours' | 'minutes' | 'seconds'> - > & - Partial & - Partial; - export class LocalDateTime { - private _abs; - private _tz; - private _dt; - /** - * Construct a new `Temporal.LocalDateTime` instance from an absolute - * timestamp, time zone, and optional calendar. - * - * To construct a `Temporal.LocalDateTime` from an ISO 8601 string a - * `DateTime` and time zone, use `.from()`. - * - * @param absolute {Temporal.Absolute} - absolute timestamp for this instance - * @param timeZone {Temporal.TimeZone} - time zone for this instance - * @param [calendar=Temporal.Calendar.from('iso8601')] {Temporal.CalendarProtocol} - - * calendar for this instance (defaults to ISO calendar) - */ - constructor(absolute: Temporal.Absolute, timeZone: Temporal.TimeZone, calendar?: Temporal.CalendarProtocol); - /** - * Build a `Temporal.LocalDateTime` instance from one of the following: - * - Another LocalDateTime instance, in which case the result will deep-clone - * the input. - * - A "LocalDateTime-like" object with a `timeZone` field and either an - * `absolute` field OR `year`, `month`, and `day` fields. All non-required - * `Temporal.LocalDateTime` fields are accepted too, with the caveat that - * fields must be consistent. For example, if an object with `absolute` and - * `hours` is provided, then the `hours` value must match the `absolute` in - * the given time zone. Note: if neither `absolute` nor - * `timeZoneOffsetNanoseconds` are provided, then the time can be ambiguous - * around DST transitions. The `disambiguation` option can resolve this - * ambiguity. - * - An extended ISO 8601 string that includes a time zone identifier, e.g. - * `2007-12-03T10:15:30+01:00[Europe/Paris]`. If a timezone offset is not - * present, then the `disambiguation` option will be used to resolve any - * ambiguity. Note that an ISO string ending in "Z" (a UTC time) will not be - * accepted via a string parameter. Instead, the caller must explicitly - * opt-in to UTC using the object form `{absolute: isoString, timeZone: - * 'utc'}` - * - An object that can be converted to the string format above. - * - * If the input contains both a time zone offset and a time zone, in rare - * cases it's possible for those values to conflict for a particular local - * date and time. For example, this could happen for future summertime events - * that were stored before a country permanently abolished DST. If the time - * zone and offset are in conflict, then the `prefer` option is used to - * resolve the conflict. Note that the default for ISO strings is `'offset'` - * which will ensure that ISO strings that were valid when they were stored - * will still parse into a valid LocalDateTime at the same UTC value, even if - * local times have changed. For object initializers, the default is `reject` - * to help developers learn that `from({...getFields(), timeZone: newTz})` - * requires removing the `timeZoneOffsetNanoseconds` field from the - * `getFields` result before passing to `from` or `with`. - * - * Available options: - * ``` - * disambiguation?: 'earlier' (default) | 'later' | 'reject' - * overflow?: 'constrain' (default) | 'reject' - * prefer?: 'offset' (default for ISO strings) | 'dateTime' | 'reject' (default for objects) - * ``` - */ - static from( - item: LocalDateTimeLike | string | Record, - options?: LocalDateTimeAssignmentOptions - ): LocalDateTime; - /** - * Merge fields into an existing `Temporal.LocalDateTime`. The provided `item` - * is a "LocalDateTime-like" object. Fields accepted include: all - * `Temporal.DateTime` fields, `timeZone`, `absolute` (as an ISO string ending - * in "Z", or an `Absolute` instance), and `timezoneOffsetNanoseconds`. - * - * If the `absolute` field is included, all other input fields must be - * consistent with this value or this method will throw. - * - * If the `timezoneOffsetNanoseconds` field is provided, then it's possible for - * it to conflict with the `timeZone` (the input `timeZone` property or, if - * omitted, the object's existing time zone). In that case, the `prefer` - * option is used to resolve the conflict. - * - * If the `timezoneOffsetNanoseconds` field is provided, then that offset will - * be used and the `disambiguation` option will be ignored unless: a) - * `timezoneOffsetNanoseconds` conflicts with the time zone, as noted above; - * AND b) `prefer: 'dateTime'` is used. - * - * If the `timeZone` field is included, the result will convert all fields to - * the new time zone, except that fields included in the input will be set - * directly. Therefore, `.with({timeZone})` is an easy way to convert to a new - * time zone while updating the local time. - * - * To keep local time unchanged while changing only the time zone, call - * `getFields()`, revise the `timeZone`, remove the - * `timeZoneOffsetNanoseconds` field so it won't conflict with the new time - * zone, and then pass the resulting object to `with`. For example: - * ``` - * const {timeZoneOffsetNanoseconds, timeZone, ...fields} = ldt.getFields(); - * const newTzSameLocalTime = ldt.with({...fields, timeZone: 'Europe/London'}); - * ``` - * - * Available options: - * ``` - * disambiguation?: 'earlier' (default) | 'later' | 'reject' - * overflow?: 'constrain' (default) | 'reject' - * prefer?: 'offset' (default) | 'dateTime' | 'reject' - * ``` - */ - with(localDateTimeLike: LocalDateTimeLike, options?: LocalDateTimeAssignmentOptions): LocalDateTime; - /** - * Returns the absolute timestamp of this `Temporal.LocalDateTime` instance as - * a `Temporal.Absolute`. - * - * It's a `get` property (not a `getAbsolute()` method) to support - * round-tripping via `getFields` and `with`. - * - * Although this property is a `Temporal.Absolute` object, `JSON.stringify` - * will automatically convert it to a JSON-friendly ISO 8601 string (ending in - * `Z`) when persisting to JSON. - */ - get absolute(): Temporal.Absolute; - /** - * Returns the `Temporal.TimeZone` representing this object's time zone. - * - * Although this property is a `Temporal.TimeZone` object, `JSON.stringify` - * will automatically convert it to a JSON-friendly IANA time zone identifier - * string (e.g. `'Europe/Paris'`) when persisting to JSON. - */ - get timeZone(): Temporal.TimeZone; - /** - * Returns the `Temporal.Calendar` for this `Temporal.LocalDateTime` instance. - * - * ISO 8601 (the Gregorian calendar with a specific week numbering scheme - * defined) is the default calendar. - * - * Although this property is a `Temporal.Calendar` object, `JSON.stringify` - * will automatically convert it to a JSON-friendly calendar ID string IANA - * time zone identifier string (e.g. `'iso8601'` or `'japanese'`) when - * persisting to JSON. - */ - get calendar(): Temporal.CalendarProtocol; - /** - * Returns the String representation of this `Temporal.LocalDateTime` in ISO - * 8601 format extended to include the time zone. - * - * Example: `2011-12-03T10:15:30+01:00[Europe/Paris]` - * - * If the calendar is not the default ISO 8601 calendar, then it will be - * appended too. Example: `2011-12-03T10:15:30+09:00[Asia/Tokyo][c=japanese]` - */ - getDateTime(): Temporal.DateTime; - /** - * Returns the number of real-world hours between midnight of the current day - * until midnight of the next calendar day. Normally days will be 24 hours - * long, but on days where there are DST changes or other time zone - * transitions, this duration may be 23 hours or 25 hours. In rare cases, - * other integers or even non-integer values may be returned, e.g. when time - * zone definitions change by less than one hour. - * - * If a time zone offset transition happens exactly at midnight, the - * transition will be counted as part of the previous day's length. - * - * Note that transitions that skip entire days (like the 2011 - * [change](https://en.wikipedia.org/wiki/Time_in_Samoa#2011_time_zone_change) - * of `Pacific/Apia` to the opposite side of the International Date Line) will - * return `24` because there are 24 real-world hours between one day's - * midnight and the next day's midnight. - */ - get hoursInDay(): number; - /** - * True if this `Temporal.LocalDateTime` instance falls exactly on a DST - * transition or other change in time zone offset, false otherwise. - * - * To calculate if a DST transition happens on the same day (but not - * necessarily at the same time), use `.getDayDuration()`. - * */ - get isTimeZoneOffsetTransition(): boolean; - get timeZoneOffsetNanoseconds(): number; - get timeZoneOffsetString(): string; - /** - * Returns a plain object containing enough data to uniquely identify - * this object. - * - * The resulting object includes all fields returned by - * `Temporal.DateTime.prototype.getFields()`, as well as `timeZone`, - * `timeZoneOffsetNanoseconds`, and `absolute`. - * - * The result of this method can be used for round-trip serialization via - * `from()`, `with()`, or `JSON.stringify`. - */ - getFields(): LocalDateTimeFields; - /** - * Method for internal use by non-ISO calendars. Normally not used. - * - * TODO: are calendars aware of `Temporal.LocalDateTime`? If not, remove this - * method. - */ - getISOCalendarFields(): LocalDateTimeISOCalendarFields; - /** - * Compare two `Temporal.LocalDateTime` values. - * - * By default, comparison will use the absolute time because sorting is almost - * always based on when events happened in the real world, but during the hour - * before and after DST ends in the fall, sorting of clock time will not match - * the real-world sort order. - * - * Available options: - * ``` - * calculation?: 'absolute' (default) | 'dateTime' - * ``` - */ - static compare( - one: LocalDateTime, - two: LocalDateTime, - options?: CompareCalculationOptions - ): Temporal.ComparisonResult; - /** - * Returns `true` if both the absolute timestamp and time zone are identical - * to the other `Temporal.LocalDateTime` instance, and `false` otherwise. To - * compare only the absolute timestamps and ignore time zones, use - * `.absolute.compare()`. - */ - equals(other: LocalDateTime): boolean; - /** - * Add a `Temporal.Duration` and return the result. - * - * By default, the `'hybrid'` calculation method will be used where dates will - * be added using calendar dates while times will be added with absolute time. - * - * Available options: - * ``` - * durationKind?: 'hybrid' (default) | 'absolute' | 'dateTime' - * disambiguation?: 'earlier' (default) | 'later' | 'reject' - * overflow?: 'constrain' (default) | 'reject' - * ``` - */ - plus(durationLike: Temporal.DurationLike, options?: LocalDateTimeMathOptions): LocalDateTime; - /** - * Subtract a `Temporal.Duration` and return the result. - * - * By default, the `'hybrid'` calculation method will be used where dates will - * be added using calendar dates while times will be subtracted with absolute - * time. - * - * Available options: - * ``` - * durationKind?: 'hybrid' (default) | 'absolute' | 'dateTime' - * disambiguation?: 'earlier' (default) | 'later' | 'reject' - * overflow?: 'constrain' (default) | 'reject' - * ``` - */ - minus(durationLike: Temporal.DurationLike, options?: LocalDateTimeMathOptions): LocalDateTime; - /** - * Calculate the difference between two `Temporal.LocalDateTime` values and - * return the `Temporal.Duration` result. - * - * The kind of duration returned depends on the `durationKind` option: - * - `absolute` will calculate the difference using real-world elapsed time. - * - `dateTime` will calculate the difference in clock time and calendar - * dates. - * - By default, `'hybrid'` durations are returned because they usually match - * users' expectations that short durations are measured in real-world - * elapsed time that ignores DST transitions, while differences of calendar - * days are calculated by taking DST transitions into account. - * - * If `'hybrid'` is chosen but `largestUnit` is hours or less, then the - * calculation will be the same as if `absolute` was chosen. - * - * However, if `'hybrid'` is used with `largestUnit` of `'days'` or larger, - * then (as RFC 5545 requires) date differences will be calculated using - * `dateTime` math which adjusts for DST, while the time remainder will be - * calculated using real-world elapsed time. Examples: - * - 2:30AM on the day before DST starts -> 3:30AM on the day DST starts = - * P1DT1H (even though it's only 24 hours of real-world elapsed time) - * - 1:45AM on the day before DST starts -> "second" 1:15AM on the day DST - * ends = PT24H30M (because it hasn't been a full calendar day even though - * it's been 24.5 real-world hours). - * - * Calculations using `durationKind: 'absolute'` are limited to `largestUnit: - * 'days'` or smaller units. For larger units, use `'hybrid'` or - * `'dateTime'`. - * - * If the other `Temporal.LocalDateTime` is in a different time zone, then the - * same days can be different lengths in each time zone, e.g. if only one of - * them observes DST. Therefore, a `RangeError` will be thrown if all of the - * following conditions are true: - * - `durationKind` is `'hybrid'` or `'dateTime'` - * - `largestUnit` is `'days'` or larger - * - the two instances' time zones have different `name` fields. - * - * Here are commonly used alternatives for cross-timezone calculations: - * - Use `durationKind: 'absolute'`, as long as it's OK if all days are - * assumed to be 24 hours long and DST is ignored. - * - If you need weeks, months, or years in the result, or if you need to take - * DST transitions into account, transform one of the instances to the - * other's time zone using `.with({timeZone: other.timeZone})` and then - * calculate the same-timezone difference. - * - To calculate with calendar dates only, use - * `.getDate().difference(other.getDate())`. - * - To calculate with clock times only, use - * `.getTime().difference(other.getTime())`. - * - * Because of the complexity and ambiguity involved in cross-timezone - * calculations, `hours` is the default for `largestUnit`. - * - * Available options: - * ``` - * largestUnit: 'years' | 'months' | 'weeks' | 'days' | 'hours' (default) | 'minutes' | 'seconds' - * durationKind?: 'hybrid' (default) | 'absolute' | 'dateTime' - * ``` - */ - difference(other: LocalDateTime, options?: LocalDateTimeDifferenceOptions): Temporal.Duration; - toLocaleString(locales?: string | string[], options?: Intl.DateTimeFormatOptions): string; - /** - * String representation of this `Temporal.LocalDateTime` in ISO 8601 format - * extended to include the time zone. - * - * Example: `2011-12-03T10:15:30+01:00[Europe/Paris]` - * - * If the calendar is not the default ISO 8601 calendar, then it will be - * appended too. Example: `2011-12-03T10:15:30+09:00[Asia/Tokyo][c=japanese]` - */ - toJSON(): string; - /** - * String representation of this `Temporal.LocalDateTime` in ISO 8601 format - * extended to include the time zone. - * - * Example: `2011-12-03T10:15:30+01:00[Europe/Paris]` - * - * If the calendar is not the default ISO 8601 calendar, then it will be - * appended too. Example: `2011-12-03T10:15:30+09:00[Asia/Tokyo][c=japanese]` - */ - toString(): string; - get era(): string | undefined; - get year(): number; - get month(): number; - get day(): number; - get hour(): number; - get minute(): number; - get second(): number; - get millisecond(): number; - get microsecond(): number; - get nanosecond(): number; - get dayOfWeek(): number; - get dayOfYear(): number; - get weekOfYear(): number; - get daysInYear(): number; - get daysInMonth(): number; - get isLeapYear(): boolean; - getDate(): Temporal.Date; - getYearMonth(): Temporal.YearMonth; - getMonthDay(): Temporal.MonthDay; - getTime(): Temporal.Time; - valueOf(): never; - } -} diff --git a/polyfill/lib/localdatetime.mjs b/polyfill/lib/localdatetime.mjs index 9c6fdc34b0..fc63a2f4b0 100644 --- a/polyfill/lib/localdatetime.mjs +++ b/polyfill/lib/localdatetime.mjs @@ -131,88 +131,60 @@ function fromCommon(dt, timeZone, offset, disambiguation, offsetOption) { /** Identical logic for `plus` and `minus` */ function doPlusOrMinus(op, durationLike, options, localDateTime) { - const disambiguation = getOption(options, 'disambiguation', DISAMBIGUATION_OPTIONS, 'compatible'); - const durationKind = getOption(options, 'durationKind', CALCULATION_OPTIONS, 'hybrid'); const overflow = getOption(options, 'overflow', OVERFLOW_OPTIONS, 'constrain'); // TODO: edit below depending on https://github.com/tc39/proposal-temporal/issues/607 const dateTimeOverflowOption = { disambiguation: overflow }; const { timeZone, calendar } = localDateTime; - // Absolute doesn't use disambiguation, while RFC 5455 specifies 'compatible' behavior - // for disambiguation. Therefore, only 'dateTime' durations can use this option. - if (disambiguation !== 'compatible' && durationKind !== 'dateTime') { - throw new RangeError('Disambiguation options are only valid for `dateTime` durations'); + const { timeDuration, dateDuration } = splitDuration(durationLike); + if (isZeroDuration(dateDuration)) { + // If there's only a time to add/subtract, then use absolute math + // because RFC 5545 specifies using absolute math for time units. + const result = localDateTime.absolute[op](durationLike); + return new LocalDateTime(result, timeZone, calendar); } - switch (durationKind) { - case 'absolute': { - const result = localDateTime.absolute[op](durationLike); - return new LocalDateTime(result, timeZone, calendar); - } - case 'dateTime': { - const dateTime = localDateTime.toDateTime(); - const newDateTime = dateTime[op](durationLike, dateTimeOverflowOption); - // If empty duration (no local date/time change), then clone `this` to - // avoid disambiguation that might change the absolute. - if (newDateTime.equals(dateTime)) return LocalDateTime.from(localDateTime); - // Otherwise, return the result. - const abs = newDateTime.toAbsolute(timeZone, { disambiguation }); - return new LocalDateTime(abs, timeZone, calendar); - } - case 'hybrid': { - const { timeDuration, dateDuration } = splitDuration(durationLike); - if (isZeroDuration(dateDuration)) { - // If there's only a time to add/subtract, then use absolute math - // because RFC 5545 specifies using absolute math for time units. - const result = localDateTime.absolute[op](durationLike); - return new LocalDateTime(result, timeZone, calendar); - } - - // Add the units according to the largest-to-smallest order of operations - // required by RFC 5545. Note that the same breakout is not required for - // the time duration because all time units are the same length (because - // Temporal ignores leap seconds). - let newDateTime = localDateTime.toDateTime(); - const { years, months, weeks, days } = dateDuration; - // TODO: if https://github.com/tc39/proposal-temporal/issues/653 - // changes order of operations, then coalesce 4 calls to 1. - if (years) newDateTime = newDateTime[op]({ years }, dateTimeOverflowOption); - if (months) newDateTime = newDateTime[op]({ months }, dateTimeOverflowOption); - if (weeks) newDateTime = newDateTime[op]({ weeks }, dateTimeOverflowOption); - if (days) newDateTime = newDateTime[op]({ days }, dateTimeOverflowOption); - if (isZeroDuration(timeDuration)) { - const absolute = newDateTime.toAbsolute(timeZone); - return LocalDateTime.from({ absolute, timeZone, calendar: localDateTime.calendar }); - } else { - // Now add/subtract the time. Because all time units are always the same - // length, we can add/subtract all of them together without worrying about - // order of operations. - newDateTime = newDateTime[op](timeDuration, dateTimeOverflowOption); - let absolute = newDateTime.toAbsolute(timeZone); - const reverseOp = op === 'plus' ? 'minus' : 'plus'; - const backUpAbs = absolute[reverseOp]({ nanoseconds: totalNanoseconds(timeDuration) }); - const backUpOffset = timeZone.getOffsetNanosecondsFor(backUpAbs); - const absOffset = timeZone.getOffsetNanosecondsFor(absolute); - const backUpNanoseconds = absOffset - backUpOffset; - if (backUpNanoseconds) { - // RFC 5545 specifies that time units are always "exact time" meaning - // they aren't affected by DST. Therefore, if there was a TZ - // transition during the time duration that was added, then undo the - // impact of that transition. However, don't adjust if applying the - // adjustment would cause us to back up onto the other side of the - // transition. - const backUpOp = backUpNanoseconds < 0 ? 'minus' : 'plus'; - const adjustedAbs = absolute[backUpOp]({ nanoseconds: backUpNanoseconds }); - if (timeZone.getOffsetNanosecondsFor(adjustedAbs) === timeZone.getOffsetNanosecondsFor(absolute)) { - absolute = adjustedAbs; - } - } - - return LocalDateTime.from({ absolute, timeZone, calendar }); + // Add the units according to the largest-to-smallest order of operations + // required by RFC 5545. Note that the same breakout is not required for + // the time duration because all time units are the same length (because + // Temporal ignores leap seconds). + let newDateTime = localDateTime.toDateTime(); + const { years, months, weeks, days } = dateDuration; + // TODO: if https://github.com/tc39/proposal-temporal/issues/653 + // changes order of operations, then coalesce 4 calls to 1. + if (years) newDateTime = newDateTime[op]({ years }, dateTimeOverflowOption); + if (months) newDateTime = newDateTime[op]({ months }, dateTimeOverflowOption); + if (weeks) newDateTime = newDateTime[op]({ weeks }, dateTimeOverflowOption); + if (days) newDateTime = newDateTime[op]({ days }, dateTimeOverflowOption); + if (isZeroDuration(timeDuration)) { + const absolute = newDateTime.toAbsolute(timeZone); + return LocalDateTime.from({ absolute, timeZone, calendar: localDateTime.calendar }); + } else { + // Now add/subtract the time. Because all time units are always the same + // length, we can add/subtract all of them together without worrying about + // order of operations. + newDateTime = newDateTime[op](timeDuration, dateTimeOverflowOption); + let absolute = newDateTime.toAbsolute(timeZone); + const reverseOp = op === 'plus' ? 'minus' : 'plus'; + const backUpAbs = absolute[reverseOp]({ nanoseconds: totalNanoseconds(timeDuration) }); + const backUpOffset = timeZone.getOffsetNanosecondsFor(backUpAbs); + const absOffset = timeZone.getOffsetNanosecondsFor(absolute); + const backUpNanoseconds = absOffset - backUpOffset; + if (backUpNanoseconds) { + // RFC 5545 specifies that time units are always "exact time" meaning + // they aren't affected by DST. Therefore, if there was a TZ + // transition during the time duration that was added, then undo the + // impact of that transition. However, don't adjust if applying the + // adjustment would cause us to back up onto the other side of the + // transition. + const backUpOp = backUpNanoseconds < 0 ? 'minus' : 'plus'; + const adjustedAbs = absolute[backUpOp]({ nanoseconds: backUpNanoseconds }); + if (timeZone.getOffsetNanosecondsFor(adjustedAbs) === timeZone.getOffsetNanosecondsFor(absolute)) { + absolute = adjustedAbs; } } - default: - throw new Error(`Invalid \`durationKind\` option value: ${durationKind}`); + + return LocalDateTime.from({ absolute, timeZone, calendar }); } } @@ -592,21 +564,17 @@ export class LocalDateTime { /** * Compare two `Temporal.LocalDateTime` values. * - * By default, comparison will use the absolute time because sorting is almost - * always based on when events happened in the real world, but during the hour - * before and after DST ends in the fall, sorting of clock time will not match - * the real-world sort order. + * Comparison will use the absolute time because sorting is almost always + * based on when events happened in the real world, but during the hour before + * and after DST ends in the fall, sorting of clock time will not match the + * real-world sort order. * - * Available options: - * ``` - * calculation?: 'absolute' (default) | 'dateTime' - * ``` + * In the very unusual case of sorting by clock time instead, use + * `.toDateTime()` on both instances and use `Temporal.DateTime`'s `compare` + * method. */ - static compare(one, two, options) { - const calculation = getOption(options, 'calculation', COMPARE_CALCULATION_OPTIONS, 'absolute'); - return calculation === 'dateTime' - ? Temporal.DateTime.compare(one._dt, two._dt) - : Temporal.Absolute.compare(one._abs, two._abs); + static compare(one, two) { + return Temporal.Absolute.compare(one._abs, two._abs); } /** @@ -622,13 +590,11 @@ export class LocalDateTime { /** * Add a `Temporal.Duration` and return the result. * - * By default, the `'hybrid'` calculation method will be used where dates will - * be added using calendar dates while times will be added with absolute time. + * Dates will be added using calendar dates while times will be added with + * absolute time. * * Available options: * ``` - * durationKind?: 'hybrid' (default) | 'absolute' | 'dateTime' - * disambiguation?: 'compatible' (default) | 'earlier' | 'later' | 'reject' * overflow?: 'constrain' (default) | 'reject' * ``` */ @@ -639,14 +605,11 @@ export class LocalDateTime { /** * Subtract a `Temporal.Duration` and return the result. * - * By default, the `'hybrid'` calculation method will be used where dates will - * be added using calendar dates while times will be subtracted with absolute - * time. + * Dates will be subtracted using calendar dates while times will be + * subtracted with absolute time. * * Available options: * ``` - * durationKind?: 'hybrid' (default) | 'absolute' | 'dateTime' - * disambiguation?: 'compatible' (default) | 'earlier' | 'later' | 'reject' * overflow?: 'constrain' (default) | 'reject' * ``` */ @@ -658,147 +621,115 @@ export class LocalDateTime { * Calculate the difference between two `Temporal.LocalDateTime` values and * return the `Temporal.Duration` result. * - * The kind of duration returned depends on the `durationKind` option: - * - `absolute` will calculate the difference using real-world elapsed time. - * - `dateTime` will calculate the difference in clock time and calendar - * dates. - * - By default, `'hybrid'` durations are returned because they usually match - * users' expectations that short durations are measured in real-world - * elapsed time that ignores DST transitions, while differences of calendar - * days are calculated by taking DST transitions into account. + * The duration returned is a "hybrid" duration. The date portion represents + * full calendar days like `DateTime.prototype.difference` would return. The + * time portion represents real-world elapsed time like + * `Absolute.prototype.difference` would return. This "hybrid duration" + * approach matches widely-adopted industry standards like RFC 5545 + * (iCalendar). It also matches the behavior of popular JavaScript libraries + * like moment.js and date-fns. * - * If `'hybrid'` is chosen but `largestUnit` is hours or less, then the - * calculation will be the same as if `absolute` was chosen. + * Examples: + * - Difference between 2:30AM on the day before DST starts and 3:30AM on the + * day DST starts = `P1DT1H` (even though it's only 24 hours of real-world + * elapsed time) + * - Difference between 1:45AM on the day before DST starts and the "second" + * 1:15AM on the day DST ends => `PT24H30M` (because it hasn't been a full + * calendar day even though it's been 24.5 real-world hours). * - * However, if `'hybrid'` is used with `largestUnit` of `'days'` or larger, - * then (as RFC 5545 requires) date differences will be calculated using - * `dateTime` math which adjusts for DST, while the time remainder will be - * calculated using real-world elapsed time. Examples: - * - 2:30AM on the day before DST starts -> 3:30AM on the day DST starts = - * P1DT1H (even though it's only 24 hours of real-world elapsed time) - * - 1:45AM on the day before DST starts -> "second" 1:15AM on the day DST - * ends = PT24H30M (because it hasn't been a full calendar day even though - * it's been 24.5 real-world hours). + * If `largestUnit` is `'hours'` or smaller, then the result will be the same + * as if `Temporal.Absolute.prototype.difference` was used. * - * The `'disambiguation'` option is ony used if all of the following are true: - * - `durationKind: 'hybrid'` is used. - * - The difference between `this` and `other` is larger than one full - * calendar day. - * - `this` and `other` have different clock times. If clock times are the - * same then an integer number of days will be returned. - * - When the date portion of the difference is subtracted from `this`, the - * resulting local time is ambiguous (e.g. the repeated hour after DST ends, - * or the skipped hour after DST starts). If all of the above conditions are - * true, then the `'disambiguation'` option determines the - * `Temporal.Absolute` chosen for the end of the date portion. The time - * portion of the resulting duration will be calculated from that - * `Temporal.Absolute`. - * - * Calculations using `durationKind: 'absolute'` are limited to `largestUnit: - * 'days'` or smaller units. For larger units, use `'hybrid'` or - * `'dateTime'`. + * If both values have the same local time, then the result will be the same + * as if `Temporal.DateTime.prototype.difference` was used. * * If the other `Temporal.LocalDateTime` is in a different time zone, then the * same days can be different lengths in each time zone, e.g. if only one of - * them observes DST. Therefore, a `RangeError` will be thrown if all of the - * following conditions are true: - * - `durationKind` is `'hybrid'` or `'dateTime'` - * - `largestUnit` is `'days'` or larger - * - the two instances' time zones have different `name` fields. + * them observes DST. Therefore, a `RangeError` will be thrown if + * `largestUnit` is `'days'` or larger and the two instances' time zones have + * different `name` fields. To work around this limitation, transform one of + * the instances to the other's time zone using `.with({timeZone: + * other.timeZone})` and then calculate the same-timezone difference. * - * Here are commonly used alternatives for cross-timezone calculations: - * - Use `durationKind: 'absolute'`, as long as it's OK if all days are - * assumed to be 24 hours long and DST is ignored. - * - If you need weeks, months, or years in the result, or if you need to take - * DST transitions into account, transform one of the instances to the - * other's time zone using `.with({timeZone: other.timeZone})` and then - * calculate the same-timezone difference. - * - To calculate with calendar dates only, use + * To calculate the difference between calendar dates only, use * `.toDate().difference(other.toDate())`. - * - To calculate with clock times only, use + * + * To calculate the difference between clock times only, use * `.toTime().difference(other.toTime())`. * * Because of the complexity and ambiguity involved in cross-timezone - * calculations, `hours` is the default for `largestUnit`. + * calculations involving days or larger units, `hours` is the default for + * `largestUnit`. * * Available options: * ``` * largestUnit: 'years' | 'months' | 'weeks' | 'days' | 'hours' (default) | 'minutes' | 'seconds' - * durationKind?: 'hybrid' (default) | 'absolute' | 'dateTime' - * disambiguation?: 'compatible' (default) | 'earlier' | 'later' | 'reject' * ``` */ difference(other, options) { - const durationKind = getOption(options, 'durationKind', CALCULATION_OPTIONS, 'hybrid'); - const disambiguation = getOption(options, 'disambiguation', DISAMBIGUATION_OPTIONS, 'compatible'); - const largestUnit = toLargestTemporalUnit(options, 'years'); const dateUnits = ['years', 'months', 'weeks', 'days']; const wantDate = dateUnits.includes(largestUnit); - // treat hybrid as absolute if the user is only asking for a time difference - if (durationKind === 'absolute' || (durationKind === 'hybrid' && !wantDate)) { - if (wantDate) throw new Error("For absolute difference calculations, `largestUnit` must be 'hours' or smaller"); + // treat as absolute if the user is only asking for a time difference + if (!wantDate) { const largestUnitOptionBag = { largestUnit: largestUnit }; return this._abs.difference(other._abs, largestUnitOptionBag); - } else if (durationKind === 'dateTime') { - return this._dt.difference(other._dt, { largestUnit }); + } + const dtDiff = this._dt.difference(other._dt, { largestUnit }); + + // If there's no change in timezone offset between this and other, then we + // don't have to do any DST-related fixups. Just return the simple + // DateTime difference. + const diffOffset = this.timeZoneOffsetNanoseconds - other.timeZoneOffsetNanoseconds; + if (diffOffset === 0) return dtDiff; + + // It's the hard case: the timezone offset is different so there's a + // transition in the middle and we may need to adjust the result for DST. + // RFC 5545 expects that date durations are measured in nominal (DateTime) + // days, while time durations are measured in exact (Absolute) time. + const { dateDuration, timeDuration } = splitDuration(dtDiff); + if (isZeroDuration(timeDuration)) return dateDuration; // even number of calendar days + + // If we get here, there's both a time and date part of the duration AND + // there's a time zone offset transition during the duration. RFC 5545 + // says that we should calculate full days using DateTime math and + // remainder times using absolute time. To do this, we calculate a + // `dateTime` difference, split it into date and time portions, and then + // convert the time portion to an `absolute` duration before returning to + // the caller. A challenge: converting the time duration involves a + // conversion from `DateTime` to `Absolute` which can be ambiguous. This + // can cause unpredictable behavior because the disambiguation is + // happening inside of the duration, not at its edges like in `plus` or + // `from`. We'll reduce the chance of this unpredictability as follows: + // 1. First, calculate the time portion as if it's closest to `other`. + // 2. If the time portion in (1) contains a tz offset transition, then + // reverse the calculation and assume that the time portion is closest + // to `this`. + // + // The approach above ensures that in almost all cases, there will be no + // "internal disambiguation" required. It's possible to construct a test + // case where both `this` and `other` are both within 25 hours of a + // different offset transition, but in practice this will be exceedingly + // rare. + let intermediateDt = this._dt.minus(dateDuration); + let intermediateAbs = intermediateDt.toAbsolute(this._tz); + let adjustedTimeDuration; + if (this._tz.getOffsetNanosecondsFor(intermediateAbs) === other.timeZoneOffsetNanoseconds) { + // The transition was in the date portion which is what we want. + adjustedTimeDuration = intermediateAbs.difference(other._abs, { largestUnit: 'hours' }); } else { - // durationKind === 'hybrid' - const dtDiff = this._dt.difference(other._dt, { largestUnit }); - - // If there's no change in timezone offset between this and other, then we - // don't have to do any DST-related fixups. Just return the simple - // DateTime difference. - const diffOffset = this.timeZoneOffsetNanoseconds - other.timeZoneOffsetNanoseconds; - if (diffOffset === 0) return dtDiff; - - // It's the hard case: the timezone offset is different so there's a - // transition in the middle and we may need to adjust the result for DST. - // RFC 5545 expects that date durations are measured in nominal (DateTime) - // days, while time durations are measured in exact (Absolute) time. - const { dateDuration, timeDuration } = splitDuration(dtDiff); - if (isZeroDuration(timeDuration)) return dateDuration; // even number of calendar days - - // If we get here, there's both a time and date part of the duration AND - // there's a time zone offset transition during the duration. RFC 5545 - // says that we should calculate full days using DateTime math and - // remainder times using absolute time. To do this, we calculate a - // `dateTime` difference, split it into date and time portions, and then - // convert the time portion to an `absolute` duration before returning to - // the caller. A challenge: converting the time duration involves a - // conversion from `DateTime` to `Absolute` which can be ambiguous. This - // can cause unpredictable behavior because the disambiguation is - // happening inside of the duration, not at its edges like in `plus` or - // `from`. We'll reduce the chance of this unpredictability as follows: - // 1. First, calculate the time portion as if it's closest to `other`. - // 2. If the time portion in (1) contains a tz offset transition, then - // reverse the calculation and assume that the time portion is closest - // to `this`. - // - // The approach above ensures that in almost all cases, there will be no - // "internal disambiguation" required. It's possible to construct a test - // case where both `this` and `other` are both within 25 hours of an - // offset transition, but in practice this will be exceedingly rare. - let intermediateDt = this._dt.minus(dateDuration); - let intermediateAbs = intermediateDt.toAbsolute(this._tz, { disambiguation }); - let adjustedTimeDuration; - if (this._tz.getOffsetNanosecondsFor(intermediateAbs) === other.timeZoneOffsetNanoseconds) { - // The transition was in the date portion which is what we want. - adjustedTimeDuration = intermediateAbs.difference(other._abs, { largestUnit: 'hours' }); - } else { - // There was a transition in the time portion, so try assuming that the - // time portion is on the other side next to `this`, where there's - // unlikely to be another transition. - intermediateDt = other._dt.plus(dateDuration); - intermediateAbs = intermediateDt.toAbsolute(this._tz, { disambiguation }); - adjustedTimeDuration = this._abs.difference(intermediateAbs, { largestUnit: 'hours' }); - } - - const hybridDuration = mergeDuration({ dateDuration, timeDuration: adjustedTimeDuration }); - return hybridDuration; - // TODO: tests for cases where intermediate value lands on a discontinuity + // There was a transition in the time portion, so try assuming that the + // time portion is on the other side next to `this`, where there's + // unlikely to be another transition. + intermediateDt = other._dt.plus(dateDuration); + intermediateAbs = intermediateDt.toAbsolute(this._tz); + adjustedTimeDuration = this._abs.difference(intermediateAbs, { largestUnit: 'hours' }); } + + const hybridDuration = mergeDuration({ dateDuration, timeDuration: adjustedTimeDuration }); + return hybridDuration; + // TODO: more tests for cases where intermediate value lands on a discontinuity } /** @@ -1029,8 +960,6 @@ const PARSE = (() => { const LARGEST_DIFFERENCE_UNITS = ['years', 'months', 'weeks', 'days', 'hours', 'minutes', 'seconds']; -const CALCULATION_OPTIONS = ['absolute', 'dateTime', 'hybrid']; -const COMPARE_CALCULATION_OPTIONS = ['absolute', 'dateTime']; const DISAMBIGUATION_OPTIONS = ['compatible', 'earlier', 'later', 'reject']; const OFFSET_OPTIONS = ['use', 'prefer', 'ignore', 'reject']; diff --git a/polyfill/lib/poc/LocalDateTime.d.ts b/polyfill/lib/poc/LocalDateTime.d.ts index 9510f63f08..9546b595cb 100644 --- a/polyfill/lib/poc/LocalDateTime.d.ts +++ b/polyfill/lib/poc/LocalDateTime.d.ts @@ -15,72 +15,6 @@ declare type LocalDateTimeISOCalendarFields = ReturnType 4:30AM on the day that DST starts is "2 hours", because that's - * how much time elapsed in the real word despite a 3-hour difference on the - * wall clock. - * - 1:30AM on the day DST starts -> next day 1:30AM is "1 day" even though only - * 23 hours have elapsed in the real world. - * - * To support these expectations, `'hybrid'` for `difference` works as follows: - * - If `hours` in clock time is identical at start and end, then an integer - * number of days is reported with no `hours` remainder, even if there was a - * DST transition in between. - * - Otherwise, periods of 24 or more real-world hours are reported using clock - * time, while periods less than 24 hours are reported using elapsed time. - */ -export interface DurationKindOptions { - durationKind: 'absolute' | 'dateTime' | 'hybrid'; -} -/** - * For `compare` operations, the default is `'absolute'` because sorting - * almost always is based on the actual instant that something happened in the - * real world, even during unusual periods like the hour before and after DST - * ends where the same clock hour is replayed twice in the real world. During - * that period, an earlier clock time like "2:30AM Pacific Standard Time" is - * actually later in the real world than "2:15AM Pacific Daylight Time" which - * was 45 minutes earlier in the real world but 15 minutes later according to - * a wall clock. To sort by wall clock times instead, use `'dateTime'`. (`'hybrid'` - * is not needed nor available for `compare` operations.) - */ -export interface CompareCalculationOptions { - calculation: 'absolute' | 'dateTime'; -} export interface OverflowOptions { /** * How to deal with out-of-range values @@ -106,7 +40,7 @@ export interface OverflowOptions { * - `'prefer'` uses the offset if it's valid for the date/time in this time * zone, but if it's not valid then the time zone will be used as a fallback * to calculate the absolute time. - * - `'ignore'` will disregard any provided offset. Instead, the time zone and + * - `'ignore'` will disregard any provided offset. Instead, the time zone and * date/time value are used to calculate the absolute time. This will keep * local clock time unchanged but may result in a different real-world * instant. @@ -127,13 +61,9 @@ export interface TimeZoneOffsetDisambiguationOptions { export declare type LocalDateTimeAssignmentOptions = Partial< OverflowOptions & Temporal.ToAbsoluteOptions & TimeZoneOffsetDisambiguationOptions >; -export declare type LocalDateTimeMathOptions = Partial< - DurationKindOptions & Temporal.ToAbsoluteOptions & OverflowOptions ->; +export declare type LocalDateTimeMathOptions = OverflowOptions; export declare type LocalDateTimeDifferenceOptions = Partial< - Temporal.DifferenceOptions<'years' | 'months' | 'weeks' | 'days' | 'hours' | 'minutes' | 'seconds'> & - DurationKindOptions & - Temporal.ToAbsoluteOptions + Temporal.DifferenceOptions<'years' | 'months' | 'weeks' | 'days' | 'hours' | 'minutes' | 'seconds'> >; export declare class LocalDateTime { private _abs; @@ -366,21 +296,16 @@ export declare class LocalDateTime { /** * Compare two `Temporal.LocalDateTime` values. * - * By default, comparison will use the absolute time because sorting is almost - * always based on when events happened in the real world, but during the hour - * before and after DST ends in the fall, sorting of clock time will not match - * the real-world sort order. + * Comparison will use the absolute time because sorting is almost always + * based on when events happened in the real world, but during the hour before + * and after DST ends in the fall, sorting of clock time will not match the + * real-world sort order. * - * Available options: - * ``` - * calculation?: 'absolute' (default) | 'dateTime' - * ``` + * In the very unusual case of sorting by clock time instead, use + * `.toDateTime()` on both instances and use `Temporal.DateTime`'s `compare` + * method. */ - static compare( - one: LocalDateTime, - two: LocalDateTime, - options?: CompareCalculationOptions - ): Temporal.ComparisonResult; + static compare(one: LocalDateTime, two: LocalDateTime): Temporal.ComparisonResult; /** * Returns `true` if both the absolute timestamp and time zone are identical * to the other `Temporal.LocalDateTime` instance, and `false` otherwise. To @@ -391,13 +316,11 @@ export declare class LocalDateTime { /** * Add a `Temporal.Duration` and return the result. * - * By default, the `'hybrid'` calculation method will be used where dates will - * be added using calendar dates while times will be added with absolute time. + * Dates will be added using calendar dates while times will be added with + * absolute time. * * Available options: * ``` - * durationKind?: 'hybrid' (default) | 'absolute' | 'dateTime' - * disambiguation?: 'compatible' (default) | 'earlier' | 'later' | 'reject' * overflow?: 'constrain' (default) | 'reject' * ``` */ @@ -405,14 +328,11 @@ export declare class LocalDateTime { /** * Subtract a `Temporal.Duration` and return the result. * - * By default, the `'hybrid'` calculation method will be used where dates will - * be added using calendar dates while times will be subtracted with absolute - * time. + * Dates will be subtracted using calendar dates while times will be + * subtracted with absolute time. * * Available options: * ``` - * durationKind?: 'hybrid' (default) | 'absolute' | 'dateTime' - * disambiguation?: 'compatible' (default) | 'earlier' | 'later' | 'reject' * overflow?: 'constrain' (default) | 'reject' * ``` */ @@ -421,74 +341,49 @@ export declare class LocalDateTime { * Calculate the difference between two `Temporal.LocalDateTime` values and * return the `Temporal.Duration` result. * - * The kind of duration returned depends on the `durationKind` option: - * - `absolute` will calculate the difference using real-world elapsed time. - * - `dateTime` will calculate the difference in clock time and calendar - * dates. - * - By default, `'hybrid'` durations are returned because they usually match - * users' expectations that short durations are measured in real-world - * elapsed time that ignores DST transitions, while differences of calendar - * days are calculated by taking DST transitions into account. - * - * If `'hybrid'` is chosen but `largestUnit` is hours or less, then the - * calculation will be the same as if `absolute` was chosen. - * - * However, if `'hybrid'` is used with `largestUnit` of `'days'` or larger, - * then (as RFC 5545 requires) date differences will be calculated using - * `dateTime` math which adjusts for DST, while the time remainder will be - * calculated using real-world elapsed time. Examples: - * - 2:30AM on the day before DST starts -> 3:30AM on the day DST starts = - * P1DT1H (even though it's only 24 hours of real-world elapsed time) - * - 1:45AM on the day before DST starts -> "second" 1:15AM on the day DST - * ends = PT24H30M (because it hasn't been a full calendar day even though - * it's been 24.5 real-world hours). - * - * The `'disambiguation'` option is ony used if all of the following are true: - * - `durationKind: 'hybrid'` is used. - * - The difference between `this` and `other` is larger than one full - * calendar day. - * - `this` and `other` have different clock times. If clock times are the - * same then an integer number of days will be returned. - * - When the date portion of the difference is subtracted from `this`, the - * resulting local time is ambiguous (e.g. the repeated hour after DST ends, - * or the skipped hour after DST starts). If all of the above conditions are - * true, then the `'disambiguation'` option determines the - * `Temporal.Absolute` chosen for the end of the date portion. The time - * portion of the resulting duration will be calculated from that - * `Temporal.Absolute`. - * - * Calculations using `durationKind: 'absolute'` are limited to `largestUnit: - * 'days'` or smaller units. For larger units, use `'hybrid'` or - * `'dateTime'`. + * The duration returned is a "hybrid" duration. The date portion represents + * full calendar days like `DateTime.prototype.difference` would return. The + * time portion represents real-world elapsed time like + * `Absolute.prototype.difference` would return. This "hybrid duration" + * approach matches widely-adopted industry standards like RFC 5545 + * (iCalendar). It also matches the behavior of popular JavaScript libraries + * like moment.js and date-fns. + * + * Examples: + * - Difference between 2:30AM on the day before DST starts and 3:30AM on the + * day DST starts = `P1DT1H` (even though it's only 24 hours of real-world + * elapsed time) + * - Difference between 1:45AM on the day before DST starts and the "second" + * 1:15AM on the day DST ends => `PT24H30M` (because it hasn't been a full + * calendar day even though it's been 24.5 real-world hours). + * + * If `largestUnit` is `'hours'` or smaller, then the result will be the same + * as if `Temporal.Absolute.prototype.difference` was used. + * + * If both values have the same local time, then the result will be the same + * as if `Temporal.DateTime.prototype.difference` was used. * * If the other `Temporal.LocalDateTime` is in a different time zone, then the * same days can be different lengths in each time zone, e.g. if only one of - * them observes DST. Therefore, a `RangeError` will be thrown if all of the - * following conditions are true: - * - `durationKind` is `'hybrid'` or `'dateTime'` - * - `largestUnit` is `'days'` or larger - * - the two instances' time zones have different `name` fields. - * - * Here are commonly used alternatives for cross-timezone calculations: - * - Use `durationKind: 'absolute'`, as long as it's OK if all days are - * assumed to be 24 hours long and DST is ignored. - * - If you need weeks, months, or years in the result, or if you need to take - * DST transitions into account, transform one of the instances to the - * other's time zone using `.with({timeZone: other.timeZone})` and then - * calculate the same-timezone difference. - * - To calculate with calendar dates only, use + * them observes DST. Therefore, a `RangeError` will be thrown if + * `largestUnit` is `'days'` or larger and the two instances' time zones have + * different `name` fields. To work around this limitation, transform one of + * the instances to the other's time zone using `.with({timeZone: + * other.timeZone})` and then calculate the same-timezone difference. + * + * To calculate the difference between calendar dates only, use * `.toDate().difference(other.toDate())`. - * - To calculate with clock times only, use + * + * To calculate the difference between clock times only, use * `.toTime().difference(other.toTime())`. * * Because of the complexity and ambiguity involved in cross-timezone - * calculations, `hours` is the default for `largestUnit`. + * calculations involving days or larger units, `hours` is the default for + * `largestUnit`. * * Available options: * ``` * largestUnit: 'years' | 'months' | 'weeks' | 'days' | 'hours' (default) | 'minutes' | 'seconds' - * durationKind?: 'hybrid' (default) | 'absolute' | 'dateTime' - * disambiguation?: 'compatible' (default) | 'earlier' | 'later' | 'reject' * ``` */ difference(other: LocalDateTime, options?: LocalDateTimeDifferenceOptions): Temporal.Duration; diff --git a/polyfill/lib/poc/LocalDateTime.nocomments.d.ts b/polyfill/lib/poc/LocalDateTime.nocomments.d.ts index a631afae21..167601fd95 100644 --- a/polyfill/lib/poc/LocalDateTime.nocomments.d.ts +++ b/polyfill/lib/poc/LocalDateTime.nocomments.d.ts @@ -12,12 +12,6 @@ declare type LocalDateTimeISOCalendarFields = ReturnType; -export declare type LocalDateTimeMathOptions = Partial< - DurationKindOptions & Temporal.ToAbsoluteOptions & OverflowOptions ->; +export declare type LocalDateTimeMathOptions = OverflowOptions; export declare type LocalDateTimeDifferenceOptions = Partial< - Temporal.DifferenceOptions<'years' | 'months' | 'weeks' | 'days' | 'hours' | 'minutes' | 'seconds'> & - DurationKindOptions & - Temporal.ToAbsoluteOptions + Temporal.DifferenceOptions<'years' | 'months' | 'weeks' | 'days' | 'hours' | 'minutes' | 'seconds'> >; export declare class LocalDateTime { private _abs; @@ -56,11 +46,7 @@ export declare class LocalDateTime { get timeZoneOffsetString(): string; getFields(): LocalDateTimeFields; getISOCalendarFields(): LocalDateTimeISOCalendarFields; - static compare( - one: LocalDateTime, - two: LocalDateTime, - options?: CompareCalculationOptions - ): Temporal.ComparisonResult; + static compare(one: LocalDateTime, two: LocalDateTime): Temporal.ComparisonResult; equals(other: LocalDateTime): boolean; plus(durationLike: Temporal.DurationLike, options?: LocalDateTimeMathOptions): LocalDateTime; minus(durationLike: Temporal.DurationLike, options?: LocalDateTimeMathOptions): LocalDateTime; diff --git a/polyfill/lib/poc/LocalDateTime.ts b/polyfill/lib/poc/LocalDateTime.ts index bb699d994e..a1fbe4c487 100644 --- a/polyfill/lib/poc/LocalDateTime.ts +++ b/polyfill/lib/poc/LocalDateTime.ts @@ -28,76 +28,6 @@ type LocalDateTimeISOCalendarFields = ReturnType 4:30AM on the day that DST starts is "2 hours", because that's - * how much time elapsed in the real word despite a 3-hour difference on the - * wall clock. - * - 1:30AM on the day DST starts -> next day 1:30AM is "1 day" even though only - * 23 hours have elapsed in the real world. - * - * To support these expectations, `'hybrid'` for `difference` works as follows: - * - If `hours` in clock time is identical at start and end, then an integer - * number of days is reported with no `hours` remainder, even if there was a - * DST transition in between. - * - Otherwise, periods of 24 or more real-world hours are reported using clock - * time, while periods less than 24 hours are reported using elapsed time. - */ -export interface DurationKindOptions { - durationKind: 'absolute' | 'dateTime' | 'hybrid'; -} - -type DurationKinds = DurationKindOptions['durationKind']; - -/** - * For `compare` operations, the default is `'absolute'` because sorting - * almost always is based on the actual instant that something happened in the - * real world, even during unusual periods like the hour before and after DST - * ends where the same clock hour is replayed twice in the real world. During - * that period, an earlier clock time like "2:30AM Pacific Standard Time" is - * actually later in the real world than "2:15AM Pacific Daylight Time" which - * was 45 minutes earlier in the real world but 15 minutes later according to - * a wall clock. To sort by wall clock times instead, use `'dateTime'`. (`'hybrid'` - * is not needed nor available for `compare` operations.) - */ -export interface CompareCalculationOptions { - calculation: 'absolute' | 'dateTime'; -} - export interface OverflowOptions { /** * How to deal with out-of-range values @@ -125,7 +55,7 @@ export interface OverflowOptions { * - `'prefer'` uses the offset if it's valid for the date/time in this time * zone, but if it's not valid then the time zone will be used as a fallback * to calculate the absolute time. - * - `'ignore'` will disregard any provided offset. Instead, the time zone and + * - `'ignore'` will disregard any provided offset. Instead, the time zone and * date/time value are used to calculate the absolute time. This will keep * local clock time unchanged but may result in a different real-world * instant. @@ -147,11 +77,9 @@ export interface TimeZoneOffsetDisambiguationOptions { export type LocalDateTimeAssignmentOptions = Partial< OverflowOptions & Temporal.ToAbsoluteOptions & TimeZoneOffsetDisambiguationOptions >; -export type LocalDateTimeMathOptions = Partial; +export type LocalDateTimeMathOptions = OverflowOptions; export type LocalDateTimeDifferenceOptions = Partial< - Temporal.DifferenceOptions<'years' | 'months' | 'weeks' | 'days' | 'hours' | 'minutes' | 'seconds'> & - DurationKindOptions & - Temporal.ToAbsoluteOptions + Temporal.DifferenceOptions<'years' | 'months' | 'weeks' | 'days' | 'hours' | 'minutes' | 'seconds'> >; /** Build a `Temporal.LocalDateTime` instance from a property bag object */ @@ -275,88 +203,60 @@ function doPlusOrMinus( options: LocalDateTimeMathOptions | undefined, localDateTime: LocalDateTime ): LocalDateTime { - const disambiguation = getOption(options, 'disambiguation', DISAMBIGUATION_OPTIONS, 'compatible'); - const durationKind = getOption(options, 'durationKind', CALCULATION_OPTIONS, 'hybrid'); const overflow = getOption(options, 'overflow', OVERFLOW_OPTIONS, 'constrain'); // TODO: edit below depending on https://github.com/tc39/proposal-temporal/issues/607 const dateTimeOverflowOption = { disambiguation: overflow }; const { timeZone, calendar } = localDateTime; - // Absolute doesn't use disambiguation, while RFC 5455 specifies 'compatible' behavior - // for disambiguation. Therefore, only 'dateTime' durations can use this option. - if (disambiguation !== 'compatible' && durationKind !== 'dateTime') { - throw new RangeError('Disambiguation options are only valid for `dateTime` durations'); + const { timeDuration, dateDuration } = splitDuration(durationLike); + if (isZeroDuration(dateDuration)) { + // If there's only a time to add/subtract, then use absolute math + // because RFC 5545 specifies using absolute math for time units. + const result = localDateTime.absolute[op](durationLike); + return new LocalDateTime(result, timeZone, calendar); } - switch (durationKind) { - case 'absolute': { - const result = localDateTime.absolute[op](durationLike); - return new LocalDateTime(result, timeZone, calendar); - } - case 'dateTime': { - const dateTime = localDateTime.toDateTime(); - const newDateTime = dateTime[op](durationLike, dateTimeOverflowOption); - // If empty duration (no local date/time change), then clone `this` to - // avoid disambiguation that might change the absolute. - if (newDateTime.equals(dateTime)) return LocalDateTime.from(localDateTime); - // Otherwise, return the result. - const abs = newDateTime.toAbsolute(timeZone, { disambiguation }); - return new LocalDateTime(abs, timeZone, calendar); - } - case 'hybrid': { - const { timeDuration, dateDuration } = splitDuration(durationLike); - if (isZeroDuration(dateDuration)) { - // If there's only a time to add/subtract, then use absolute math - // because RFC 5545 specifies using absolute math for time units. - const result = localDateTime.absolute[op](durationLike); - return new LocalDateTime(result, timeZone, calendar); - } - - // Add the units according to the largest-to-smallest order of operations - // required by RFC 5545. Note that the same breakout is not required for - // the time duration because all time units are the same length (because - // Temporal ignores leap seconds). - let newDateTime: Temporal.DateTime = localDateTime.toDateTime(); - const { years, months, weeks, days } = dateDuration; - // TODO: if https://github.com/tc39/proposal-temporal/issues/653 - // changes order of operations, then coalesce 4 calls to 1. - if (years) newDateTime = newDateTime[op]({ years }, dateTimeOverflowOption); - if (months) newDateTime = newDateTime[op]({ months }, dateTimeOverflowOption); - if (weeks) newDateTime = newDateTime[op]({ weeks }, dateTimeOverflowOption); - if (days) newDateTime = newDateTime[op]({ days }, dateTimeOverflowOption); - if (isZeroDuration(timeDuration)) { - const absolute = newDateTime.toAbsolute(timeZone); - return LocalDateTime.from({ absolute, timeZone, calendar: localDateTime.calendar }); - } else { - // Now add/subtract the time. Because all time units are always the same - // length, we can add/subtract all of them together without worrying about - // order of operations. - newDateTime = newDateTime[op](timeDuration, dateTimeOverflowOption); - let absolute = newDateTime.toAbsolute(timeZone); - const reverseOp = op === 'plus' ? 'minus' : 'plus'; - const backUpAbs = absolute[reverseOp]({ nanoseconds: totalNanoseconds(timeDuration) }); - const backUpOffset = timeZone.getOffsetNanosecondsFor(backUpAbs); - const absOffset = timeZone.getOffsetNanosecondsFor(absolute); - const backUpNanoseconds = absOffset - backUpOffset; - if (backUpNanoseconds) { - // RFC 5545 specifies that time units are always "exact time" meaning - // they aren't affected by DST. Therefore, if there was a TZ - // transition during the time duration that was added, then undo the - // impact of that transition. However, don't adjust if applying the - // adjustment would cause us to back up onto the other side of the - // transition. - const backUpOp = backUpNanoseconds < 0 ? 'minus' : 'plus'; - const adjustedAbs = absolute[backUpOp]({ nanoseconds: backUpNanoseconds }); - if (timeZone.getOffsetNanosecondsFor(adjustedAbs) === timeZone.getOffsetNanosecondsFor(absolute)) { - absolute = adjustedAbs; - } - } - - return LocalDateTime.from({ absolute, timeZone, calendar }); + // Add the units according to the largest-to-smallest order of operations + // required by RFC 5545. Note that the same breakout is not required for + // the time duration because all time units are the same length (because + // Temporal ignores leap seconds). + let newDateTime: Temporal.DateTime = localDateTime.toDateTime(); + const { years, months, weeks, days } = dateDuration; + // TODO: if https://github.com/tc39/proposal-temporal/issues/653 + // changes order of operations, then coalesce 4 calls to 1. + if (years) newDateTime = newDateTime[op]({ years }, dateTimeOverflowOption); + if (months) newDateTime = newDateTime[op]({ months }, dateTimeOverflowOption); + if (weeks) newDateTime = newDateTime[op]({ weeks }, dateTimeOverflowOption); + if (days) newDateTime = newDateTime[op]({ days }, dateTimeOverflowOption); + if (isZeroDuration(timeDuration)) { + const absolute = newDateTime.toAbsolute(timeZone); + return LocalDateTime.from({ absolute, timeZone, calendar: localDateTime.calendar }); + } else { + // Now add/subtract the time. Because all time units are always the same + // length, we can add/subtract all of them together without worrying about + // order of operations. + newDateTime = newDateTime[op](timeDuration, dateTimeOverflowOption); + let absolute = newDateTime.toAbsolute(timeZone); + const reverseOp = op === 'plus' ? 'minus' : 'plus'; + const backUpAbs = absolute[reverseOp]({ nanoseconds: totalNanoseconds(timeDuration) }); + const backUpOffset = timeZone.getOffsetNanosecondsFor(backUpAbs); + const absOffset = timeZone.getOffsetNanosecondsFor(absolute); + const backUpNanoseconds = absOffset - backUpOffset; + if (backUpNanoseconds) { + // RFC 5545 specifies that time units are always "exact time" meaning + // they aren't affected by DST. Therefore, if there was a TZ + // transition during the time duration that was added, then undo the + // impact of that transition. However, don't adjust if applying the + // adjustment would cause us to back up onto the other side of the + // transition. + const backUpOp = backUpNanoseconds < 0 ? 'minus' : 'plus'; + const adjustedAbs = absolute[backUpOp]({ nanoseconds: backUpNanoseconds }); + if (timeZone.getOffsetNanosecondsFor(adjustedAbs) === timeZone.getOffsetNanosecondsFor(absolute)) { + absolute = adjustedAbs; } } - default: - throw new Error(`Invalid \`durationKind\` option value: ${durationKind}`); + + return LocalDateTime.from({ absolute, timeZone, calendar }); } } @@ -745,25 +645,17 @@ export class LocalDateTime { /** * Compare two `Temporal.LocalDateTime` values. * - * By default, comparison will use the absolute time because sorting is almost - * always based on when events happened in the real world, but during the hour - * before and after DST ends in the fall, sorting of clock time will not match - * the real-world sort order. + * Comparison will use the absolute time because sorting is almost always + * based on when events happened in the real world, but during the hour before + * and after DST ends in the fall, sorting of clock time will not match the + * real-world sort order. * - * Available options: - * ``` - * calculation?: 'absolute' (default) | 'dateTime' - * ``` + * In the very unusual case of sorting by clock time instead, use + * `.toDateTime()` on both instances and use `Temporal.DateTime`'s `compare` + * method. */ - static compare( - one: LocalDateTime, - two: LocalDateTime, - options?: CompareCalculationOptions - ): Temporal.ComparisonResult { - const calculation = getOption(options, 'calculation', COMPARE_CALCULATION_OPTIONS, 'absolute'); - return calculation === 'dateTime' - ? Temporal.DateTime.compare(one._dt, two._dt) - : Temporal.Absolute.compare(one._abs, two._abs); + static compare(one: LocalDateTime, two: LocalDateTime): Temporal.ComparisonResult { + return Temporal.Absolute.compare(one._abs, two._abs); } /** @@ -779,13 +671,11 @@ export class LocalDateTime { /** * Add a `Temporal.Duration` and return the result. * - * By default, the `'hybrid'` calculation method will be used where dates will - * be added using calendar dates while times will be added with absolute time. + * Dates will be added using calendar dates while times will be added with + * absolute time. * * Available options: * ``` - * durationKind?: 'hybrid' (default) | 'absolute' | 'dateTime' - * disambiguation?: 'compatible' (default) | 'earlier' | 'later' | 'reject' * overflow?: 'constrain' (default) | 'reject' * ``` */ @@ -796,14 +686,11 @@ export class LocalDateTime { /** * Subtract a `Temporal.Duration` and return the result. * - * By default, the `'hybrid'` calculation method will be used where dates will - * be added using calendar dates while times will be subtracted with absolute - * time. + * Dates will be subtracted using calendar dates while times will be + * subtracted with absolute time. * * Available options: * ``` - * durationKind?: 'hybrid' (default) | 'absolute' | 'dateTime' - * disambiguation?: 'compatible' (default) | 'earlier' | 'later' | 'reject' * overflow?: 'constrain' (default) | 'reject' * ``` */ @@ -815,147 +702,115 @@ export class LocalDateTime { * Calculate the difference between two `Temporal.LocalDateTime` values and * return the `Temporal.Duration` result. * - * The kind of duration returned depends on the `durationKind` option: - * - `absolute` will calculate the difference using real-world elapsed time. - * - `dateTime` will calculate the difference in clock time and calendar - * dates. - * - By default, `'hybrid'` durations are returned because they usually match - * users' expectations that short durations are measured in real-world - * elapsed time that ignores DST transitions, while differences of calendar - * days are calculated by taking DST transitions into account. - * - * If `'hybrid'` is chosen but `largestUnit` is hours or less, then the - * calculation will be the same as if `absolute` was chosen. + * The duration returned is a "hybrid" duration. The date portion represents + * full calendar days like `DateTime.prototype.difference` would return. The + * time portion represents real-world elapsed time like + * `Absolute.prototype.difference` would return. This "hybrid duration" + * approach matches widely-adopted industry standards like RFC 5545 + * (iCalendar). It also matches the behavior of popular JavaScript libraries + * like moment.js and date-fns. * - * However, if `'hybrid'` is used with `largestUnit` of `'days'` or larger, - * then (as RFC 5545 requires) date differences will be calculated using - * `dateTime` math which adjusts for DST, while the time remainder will be - * calculated using real-world elapsed time. Examples: - * - 2:30AM on the day before DST starts -> 3:30AM on the day DST starts = - * P1DT1H (even though it's only 24 hours of real-world elapsed time) - * - 1:45AM on the day before DST starts -> "second" 1:15AM on the day DST - * ends = PT24H30M (because it hasn't been a full calendar day even though - * it's been 24.5 real-world hours). + * Examples: + * - Difference between 2:30AM on the day before DST starts and 3:30AM on the + * day DST starts = `P1DT1H` (even though it's only 24 hours of real-world + * elapsed time) + * - Difference between 1:45AM on the day before DST starts and the "second" + * 1:15AM on the day DST ends => `PT24H30M` (because it hasn't been a full + * calendar day even though it's been 24.5 real-world hours). * - * The `'disambiguation'` option is ony used if all of the following are true: - * - `durationKind: 'hybrid'` is used. - * - The difference between `this` and `other` is larger than one full - * calendar day. - * - `this` and `other` have different clock times. If clock times are the - * same then an integer number of days will be returned. - * - When the date portion of the difference is subtracted from `this`, the - * resulting local time is ambiguous (e.g. the repeated hour after DST ends, - * or the skipped hour after DST starts). If all of the above conditions are - * true, then the `'disambiguation'` option determines the - * `Temporal.Absolute` chosen for the end of the date portion. The time - * portion of the resulting duration will be calculated from that - * `Temporal.Absolute`. + * If `largestUnit` is `'hours'` or smaller, then the result will be the same + * as if `Temporal.Absolute.prototype.difference` was used. * - * Calculations using `durationKind: 'absolute'` are limited to `largestUnit: - * 'days'` or smaller units. For larger units, use `'hybrid'` or - * `'dateTime'`. + * If both values have the same local time, then the result will be the same + * as if `Temporal.DateTime.prototype.difference` was used. * * If the other `Temporal.LocalDateTime` is in a different time zone, then the * same days can be different lengths in each time zone, e.g. if only one of - * them observes DST. Therefore, a `RangeError` will be thrown if all of the - * following conditions are true: - * - `durationKind` is `'hybrid'` or `'dateTime'` - * - `largestUnit` is `'days'` or larger - * - the two instances' time zones have different `name` fields. + * them observes DST. Therefore, a `RangeError` will be thrown if + * `largestUnit` is `'days'` or larger and the two instances' time zones have + * different `name` fields. To work around this limitation, transform one of + * the instances to the other's time zone using `.with({timeZone: + * other.timeZone})` and then calculate the same-timezone difference. * - * Here are commonly used alternatives for cross-timezone calculations: - * - Use `durationKind: 'absolute'`, as long as it's OK if all days are - * assumed to be 24 hours long and DST is ignored. - * - If you need weeks, months, or years in the result, or if you need to take - * DST transitions into account, transform one of the instances to the - * other's time zone using `.with({timeZone: other.timeZone})` and then - * calculate the same-timezone difference. - * - To calculate with calendar dates only, use + * To calculate the difference between calendar dates only, use * `.toDate().difference(other.toDate())`. - * - To calculate with clock times only, use + * + * To calculate the difference between clock times only, use * `.toTime().difference(other.toTime())`. * * Because of the complexity and ambiguity involved in cross-timezone - * calculations, `hours` is the default for `largestUnit`. + * calculations involving days or larger units, `hours` is the default for + * `largestUnit`. * * Available options: * ``` * largestUnit: 'years' | 'months' | 'weeks' | 'days' | 'hours' (default) | 'minutes' | 'seconds' - * durationKind?: 'hybrid' (default) | 'absolute' | 'dateTime' - * disambiguation?: 'compatible' (default) | 'earlier' | 'later' | 'reject' * ``` */ difference(other: LocalDateTime, options?: LocalDateTimeDifferenceOptions): Temporal.Duration { - const durationKind = getOption(options, 'durationKind', CALCULATION_OPTIONS, 'hybrid'); - const disambiguation = getOption(options, 'disambiguation', DISAMBIGUATION_OPTIONS, 'compatible'); - const largestUnit = toLargestTemporalUnit(options, 'years'); const dateUnits = ['years', 'months', 'weeks', 'days'] as LargestDifferenceUnit[]; const wantDate = dateUnits.includes(largestUnit); - // treat hybrid as absolute if the user is only asking for a time difference - if (durationKind === 'absolute' || (durationKind === 'hybrid' && !wantDate)) { - if (wantDate) throw new Error("For absolute difference calculations, `largestUnit` must be 'hours' or smaller"); + // treat as absolute if the user is only asking for a time difference + if (!wantDate) { const largestUnitOptionBag = { largestUnit: largestUnit as 'hours' | 'minutes' | 'seconds' }; return this._abs.difference(other._abs, largestUnitOptionBag); - } else if (durationKind === 'dateTime') { - return this._dt.difference(other._dt, { largestUnit }); + } + const dtDiff = this._dt.difference(other._dt, { largestUnit }); + + // If there's no change in timezone offset between this and other, then we + // don't have to do any DST-related fixups. Just return the simple + // DateTime difference. + const diffOffset = this.timeZoneOffsetNanoseconds - other.timeZoneOffsetNanoseconds; + if (diffOffset === 0) return dtDiff; + + // It's the hard case: the timezone offset is different so there's a + // transition in the middle and we may need to adjust the result for DST. + // RFC 5545 expects that date durations are measured in nominal (DateTime) + // days, while time durations are measured in exact (Absolute) time. + const { dateDuration, timeDuration } = splitDuration(dtDiff); + if (isZeroDuration(timeDuration)) return dateDuration; // even number of calendar days + + // If we get here, there's both a time and date part of the duration AND + // there's a time zone offset transition during the duration. RFC 5545 + // says that we should calculate full days using DateTime math and + // remainder times using absolute time. To do this, we calculate a + // `dateTime` difference, split it into date and time portions, and then + // convert the time portion to an `absolute` duration before returning to + // the caller. A challenge: converting the time duration involves a + // conversion from `DateTime` to `Absolute` which can be ambiguous. This + // can cause unpredictable behavior because the disambiguation is + // happening inside of the duration, not at its edges like in `plus` or + // `from`. We'll reduce the chance of this unpredictability as follows: + // 1. First, calculate the time portion as if it's closest to `other`. + // 2. If the time portion in (1) contains a tz offset transition, then + // reverse the calculation and assume that the time portion is closest + // to `this`. + // + // The approach above ensures that in almost all cases, there will be no + // "internal disambiguation" required. It's possible to construct a test + // case where both `this` and `other` are both within 25 hours of a + // different offset transition, but in practice this will be exceedingly + // rare. + let intermediateDt = this._dt.minus(dateDuration); + let intermediateAbs = intermediateDt.toAbsolute(this._tz); + let adjustedTimeDuration: Temporal.Duration; + if (this._tz.getOffsetNanosecondsFor(intermediateAbs) === other.timeZoneOffsetNanoseconds) { + // The transition was in the date portion which is what we want. + adjustedTimeDuration = intermediateAbs.difference(other._abs, { largestUnit: 'hours' }); } else { - // durationKind === 'hybrid' - const dtDiff = this._dt.difference(other._dt, { largestUnit }); - - // If there's no change in timezone offset between this and other, then we - // don't have to do any DST-related fixups. Just return the simple - // DateTime difference. - const diffOffset = this.timeZoneOffsetNanoseconds - other.timeZoneOffsetNanoseconds; - if (diffOffset === 0) return dtDiff; - - // It's the hard case: the timezone offset is different so there's a - // transition in the middle and we may need to adjust the result for DST. - // RFC 5545 expects that date durations are measured in nominal (DateTime) - // days, while time durations are measured in exact (Absolute) time. - const { dateDuration, timeDuration } = splitDuration(dtDiff); - if (isZeroDuration(timeDuration)) return dateDuration; // even number of calendar days - - // If we get here, there's both a time and date part of the duration AND - // there's a time zone offset transition during the duration. RFC 5545 - // says that we should calculate full days using DateTime math and - // remainder times using absolute time. To do this, we calculate a - // `dateTime` difference, split it into date and time portions, and then - // convert the time portion to an `absolute` duration before returning to - // the caller. A challenge: converting the time duration involves a - // conversion from `DateTime` to `Absolute` which can be ambiguous. This - // can cause unpredictable behavior because the disambiguation is - // happening inside of the duration, not at its edges like in `plus` or - // `from`. We'll reduce the chance of this unpredictability as follows: - // 1. First, calculate the time portion as if it's closest to `other`. - // 2. If the time portion in (1) contains a tz offset transition, then - // reverse the calculation and assume that the time portion is closest - // to `this`. - // - // The approach above ensures that in almost all cases, there will be no - // "internal disambiguation" required. It's possible to construct a test - // case where both `this` and `other` are both within 25 hours of an - // offset transition, but in practice this will be exceedingly rare. - let intermediateDt = this._dt.minus(dateDuration); - let intermediateAbs = intermediateDt.toAbsolute(this._tz, { disambiguation }); - let adjustedTimeDuration: Temporal.Duration; - if (this._tz.getOffsetNanosecondsFor(intermediateAbs) === other.timeZoneOffsetNanoseconds) { - // The transition was in the date portion which is what we want. - adjustedTimeDuration = intermediateAbs.difference(other._abs, { largestUnit: 'hours' }); - } else { - // There was a transition in the time portion, so try assuming that the - // time portion is on the other side next to `this`, where there's - // unlikely to be another transition. - intermediateDt = other._dt.plus(dateDuration); - intermediateAbs = intermediateDt.toAbsolute(this._tz, { disambiguation }); - adjustedTimeDuration = this._abs.difference(intermediateAbs, { largestUnit: 'hours' }); - } - - const hybridDuration = mergeDuration({ dateDuration, timeDuration: adjustedTimeDuration }); - return hybridDuration; - // TODO: tests for cases where intermediate value lands on a discontinuity + // There was a transition in the time portion, so try assuming that the + // time portion is on the other side next to `this`, where there's + // unlikely to be another transition. + intermediateDt = other._dt.plus(dateDuration); + intermediateAbs = intermediateDt.toAbsolute(this._tz); + adjustedTimeDuration = this._abs.difference(intermediateAbs, { largestUnit: 'hours' }); } + + const hybridDuration = mergeDuration({ dateDuration, timeDuration: adjustedTimeDuration }); + return hybridDuration; + // TODO: more tests for cases where intermediate value lands on a discontinuity } /** @@ -1220,8 +1075,6 @@ const LARGEST_DIFFERENCE_UNITS: LargestDifferenceUnit[] = [ 'minutes', 'seconds' ]; -const CALCULATION_OPTIONS: DurationKinds[] = ['absolute', 'dateTime', 'hybrid']; -const COMPARE_CALCULATION_OPTIONS: CompareCalculationOptions['calculation'][] = ['absolute', 'dateTime']; const DISAMBIGUATION_OPTIONS: Temporal.ToAbsoluteOptions['disambiguation'][] = [ 'compatible', 'earlier', diff --git a/polyfill/poc.d.ts b/polyfill/poc.d.ts index ee60157660..652e48d4c1 100644 --- a/polyfill/poc.d.ts +++ b/polyfill/poc.d.ts @@ -519,7 +519,7 @@ export namespace Temporal { readonly calendar: CalendarProtocol; equals(other: Temporal.MonthDay): boolean; with(monthDayLike: MonthDayLike, options?: AssignmentOptions): Temporal.MonthDay; - toDate(year: number | { era?: string | undefined; year: number }): Temporal.Date; + toDateInYear(year: number | { era?: string | undefined; year: number }, options?: AssignmentOptions): Temporal.Date; getFields(): MonthDayFields; getISOCalendarFields(): DateISOCalendarFields; toLocaleString(locales?: string | string[], options?: Intl.DateTimeFormatOptions): string; @@ -668,7 +668,7 @@ export namespace Temporal { plus(durationLike: Temporal.Duration | DurationLike, options?: ArithmeticOptions): Temporal.YearMonth; minus(durationLike: Temporal.Duration | DurationLike, options?: ArithmeticOptions): Temporal.YearMonth; difference(other: Temporal.YearMonth, options?: DifferenceOptions<'years' | 'months'>): Temporal.Duration; - toDate(day: number): Temporal.Date; + toDateOnDay(day: number): Temporal.Date; getFields(): YearMonthFields; getISOCalendarFields(): DateISOCalendarFields; toLocaleString(locales?: string | string[], options?: Intl.DateTimeFormatOptions): string; @@ -763,72 +763,6 @@ export namespace Temporal { timeZone: Temporal.TimeZone; absolute: Temporal.Absolute; }; - /** - * The `durationKind` option allows users to customize how calculations behave - * when days aren't exactly 24 hours long. This occurs on days when Daylight - * Savings Time (DST) starts or ends, or when a country or region legally - * changes its time zone offset. - * - * Choices are: - * - `'absolute'` - Days are treated as 24 hours long, even if there's a - * change in local timezone offset. Math is performed on the underlying - * Absolute timestamp and then local time fields are refreshed to match the - * updated timestamp. - * - `'dateTime'` - Day length will vary according to time zone offset changes - * like DST transitions. Math is performed on the calendar date and clock - * time, and then the Absolute timestamp is refreshed to match the new - * calendar date and clock time. - * - `'hybrid'` - Math is performed by using `'absolute'` math on the time - * portion, and `'dateTime'` math on the date portion. - * - * Days are almost always 24 hours long, these options produce identical - * results if the time zone offset of the endpoint matches the time zone offset - * of the original LocalDateTime. But they may return different results if - * there's a time zone offset change like a DST transition. - * - * For `plus` and `minus` operations the default is `'hybrid'` which matches - * most users' expectations: - * - Adding or subtracting whole days should keep clock time unchanged, even - * if a DST transition happens. For example: "Postpone my 6:00PM dinner by 7 - * days, but make sure that the time stays 6:00PM, not 5:00PM or 7:00PM if - * DST starts over the weekend." - * - Adding or removing time should ignore DST changes. For example: "Meet me - * at the party in 2 hours, not 1 hour or 3 hours if DST starts tonight". - * - * The default is also `'hybrid'` for `difference` operations. In this case, - * typical users expect that short durations that span a DST boundary - * are measured using real-world durations, while durations of one day or longer - * are measured by default using calendar days and clock time. For example: - * - 1:30AM -> 4:30AM on the day that DST starts is "2 hours", because that's - * how much time elapsed in the real word despite a 3-hour difference on the - * wall clock. - * - 1:30AM on the day DST starts -> next day 1:30AM is "1 day" even though only - * 23 hours have elapsed in the real world. - * - * To support these expectations, `'hybrid'` for `difference` works as follows: - * - If `hours` in clock time is identical at start and end, then an integer - * number of days is reported with no `hours` remainder, even if there was a - * DST transition in between. - * - Otherwise, periods of 24 or more real-world hours are reported using clock - * time, while periods less than 24 hours are reported using elapsed time. - */ - export interface DurationKindOptions { - durationKind: 'absolute' | 'dateTime' | 'hybrid'; - } - /** - * For `compare` operations, the default is `'absolute'` because sorting - * almost always is based on the actual instant that something happened in the - * real world, even during unusual periods like the hour before and after DST - * ends where the same clock hour is replayed twice in the real world. During - * that period, an earlier clock time like "2:30AM Pacific Standard Time" is - * actually later in the real world than "2:15AM Pacific Daylight Time" which - * was 45 minutes earlier in the real world but 15 minutes later according to - * a wall clock. To sort by wall clock times instead, use `'dateTime'`. (`'hybrid'` - * is not needed nor available for `compare` operations.) - */ - export interface CompareCalculationOptions { - calculation: 'absolute' | 'dateTime'; - } export interface OverflowOptions { /** * How to deal with out-of-range values @@ -854,7 +788,7 @@ export namespace Temporal { * - `'prefer'` uses the offset if it's valid for the date/time in this time * zone, but if it's not valid then the time zone will be used as a fallback * to calculate the absolute time. - * - `'ignore'` will disregard any provided offset. Instead, the time zone and + * - `'ignore'` will disregard any provided offset. Instead, the time zone and * date/time value are used to calculate the absolute time. This will keep * local clock time unchanged but may result in a different real-world * instant. @@ -875,11 +809,9 @@ export namespace Temporal { export type LocalDateTimeAssignmentOptions = Partial< OverflowOptions & Temporal.ToAbsoluteOptions & TimeZoneOffsetDisambiguationOptions >; - export type LocalDateTimeMathOptions = Partial; + export type LocalDateTimeMathOptions = OverflowOptions; export type LocalDateTimeDifferenceOptions = Partial< - Temporal.DifferenceOptions<'years' | 'months' | 'weeks' | 'days' | 'hours' | 'minutes' | 'seconds'> & - DurationKindOptions & - Temporal.ToAbsoluteOptions + Temporal.DifferenceOptions<'years' | 'months' | 'weeks' | 'days' | 'hours' | 'minutes' | 'seconds'> >; export class LocalDateTime { private _abs; @@ -1112,21 +1044,16 @@ export namespace Temporal { /** * Compare two `Temporal.LocalDateTime` values. * - * By default, comparison will use the absolute time because sorting is almost - * always based on when events happened in the real world, but during the hour - * before and after DST ends in the fall, sorting of clock time will not match - * the real-world sort order. + * Comparison will use the absolute time because sorting is almost always + * based on when events happened in the real world, but during the hour before + * and after DST ends in the fall, sorting of clock time will not match the + * real-world sort order. * - * Available options: - * ``` - * calculation?: 'absolute' (default) | 'dateTime' - * ``` + * In the very unusual case of sorting by clock time instead, use + * `.toDateTime()` on both instances and use `Temporal.DateTime`'s `compare` + * method. */ - static compare( - one: LocalDateTime, - two: LocalDateTime, - options?: CompareCalculationOptions - ): Temporal.ComparisonResult; + static compare(one: LocalDateTime, two: LocalDateTime): Temporal.ComparisonResult; /** * Returns `true` if both the absolute timestamp and time zone are identical * to the other `Temporal.LocalDateTime` instance, and `false` otherwise. To @@ -1137,13 +1064,11 @@ export namespace Temporal { /** * Add a `Temporal.Duration` and return the result. * - * By default, the `'hybrid'` calculation method will be used where dates will - * be added using calendar dates while times will be added with absolute time. + * Dates will be added using calendar dates while times will be added with + * absolute time. * * Available options: * ``` - * durationKind?: 'hybrid' (default) | 'absolute' | 'dateTime' - * disambiguation?: 'compatible' (default) | 'earlier' | 'later' | 'reject' * overflow?: 'constrain' (default) | 'reject' * ``` */ @@ -1151,14 +1076,11 @@ export namespace Temporal { /** * Subtract a `Temporal.Duration` and return the result. * - * By default, the `'hybrid'` calculation method will be used where dates will - * be added using calendar dates while times will be subtracted with absolute - * time. + * Dates will be subtracted using calendar dates while times will be + * subtracted with absolute time. * * Available options: * ``` - * durationKind?: 'hybrid' (default) | 'absolute' | 'dateTime' - * disambiguation?: 'compatible' (default) | 'earlier' | 'later' | 'reject' * overflow?: 'constrain' (default) | 'reject' * ``` */ @@ -1167,74 +1089,49 @@ export namespace Temporal { * Calculate the difference between two `Temporal.LocalDateTime` values and * return the `Temporal.Duration` result. * - * The kind of duration returned depends on the `durationKind` option: - * - `absolute` will calculate the difference using real-world elapsed time. - * - `dateTime` will calculate the difference in clock time and calendar - * dates. - * - By default, `'hybrid'` durations are returned because they usually match - * users' expectations that short durations are measured in real-world - * elapsed time that ignores DST transitions, while differences of calendar - * days are calculated by taking DST transitions into account. - * - * If `'hybrid'` is chosen but `largestUnit` is hours or less, then the - * calculation will be the same as if `absolute` was chosen. - * - * However, if `'hybrid'` is used with `largestUnit` of `'days'` or larger, - * then (as RFC 5545 requires) date differences will be calculated using - * `dateTime` math which adjusts for DST, while the time remainder will be - * calculated using real-world elapsed time. Examples: - * - 2:30AM on the day before DST starts -> 3:30AM on the day DST starts = - * P1DT1H (even though it's only 24 hours of real-world elapsed time) - * - 1:45AM on the day before DST starts -> "second" 1:15AM on the day DST - * ends = PT24H30M (because it hasn't been a full calendar day even though - * it's been 24.5 real-world hours). - * - * The `'disambiguation'` option is ony used if all of the following are true: - * - `durationKind: 'hybrid'` is used. - * - The difference between `this` and `other` is larger than one full - * calendar day. - * - `this` and `other` have different clock times. If clock times are the - * same then an integer number of days will be returned. - * - When the date portion of the difference is subtracted from `this`, the - * resulting local time is ambiguous (e.g. the repeated hour after DST ends, - * or the skipped hour after DST starts). If all of the above conditions are - * true, then the `'disambiguation'` option determines the - * `Temporal.Absolute` chosen for the end of the date portion. The time - * portion of the resulting duration will be calculated from that - * `Temporal.Absolute`. - * - * Calculations using `durationKind: 'absolute'` are limited to `largestUnit: - * 'days'` or smaller units. For larger units, use `'hybrid'` or - * `'dateTime'`. + * The duration returned is a "hybrid" duration. The date portion represents + * full calendar days like `DateTime.prototype.difference` would return. The + * time portion represents real-world elapsed time like + * `Absolute.prototype.difference` would return. This "hybrid duration" + * approach matches widely-adopted industry standards like RFC 5545 + * (iCalendar). It also matches the behavior of popular JavaScript libraries + * like moment.js and date-fns. + * + * Examples: + * - Difference between 2:30AM on the day before DST starts and 3:30AM on the + * day DST starts = `P1DT1H` (even though it's only 24 hours of real-world + * elapsed time) + * - Difference between 1:45AM on the day before DST starts and the "second" + * 1:15AM on the day DST ends => `PT24H30M` (because it hasn't been a full + * calendar day even though it's been 24.5 real-world hours). + * + * If `largestUnit` is `'hours'` or smaller, then the result will be the same + * as if `Temporal.Absolute.prototype.difference` was used. + * + * If both values have the same local time, then the result will be the same + * as if `Temporal.DateTime.prototype.difference` was used. * * If the other `Temporal.LocalDateTime` is in a different time zone, then the * same days can be different lengths in each time zone, e.g. if only one of - * them observes DST. Therefore, a `RangeError` will be thrown if all of the - * following conditions are true: - * - `durationKind` is `'hybrid'` or `'dateTime'` - * - `largestUnit` is `'days'` or larger - * - the two instances' time zones have different `name` fields. - * - * Here are commonly used alternatives for cross-timezone calculations: - * - Use `durationKind: 'absolute'`, as long as it's OK if all days are - * assumed to be 24 hours long and DST is ignored. - * - If you need weeks, months, or years in the result, or if you need to take - * DST transitions into account, transform one of the instances to the - * other's time zone using `.with({timeZone: other.timeZone})` and then - * calculate the same-timezone difference. - * - To calculate with calendar dates only, use + * them observes DST. Therefore, a `RangeError` will be thrown if + * `largestUnit` is `'days'` or larger and the two instances' time zones have + * different `name` fields. To work around this limitation, transform one of + * the instances to the other's time zone using `.with({timeZone: + * other.timeZone})` and then calculate the same-timezone difference. + * + * To calculate the difference between calendar dates only, use * `.toDate().difference(other.toDate())`. - * - To calculate with clock times only, use + * + * To calculate the difference between clock times only, use * `.toTime().difference(other.toTime())`. * * Because of the complexity and ambiguity involved in cross-timezone - * calculations, `hours` is the default for `largestUnit`. + * calculations involving days or larger units, `hours` is the default for + * `largestUnit`. * * Available options: * ``` * largestUnit: 'years' | 'months' | 'weeks' | 'days' | 'hours' (default) | 'minutes' | 'seconds' - * durationKind?: 'hybrid' (default) | 'absolute' | 'dateTime' - * disambiguation?: 'compatible' (default) | 'earlier' | 'later' | 'reject' * ``` */ difference(other: LocalDateTime, options?: LocalDateTimeDifferenceOptions): Temporal.Duration;