From 090262fd1f644eebf4fb05446f3fea87c1a3880a Mon Sep 17 00:00:00 2001 From: Justin Grant Date: Thu, 22 Jun 2023 18:42:42 -0700 Subject: [PATCH 01/39] chore: Ignore Test262 in ESLint and Prettier This commit changes the ESLint and Prettier configuration to ignore test262. This is different from how upstream handles things. UPSTREAM_COMMIT=bc7639cb0acf5b92eddfa562d5f8082cb4b29204 --- .eslintrc.yml | 10 ++++++++++ package.json | 10 +++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/.eslintrc.yml b/.eslintrc.yml index 21e6d652..fe728843 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -117,3 +117,13 @@ overrides: '@typescript-eslint/consistent-type-exports': error '@typescript-eslint/consistent-type-imports': error prefer-const: off + - files: + - polyfill/test262/** + rules: + quotes: + - error + - double + - avoidEscape: true + prettier/prettier: + - error + - singleQuote: false diff --git a/package.json b/package.json index 4ad39a78..0ff525bc 100644 --- a/package.json +++ b/package.json @@ -124,7 +124,15 @@ "semi": true, "singleQuote": true, "bracketSpacing": true, - "arrowParens": "always" + "arrowParens": "always", + "overrides": [ + { + "files": "polyfill/test262/**", + "options": { + "singleQuote": false + } + } + ] }, "directories": { "lib": "lib", From 1b0d50bf62036e906dcdb18fdd5e41e8718f4e20 Mon Sep 17 00:00:00 2001 From: Guillaume Emont Date: Wed, 3 May 2023 17:30:59 +0200 Subject: [PATCH 02/39] Normative: Throw on duplicates in PrepareTemporalFields It should be an error to return duplicate fields in a Calendar's field() method, thus we throw if we encounter one in PrepareTemporalFields. There are two cases (PlainMonthDay.prototype.toPlainDate and PlainYearMonth.prototype.toPlainDate) where, after checking on the return values of field() in previous invocations of PrepareTemporalFields, we want to deduplicate the field name list in a later call to PrepareTemporalFields after concatenating them, since we sort them in PrepareTemporalFields. For this reason, we add a duplicateBehavior parameter to PrepareTemporalFields. This allows us to remove the MergeLists abstract operation, now replaced by a simple list concatenation and deduplication in PrepareTemporalFields with duplicateBehavior set to ignore. It should also be an error to return field names 'constructor' or '__proto__' in a Calendar's field() method, and we throw if that happens. Fixes #2532, 2576. UPSTREAM_COMMIT=ca1e1c7c309f93dc6ebecadad5ab8c1ecd29fbe8 --- lib/ecmascript.ts | 43 +++++++++++++++++++++++++------------------ lib/plainmonthday.ts | 6 ++---- lib/plainyearmonth.ts | 6 ++---- 3 files changed, 29 insertions(+), 26 deletions(-) diff --git a/lib/ecmascript.ts b/lib/ecmascript.ts index 396c448c..b6f903be 100644 --- a/lib/ecmascript.ts +++ b/lib/ecmascript.ts @@ -1382,31 +1382,36 @@ export function PrepareTemporalFields< bag: Partial>, fields: Array, requiredFields: RequiredFields, + duplicateBehaviour: 'throw' | 'ignore' = 'throw', { emptySourceErrorMessage }: FieldPrepareOptions = { emptySourceErrorMessage: 'no supported properties found' } ): PrepareTemporalFieldsReturn> { const result: Partial> = ObjectCreate(null); let any = false; fields.sort(); + let previousProperty = undefined; for (const property of fields) { - let value = bag[property]; - if (value !== undefined) { - any = true; - if (BUILTIN_CASTS.has(property)) { - // We just has-checked this map access, so there will definitely be a - // value. - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - value = BUILTIN_CASTS.get(property)!(value); - } - result[property] = value; - } else if (requiredFields !== 'partial') { - // TODO: using .call in this way is not correctly type-checked by tsc. - // We might need a type-safe Call wrapper? - if (ArrayIncludes.call(requiredFields, property)) { - throw new TypeError(`required property '${property}' missing or undefined`); + if ((property as string) === 'constructor' || (property as string) === '__proto__') { + throw new RangeError(`Calendar fields cannot be named ${property}`); + } + if (property !== previousProperty) { + let value = bag[property]; + if (value !== undefined) { + any = true; + if (BUILTIN_CASTS.has(property)) { + value = castExists(BUILTIN_CASTS.get(property))(value); + } + result[property] = value; + } else if (requiredFields !== 'partial') { + if (Call(ArrayIncludes, requiredFields, [property])) { + throw new TypeError(`required property '${property}' missing or undefined`); + } + value = BUILTIN_DEFAULTS.get(property); + result[property] = value; } - value = BUILTIN_DEFAULTS.get(property); - result[property] = value; + } else if (duplicateBehaviour === 'throw') { + throw new RangeError('Duplicate calendar fields'); } + previousProperty = property; } if (requiredFields === 'partial' && !any) { throw new TypeError(emptySourceErrorMessage); @@ -1437,7 +1442,9 @@ export function ToTemporalTimeRecord( ): Partial { // NOTE: Field order is sorted to make the sort in PrepareTemporalFields more efficient. const fields: (keyof TimeRecord)[] = ['hour', 'microsecond', 'millisecond', 'minute', 'nanosecond', 'second']; - const partial = PrepareTemporalFields(bag, fields, 'partial', { emptySourceErrorMessage: 'invalid time-like' }); + const partial = PrepareTemporalFields(bag, fields, 'partial', undefined, { + emptySourceErrorMessage: 'invalid time-like' + }); const result: Partial = {}; for (const field of fields) { const valueDesc = ObjectGetOwnPropertyDescriptor(partial, field); diff --git a/lib/plainmonthday.ts b/lib/plainmonthday.ts index 5bb4ac5b..69b4583f 100644 --- a/lib/plainmonthday.ts +++ b/lib/plainmonthday.ts @@ -93,10 +93,8 @@ export class PlainMonthDay implements Temporal.PlainMonthDay { const inputFieldNames = ES.CalendarFields(calendar, ['year'] as const); const inputFields = ES.PrepareTemporalFields(item, inputFieldNames, []); let mergedFields = ES.CalendarMergeFields(calendar, fields, inputFields); - - // TODO: Use MergeLists abstract operation. - const mergedFieldNames = [...new Set([...receiverFieldNames, ...inputFieldNames])]; - mergedFields = ES.PrepareTemporalFields(mergedFields, mergedFieldNames, []); + const concatenatedFieldNames = [...receiverFieldNames, ...inputFieldNames]; + mergedFields = ES.PrepareTemporalFields(mergedFields, concatenatedFieldNames, [], 'ignore'); const options = ObjectCreate(null); options.overflow = 'reject'; return ES.CalendarDateFromFields(calendar, mergedFields, options); diff --git a/lib/plainyearmonth.ts b/lib/plainyearmonth.ts index 3957fb3a..52983197 100644 --- a/lib/plainyearmonth.ts +++ b/lib/plainyearmonth.ts @@ -138,10 +138,8 @@ export class PlainYearMonth implements Temporal.PlainYearMonth { const inputFieldNames = ES.CalendarFields(calendar, ['day'] as const); const inputFields = ES.PrepareTemporalFields(item, inputFieldNames, []); let mergedFields = ES.CalendarMergeFields(calendar, fields, inputFields); - - // TODO: Use MergeLists abstract operation. - const mergedFieldNames = [...new Set([...receiverFieldNames, ...inputFieldNames])]; - mergedFields = ES.PrepareTemporalFields(mergedFields, mergedFieldNames, []); + const mergedFieldNames = [...receiverFieldNames, ...inputFieldNames]; + mergedFields = ES.PrepareTemporalFields(mergedFields, mergedFieldNames, [], 'ignore'); const options = ObjectCreate(null); options.overflow = 'reject'; return ES.CalendarDateFromFields(calendar, mergedFields, options); From b4fbcb46c77f03a8441919b546a0f333cf8e4c6b Mon Sep 17 00:00:00 2001 From: Philip Chimento Date: Mon, 17 Jul 2023 15:56:18 +0100 Subject: [PATCH 03/39] Update test262 UPSTREAM_COMMIT=8b807320788c79769724d7be27bdd0a880129c0f --- test262 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test262 b/test262 index c5b24c64..016e4bf8 160000 --- a/test262 +++ b/test262 @@ -1 +1 @@ -Subproject commit c5b24c64c3c27544f15e1c18ef274924cff1e32c +Subproject commit 016e4bf8e84c36d1653c6bd53e74c93580d8b1ff From ca43c8c368b8449c5cf5d1807b6b1976859ad1e7 Mon Sep 17 00:00:00 2001 From: Justin Grant Date: Mon, 17 Jul 2023 11:42:47 -0700 Subject: [PATCH 04/39] Editorial: DRY refactor of time formatting This editorial commit removes redundant spec text dealing with time formatting: * Adds a new AO FormatTimeString, and calls it in TemporalTimeToString, TemporalDateTimeToString, and TimeString (the legacy date AO in 262). * Removes FormatSecondsStringPart and replaces it with a new AO FormatFractionalSeconds, and call it in FormatTimeString and TemporalDurationToString. The text of this new AO is aligned with similar text in GetOffsetStringFor in #2607. * Replaces sub-second formatting text in TemporalDurationToString with a call to FormatFractionalSeconds. * Aligns polyfill code to these spec changes. * Adjusts polyfill code in a few places to better match the spec. Note that this commit doesn't touch spec text for formatting time zone offsets because there are several in-flight PRs dealing with offsets and I wanted to keep this PR merge-conflict-free. But once those PRs land and the dust settles, then I'll send another editorial PR to DRY offset string formatting too, by using FormatTimeString to replace bespoke formatting text for offsets. UPSTREAM_COMMIT=c429eed6a3e3d31d8f050ba692ea11a9150a62a2 --- lib/ecmascript.ts | 157 +++++++++++++++++++++------------------------- lib/plaintime.ts | 6 +- 2 files changed, 74 insertions(+), 89 deletions(-) diff --git a/lib/ecmascript.ts b/lib/ecmascript.ts index b6f903be..594771f5 100644 --- a/lib/ecmascript.ts +++ b/lib/ecmascript.ts @@ -18,6 +18,7 @@ const ObjectGetOwnPropertyDescriptor = Object.getOwnPropertyDescriptor; const ReflectApply = Reflect.apply; const ReflectOwnKeys = Reflect.ownKeys; const NumberIsNaN = Number.isNaN; +const StringPrototypeSlice = String.prototype.slice; import { DEBUG, ENABLE_ASSERTS } from './debug'; import JSBI from 'jsbi'; @@ -1047,14 +1048,16 @@ export function ToFractionalSecondDigits( return digitCount as Exclude; } -export function ToSecondsStringPrecisionRecord( - smallestUnit: Temporal.ToStringPrecisionOptions['smallestUnit'], - precision: Temporal.ToStringPrecisionOptions['fractionalSecondDigits'] -): { +interface SecondsStringPrecisionRecord { precision: Temporal.ToStringPrecisionOptions['fractionalSecondDigits'] | 'minute'; unit: UnitSmallerThanOrEqualTo<'minute'>; increment: number; -} { +} + +export function ToSecondsStringPrecisionRecord( + smallestUnit: Temporal.ToStringPrecisionOptions['smallestUnit'], + precision: Temporal.ToStringPrecisionOptions['fractionalSecondDigits'] +): SecondsStringPrecisionRecord { switch (smallestUnit) { case 'minute': return { precision: 'minute', unit: 'minute', increment: 1 }; @@ -2880,79 +2883,87 @@ function GetPossibleInstantsFor( export function ISOYearString(year: number) { let yearString; if (year < 0 || year > 9999) { - const sign = year < 0 ? '-' : '+'; - const yearNumber = MathAbs(year); - yearString = sign + `000000${yearNumber}`.slice(-6); + let sign = year < 0 ? '-' : '+'; + let yearNumber = MathAbs(year); + yearString = sign + ToZeroPaddedDecimalString(yearNumber, 6); } else { - yearString = `0000${year}`.slice(-4); + yearString = ToZeroPaddedDecimalString(year, 4); } return yearString; } export function ISODateTimePartString(part: number) { - return `00${part}`.slice(-2); + return ToZeroPaddedDecimalString(part, 2); } -export function FormatSecondsStringPart( - second: number, - millisecond: number, - microsecond: number, - nanosecond: number, - precision: ReturnType['precision'] -) { - if (precision === 'minute') return ''; - - const secs = `:${ISODateTimePartString(second)}`; - let fractionNumber = millisecond * 1e6 + microsecond * 1e3 + nanosecond; - let fraction: string; +export function FormatFractionalSeconds( + subSecondNanoseconds: number, + precision: Exclude +) { + let fraction; if (precision === 'auto') { - if (fractionNumber === 0) return secs; - fraction = `${fractionNumber}`.padStart(9, '0'); - while (fraction[fraction.length - 1] === '0') fraction = fraction.slice(0, -1); + if (subSecondNanoseconds === 0) return ''; + const fractionFullPrecision = ToZeroPaddedDecimalString(subSecondNanoseconds, 9); + // now remove any trailing zeroes + fraction = fractionFullPrecision.replace(/0+$/, ''); } else { - if (precision === 0) return secs; - fraction = `${fractionNumber}`.padStart(9, '0').slice(0, precision); + if (precision === 0) return ''; + const fractionFullPrecision = ToZeroPaddedDecimalString(subSecondNanoseconds, 9); + fraction = Call(StringPrototypeSlice, fractionFullPrecision, [0, precision]); } - return `${secs}.${fraction}`; + return `.${fraction}`; +} + +export function FormatTimeString( + hour: number, + minute: number, + second: number, + subSecondNanoseconds: number, + precision: SecondsStringPrecisionRecord['precision'] +) { + let result = `${ISODateTimePartString(hour)}:${ISODateTimePartString(minute)}`; + if (precision === 'minute') return result; + + result += `:${ISODateTimePartString(second)}`; + result += FormatFractionalSeconds(subSecondNanoseconds, precision); + return result; } export function TemporalInstantToString( instant: Temporal.Instant, timeZone: string | Temporal.TimeZoneProtocol | undefined, - precision: ReturnType['precision'] + precision: SecondsStringPrecisionRecord['precision'] ) { let outputTimeZone = timeZone; if (outputTimeZone === undefined) outputTimeZone = 'UTC'; const dateTime = GetPlainDateTimeFor(outputTimeZone, instant, 'iso8601'); - const year = ISOYearString(GetSlot(dateTime, ISO_YEAR)); - const month = ISODateTimePartString(GetSlot(dateTime, ISO_MONTH)); - const day = ISODateTimePartString(GetSlot(dateTime, ISO_DAY)); - const hour = ISODateTimePartString(GetSlot(dateTime, ISO_HOUR)); - const minute = ISODateTimePartString(GetSlot(dateTime, ISO_MINUTE)); - const seconds = FormatSecondsStringPart( - GetSlot(dateTime, ISO_SECOND), - GetSlot(dateTime, ISO_MILLISECOND), - GetSlot(dateTime, ISO_MICROSECOND), - GetSlot(dateTime, ISO_NANOSECOND), - precision - ); + const dateTimeString = TemporalDateTimeToString(dateTime, precision, 'never'); let timeZoneString = 'Z'; if (timeZone !== undefined) { const offsetNs = GetOffsetNanosecondsFor(outputTimeZone, instant); timeZoneString = FormatDateTimeUTCOffsetRounded(offsetNs); } - return `${year}-${month}-${day}T${hour}:${minute}${seconds}${timeZoneString}`; + return `${dateTimeString}${timeZoneString}`; } interface ToStringOptions { - unit: ReturnType['unit']; + unit: SecondsStringPrecisionRecord['unit']; increment: number; roundingMode: ReturnType; } -function formatAsDecimalNumber(num: number) { - if (num <= NumberMaxSafeInteger) return num.toString(10); - return JSBI.BigInt(num).toString(); +// Because of JSBI, this helper function is quite a bit more complicated +// than in proposal-temporal. If we remove JSBI later, then we can simplify it +// to just the `typeof num === 'number'` branch. +const JSBI_NUMBER_MAX_SAFE_INTEGER = JSBI.BigInt(Number.MAX_SAFE_INTEGER); +function formatAsDecimalNumber(num: number | JSBI) { + if (typeof num === 'number') { + if (num <= NumberMaxSafeInteger) return num.toString(10); + return JSBI.BigInt(num).toString(); + } else { + if (JSBI.lessThanOrEqual(num, JSBI_NUMBER_MAX_SAFE_INTEGER)) return JSBI.toNumber(num).toString(10); + return num.toString(); + } } export function TemporalDurationToString( @@ -2966,7 +2977,7 @@ export function TemporalDurationToString( ms: number, µs: number, ns: number, - precision: number | 'auto' = 'auto' + precision: Exclude = 'auto' ) { const sign = DurationSign(years, months, weeks, days, hours, minutes, seconds, ms, µs, ns); @@ -2994,23 +3005,13 @@ export function TemporalDurationToString( (years === 0 && months === 0 && weeks === 0 && days === 0 && hours === 0 && minutes === 0) || precision !== 'auto' ) { - const fraction = + const secondsPart = formatAsDecimalNumber(abs(secondsBigInt)); + const subSecondNanoseconds = MathAbs(JSBI.toNumber(msBigInt)) * 1e6 + MathAbs(JSBI.toNumber(µsBigInt)) * 1e3 + MathAbs(JSBI.toNumber(nsBigInt)); - let decimalPart = ToZeroPaddedDecimalString(fraction, 9); - if (precision === 'auto') { - while (decimalPart[decimalPart.length - 1] === '0') { - decimalPart = decimalPart.slice(0, -1); - } - } else if (precision === 0) { - decimalPart = ''; - } else { - decimalPart = decimalPart.slice(0, precision); - } - let secondsPart = abs(secondsBigInt).toString(); - if (decimalPart) secondsPart += `.${decimalPart}`; - timePart += `${secondsPart}S`; + const subSecondsPart = FormatFractionalSeconds(subSecondNanoseconds, precision); + timePart += `${secondsPart}${subSecondsPart}S`; } let result = `${sign < 0 ? '-' : ''}P${datePart}`; if (timePart) result = `${result}T${timePart}`; @@ -3030,7 +3031,7 @@ export function TemporalDateToString( export function TemporalDateTimeToString( dateTime: Temporal.PlainDateTime, - precision: ReturnType['precision'], + precision: SecondsStringPrecisionRecord['precision'], showCalendar: ReturnType = 'auto', options: ToStringOptions | undefined = undefined ) { @@ -3065,11 +3066,10 @@ export function TemporalDateTimeToString( const yearString = ISOYearString(year); const monthString = ISODateTimePartString(month); const dayString = ISODateTimePartString(day); - const hourString = ISODateTimePartString(hour); - const minuteString = ISODateTimePartString(minute); - const secondsString = FormatSecondsStringPart(second, millisecond, microsecond, nanosecond, precision); + const subSecondNanoseconds = millisecond * 1e6 + microsecond * 1e3 + nanosecond; + const timeString = FormatTimeString(hour, minute, second, subSecondNanoseconds, precision); const calendar = MaybeFormatCalendarAnnotation(GetSlot(dateTime, CALENDAR), showCalendar); - return `${yearString}-${monthString}-${dayString}T${hourString}:${minuteString}${secondsString}${calendar}`; + return `${yearString}-${monthString}-${dayString}T${timeString}${calendar}`; } export function TemporalMonthDayToString( @@ -3110,7 +3110,7 @@ export function TemporalYearMonthToString( export function TemporalZonedDateTimeToString( zdt: Temporal.ZonedDateTime, - precision: ReturnType['precision'], + precision: SecondsStringPrecisionRecord['precision'], showCalendar: ReturnType = 'auto', showTimeZone: ReturnType = 'auto', showOffset: ReturnType = 'auto', @@ -3127,31 +3127,18 @@ export function TemporalZonedDateTimeToString( const tz = GetSlot(zdt, TIME_ZONE); const dateTime = GetPlainDateTimeFor(tz, instant, 'iso8601'); - - const year = ISOYearString(GetSlot(dateTime, ISO_YEAR)); - const month = ISODateTimePartString(GetSlot(dateTime, ISO_MONTH)); - const day = ISODateTimePartString(GetSlot(dateTime, ISO_DAY)); - const hour = ISODateTimePartString(GetSlot(dateTime, ISO_HOUR)); - const minute = ISODateTimePartString(GetSlot(dateTime, ISO_MINUTE)); - const seconds = FormatSecondsStringPart( - GetSlot(dateTime, ISO_SECOND), - GetSlot(dateTime, ISO_MILLISECOND), - GetSlot(dateTime, ISO_MICROSECOND), - GetSlot(dateTime, ISO_NANOSECOND), - precision - ); - let result = `${year}-${month}-${day}T${hour}:${minute}${seconds}`; + let dateTimeString = TemporalDateTimeToString(dateTime, precision, 'never'); if (showOffset !== 'never') { const offsetNs = GetOffsetNanosecondsFor(tz, instant); - result += FormatDateTimeUTCOffsetRounded(offsetNs); + dateTimeString += FormatDateTimeUTCOffsetRounded(offsetNs); } if (showTimeZone !== 'never') { const identifier = ToTemporalTimeZoneIdentifier(tz); const flag = showTimeZone === 'critical' ? '!' : ''; - result += `[${flag}${identifier}]`; + dateTimeString += `[${flag}${identifier}]`; } - result += MaybeFormatCalendarAnnotation(GetSlot(zdt, CALENDAR), showCalendar); - return result; + dateTimeString += MaybeFormatCalendarAnnotation(GetSlot(zdt, CALENDAR), showCalendar); + return dateTimeString; } export function IsOffsetTimeZoneIdentifier(string: string): boolean { diff --git a/lib/plaintime.ts b/lib/plaintime.ts index f9c7da60..2eb22644 100644 --- a/lib/plaintime.ts +++ b/lib/plaintime.ts @@ -57,10 +57,8 @@ function TemporalTimeToString( )); } - const hourString = ES.ISODateTimePartString(hour); - const minuteString = ES.ISODateTimePartString(minute); - const seconds = ES.FormatSecondsStringPart(second, millisecond, microsecond, nanosecond, precision); - return `${hourString}:${minuteString}${seconds}`; + const subSecondNanoseconds = millisecond * 1e6 + microsecond * 1e3 + nanosecond; + return ES.FormatTimeString(hour, minute, second, subSecondNanoseconds, precision); } export class PlainTime implements Temporal.PlainTime { From f4c7a9cc7821464ac41594eaa4bac5d11964212f Mon Sep 17 00:00:00 2001 From: Justin Grant Date: Mon, 17 Jul 2023 12:35:54 -0700 Subject: [PATCH 05/39] Don't coerce non-String inputs to strings This commit stops converting non-string inputs to strings when parsing ISO strings or property bag fields. When numbers are coerced to strings, the result is sometimes a valid ISO string subset but it often doesn't behave as expected. The result is often brittle, yielding "data driven exceptions" that we've tried to avoid. Numbers can also be ambiguous, e.g. is a Number passed for `offset` mean hours like an ISO string, minutes like Date.p.getTimezoneOffset, or msecs like Date.p.getTime(). This commit also removes coercing objects to strings in the same contexts (like TimeZone and Calendar constructors) because it's unclear that there are use cases for this coercion. UPSTREAM_COMMIT=772379413df5b364b79d41457798c5680da47d31 --- lib/calendar.ts | 15 ++++------- lib/ecmascript.ts | 67 +++++++++++++++++++++++++++++++++-------------- lib/timezone.ts | 7 +---- test262 | 2 +- 4 files changed, 55 insertions(+), 36 deletions(-) diff --git a/lib/calendar.ts b/lib/calendar.ts index b3cc715b..7ab85fab 100644 --- a/lib/calendar.ts +++ b/lib/calendar.ts @@ -127,21 +127,16 @@ const impl: CalendarImplementations = {} as unknown as CalendarImplementations; * 6. Call the corresponding method in the implementation object. */ export class Calendar implements Temporal.Calendar { - constructor(idParam: Params['constructor'][0]) { - // Note: if the argument is not passed, IsBuiltinCalendar("undefined") will fail. This check - // exists only to improve the error message. - if (arguments.length < 1) { - throw new RangeError('missing argument: id is required'); - } + constructor(id: Params['constructor'][0]) { + const stringId = ES.RequireString(id); - const id = ES.ToString(idParam); - if (!ES.IsBuiltinCalendar(id)) throw new RangeError(`invalid calendar identifier ${id}`); + if (!ES.IsBuiltinCalendar(stringId)) throw new RangeError(`invalid calendar identifier ${stringId}`); CreateSlots(this); - SetSlot(this, CALENDAR_ID, ES.ASCIILowercase(id)); + SetSlot(this, CALENDAR_ID, ES.ASCIILowercase(stringId)); if (DEBUG) { Object.defineProperty(this, '_repr_', { - value: `${this[Symbol.toStringTag]} <${id}>`, + value: `${this[Symbol.toStringTag]} <${stringId}>`, writable: false, enumerable: false, configurable: false diff --git a/lib/ecmascript.ts b/lib/ecmascript.ts index 594771f5..872f6063 100644 --- a/lib/ecmascript.ts +++ b/lib/ecmascript.ts @@ -10,7 +10,6 @@ const MathSign = Math.sign; const MathTrunc = Math.trunc; const NumberIsFinite = Number.isFinite; const NumberCtor = Number; -const StringCtor = String; const NumberMaxSafeInteger = Number.MAX_SAFE_INTEGER; const ObjectCreate = Object.create; const ObjectDefineProperty = Object.defineProperty; @@ -18,6 +17,7 @@ const ObjectGetOwnPropertyDescriptor = Object.getOwnPropertyDescriptor; const ReflectApply = Reflect.apply; const ReflectOwnKeys = Reflect.ownKeys; const NumberIsNaN = Number.isNaN; +const StringCtor = String; const StringPrototypeSlice = String.prototype.slice; import { DEBUG, ENABLE_ASSERTS } from './debug'; @@ -308,12 +308,40 @@ function abs(x: JSBI): JSBI { if (JSBI.lessThan(x, ZERO)) return JSBI.multiply(x, NEGATIVE_ONE); return x; } +// This convenience function isn't in the spec, but is useful in the polyfill +// for DRY and better error messages. +export function RequireString(value: unknown) { + if (typeof value !== 'string') { + // Use String() to ensure that Symbols won't throw + throw new TypeError(`expected a string, not ${StringCtor(value)}`); + } + return value; +} + +// This function is an enum in the spec, but it's helpful to make it a +// function in the polyfill. +function ToPrimitiveAndRequireString(valueParam: unknown) { + const value = ToPrimitive(valueParam, StringCtor); + return RequireString(value); +} + +// Limited implementation of ToPrimitive that only handles the string case, +// because that's all that's used in this polyfill. +function ToPrimitive(value: unknown, preferredType: typeof StringCtor): string | number { + assertExists(preferredType === StringCtor); + if (IsObject(value)) { + const result = value?.toString(); + if (typeof result === 'string' || typeof result === 'number') return result; + throw new TypeError('Cannot convert object to primitive value'); + } + return value; +} type BuiltinCastFunction = (v: unknown) => string | number; const BUILTIN_CASTS = new Map([ ['year', ToIntegerWithTruncation], ['month', ToPositiveIntegerWithTruncation], - ['monthCode', ToString], + ['monthCode', ToPrimitiveAndRequireString], ['day', ToPositiveIntegerWithTruncation], ['hour', ToIntegerWithTruncation], ['minute', ToIntegerWithTruncation], @@ -331,9 +359,9 @@ const BUILTIN_CASTS = new Map([ ['milliseconds', ToIntegerIfIntegral], ['microseconds', ToIntegerIfIntegral], ['nanoseconds', ToIntegerIfIntegral], - ['era', ToString], + ['era', ToPrimitiveAndRequireString], ['eraYear', ToIntegerOrInfinity], - ['offset', ToString] + ['offset', ToPrimitiveAndRequireString] ]); const BUILTIN_DEFAULTS = new Map([ @@ -865,7 +893,7 @@ export function RegulateISOYearMonth( function ToTemporalDurationRecord(item: Temporal.DurationLike | string) { if (!IsObject(item)) { - return ParseTemporalDurationString(ToString(item)); + return ParseTemporalDurationString(RequireString(item)); } if (IsTemporalDuration(item)) { return { @@ -1221,7 +1249,7 @@ export function ToRelativeTemporalObject(options: { } else { let tzName, z; ({ year, month, day, hour, minute, second, millisecond, microsecond, nanosecond, calendar, tzName, offset, z } = - ParseISODateTime(ToString(relativeTo))); + ParseISODateTime(RequireString(relativeTo))); if (tzName) { timeZone = ToTemporalTimeZoneSlotValue(tzName); if (z) { @@ -1486,7 +1514,7 @@ export function ToTemporalDate( return CalendarDateFromFields(calendar, fields, options); } ToTemporalOverflow(options); // validate and ignore - let { year, month, day, calendar, z } = ParseTemporalDateString(ToString(item)); + let { year, month, day, calendar, z } = ParseTemporalDateString(RequireString(item)); if (z) throw new RangeError('Z designator not supported for PlainDate'); if (!calendar) calendar = 'iso8601'; if (!IsBuiltinCalendar(calendar)) throw new RangeError(`invalid calendar identifier ${calendar}`); @@ -1573,7 +1601,7 @@ export function ToTemporalDateTime(item: PlainDateTimeParams['from'][0], options ToTemporalOverflow(options); // validate and ignore let z; ({ year, month, day, hour, minute, second, millisecond, microsecond, nanosecond, calendar, z } = - ParseTemporalDateTimeString(ToString(item))); + ParseTemporalDateTimeString(RequireString(item))); if (z) throw new RangeError('Z designator not supported for PlainDateTime'); RejectDateTime(year, month, day, hour, minute, second, millisecond, microsecond, nanosecond); if (!calendar) calendar = 'iso8601'; @@ -1602,13 +1630,14 @@ export function ToTemporalDuration(item: DurationParams['from'][0]) { ); } -export function ToTemporalInstant(item: InstantParams['from'][0]) { - if (IsTemporalInstant(item)) return item; - if (IsTemporalZonedDateTime(item)) { +export function ToTemporalInstant(itemParam: InstantParams['from'][0]) { + if (IsTemporalInstant(itemParam)) return itemParam; + if (IsTemporalZonedDateTime(itemParam)) { const TemporalInstant = GetIntrinsic('%Temporal.Instant%'); - return new TemporalInstant(GetSlot(item, EPOCHNANOSECONDS)); + return new TemporalInstant(GetSlot(itemParam, EPOCHNANOSECONDS)); } - const ns = ParseTemporalInstant(ToString(item)); + const item = ToPrimitive(itemParam, StringCtor); + const ns = ParseTemporalInstant(RequireString(item)); const TemporalInstant = GetIntrinsic('%Temporal.Instant%'); return new TemporalInstant(ns); } @@ -1645,7 +1674,7 @@ export function ToTemporalMonthDay( } ToTemporalOverflow(options); // validate and ignore - let { month, day, referenceISOYear, calendar } = ParseTemporalMonthDayString(ToString(item)); + let { month, day, referenceISOYear, calendar } = ParseTemporalMonthDayString(RequireString(item)); if (calendar === undefined) calendar = 'iso8601'; if (!IsBuiltinCalendar(calendar)) throw new RangeError(`invalid calendar identifier ${calendar}`); calendar = ASCIILowercase(calendar); @@ -1691,7 +1720,7 @@ export function ToTemporalTime( overflow )); } else { - ({ hour, minute, second, millisecond, microsecond, nanosecond } = ParseTemporalTimeString(ToString(item))); + ({ hour, minute, second, millisecond, microsecond, nanosecond } = ParseTemporalTimeString(RequireString(item))); RejectTime(hour, minute, second, millisecond, microsecond, nanosecond); } const TemporalPlainTime = GetIntrinsic('%Temporal.PlainTime%'); @@ -1711,7 +1740,7 @@ export function ToTemporalYearMonth( } ToTemporalOverflow(options); // validate and ignore - let { year, month, referenceISODay, calendar } = ParseTemporalYearMonthString(ToString(item)); + let { year, month, referenceISODay, calendar } = ParseTemporalYearMonthString(RequireString(item)); if (calendar === undefined) calendar = 'iso8601'; if (!IsBuiltinCalendar(calendar)) throw new RangeError(`invalid calendar identifier ${calendar}`); calendar = ASCIILowercase(calendar); @@ -1853,7 +1882,7 @@ export function ToTemporalZonedDateTime( } else { let tzName, z; ({ year, month, day, hour, minute, second, millisecond, microsecond, nanosecond, tzName, offset, z, calendar } = - ParseTemporalZonedDateTimeString(ToString(item))); + ParseTemporalZonedDateTimeString(RequireString(item))); timeZone = ToTemporalTimeZoneSlotValue(tzName); if (z) { offsetBehaviour = 'exact'; @@ -2473,7 +2502,7 @@ export function ToTemporalCalendarSlotValue(calendarLike: CalendarParams['from'] } return calendarLike; } - const identifier = ToString(calendarLike); + const identifier = RequireString(calendarLike); if (IsBuiltinCalendar(identifier)) return ASCIILowercase(identifier); let calendar; try { @@ -2613,7 +2642,7 @@ export function ToTemporalTimeZoneSlotValue(temporalTimeZoneLike: TimeZoneParams } return temporalTimeZoneLike; } - const identifier = ToString(temporalTimeZoneLike); + const identifier = RequireString(temporalTimeZoneLike); const { tzName, offset, z } = ParseTemporalTimeZoneString(identifier); if (tzName) { // tzName is any valid identifier string in brackets, and could be an offset identifier diff --git a/lib/timezone.ts b/lib/timezone.ts index d6a5fc4d..78b9ce4c 100644 --- a/lib/timezone.ts +++ b/lib/timezone.ts @@ -23,12 +23,7 @@ import type { TimeZoneParams as Params, TimeZoneReturn as Return } from './inter export class TimeZone implements Temporal.TimeZone { constructor(identifier: string) { - // Note: if the argument is not passed, GetCanonicalTimeZoneIdentifier(undefined) will throw. - // This check exists only to improve the error message. - if (arguments.length < 1) { - throw new RangeError('missing argument: identifier is required'); - } - let stringIdentifier = ES.ToString(identifier); + let stringIdentifier = ES.RequireString(identifier); const parseResult = ES.ParseTimeZoneIdentifier(identifier); if (parseResult.offsetNanoseconds !== undefined) { stringIdentifier = ES.FormatOffsetTimeZoneIdentifier(parseResult.offsetNanoseconds); diff --git a/test262 b/test262 index 016e4bf8..b213636f 160000 --- a/test262 +++ b/test262 @@ -1 +1 @@ -Subproject commit 016e4bf8e84c36d1653c6bd53e74c93580d8b1ff +Subproject commit b213636ff1b93dded2642c3895fb5d41a3a516d9 From 366fce3bed76a200d9de96c5f93216a7ba58ae4c Mon Sep 17 00:00:00 2001 From: Philip Chimento Date: Tue, 18 Jul 2023 16:11:19 +0100 Subject: [PATCH 06/39] Update test262 UPSTREAM_COMMIT=9253db8069db07c4094208dc284913e4b45bafba --- test262 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test262 b/test262 index b213636f..60e47524 160000 --- a/test262 +++ b/test262 @@ -1 +1 @@ -Subproject commit b213636ff1b93dded2642c3895fb5d41a3a516d9 +Subproject commit 60e475248d99a69d8ce27ff05ba7b2dd6a1ca0f9 From 54ac3b1395a96005c99f734f76de75e2ef0e0d26 Mon Sep 17 00:00:00 2001 From: Justin Grant Date: Sat, 15 Jul 2023 16:38:33 -0700 Subject: [PATCH 07/39] Normative: Limit offset time zones to minutes At implementers' request to reduce the storage requirements of Temporal.TimeZone from 49+ bits to 12-13 bits, this commit requires that the [[OffsetNanoseconds]] internal slot of Temporal.TimeZone is limited to minute precision. Sub-minute precision is still allowed for custom time zone objects and built-in named time zones. In other words, this commit changes storage requirements but not internal calculation requirements. This commit is fairly narrow: * Changes |TimeZoneUTCOffsetName| production to restrict allowed offset syntax for parsing. * Changes FormatOffsetTimeZoneIdentifier AO to format minute strings only. * Moves sub-minute offset formatting from FormatOffsetTimeZoneIdentifier to instead be inlined in GetOffsetStringFor, which is now the only place where sub-minute offsets are formatted. Fixes #2593. UPSTREAM_COMMIT=70bc509f7e8079e3e7966dc321d857723a7ac802 --- lib/ecmascript.ts | 58 +++++++++++++++++++++++++++---------------- lib/regex.ts | 2 +- test/validStrings.mjs | 16 ++++++------ 3 files changed, 46 insertions(+), 30 deletions(-) diff --git a/lib/ecmascript.ts b/lib/ecmascript.ts index 872f6063..ba92f512 100644 --- a/lib/ecmascript.ts +++ b/lib/ecmascript.ts @@ -712,6 +712,7 @@ const OFFSET_IDENTIFIER = new RegExp(`^${PARSE.offsetIdentifier.source}$`); export function ParseTimeZoneIdentifier(identifier: string): { tzName?: string; offsetNanoseconds?: number } { if (!TIMEZONE_IDENTIFIER.test(identifier)) throw new RangeError(`Invalid time zone identifier: ${identifier}`); if (OFFSET_IDENTIFIER.test(identifier)) { + // The regex limits the input to minutes precision const { offsetNanoseconds } = ParseDateTimeUTCOffset(identifier); return { offsetNanoseconds }; } @@ -1817,7 +1818,7 @@ export function InterpretISODateTimeOffset( // the user-provided offset doesn't match any instants for this time // zone and date/time. if (offsetOpt === 'reject') { - const offsetStr = FormatOffsetTimeZoneIdentifier(offsetNs); + const offsetStr = formatOffsetStringNanoseconds(offsetNs); const timeZoneString = IsTemporalTimeZone(timeZone) ? GetSlot(timeZone, TIMEZONE_ID) : 'time zone'; // The tsc emit for this line rewrites to invoke the PlainDateTime's valueOf method, NOT // toString (which is invoked by Node when using template literals directly). @@ -2655,7 +2656,10 @@ export function ToTemporalTimeZoneSlotValue(temporalTimeZoneLike: TimeZoneParams } if (z) return 'UTC'; // if !tzName && !z then offset must be present - const { offsetNanoseconds } = ParseDateTimeUTCOffset(castExists(offset)); + const { offsetNanoseconds, hasSubMinutePrecision } = ParseDateTimeUTCOffset(castExists(offset)); + if (hasSubMinutePrecision) { + throw new RangeError(`Seconds not allowed in offset time zone: ${offset}`); + } return FormatOffsetTimeZoneIdentifier(offsetNanoseconds); } @@ -2723,7 +2727,28 @@ export function GetOffsetNanosecondsFor( export function GetOffsetStringFor(timeZone: string | Temporal.TimeZoneProtocol, instant: Temporal.Instant) { const offsetNs = GetOffsetNanosecondsFor(timeZone, instant); - return FormatOffsetTimeZoneIdentifier(offsetNs); + return formatOffsetStringNanoseconds(offsetNs); +} + +// In the spec, the code below only exists as part of GetOffsetStringFor. +// But in the polyfill, we re-use it to provide clearer error messages. +function formatOffsetStringNanoseconds(offsetNs: number) { + const offsetMinutes = MathTrunc(offsetNs / 6e10); + let offsetStringMinutes = FormatOffsetTimeZoneIdentifier(offsetMinutes * 6e10); + const subMinuteNanoseconds = MathAbs(offsetNs) % 6e10; + if (!subMinuteNanoseconds) return offsetStringMinutes; + + // For offsets between -1s and 0, exclusive, FormatOffsetTimeZoneIdentifier's + // return value of "+00:00" is incorrect if there are sub-minute units. + if (!offsetMinutes && offsetNs < 0) offsetStringMinutes = '-00:00'; + + const seconds = MathFloor(subMinuteNanoseconds / 1e9) % 60; + const secondString = ISODateTimePartString(seconds); + const nanoseconds = subMinuteNanoseconds % 1e9; + if (!nanoseconds) return `${offsetStringMinutes}:${secondString}`; + + let fractionString = `${nanoseconds}`.padStart(9, '0').replace(/0+$/, ''); + return `${offsetStringMinutes}:${secondString}.${fractionString}`; } export function GetPlainDateTimeFor( @@ -3303,25 +3328,14 @@ export function GetNamedTimeZoneOffsetNanoseconds(id: string, epochNanoseconds: return JSBI.toNumber(JSBI.subtract(utc, epochNanoseconds)); } -export function FormatOffsetTimeZoneIdentifier(offsetNanosecondsParam: number): string { - const sign = offsetNanosecondsParam < 0 ? '-' : '+'; - const offsetNanoseconds = MathAbs(offsetNanosecondsParam); - const hours = MathFloor(offsetNanoseconds / 3600e9); - const hourString = ISODateTimePartString(hours); - const minutes = MathFloor(offsetNanoseconds / 60e9) % 60; - const minuteString = ISODateTimePartString(minutes); - const seconds = MathFloor(offsetNanoseconds / 1e9) % 60; - const secondString = ISODateTimePartString(seconds); - const nanoseconds = offsetNanoseconds % 1e9; - let post = ''; - if (nanoseconds) { - let fraction = `${nanoseconds}`.padStart(9, '0'); - while (fraction[fraction.length - 1] === '0') fraction = fraction.slice(0, -1); - post = `:${secondString}.${fraction}`; - } else if (seconds) { - post = `:${secondString}`; - } - return `${sign}${hourString}:${minuteString}${post}`; +export function FormatOffsetTimeZoneIdentifier(offsetNanoseconds: number) { + const sign = offsetNanoseconds < 0 ? '-' : '+'; + const absoluteMinutes = MathAbs(offsetNanoseconds / 6e10); + const intHours = MathFloor(absoluteMinutes / 60); + const hh = ISODateTimePartString(intHours); + const intMinutes = absoluteMinutes % 60; + const mm = ISODateTimePartString(intMinutes); + return `${sign}${hh}:${mm}`; } function FormatDateTimeUTCOffsetRounded(offsetNanoseconds: number): string { diff --git a/lib/regex.ts b/lib/regex.ts index 88ec7d20..3b224f4a 100644 --- a/lib/regex.ts +++ b/lib/regex.ts @@ -24,7 +24,7 @@ const datesplit = new RegExp( const timesplit = /(\d{2})(?::(\d{2})(?::(\d{2})(?:[.,](\d{1,9}))?)?|(\d{2})(?:(\d{2})(?:[.,](\d{1,9}))?)?)?/; export const offset = /([+\u2212-])([01][0-9]|2[0-3])(?::?([0-5][0-9])(?::?([0-5][0-9])(?:[.,](\d{1,9}))?)?)?/; const offsetpart = new RegExp(`([zZ])|${offset.source}?`); -export const offsetIdentifier = offset; +export const offsetIdentifier = /([+\u2212-])([01][0-9]|2[0-3])(?::?([0-5][0-9])?)?/; export const annotation = /\[(!)?([a-z_][a-z0-9_-]*)=([A-Za-z0-9]+(?:-[A-Za-z0-9]+)*)\]/g; export const zoneddatetime = new RegExp( diff --git a/test/validStrings.mjs b/test/validStrings.mjs index 02bc0929..454e4072 100644 --- a/test/validStrings.mjs +++ b/test/validStrings.mjs @@ -8,7 +8,8 @@ node --experimental-modules --experimental-specifier-resolution=node --no-warnin */ import assert from 'assert'; -import * as ES from '../lib/ecmascript'; +import * as ES from '../lib/ecmascript.mjs'; +import { Instant } from '../lib/instant.mjs'; const timezoneNames = Intl.supportedValuesOf('timeZone'); const calendarNames = Intl.supportedValuesOf('calendar'); @@ -251,7 +252,12 @@ const temporalSign = withCode( ); const temporalDecimalFraction = fraction; function saveOffset(data, result) { - data.offset = ES.FormatOffsetTimeZoneIdentifier(ES.ParseDateTimeUTCOffset(result).offsetNanoseconds); + // To canonicalize an offset string that may include nanoseconds, we use GetOffsetStringFor + const instant = new Instant(0n); + const fakeTimeZone = { + getOffsetNanosecondsFor: () => ES.ParseDateTimeUTCOffset(result).offsetNanoseconds + }; + data.offset = ES.GetOffsetStringFor(fakeTimeZone, instant); } const utcOffsetSubMinutePrecision = withCode( seq( @@ -265,11 +271,7 @@ const utcOffsetSubMinutePrecision = withCode( saveOffset ); const dateTimeUTCOffset = choice(utcDesignator, utcOffsetSubMinutePrecision); -const timeZoneUTCOffsetName = seq( - sign, - hour, - choice([minuteSecond, [minuteSecond, [fraction]]], seq(':', minuteSecond, [':', minuteSecond, [fraction]])) -); +const timeZoneUTCOffsetName = seq(sign, hour, choice([minuteSecond], seq(':', minuteSecond))); const timeZoneIANAName = choice(...timezoneNames); const timeZoneIdentifier = withCode( choice(timeZoneUTCOffsetName, timeZoneIANAName), From bfb5df9d01de8ba08091de14e0db5a70b3de72ac Mon Sep 17 00:00:00 2001 From: Justin Grant Date: Mon, 17 Jul 2023 12:37:22 -0700 Subject: [PATCH 08/39] Update Test262 UPSTREAM_COMMIT=861aba9d97335b99f4f2a4df9f6713a425c3657b --- test262 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test262 b/test262 index 60e47524..6f146e6f 160000 --- a/test262 +++ b/test262 @@ -1 +1 @@ -Subproject commit 60e475248d99a69d8ce27ff05ba7b2dd6a1ca0f9 +Subproject commit 6f146e6f30390ac87d2b6b0198639d8c2ebbdf91 From 8286a615de033b8687dc8d04bf81cbb71cab6ac4 Mon Sep 17 00:00:00 2001 From: Justin Grant Date: Tue, 18 Jul 2023 21:12:08 -0700 Subject: [PATCH 09/39] Editorial: rename TimeZone nanoseconds slot/AOs Now that we've limited TimeZone's [[OffsetNanoseconds]] internal slot to minute precision, this commit refactors TimeZone to clarify that only minutes are allowed in that slot and related abstract operations. Changes: * Renames TimeZone's [[OffsetNanoseconds]] internal slot to [[OffsetMinutes]] * Changes ParseTimeZoneIdentifier to return an [[OffsetMinutes]] field instead of an [[OffsetNanoseconds]] field. * Changes FormatOffsetTimeZoneIdentifier to expect a minutes argument. The goal of this change is to avoid the complexity and potential confusion from a slot and AOs that deal with "nanoseconds" values that nonetheless are restricted to minutes. UPSTREAM_COMMIT=683448732ce4c0dbcf9a74aceea845cd1c3a2411 --- lib/ecmascript.ts | 32 +++++++++++++++++--------------- lib/timezone.ts | 14 +++++++------- 2 files changed, 24 insertions(+), 22 deletions(-) diff --git a/lib/ecmascript.ts b/lib/ecmascript.ts index ba92f512..8a7e6edb 100644 --- a/lib/ecmascript.ts +++ b/lib/ecmascript.ts @@ -709,12 +709,12 @@ export function ParseTemporalMonthDayString(isoString: string) { const TIMEZONE_IDENTIFIER = new RegExp(`^${PARSE.timeZoneID.source}$`, 'i'); const OFFSET_IDENTIFIER = new RegExp(`^${PARSE.offsetIdentifier.source}$`); -export function ParseTimeZoneIdentifier(identifier: string): { tzName?: string; offsetNanoseconds?: number } { +export function ParseTimeZoneIdentifier(identifier: string): { tzName?: string; offsetMinutes?: number } { if (!TIMEZONE_IDENTIFIER.test(identifier)) throw new RangeError(`Invalid time zone identifier: ${identifier}`); if (OFFSET_IDENTIFIER.test(identifier)) { // The regex limits the input to minutes precision const { offsetNanoseconds } = ParseDateTimeUTCOffset(identifier); - return { offsetNanoseconds }; + return { offsetMinutes: offsetNanoseconds / 60e9 }; } return { tzName: identifier }; } @@ -786,7 +786,7 @@ export function ParseTemporalDurationString(isoString: string) { const microseconds = MathTrunc(excessNanoseconds / 1000) % 1000; const milliseconds = MathTrunc(excessNanoseconds / 1e6) % 1000; seconds += MathTrunc(excessNanoseconds / 1e9) % 60; - minutes += MathTrunc(excessNanoseconds / 6e10); + minutes += MathTrunc(excessNanoseconds / 60e9); RejectDuration(years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds); return { years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds }; @@ -2647,8 +2647,8 @@ export function ToTemporalTimeZoneSlotValue(temporalTimeZoneLike: TimeZoneParams const { tzName, offset, z } = ParseTemporalTimeZoneString(identifier); if (tzName) { // tzName is any valid identifier string in brackets, and could be an offset identifier - const { offsetNanoseconds } = ParseTimeZoneIdentifier(tzName); - if (offsetNanoseconds !== undefined) return FormatOffsetTimeZoneIdentifier(offsetNanoseconds); + const { offsetMinutes } = ParseTimeZoneIdentifier(tzName); + if (offsetMinutes !== undefined) return FormatOffsetTimeZoneIdentifier(offsetMinutes); const record = GetAvailableNamedTimeZoneIdentifier(tzName); if (!record) throw new RangeError(`Unrecognized time zone ${tzName}`); @@ -2660,7 +2660,7 @@ export function ToTemporalTimeZoneSlotValue(temporalTimeZoneLike: TimeZoneParams if (hasSubMinutePrecision) { throw new RangeError(`Seconds not allowed in offset time zone: ${offset}`); } - return FormatOffsetTimeZoneIdentifier(offsetNanoseconds); + return FormatOffsetTimeZoneIdentifier(offsetNanoseconds / 60e9); } export function ToTemporalTimeZoneIdentifier(slotValue: TimeZoneSlot) { @@ -2733,9 +2733,9 @@ export function GetOffsetStringFor(timeZone: string | Temporal.TimeZoneProtocol, // In the spec, the code below only exists as part of GetOffsetStringFor. // But in the polyfill, we re-use it to provide clearer error messages. function formatOffsetStringNanoseconds(offsetNs: number) { - const offsetMinutes = MathTrunc(offsetNs / 6e10); - let offsetStringMinutes = FormatOffsetTimeZoneIdentifier(offsetMinutes * 6e10); - const subMinuteNanoseconds = MathAbs(offsetNs) % 6e10; + const offsetMinutes = MathTrunc(offsetNs / 60e9); + let offsetStringMinutes = FormatOffsetTimeZoneIdentifier(offsetMinutes); + const subMinuteNanoseconds = MathAbs(offsetNs) % 60e9; if (!subMinuteNanoseconds) return offsetStringMinutes; // For offsets between -1s and 0, exclusive, FormatOffsetTimeZoneIdentifier's @@ -3328,9 +3328,9 @@ export function GetNamedTimeZoneOffsetNanoseconds(id: string, epochNanoseconds: return JSBI.toNumber(JSBI.subtract(utc, epochNanoseconds)); } -export function FormatOffsetTimeZoneIdentifier(offsetNanoseconds: number) { - const sign = offsetNanoseconds < 0 ? '-' : '+'; - const absoluteMinutes = MathAbs(offsetNanoseconds / 6e10); +export function FormatOffsetTimeZoneIdentifier(offsetMinutes: number) { + const sign = offsetMinutes < 0 ? '-' : '+'; + const absoluteMinutes = MathAbs(offsetMinutes); const intHours = MathFloor(absoluteMinutes / 60); const hh = ISODateTimePartString(intHours); const intMinutes = absoluteMinutes % 60; @@ -3338,9 +3338,11 @@ export function FormatOffsetTimeZoneIdentifier(offsetNanoseconds: number) { return `${sign}${hh}:${mm}`; } -function FormatDateTimeUTCOffsetRounded(offsetNanoseconds: number): string { - const ns = JSBI.toNumber(RoundNumberToIncrement(JSBI.BigInt(offsetNanoseconds), JSBI.BigInt(60e9), 'halfExpand')); - return FormatOffsetTimeZoneIdentifier(ns); +export function FormatDateTimeUTCOffsetRounded(offsetNanosecondsParam: number) { + const offsetNanoseconds = JSBI.toNumber( + RoundNumberToIncrement(JSBI.BigInt(offsetNanosecondsParam), MINUTE_NANOS, 'halfExpand') + ); + return FormatOffsetTimeZoneIdentifier(offsetNanoseconds / 60e9); } export function GetUTCEpochNanoseconds( year: number, diff --git a/lib/timezone.ts b/lib/timezone.ts index 78b9ce4c..6c5c6661 100644 --- a/lib/timezone.ts +++ b/lib/timezone.ts @@ -25,8 +25,8 @@ export class TimeZone implements Temporal.TimeZone { constructor(identifier: string) { let stringIdentifier = ES.RequireString(identifier); const parseResult = ES.ParseTimeZoneIdentifier(identifier); - if (parseResult.offsetNanoseconds !== undefined) { - stringIdentifier = ES.FormatOffsetTimeZoneIdentifier(parseResult.offsetNanoseconds); + if (parseResult.offsetMinutes !== undefined) { + stringIdentifier = ES.FormatOffsetTimeZoneIdentifier(parseResult.offsetMinutes); } else { const record = ES.GetAvailableNamedTimeZoneIdentifier(stringIdentifier); if (!record) throw new RangeError(`Invalid time zone identifier: ${stringIdentifier}`); @@ -53,8 +53,8 @@ export class TimeZone implements Temporal.TimeZone { const instant = ES.ToTemporalInstant(instantParam); const id = GetSlot(this, TIMEZONE_ID); - const offsetNanoseconds = ES.ParseTimeZoneIdentifier(id).offsetNanoseconds; - if (offsetNanoseconds !== undefined) return offsetNanoseconds; + const offsetMinutes = ES.ParseTimeZoneIdentifier(id).offsetMinutes; + if (offsetMinutes !== undefined) return offsetMinutes * 60e9; return ES.GetNamedTimeZoneOffsetNanoseconds(id, GetSlot(instant, EPOCHNANOSECONDS)); } @@ -88,8 +88,8 @@ export class TimeZone implements Temporal.TimeZone { const Instant = GetIntrinsic('%Temporal.Instant%'); const id = GetSlot(this, TIMEZONE_ID); - const offsetNanoseconds = ES.ParseTimeZoneIdentifier(id).offsetNanoseconds; - if (offsetNanoseconds !== undefined) { + const offsetMinutes = ES.ParseTimeZoneIdentifier(id).offsetMinutes; + if (offsetMinutes !== undefined) { const epochNs = ES.GetUTCEpochNanoseconds( GetSlot(dateTime, ISO_YEAR), GetSlot(dateTime, ISO_MONTH), @@ -102,7 +102,7 @@ export class TimeZone implements Temporal.TimeZone { GetSlot(dateTime, ISO_NANOSECOND) ); if (epochNs === null) throw new RangeError('DateTime outside of supported range'); - return [new Instant(JSBI.subtract(epochNs, JSBI.BigInt(offsetNanoseconds)))]; + return [new Instant(JSBI.subtract(epochNs, JSBI.BigInt(offsetMinutes * 60e9)))]; } const possibleEpochNs = ES.GetNamedTimeZoneEpochNanoseconds( From d715ac08fc50f40ac19e0820f988c02ede7cd075 Mon Sep 17 00:00:00 2001 From: Justin Grant Date: Tue, 18 Jul 2023 21:22:23 -0700 Subject: [PATCH 10/39] Editorial: refactor offset string formatting Simplify offset formatting by using FormatTimeString instead of bespoke formatting logic. This commit completes the time-formatting refactor that UPSTREAM_COMMIT=9eb39e1d61c08c8bf8f3e0694412d4dbf3f4fbc9 --- lib/ecmascript.ts | 34 +++++++++++++--------------------- 1 file changed, 13 insertions(+), 21 deletions(-) diff --git a/lib/ecmascript.ts b/lib/ecmascript.ts index 8a7e6edb..2f88db1f 100644 --- a/lib/ecmascript.ts +++ b/lib/ecmascript.ts @@ -2733,22 +2733,15 @@ export function GetOffsetStringFor(timeZone: string | Temporal.TimeZoneProtocol, // In the spec, the code below only exists as part of GetOffsetStringFor. // But in the polyfill, we re-use it to provide clearer error messages. function formatOffsetStringNanoseconds(offsetNs: number) { - const offsetMinutes = MathTrunc(offsetNs / 60e9); - let offsetStringMinutes = FormatOffsetTimeZoneIdentifier(offsetMinutes); - const subMinuteNanoseconds = MathAbs(offsetNs) % 60e9; - if (!subMinuteNanoseconds) return offsetStringMinutes; - - // For offsets between -1s and 0, exclusive, FormatOffsetTimeZoneIdentifier's - // return value of "+00:00" is incorrect if there are sub-minute units. - if (!offsetMinutes && offsetNs < 0) offsetStringMinutes = '-00:00'; - - const seconds = MathFloor(subMinuteNanoseconds / 1e9) % 60; - const secondString = ISODateTimePartString(seconds); - const nanoseconds = subMinuteNanoseconds % 1e9; - if (!nanoseconds) return `${offsetStringMinutes}:${secondString}`; - - let fractionString = `${nanoseconds}`.padStart(9, '0').replace(/0+$/, ''); - return `${offsetStringMinutes}:${secondString}.${fractionString}`; + const sign = offsetNs < 0 ? '-' : '+'; + const absoluteNs = MathAbs(offsetNs); + const hour = MathFloor(absoluteNs / 3600e9); + const minute = MathFloor(absoluteNs / 60e9) % 60; + const second = MathFloor(absoluteNs / 1e9) % 60; + const subSecondNs = absoluteNs % 1e9; + const precision = second === 0 && subSecondNs === 0 ? 'minute' : 'auto'; + const timeString = FormatTimeString(hour, minute, second, subSecondNs, precision); + return `${sign}${timeString}`; } export function GetPlainDateTimeFor( @@ -3331,11 +3324,10 @@ export function GetNamedTimeZoneOffsetNanoseconds(id: string, epochNanoseconds: export function FormatOffsetTimeZoneIdentifier(offsetMinutes: number) { const sign = offsetMinutes < 0 ? '-' : '+'; const absoluteMinutes = MathAbs(offsetMinutes); - const intHours = MathFloor(absoluteMinutes / 60); - const hh = ISODateTimePartString(intHours); - const intMinutes = absoluteMinutes % 60; - const mm = ISODateTimePartString(intMinutes); - return `${sign}${hh}:${mm}`; + const hour = MathFloor(absoluteMinutes / 60); + const minute = absoluteMinutes % 60; + const timeString = FormatTimeString(hour, minute, 0, 0, 'minute'); + return `${sign}${timeString}`; } export function FormatDateTimeUTCOffsetRounded(offsetNanosecondsParam: number) { From 4f3a7bbc37d8277993297800694e5d9578913734 Mon Sep 17 00:00:00 2001 From: Justin Grant Date: Wed, 19 Jul 2023 00:49:01 -0700 Subject: [PATCH 11/39] Polyfill: Implement proposal-canonical-tz Stage 3 UPSTREAM_COMMIT=e5577276a33ceb69bf26d472b98fa456187890cc --- index.d.ts | 1 + lib/ecmascript.ts | 20 ++++++++++++++++++-- lib/intl.ts | 2 +- lib/timezone.ts | 7 ++++++- lib/zoneddatetime.ts | 2 +- 5 files changed, 27 insertions(+), 5 deletions(-) diff --git a/index.d.ts b/index.d.ts index 784677c9..b11758ea 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1138,6 +1138,7 @@ export namespace Temporal { static from(timeZone: TimeZoneLike): Temporal.TimeZone | TimeZoneProtocol; constructor(timeZoneIdentifier: string); readonly id: string; + equals(timeZone: TimeZoneLike): boolean; getOffsetNanosecondsFor(instant: Temporal.Instant | string): number; getOffsetStringFor(instant: Temporal.Instant | string): string; getPlainDateTimeFor(instant: Temporal.Instant | string, calendar?: CalendarLike): Temporal.PlainDateTime; diff --git a/lib/ecmascript.ts b/lib/ecmascript.ts index 2f88db1f..642b4d69 100644 --- a/lib/ecmascript.ts +++ b/lib/ecmascript.ts @@ -2652,7 +2652,7 @@ export function ToTemporalTimeZoneSlotValue(temporalTimeZoneLike: TimeZoneParams const record = GetAvailableNamedTimeZoneIdentifier(tzName); if (!record) throw new RangeError(`Unrecognized time zone ${tzName}`); - return record.primaryIdentifier; + return record.identifier; } if (z) return 'UTC'; // if !tzName && !z then offset must be present @@ -2680,7 +2680,23 @@ export function TimeZoneEquals(one: string | Temporal.TimeZoneProtocol, two: str if (one === two) return true; const tz1 = ToTemporalTimeZoneIdentifier(one); const tz2 = ToTemporalTimeZoneIdentifier(two); - return tz1 === tz2; + if (tz1 === tz2) return true; + const offsetMinutes1 = ParseTimeZoneIdentifier(tz1).offsetMinutes; + const offsetMinutes2 = ParseTimeZoneIdentifier(tz2).offsetMinutes; + if (offsetMinutes1 === undefined && offsetMinutes2 === undefined) { + // Calling GetAvailableNamedTimeZoneIdentifier is costly, so (unlike the + // spec) the polyfill will early-return if one of them isn't recognized. Try + // the second ID first because it's more likely to be unknown, because it + // can come from the argument of TimeZone.p.equals as opposed to the first + // ID which comes from the receiver. + const idRecord2 = GetAvailableNamedTimeZoneIdentifier(tz2); + if (!idRecord2) return false; + const idRecord1 = GetAvailableNamedTimeZoneIdentifier(tz1); + if (!idRecord1) return false; + return idRecord1.primaryIdentifier === idRecord2.primaryIdentifier; + } else { + return offsetMinutes1 === offsetMinutes2; + } } export function TemporalDateTimeToDate(dateTime: Temporal.PlainDateTime) { diff --git a/lib/intl.ts b/lib/intl.ts index 0afbcd98..7c420cce 100644 --- a/lib/intl.ts +++ b/lib/intl.ts @@ -202,7 +202,7 @@ export const DateTimeFormat = DateTimeFormatImpl as unknown as typeof Intl.DateT function resolvedOptions(this: DateTimeFormatImpl): Return['resolvedOptions'] { const resolved = this[ORIGINAL].resolvedOptions(); - resolved.timeZone = this[TZ_CANONICAL]; + resolved.timeZone = this[TZ_ORIGINAL]; return resolved; } diff --git a/lib/timezone.ts b/lib/timezone.ts index 6c5c6661..53c0534a 100644 --- a/lib/timezone.ts +++ b/lib/timezone.ts @@ -30,7 +30,7 @@ export class TimeZone implements Temporal.TimeZone { } else { const record = ES.GetAvailableNamedTimeZoneIdentifier(stringIdentifier); if (!record) throw new RangeError(`Invalid time zone identifier: ${stringIdentifier}`); - stringIdentifier = record.primaryIdentifier; + stringIdentifier = record.identifier; } CreateSlots(this); SetSlot(this, TIMEZONE_ID, stringIdentifier); @@ -48,6 +48,11 @@ export class TimeZone implements Temporal.TimeZone { if (!ES.IsTemporalTimeZone(this)) throw new TypeError('invalid receiver'); return GetSlot(this, TIMEZONE_ID); } + equals(other: Params['equals'][0]): Return['equals'] { + if (!ES.IsTemporalTimeZone(this)) throw new TypeError('invalid receiver'); + const timeZoneSlotValue = ES.ToTemporalTimeZoneSlotValue(other); + return ES.TimeZoneEquals(this, timeZoneSlotValue); + } getOffsetNanosecondsFor(instantParam: Params['getOffsetNanosecondsFor'][0]): Return['getOffsetNanosecondsFor'] { if (!ES.IsTemporalTimeZone(this)) throw new TypeError('invalid receiver'); const instant = ES.ToTemporalInstant(instantParam); diff --git a/lib/zoneddatetime.ts b/lib/zoneddatetime.ts index 96b9a72f..9dc294d1 100644 --- a/lib/zoneddatetime.ts +++ b/lib/zoneddatetime.ts @@ -487,7 +487,7 @@ export class ZonedDateTime implements Temporal.ZonedDateTime { } else { const record = ES.GetAvailableNamedTimeZoneIdentifier(timeZoneIdentifier); if (!record) throw new RangeError(`toLocaleString formats built-in time zones, not ${timeZoneIdentifier}`); - optionsCopy.timeZone = record.primaryIdentifier; + optionsCopy.timeZone = record.identifier; } const formatter = new DateTimeFormat(locales, optionsCopy); From 81fdfd380c8d93f75ae8075a1dce09a125dcfb87 Mon Sep 17 00:00:00 2001 From: Justin Grant Date: Wed, 19 Jul 2023 00:52:53 -0700 Subject: [PATCH 12/39] Update Test262 for proposal-canonical-tz Stage 3 UPSTREAM_COMMIT=1dc2bbbd8a4ebf11b3e98e2bba21e0fb9e9bcc79 --- test262 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test262 b/test262 index 6f146e6f..29dde1ce 160000 --- a/test262 +++ b/test262 @@ -1 +1 @@ -Subproject commit 6f146e6f30390ac87d2b6b0198639d8c2ebbdf91 +Subproject commit 29dde1ce0e97a8bd6423c4397b9d3b51df0a1d8e From 8f295237e44db5fa77d7d93b255400b1f0c005d7 Mon Sep 17 00:00:00 2001 From: Justin Grant Date: Fri, 21 Jul 2023 03:19:11 -0700 Subject: [PATCH 13/39] Polyfill: clean up ISO parser & align to spec To make it easier to find actual Test262 gaps, this PR removes unreachable and unused code in ISO parsing. It also removes offset normalization code from ParseISODateTime, to better mach the spec that only normalizes downstream from ISO string parsing. Doing this exposed a test bug that's fixed in the second commit of https://github.com/tc39/test262/pull/3877. UPSTREAM_COMMIT=212fffcd2df3d5ac6dbcc6abfb628d696c5de146 --- lib/ecmascript.ts | 25 +++++++------------------ lib/regex.ts | 4 +++- test/validStrings.mjs | 16 ++++++++-------- test262 | 2 +- 4 files changed, 19 insertions(+), 28 deletions(-) diff --git a/lib/ecmascript.ts b/lib/ecmascript.ts index 642b4d69..086946f1 100644 --- a/lib/ecmascript.ts +++ b/lib/ecmascript.ts @@ -566,23 +566,11 @@ function ParseISODateTime(isoString: string) { if (match[13]) { offset = undefined; z = true; - } else if (match[14] && match[15]) { - const offsetSign = match[14] === '-' || match[14] === '\u2212' ? '-' : '+'; - const offsetHours = match[15] || '00'; - const offsetMinutes = match[16] || '00'; - const offsetSeconds = match[17] || '00'; - let offsetFraction = match[18] || '0'; - offset = `${offsetSign}${offsetHours}:${offsetMinutes}`; - if (+offsetFraction) { - while (offsetFraction.endsWith('0')) offsetFraction = offsetFraction.slice(0, -1); - offset += `:${offsetSeconds}.${offsetFraction}`; - } else if (+offsetSeconds) { - offset += `:${offsetSeconds}`; - } - if (offset === '-00:00') offset = '+00:00'; + } else if (match[14]) { + offset = match[14]; } - const tzName = match[19]; - const calendar = processAnnotations(match[20]); + const tzName = match[15]; + const calendar = processAnnotations(match[16]); RejectDateTime(year, month, day, hour, minute, second, millisecond, microsecond, nanosecond); return { year, @@ -639,7 +627,7 @@ export function ParseTemporalTimeString(isoString: string) { millisecond = ToIntegerOrInfinity(fraction.slice(0, 3)); microsecond = ToIntegerOrInfinity(fraction.slice(3, 6)); nanosecond = ToIntegerOrInfinity(fraction.slice(6, 9)); - processAnnotations(match[14]); // for validation only; ignore found calendar + processAnnotations(match[10]); // ignore found calendar if (match[8]) throw new RangeError('Z designator not supported for PlainTime'); } else { let z, hasTime; @@ -3209,7 +3197,7 @@ export function IsOffsetTimeZoneIdentifier(string: string): boolean { } export function ParseDateTimeUTCOffset(string: string): { offsetNanoseconds: number; hasSubMinutePrecision: boolean } { - const match = OFFSET.exec(string); + const match = OFFSET_WITH_PARTS.exec(string); if (!match) { throw new RangeError(`invalid time zone offset: ${string}`); } @@ -6741,6 +6729,7 @@ export function ASCIILowercase(str: T): T { } const OFFSET = new RegExp(`^${PARSE.offset.source}$`); +const OFFSET_WITH_PARTS = new RegExp(`^${PARSE.offsetWithParts.source}$`); function bisect( getState: (epochNs: JSBI) => number, diff --git a/lib/regex.ts b/lib/regex.ts index 3b224f4a..255fbba0 100644 --- a/lib/regex.ts +++ b/lib/regex.ts @@ -22,7 +22,9 @@ const datesplit = new RegExp( `(${yearpart.source})(?:-(${monthpart.source})-(${daypart.source})|(${monthpart.source})(${daypart.source}))` ); const timesplit = /(\d{2})(?::(\d{2})(?::(\d{2})(?:[.,](\d{1,9}))?)?|(\d{2})(?:(\d{2})(?:[.,](\d{1,9}))?)?)?/; -export const offset = /([+\u2212-])([01][0-9]|2[0-3])(?::?([0-5][0-9])(?::?([0-5][0-9])(?:[.,](\d{1,9}))?)?)?/; +export const offsetWithParts = /([+\u2212-])([01][0-9]|2[0-3])(?::?([0-5][0-9])(?::?([0-5][0-9])(?:[.,](\d{1,9}))?)?)?/; +export const offset = + /((?:[+\u2212-])(?:[01][0-9]|2[0-3])(?::?(?:[0-5][0-9])(?::?(?:[0-5][0-9])(?:[.,](?:\d{1,9}))?)?)?)/; const offsetpart = new RegExp(`([zZ])|${offset.source}?`); export const offsetIdentifier = /([+\u2212-])([01][0-9]|2[0-3])(?::?([0-5][0-9])?)?/; export const annotation = /\[(!)?([a-z_][a-z0-9_-]*)=([A-Za-z0-9]+(?:-[A-Za-z0-9]+)*)\]/g; diff --git a/test/validStrings.mjs b/test/validStrings.mjs index 454e4072..5eb98226 100644 --- a/test/validStrings.mjs +++ b/test/validStrings.mjs @@ -9,7 +9,6 @@ node --experimental-modules --experimental-specifier-resolution=node --no-warnin import assert from 'assert'; import * as ES from '../lib/ecmascript.mjs'; -import { Instant } from '../lib/instant.mjs'; const timezoneNames = Intl.supportedValuesOf('timeZone'); const calendarNames = Intl.supportedValuesOf('calendar'); @@ -252,12 +251,7 @@ const temporalSign = withCode( ); const temporalDecimalFraction = fraction; function saveOffset(data, result) { - // To canonicalize an offset string that may include nanoseconds, we use GetOffsetStringFor - const instant = new Instant(0n); - const fakeTimeZone = { - getOffsetNanosecondsFor: () => ES.ParseDateTimeUTCOffset(result).offsetNanoseconds - }; - data.offset = ES.GetOffsetStringFor(fakeTimeZone, instant); + data.offset = ES.ParseDateTimeUTCOffset(result); } const utcOffsetSubMinutePrecision = withCode( seq( @@ -471,7 +465,13 @@ function fuzzMode(mode) { for (let prop of comparisonItems[mode]) { let expected = generatedData[prop]; if (prop !== 'tzName' && prop !== 'offset' && prop !== 'calendar') expected = expected || 0; - assert.equal(parsed[prop], expected, prop); + if (prop === 'offset' && expected) { + const parsedResult = ES.ParseDateTimeUTCOffset(parsed[prop]); + assert.equal(parsedResult.offsetNanoseconds, expected.offsetNanoseconds); + assert.equal(parsedResult.hasSubMinutePrecision, expected.hasSubMinutePrecision); + } else { + assert.equal(parsed[prop], expected, prop); + } } console.log(`${fuzzed} => ok`); } catch (e) { diff --git a/test262 b/test262 index 29dde1ce..66f3959c 160000 --- a/test262 +++ b/test262 @@ -1 +1 @@ -Subproject commit 29dde1ce0e97a8bd6423c4397b9d3b51df0a1d8e +Subproject commit 66f3959c14646a4caba79e91adfe976c28246bf9 From 8550f781b2a0026be44f4f3e5473e7ffc1a1316e Mon Sep 17 00:00:00 2001 From: Philip Chimento Date: Thu, 10 Aug 2023 02:22:39 +0200 Subject: [PATCH 14/39] Editorial: Reintroduce an AO that formats UTC offset with ns precision Recently the code for formatting UTC offsets with nanosecond precision was inlined into GetOffsetStringFor because that was the only place it was used. However, we need to use it in more than one place after the user code audit of #2519 because we'll be pre-calculating the UTC offset in cases where it's calculated more than once. Specifically, in ZonedDateTime.p.getISOFields(), so change that to use the new FormatUTCOffsetNanoseconds operation. UPSTREAM_COMMIT=c895534de684e1b74a6ea92e0545e6b17b259cd2 --- lib/ecmascript.ts | 8 +++----- lib/zoneddatetime.ts | 3 ++- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/lib/ecmascript.ts b/lib/ecmascript.ts index 086946f1..2d93269d 100644 --- a/lib/ecmascript.ts +++ b/lib/ecmascript.ts @@ -1806,7 +1806,7 @@ export function InterpretISODateTimeOffset( // the user-provided offset doesn't match any instants for this time // zone and date/time. if (offsetOpt === 'reject') { - const offsetStr = formatOffsetStringNanoseconds(offsetNs); + const offsetStr = FormatUTCOffsetNanoseconds(offsetNs); const timeZoneString = IsTemporalTimeZone(timeZone) ? GetSlot(timeZone, TIMEZONE_ID) : 'time zone'; // The tsc emit for this line rewrites to invoke the PlainDateTime's valueOf method, NOT // toString (which is invoked by Node when using template literals directly). @@ -2731,12 +2731,10 @@ export function GetOffsetNanosecondsFor( export function GetOffsetStringFor(timeZone: string | Temporal.TimeZoneProtocol, instant: Temporal.Instant) { const offsetNs = GetOffsetNanosecondsFor(timeZone, instant); - return formatOffsetStringNanoseconds(offsetNs); + return FormatUTCOffsetNanoseconds(offsetNs); } -// In the spec, the code below only exists as part of GetOffsetStringFor. -// But in the polyfill, we re-use it to provide clearer error messages. -function formatOffsetStringNanoseconds(offsetNs: number) { +export function FormatUTCOffsetNanoseconds(offsetNs: number) { const sign = offsetNs < 0 ? '-' : '+'; const absoluteNs = MathAbs(offsetNs); const hour = MathFloor(absoluteNs / 3600e9); diff --git a/lib/zoneddatetime.ts b/lib/zoneddatetime.ts index 9dc294d1..1b56533c 100644 --- a/lib/zoneddatetime.ts +++ b/lib/zoneddatetime.ts @@ -570,6 +570,7 @@ export class ZonedDateTime implements Temporal.ZonedDateTime { if (!ES.IsTemporalZonedDateTime(this)) throw new TypeError('invalid receiver'); const dt = dateTime(this); const tz = GetSlot(this, TIME_ZONE); + const offsetNanoseconds = ES.GetOffsetNanosecondsFor(tz, GetSlot(this, INSTANT)); return { calendar: GetSlot(this, CALENDAR), isoDay: GetSlot(dt, ISO_DAY), @@ -581,7 +582,7 @@ export class ZonedDateTime implements Temporal.ZonedDateTime { isoNanosecond: GetSlot(dt, ISO_NANOSECOND), isoSecond: GetSlot(dt, ISO_SECOND), isoYear: GetSlot(dt, ISO_YEAR), - offset: ES.GetOffsetStringFor(tz, GetSlot(this, INSTANT)), + offset: ES.FormatUTCOffsetNanoseconds(offsetNanoseconds), timeZone: tz }; } From 847f45e18f883d23dc5b0c8202b421b3ebb5b1df Mon Sep 17 00:00:00 2001 From: Justin Grant Date: Thu, 10 Aug 2023 05:00:29 -0700 Subject: [PATCH 15/39] Polyfill: reject sub-minute ISO string annotations The polyfill's regexes were still allowing sub-minute offsets for ISO strings, even though in the spec an ISO string is syntactically invalid if it has a sub-minute annotation. This commit fixes this polyfill bug. Tests for Instant are in https://github.com/tc39/test262/pull/3893. At some point in the future, we should probably add similar tests for all Temporal types that accept ISO strings. UPSTREAM_COMMIT=12740d19992f58c2a487168f8e90750ef1559ac6 --- lib/regex.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/regex.ts b/lib/regex.ts index 255fbba0..f70c2f9c 100644 --- a/lib/regex.ts +++ b/lib/regex.ts @@ -1,5 +1,5 @@ const tzComponent = /\.[-A-Za-z_]|\.\.[-A-Za-z._]{1,12}|\.[-A-Za-z_][-A-Za-z._]{0,12}|[A-Za-z_][-A-Za-z._]{0,13}/; -const offsetNoCapture = /(?:[+\u2212-][0-2][0-9](?::?[0-5][0-9](?::?[0-5][0-9](?:[.,]\d{1,9})?)?)?)/; +const offsetIdentifierNoCapture = /(?:[+\u2212-][0-2][0-9](?::?[0-5][0-9])?)/; export const timeZoneID = new RegExp( '(?:' + [ @@ -10,7 +10,7 @@ export const timeZoneID = new RegExp( 'CST6CDT', 'MST7MDT', 'PST8PDT', - offsetNoCapture.source + offsetIdentifierNoCapture.source ].join('|') + ')' ); From 356d3cfb6b3946e148e5a20c0ee8f5797eae7449 Mon Sep 17 00:00:00 2001 From: Justin Grant Date: Thu, 10 Aug 2023 05:14:48 -0700 Subject: [PATCH 16/39] Polyfill: Simplify time zone parsing * Update polyfill to match the refactored time zone parsing in the spec. * Rename `tzName` => `tzAnnotation` in the output of ParseISODateTime, to clarify that this field could be a name or an offset, and to avoid confusion with the `tzName` field that's returned from the new ParseTemporalTimeZoneString which really is an IANA name and is never an offset. * Many updates to validStrings.mjs tests to accommodate the changes above. UPSTREAM_COMMIT=2a5a15ba4ddb96c53fa1d51879075af00fb07d90 --- lib/ecmascript.ts | 137 ++++++++++++++++++++++++++---------------- lib/zoneddatetime.ts | 2 +- test/validStrings.mjs | 18 +++--- 3 files changed, 95 insertions(+), 62 deletions(-) diff --git a/lib/ecmascript.ts b/lib/ecmascript.ts index 2d93269d..194c5b79 100644 --- a/lib/ecmascript.ts +++ b/lib/ecmascript.ts @@ -569,7 +569,7 @@ function ParseISODateTime(isoString: string) { } else if (match[14]) { offset = match[14]; } - const tzName = match[15]; + const tzAnnotation = match[15]; const calendar = processAnnotations(match[16]); RejectDateTime(year, month, day, hour, minute, second, millisecond, microsecond, nanosecond); return { @@ -583,7 +583,7 @@ function ParseISODateTime(isoString: string) { millisecond, microsecond, nanosecond, - tzName, + tzAnnotation, offset, z, calendar @@ -600,7 +600,7 @@ export function ParseTemporalInstantString(isoString: string) { // ts-prune-ignore-next TODO: remove if test/validStrings is converted to TS. export function ParseTemporalZonedDateTimeString(isoString: string) { const result = ParseISODateTime(isoString); - if (!result.tzName) throw new RangeError('Temporal.ZonedDateTime requires a time zone ID in brackets'); + if (!result.tzAnnotation) throw new RangeError('Temporal.ZonedDateTime requires a time zone ID in brackets'); return result; } @@ -697,34 +697,54 @@ export function ParseTemporalMonthDayString(isoString: string) { const TIMEZONE_IDENTIFIER = new RegExp(`^${PARSE.timeZoneID.source}$`, 'i'); const OFFSET_IDENTIFIER = new RegExp(`^${PARSE.offsetIdentifier.source}$`); +function throwBadTimeZoneStringError(timeZoneString: string): never { + // Offset identifiers only support minute precision, but offsets in ISO + // strings support nanosecond precision. If the identifier is invalid but + // it's a valid ISO offset, then it has sub-minute precision. Show a clearer + // error message in that case. + const msg = OFFSET.test(timeZoneString) ? 'Seconds not allowed in offset time zone' : 'Invalid time zone'; + throw new RangeError(`${msg}: ${timeZoneString}`); +} + export function ParseTimeZoneIdentifier(identifier: string): { tzName?: string; offsetMinutes?: number } { - if (!TIMEZONE_IDENTIFIER.test(identifier)) throw new RangeError(`Invalid time zone identifier: ${identifier}`); + if (!TIMEZONE_IDENTIFIER.test(identifier)) { + throwBadTimeZoneStringError(identifier); + } if (OFFSET_IDENTIFIER.test(identifier)) { - // The regex limits the input to minutes precision - const { offsetNanoseconds } = ParseDateTimeUTCOffset(identifier); + const offsetNanoseconds = ParseDateTimeUTCOffset(identifier); + // The regex limits the input to minutes precision, so we know that the + // division below will result in an integer. return { offsetMinutes: offsetNanoseconds / 60e9 }; } return { tzName: identifier }; } +// This operation doesn't exist in the spec, but in the polyfill it's split from +// ParseTemporalTimeZoneString so that parsing can be tested separately from the +// logic of converting parsed values into a named or offset identifier. // ts-prune-ignore-next TODO: remove if test/validStrings is converted to TS. -export function ParseTemporalTimeZoneString(stringIdent: string): Partial<{ - tzName: string | undefined; - offset: string | undefined; - z: boolean | undefined; -}> { - const bareID = new RegExp(`^${PARSE.timeZoneID.source}$`, 'i'); - if (bareID.test(stringIdent)) return { tzName: stringIdent }; +export function ParseTemporalTimeZoneStringRaw(timeZoneString: string) { + if (TIMEZONE_IDENTIFIER.test(timeZoneString)) { + return { tzAnnotation: timeZoneString, offset: undefined, z: false }; + } try { // Try parsing ISO string instead - const result = ParseISODateTime(stringIdent); - if (result.z || result.offset || result.tzName) { - return result; + const { tzAnnotation, offset, z } = ParseISODateTime(timeZoneString); + if (z || tzAnnotation || offset) { + return { tzAnnotation, offset, z }; } } catch { // fall through } - throw new RangeError(`Invalid time zone: ${stringIdent}`); + throwBadTimeZoneStringError(timeZoneString); +} + +export function ParseTemporalTimeZoneString(stringIdent: string) { + const { tzAnnotation, offset, z } = ParseTemporalTimeZoneStringRaw(stringIdent); + if (tzAnnotation) return ParseTimeZoneIdentifier(tzAnnotation); + if (z) return ParseTimeZoneIdentifier('UTC'); + if (offset) return ParseTimeZoneIdentifier(offset); + throw new Error('this line should not be reached'); } // ts-prune-ignore-next TODO: remove if test/validStrings is converted to TS. @@ -788,8 +808,7 @@ export function ParseTemporalInstant(isoString: string) { if (!z && !offset) throw new RangeError('Temporal.Instant requires a time zone offset'); // At least one of z or offset is defined, but TS doesn't seem to understand // that we only use offset if z is not defined (and thus offset must be defined). - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion, @typescript-eslint/no-unnecessary-type-assertion - const offsetNs = z ? 0 : ParseDateTimeUTCOffset(offset!).offsetNanoseconds; + const offsetNs = z ? 0 : ParseDateTimeUTCOffset(castExists(offset)); ({ year, month, day, hour, minute, second, millisecond, microsecond, nanosecond } = BalanceISODateTime( year, month, @@ -1236,11 +1255,24 @@ export function ToRelativeTemporalObject(options: { timeZone = fields.timeZone; if (timeZone !== undefined) timeZone = ToTemporalTimeZoneSlotValue(timeZone); } else { - let tzName, z; - ({ year, month, day, hour, minute, second, millisecond, microsecond, nanosecond, calendar, tzName, offset, z } = - ParseISODateTime(RequireString(relativeTo))); - if (tzName) { - timeZone = ToTemporalTimeZoneSlotValue(tzName); + let tzAnnotation, z; + ({ + year, + month, + day, + hour, + minute, + second, + millisecond, + microsecond, + nanosecond, + calendar, + tzAnnotation, + offset, + z + } = ParseISODateTime(RequireString(relativeTo))); + if (tzAnnotation) { + timeZone = ToTemporalTimeZoneSlotValue(tzAnnotation); if (z) { offsetBehaviour = 'exact'; } else if (!offset) { @@ -1257,8 +1289,7 @@ export function ToRelativeTemporalObject(options: { calendar = ASCIILowercase(calendar); } if (timeZone === undefined) return CreateTemporalDate(year, month, day, calendar); - // If offset is missing here, then offsetBehavior will never be be 'option'. - const offsetNs = offsetBehaviour === 'option' ? ParseDateTimeUTCOffset(castExists(offset)).offsetNanoseconds : 0; + const offsetNs = offsetBehaviour === 'option' ? ParseDateTimeUTCOffset(castExists(offset)) : 0; const epochNanoseconds = InterpretISODateTimeOffset( year, month, @@ -1869,10 +1900,23 @@ export function ToTemporalZonedDateTime( options )); } else { - let tzName, z; - ({ year, month, day, hour, minute, second, millisecond, microsecond, nanosecond, tzName, offset, z, calendar } = - ParseTemporalZonedDateTimeString(RequireString(item))); - timeZone = ToTemporalTimeZoneSlotValue(tzName); + let tzAnnotation, z; + ({ + year, + month, + day, + hour, + minute, + second, + millisecond, + microsecond, + nanosecond, + tzAnnotation, + offset, + z, + calendar + } = ParseTemporalZonedDateTimeString(RequireString(item))); + timeZone = ToTemporalTimeZoneSlotValue(tzAnnotation); if (z) { offsetBehaviour = 'exact'; } else if (!offset) { @@ -1887,9 +1931,7 @@ export function ToTemporalZonedDateTime( ToTemporalOverflow(options); // validate and ignore } let offsetNs = 0; - // The code above guarantees that if offsetBehaviour === 'option', then - // `offset` is not undefined. - if (offsetBehaviour === 'option') offsetNs = ParseDateTimeUTCOffset(castExists(offset)).offsetNanoseconds; + if (offsetBehaviour === 'option') offsetNs = ParseDateTimeUTCOffset(castExists(offset)); const epochNanoseconds = InterpretISODateTimeOffset( year, month, @@ -2631,24 +2673,16 @@ export function ToTemporalTimeZoneSlotValue(temporalTimeZoneLike: TimeZoneParams } return temporalTimeZoneLike; } - const identifier = RequireString(temporalTimeZoneLike); - const { tzName, offset, z } = ParseTemporalTimeZoneString(identifier); - if (tzName) { - // tzName is any valid identifier string in brackets, and could be an offset identifier - const { offsetMinutes } = ParseTimeZoneIdentifier(tzName); - if (offsetMinutes !== undefined) return FormatOffsetTimeZoneIdentifier(offsetMinutes); + const timeZoneString = RequireString(temporalTimeZoneLike); - const record = GetAvailableNamedTimeZoneIdentifier(tzName); - if (!record) throw new RangeError(`Unrecognized time zone ${tzName}`); - return record.identifier; - } - if (z) return 'UTC'; - // if !tzName && !z then offset must be present - const { offsetNanoseconds, hasSubMinutePrecision } = ParseDateTimeUTCOffset(castExists(offset)); - if (hasSubMinutePrecision) { - throw new RangeError(`Seconds not allowed in offset time zone: ${offset}`); + const { tzName, offsetMinutes } = ParseTemporalTimeZoneString(timeZoneString); + if (offsetMinutes !== undefined) { + return FormatOffsetTimeZoneIdentifier(offsetMinutes); } - return FormatOffsetTimeZoneIdentifier(offsetNanoseconds / 60e9); + // if offsetMinutes is undefined, then tzName must be present + const record = GetAvailableNamedTimeZoneIdentifier(castExists(tzName)); + if (!record) throw new RangeError(`Unrecognized time zone ${tzName}`); + return record.identifier; } export function ToTemporalTimeZoneIdentifier(slotValue: TimeZoneSlot) { @@ -3194,7 +3228,7 @@ export function IsOffsetTimeZoneIdentifier(string: string): boolean { return OFFSET.test(string); } -export function ParseDateTimeUTCOffset(string: string): { offsetNanoseconds: number; hasSubMinutePrecision: boolean } { +export function ParseDateTimeUTCOffset(string: string) { const match = OFFSET_WITH_PARTS.exec(string); if (!match) { throw new RangeError(`invalid time zone offset: ${string}`); @@ -3205,8 +3239,7 @@ export function ParseDateTimeUTCOffset(string: string): { offsetNanoseconds: num const seconds = +(match[4] || 0); const nanoseconds = +((match[5] || 0) + '000000000').slice(0, 9); const offsetNanoseconds = sign * (((hours * 60 + minutes) * 60 + seconds) * 1e9 + nanoseconds); - const hasSubMinutePrecision = match[4] !== undefined || match[5] !== undefined; - return { offsetNanoseconds, hasSubMinutePrecision }; + return offsetNanoseconds; } let canonicalTimeZoneIdsCache: Map | undefined | null = undefined; diff --git a/lib/zoneddatetime.ts b/lib/zoneddatetime.ts index 1b56533c..4d48e85f 100644 --- a/lib/zoneddatetime.ts +++ b/lib/zoneddatetime.ts @@ -210,7 +210,7 @@ export class ZonedDateTime implements Temporal.ZonedDateTime { let { year, month, day, hour, minute, second, millisecond, microsecond, nanosecond } = ES.InterpretTemporalDateTimeFields(calendar, fields, options); - const offsetNs = ES.ParseDateTimeUTCOffset(fields.offset).offsetNanoseconds; + const offsetNs = ES.ParseDateTimeUTCOffset(fields.offset); const timeZone = GetSlot(this, TIME_ZONE); const epochNanoseconds = ES.InterpretISODateTimeOffset( year, diff --git a/test/validStrings.mjs b/test/validStrings.mjs index 5eb98226..3f6af5b3 100644 --- a/test/validStrings.mjs +++ b/test/validStrings.mjs @@ -269,7 +269,7 @@ const timeZoneUTCOffsetName = seq(sign, hour, choice([minuteSecond], seq(':', mi const timeZoneIANAName = choice(...timezoneNames); const timeZoneIdentifier = withCode( choice(timeZoneUTCOffsetName, timeZoneIANAName), - (data, result) => (data.tzName = result) + (data, result) => (data.tzAnnotation = result) ); const timeZoneAnnotation = seq('[', [annotationCriticalFlag], timeZoneIdentifier, ']'); const aKeyLeadingChar = choice(lcalpha(), character('_')); @@ -446,9 +446,9 @@ const comparisonItems = { ], MonthDay: ['month', 'day', 'calendar'], Time: [...timeItems], - TimeZone: ['offset', 'tzName'], + TimeZone: ['offset', 'tzAnnotation'], YearMonth: ['year', 'month', 'calendar'], - ZonedDateTime: [...dateItems, ...timeItems, 'offset', 'tzName', 'calendar'] + ZonedDateTime: [...dateItems, ...timeItems, 'offset', 'tzAnnotation', 'calendar'] }; const plainModes = ['Date', 'DateTime', 'MonthDay', 'Time', 'YearMonth']; @@ -461,14 +461,14 @@ function fuzzMode(mode) { fuzzed = goals[mode].generate(generatedData); } while (plainModes.includes(mode) && /[0-9][zZ]/.test(fuzzed)); try { - const parsed = ES[`ParseTemporal${mode}String`](fuzzed); + const parsingMethod = ES[`ParseTemporal${mode}StringRaw`] ?? ES[`ParseTemporal${mode}String`]; + const parsed = parsingMethod(fuzzed); for (let prop of comparisonItems[mode]) { let expected = generatedData[prop]; - if (prop !== 'tzName' && prop !== 'offset' && prop !== 'calendar') expected = expected || 0; - if (prop === 'offset' && expected) { - const parsedResult = ES.ParseDateTimeUTCOffset(parsed[prop]); - assert.equal(parsedResult.offsetNanoseconds, expected.offsetNanoseconds); - assert.equal(parsedResult.hasSubMinutePrecision, expected.hasSubMinutePrecision); + if (!['tzAnnotation', 'offset', 'calendar'].includes(prop)) expected ??= 0; + if (prop === 'offset') { + const parsedResult = parsed[prop] === undefined ? undefined : ES.ParseDateTimeUTCOffset(parsed[prop]); + assert.equal(parsedResult, expected, prop); } else { assert.equal(parsed[prop], expected, prop); } From 31b1f180006cf80a8d37e3984c28943e68a15336 Mon Sep 17 00:00:00 2001 From: Justin Grant Date: Thu, 10 Aug 2023 11:52:30 -0700 Subject: [PATCH 17/39] Update Test262 As part of this PR, I found some Test262 gaps and bugs. This commit catches up to include those new tests. UPSTREAM_COMMIT=e84c82fad3dbf2ffe2eb534a448239329adbd040 --- test262 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test262 b/test262 index 66f3959c..0c87a86b 160000 --- a/test262 +++ b/test262 @@ -1 +1 @@ -Subproject commit 66f3959c14646a4caba79e91adfe976c28246bf9 +Subproject commit 0c87a86b58391b40aa7623b919603d87d4b77a4d From a3f78a0da4cf3e1ca0b2fd917cd1dae506fbab14 Mon Sep 17 00:00:00 2001 From: Justin Grant Date: Fri, 11 Aug 2023 10:27:46 -0700 Subject: [PATCH 18/39] Polyfill: Test parsing of Z designation UPSTREAM_COMMIT=38f3f0f369ef59dfc041cd62ec11c566714b131e --- test/validStrings.mjs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/validStrings.mjs b/test/validStrings.mjs index 3f6af5b3..048daec5 100644 --- a/test/validStrings.mjs +++ b/test/validStrings.mjs @@ -206,7 +206,7 @@ const timeDesignator = character('Tt'); const weeksDesignator = character('Ww'); const yearsDesignator = character('Yy'); const utcDesignator = withCode(character('Zz'), (data) => { - data.z = 'Z'; + data.z = true; }); const annotationCriticalFlag = character('!'); const fraction = seq(decimalSeparator, between(1, 9, digit())); @@ -429,7 +429,7 @@ const goals = { const dateItems = ['year', 'month', 'day']; const timeItems = ['hour', 'minute', 'second', 'millisecond', 'microsecond', 'nanosecond']; const comparisonItems = { - Instant: [...dateItems, ...timeItems, 'offset'], + Instant: [...dateItems, ...timeItems, 'offset', 'z'], Date: [...dateItems, 'calendar'], DateTime: [...dateItems, ...timeItems, 'calendar'], Duration: [ @@ -446,9 +446,9 @@ const comparisonItems = { ], MonthDay: ['month', 'day', 'calendar'], Time: [...timeItems], - TimeZone: ['offset', 'tzAnnotation'], + TimeZone: ['offset', 'tzAnnotation', 'z'], YearMonth: ['year', 'month', 'calendar'], - ZonedDateTime: [...dateItems, ...timeItems, 'offset', 'tzAnnotation', 'calendar'] + ZonedDateTime: [...dateItems, ...timeItems, 'offset', 'z', 'tzAnnotation', 'calendar'] }; const plainModes = ['Date', 'DateTime', 'MonthDay', 'Time', 'YearMonth']; @@ -465,7 +465,7 @@ function fuzzMode(mode) { const parsed = parsingMethod(fuzzed); for (let prop of comparisonItems[mode]) { let expected = generatedData[prop]; - if (!['tzAnnotation', 'offset', 'calendar'].includes(prop)) expected ??= 0; + if (!['tzAnnotation', 'offset', 'calendar'].includes(prop)) expected ??= prop === 'z' ? false : 0; if (prop === 'offset') { const parsedResult = parsed[prop] === undefined ? undefined : ES.ParseDateTimeUTCOffset(parsed[prop]); assert.equal(parsedResult, expected, prop); From 50b1618cb682313648d2dd5f6556f45d2f96889e Mon Sep 17 00:00:00 2001 From: Justin Grant Date: Mon, 14 Aug 2023 10:56:00 -0700 Subject: [PATCH 19/39] Polyfill: Remove throw in ParseTemporalInstant Found by codecov: ParseTemporalInstantString already throws if neither `z` nor `offset` are present, so there's no need to check again after ParseTemporalInstantString returns. UPSTREAM_COMMIT=49ba35e39ed7fc27b7c49750142913c1fa6b75c8 --- lib/ecmascript.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/ecmascript.ts b/lib/ecmascript.ts index 194c5b79..d71d3410 100644 --- a/lib/ecmascript.ts +++ b/lib/ecmascript.ts @@ -805,9 +805,7 @@ export function ParseTemporalInstant(isoString: string) { let { year, month, day, hour, minute, second, millisecond, microsecond, nanosecond, offset, z } = ParseTemporalInstantString(isoString); - if (!z && !offset) throw new RangeError('Temporal.Instant requires a time zone offset'); - // At least one of z or offset is defined, but TS doesn't seem to understand - // that we only use offset if z is not defined (and thus offset must be defined). + // ParseTemporalInstantString ensures that either `z` or `offset` are non-undefined const offsetNs = z ? 0 : ParseDateTimeUTCOffset(castExists(offset)); ({ year, month, day, hour, minute, second, millisecond, microsecond, nanosecond } = BalanceISODateTime( year, From a3bf70f5505434c9d97b2768d682fcdc8ba56c74 Mon Sep 17 00:00:00 2001 From: Justin Grant Date: Mon, 14 Aug 2023 12:05:45 -0700 Subject: [PATCH 20/39] Polyfill: RoundDuration TypeError=>assertion Align RoundDuration checking of `relativeTo` to spec. All callers of this AO guarantee that `relativeTo` is one of `undefined`, a ZDT instance, or a PlainDate instance. UPSTREAM_COMMIT=27e045263d66e1c8bec6ec01ab6bb500555ee7df --- lib/ecmascript.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/ecmascript.ts b/lib/ecmascript.ts index d71d3410..02b61b24 100644 --- a/lib/ecmascript.ts +++ b/lib/ecmascript.ts @@ -6330,7 +6330,7 @@ export function RoundDuration( zonedRelativeTo = relativeTo; relativeTo = ToTemporalDate(relativeTo); } else if (!IsTemporalDate(relativeTo)) { - throw new TypeError('starting point must be PlainDate or ZonedDateTime'); + throw new Error('assertion failure in RoundDuration: _relativeTo_ must be PlainDate'); } calendar = GetSlot(relativeTo, CALENDAR); } From 72fd4a3a79392ff6fb24dc65781d6d51d38fabe3 Mon Sep 17 00:00:00 2001 From: Philip Chimento Date: Thu, 2 Mar 2023 12:39:41 -0800 Subject: [PATCH 21/39] Normative: Avoid recalculating offset ns after GetPlainDateTimeFor If we convert an exact time into a PlainDateTime, we're already calculating the time zone's UTC offset for that exact time. In the case where we need the UTC offset for another purpose in the same method, we should not call the time zone method again to recalculate it. Instead, we call the getOffsetNanosecondsFor method once, and pass the result to the GetPlainDateTimeFor abstract operation. This affects the following methods, removing an observable property Get and observable Call to getOffsetNanosecondsFor: - Temporal.Instant.prototype.toString - Temporal.ZonedDateTime.prototype.toString - Temporal.ZonedDateTime.prototype.round - Temporal.ZonedDateTime.prototype.getISOFields UPSTREAM_COMMIT=b7a30a11afe25d7868b74ffe9008de86e7f1890d --- lib/ecmascript.ts | 13 +++++++------ lib/zoneddatetime.ts | 17 +++++++++++------ 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/lib/ecmascript.ts b/lib/ecmascript.ts index 02b61b24..e8b5fce7 100644 --- a/lib/ecmascript.ts +++ b/lib/ecmascript.ts @@ -2781,10 +2781,11 @@ export function FormatUTCOffsetNanoseconds(offsetNs: number) { export function GetPlainDateTimeFor( timeZone: string | Temporal.TimeZoneProtocol, instant: Temporal.Instant, - calendar: CalendarSlot + calendar: CalendarSlot, + precalculatedOffsetNs: number | undefined = undefined ) { const ns = GetSlot(instant, EPOCHNANOSECONDS); - const offsetNs = GetOffsetNanosecondsFor(timeZone, instant); + const offsetNs = precalculatedOffsetNs ?? GetOffsetNanosecondsFor(timeZone, instant); let { year, month, day, hour, minute, second, millisecond, microsecond, nanosecond } = GetISOPartsFromEpoch(ns); ({ year, month, day, hour, minute, second, millisecond, microsecond, nanosecond } = BalanceISODateTime( year, @@ -3017,11 +3018,11 @@ export function TemporalInstantToString( ) { let outputTimeZone = timeZone; if (outputTimeZone === undefined) outputTimeZone = 'UTC'; - const dateTime = GetPlainDateTimeFor(outputTimeZone, instant, 'iso8601'); + const offsetNs = GetOffsetNanosecondsFor(outputTimeZone, instant); + const dateTime = GetPlainDateTimeFor(outputTimeZone, instant, 'iso8601', offsetNs); const dateTimeString = TemporalDateTimeToString(dateTime, precision, 'never'); let timeZoneString = 'Z'; if (timeZone !== undefined) { - const offsetNs = GetOffsetNanosecondsFor(outputTimeZone, instant); timeZoneString = FormatDateTimeUTCOffsetRounded(offsetNs); } return `${dateTimeString}${timeZoneString}`; @@ -3207,10 +3208,10 @@ export function TemporalZonedDateTimeToString( } const tz = GetSlot(zdt, TIME_ZONE); - const dateTime = GetPlainDateTimeFor(tz, instant, 'iso8601'); + const offsetNs = GetOffsetNanosecondsFor(tz, instant); + const dateTime = GetPlainDateTimeFor(tz, instant, 'iso8601', offsetNs); let dateTimeString = TemporalDateTimeToString(dateTime, precision, 'never'); if (showOffset !== 'never') { - const offsetNs = GetOffsetNanosecondsFor(tz, instant); dateTimeString += FormatDateTimeUTCOffsetRounded(offsetNs); } if (showTimeZone !== 'never') { diff --git a/lib/zoneddatetime.ts b/lib/zoneddatetime.ts index 4d48e85f..1602ae62 100644 --- a/lib/zoneddatetime.ts +++ b/lib/zoneddatetime.ts @@ -355,7 +355,9 @@ export class ZonedDateTime implements Temporal.ZonedDateTime { ES.ValidateTemporalRoundingIncrement(roundingIncrement, maximum, inclusive); // first, round the underlying DateTime fields - const dt = dateTime(this); + const timeZone = GetSlot(this, TIME_ZONE); + const offsetNs = ES.GetOffsetNanosecondsFor(timeZone, GetSlot(this, INSTANT)); + const dt = dateTime(this, offsetNs); let year = GetSlot(dt, ISO_YEAR); let month = GetSlot(dt, ISO_MONTH); let day = GetSlot(dt, ISO_DAY); @@ -367,7 +369,6 @@ export class ZonedDateTime implements Temporal.ZonedDateTime { let nanosecond = GetSlot(dt, ISO_NANOSECOND); const DateTime = GetIntrinsic('%Temporal.PlainDateTime%'); - const timeZone = GetSlot(this, TIME_ZONE); const calendar = GetSlot(this, CALENDAR); const dtStart = new DateTime(GetSlot(dt, ISO_YEAR), GetSlot(dt, ISO_MONTH), GetSlot(dt, ISO_DAY), 0, 0, 0, 0, 0, 0); const instantStart = ES.GetInstantFor(timeZone, dtStart, 'compatible'); @@ -399,7 +400,6 @@ export class ZonedDateTime implements Temporal.ZonedDateTime { // offset. Otherwise the offset will be changed to be compatible with the // new date/time values. If DST disambiguation is required, the `compatible` // disambiguation algorithm will be used. - const offsetNs = ES.GetOffsetNanosecondsFor(timeZone, GetSlot(this, INSTANT)); const epochNanoseconds = ES.InterpretISODateTimeOffset( year, month, @@ -568,9 +568,9 @@ export class ZonedDateTime implements Temporal.ZonedDateTime { } getISOFields(): Return['getISOFields'] { if (!ES.IsTemporalZonedDateTime(this)) throw new TypeError('invalid receiver'); - const dt = dateTime(this); const tz = GetSlot(this, TIME_ZONE); const offsetNanoseconds = ES.GetOffsetNanosecondsFor(tz, GetSlot(this, INSTANT)); + const dt = dateTime(this, offsetNanoseconds); return { calendar: GetSlot(this, CALENDAR), isoDay: GetSlot(dt, ISO_DAY), @@ -623,6 +623,11 @@ export class ZonedDateTime implements Temporal.ZonedDateTime { MakeIntrinsicClass(ZonedDateTime, 'Temporal.ZonedDateTime'); -function dateTime(zdt: Temporal.ZonedDateTime) { - return ES.GetPlainDateTimeFor(GetSlot(zdt, TIME_ZONE), GetSlot(zdt, INSTANT), GetSlot(zdt, CALENDAR)); +function dateTime(zdt: Temporal.ZonedDateTime, precalculatedOffsetNs: number | undefined = undefined) { + return ES.GetPlainDateTimeFor( + GetSlot(zdt, TIME_ZONE), + GetSlot(zdt, INSTANT), + GetSlot(zdt, CALENDAR), + precalculatedOffsetNs + ); } From 048610024501b5e881f51e717ba56bc66780c624 Mon Sep 17 00:00:00 2001 From: Philip Chimento Date: Thu, 2 Mar 2023 12:55:16 -0800 Subject: [PATCH 22/39] Normative: Limit allowed values for Calendar.p.fields to date units Time units are no longer included in the array passed as the argument of Calendar.p.fields(). (And as long as we're doing this, we may as well limit fields() so that it doesn't accept time units. They are never used.) This doesn't eliminate any user-visible calls by itself, but is a prerequisite for eliminating the visible Gets of time unit properties on the receiver of PlainDateTime.p.with() and ZonedDateTime.p.with(). UPSTREAM_COMMIT=32cbd55dbf64838a6113d460705f674c3c4bc67d --- lib/calendar.ts | 13 +------------ lib/ecmascript.ts | 46 ++++++++++++++++++-------------------------- lib/plaindatetime.ts | 16 ++++----------- lib/zoneddatetime.ts | 17 +++++++++------- 4 files changed, 34 insertions(+), 58 deletions(-) diff --git a/lib/calendar.ts b/lib/calendar.ts index 7ab85fab..e5b513dc 100644 --- a/lib/calendar.ts +++ b/lib/calendar.ts @@ -180,18 +180,7 @@ export class Calendar implements Temporal.Calendar { fields(fields: Params['fields'][0]): Return['fields'] { if (!ES.IsTemporalCalendar(this)) throw new TypeError('invalid receiver'); const fieldsArray = [] as string[]; - const allowed = new Set([ - 'year', - 'month', - 'monthCode', - 'day', - 'hour', - 'minute', - 'second', - 'millisecond', - 'microsecond', - 'nanosecond' - ]); + const allowed = new Set(['year', 'month', 'monthCode', 'day']); for (const name of fields) { if (typeof name !== 'string') throw new TypeError('invalid fields'); if (!allowed.has(name)) throw new RangeError(`invalid field name ${name}`); diff --git a/lib/ecmascript.ts b/lib/ecmascript.ts index e8b5fce7..3d0753d1 100644 --- a/lib/ecmascript.ts +++ b/lib/ecmascript.ts @@ -138,9 +138,6 @@ export function uncheckedAssertNarrowedType( ): asserts arg is T extends typeof arg ? T : never {} /* eslint-enable */ -type ArrayElement = ArrayType extends readonly (infer ElementType)[] ? ElementType : never; -type ArrayWithNewKeys = Array | Keys>; - /** * In debug builds, this function verifies that the given argument "exists" (is not * null or undefined). This function becomes a no-op in the final bundles distributed via NPM. @@ -1226,20 +1223,22 @@ export function ToRelativeTemporalObject(options: { if (IsTemporalZonedDateTime(relativeTo) || IsTemporalDate(relativeTo)) return relativeTo; if (IsTemporalDateTime(relativeTo)) return TemporalDateTimeToDate(relativeTo); calendar = GetTemporalCalendarSlotValueWithISODefault(relativeTo); - const fieldNames = CalendarFields(calendar, [ + const fieldNames: (keyof Temporal.ZonedDateTimeLike)[] = CalendarFields(calendar, [ 'day', + 'month', + 'monthCode', + 'year' + ]); + Call(ArrayPrototypePush, fieldNames, [ 'hour', 'microsecond', 'millisecond', 'minute', - 'month', - 'monthCode', 'nanosecond', + 'offset', 'second', - 'year' - ] as const); - type FieldNamesWithTimeZoneAndOffset = ArrayWithNewKeys; - (fieldNames as FieldNamesWithTimeZoneAndOffset).push('timeZone', 'offset'); + 'timeZone' + ]); const fields = PrepareTemporalFields(relativeTo, fieldNames, []); const dateOptions = ObjectCreate(null) as Temporal.AssignmentOptions; dateOptions.overflow = 'constrain'; @@ -1597,18 +1596,8 @@ export function ToTemporalDateTime(item: PlainDateTimeParams['from'][0], options } calendar = GetTemporalCalendarSlotValueWithISODefault(item); - const fieldNames = CalendarFields(calendar, [ - 'day', - 'hour', - 'microsecond', - 'millisecond', - 'minute', - 'month', - 'monthCode', - 'nanosecond', - 'second', - 'year' - ] as const); + const fieldNames = CalendarFields(calendar, ['day', 'month', 'monthCode', 'year']); + Call(ArrayPrototypePush, fieldNames, ['hour', 'microsecond', 'millisecond', 'minute', 'nanosecond', 'second']); const fields = PrepareTemporalFields(item, fieldNames, []); ({ year, month, day, hour, minute, second, millisecond, microsecond, nanosecond } = InterpretTemporalDateTimeFields( calendar, @@ -1873,17 +1862,20 @@ export function ToTemporalZonedDateTime( calendar = GetTemporalCalendarSlotValueWithISODefault(item); const fieldNames: (keyof Temporal.ZonedDateTimeLike)[] = CalendarFields(calendar, [ 'day', + 'month', + 'monthCode', + 'year' + ]); + Call(ArrayPrototypePush, fieldNames, [ 'hour', 'microsecond', 'millisecond', 'minute', - 'month', - 'monthCode', 'nanosecond', + 'offset', 'second', - 'year' - ] as const); - fieldNames.push('timeZone', 'offset'); + 'timeZone' + ]); const fields = PrepareTemporalFields(item, fieldNames, ['timeZone']); timeZone = ToTemporalTimeZoneSlotValue(fields.timeZone); offset = fields.offset; diff --git a/lib/plaindatetime.ts b/lib/plaindatetime.ts index c9b0f11d..e7591684 100644 --- a/lib/plaindatetime.ts +++ b/lib/plaindatetime.ts @@ -19,6 +19,8 @@ import type { Temporal } from '..'; import { DateTimeFormat } from './intl'; import type { PlainDateTimeParams as Params, PlainDateTimeReturn as Return } from './internaltypes'; +const ArrayPrototypePush = Array.prototype.push; + export class PlainDateTime implements Temporal.PlainDateTime { constructor( isoYearParam: Params['constructor'][0], @@ -154,18 +156,8 @@ export class PlainDateTime implements Temporal.PlainDateTime { const options = ES.GetOptionsObject(optionsParam); const calendar = GetSlot(this, CALENDAR); - const fieldNames = ES.CalendarFields(calendar, [ - 'day', - 'hour', - 'microsecond', - 'millisecond', - 'minute', - 'month', - 'monthCode', - 'nanosecond', - 'second', - 'year' - ] as const); + const fieldNames = ES.CalendarFields(calendar, ['day', 'month', 'monthCode', 'year']); + ES.Call(ArrayPrototypePush, fieldNames, ['hour', 'microsecond', 'millisecond', 'minute', 'nanosecond', 'second']); let fields = ES.PrepareTemporalFields(this, fieldNames, []); const partialDateTime = ES.PrepareTemporalFields(temporalDateTimeLike, fieldNames, 'partial'); fields = ES.CalendarMergeFields(calendar, fields, partialDateTime); diff --git a/lib/zoneddatetime.ts b/lib/zoneddatetime.ts index 1602ae62..83d33c7f 100644 --- a/lib/zoneddatetime.ts +++ b/lib/zoneddatetime.ts @@ -23,6 +23,7 @@ import type { ZonedDateTimeParams as Params, ZonedDateTimeReturn as Return } fro import JSBI from 'jsbi'; import { BILLION, MILLION, THOUSAND, ZERO, HOUR_NANOS } from './ecmascript'; +const ArrayPrototypePush = Array.prototype.push; const customResolvedOptions = DateTimeFormat.prototype.resolvedOptions as Intl.DateTimeFormat['resolvedOptions']; export class ZonedDateTime implements Temporal.ZonedDateTime { @@ -187,19 +188,21 @@ export class ZonedDateTime implements Temporal.ZonedDateTime { const options = ES.GetOptionsObject(optionsParam); const calendar = GetSlot(this, CALENDAR); - let fieldNames: (keyof Temporal.ZonedDateTimeLike)[] = ES.CalendarFields(calendar, [ + const fieldNames: (keyof Temporal.ZonedDateTimeLike)[] = ES.CalendarFields(calendar, [ 'day', + 'month', + 'monthCode', + 'year' + ]); + ES.Call(ArrayPrototypePush, fieldNames, [ 'hour', 'microsecond', 'millisecond', 'minute', - 'month', - 'monthCode', 'nanosecond', - 'second', - 'year' - ] as const); - fieldNames.push('offset'); + 'offset', + 'second' + ]); let fields = ES.PrepareTemporalFields(this, fieldNames, ['offset']); const partialZonedDateTime = ES.PrepareTemporalFields(temporalZonedDateTimeLike, fieldNames, 'partial'); fields = ES.CalendarMergeFields(calendar, fields, partialZonedDateTime); From 31f8b39a5694e5fdd1494f44ef1462bd726b2719 Mon Sep 17 00:00:00 2001 From: Philip Chimento Date: Thu, 2 Mar 2023 15:14:43 -0800 Subject: [PATCH 23/39] Normative: Get receiver's time units from internal slots in with() In PlainDateTime.p.with() and ZonedDateTime.p.with(), avoid calling the property getters to obtain the values for the time units, since they do not need to go through the calendar; we can unobservably get the same values from the receiver's internal slots. In ZonedDateTime.p.with(), additionally we don't need to call the getter for the `offset` property. Since we have the offset nanoseconds already, we can do what the getter does and format an offset string with FormatTimeZoneOffsetString. UPSTREAM_COMMIT=e8e4501d27d463ffbe25994cb6a46689c9a3c9d2 --- lib/plaindatetime.ts | 8 +++++++- lib/zoneddatetime.ts | 17 +++++++++++++---- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/lib/plaindatetime.ts b/lib/plaindatetime.ts index e7591684..d67baf47 100644 --- a/lib/plaindatetime.ts +++ b/lib/plaindatetime.ts @@ -157,8 +157,14 @@ export class PlainDateTime implements Temporal.PlainDateTime { const options = ES.GetOptionsObject(optionsParam); const calendar = GetSlot(this, CALENDAR); const fieldNames = ES.CalendarFields(calendar, ['day', 'month', 'monthCode', 'year']); - ES.Call(ArrayPrototypePush, fieldNames, ['hour', 'microsecond', 'millisecond', 'minute', 'nanosecond', 'second']); let fields = ES.PrepareTemporalFields(this, fieldNames, []); + fields.hour = GetSlot(this, ISO_HOUR); + fields.minute = GetSlot(this, ISO_MINUTE); + fields.second = GetSlot(this, ISO_SECOND); + fields.millisecond = GetSlot(this, ISO_MILLISECOND); + fields.microsecond = GetSlot(this, ISO_MICROSECOND); + fields.nanosecond = GetSlot(this, ISO_NANOSECOND); + ES.Call(ArrayPrototypePush, fieldNames, ['hour', 'microsecond', 'millisecond', 'minute', 'nanosecond', 'second']); const partialDateTime = ES.PrepareTemporalFields(temporalDateTimeLike, fieldNames, 'partial'); fields = ES.CalendarMergeFields(calendar, fields, partialDateTime); fields = ES.PrepareTemporalFields(fields, fieldNames, []); diff --git a/lib/zoneddatetime.ts b/lib/zoneddatetime.ts index 83d33c7f..42d7bce0 100644 --- a/lib/zoneddatetime.ts +++ b/lib/zoneddatetime.ts @@ -194,6 +194,17 @@ export class ZonedDateTime implements Temporal.ZonedDateTime { 'monthCode', 'year' ]); + let fields = ES.PrepareTemporalFields(this, fieldNames, []); + const timeZone = GetSlot(this, TIME_ZONE); + const offsetNs = ES.GetOffsetNanosecondsFor(timeZone, GetSlot(this, INSTANT)); + const dt = dateTime(this, offsetNs); + fields.hour = GetSlot(dt, ISO_HOUR); + fields.minute = GetSlot(dt, ISO_MINUTE); + fields.second = GetSlot(dt, ISO_SECOND); + fields.millisecond = GetSlot(dt, ISO_MILLISECOND); + fields.microsecond = GetSlot(dt, ISO_MICROSECOND); + fields.nanosecond = GetSlot(dt, ISO_NANOSECOND); + fields.offset = ES.FormatUTCOffsetNanoseconds(offsetNs); ES.Call(ArrayPrototypePush, fieldNames, [ 'hour', 'microsecond', @@ -203,7 +214,6 @@ export class ZonedDateTime implements Temporal.ZonedDateTime { 'offset', 'second' ]); - let fields = ES.PrepareTemporalFields(this, fieldNames, ['offset']); const partialZonedDateTime = ES.PrepareTemporalFields(temporalZonedDateTimeLike, fieldNames, 'partial'); fields = ES.CalendarMergeFields(calendar, fields, partialZonedDateTime); fields = ES.PrepareTemporalFields(fields, fieldNames, ['offset']); @@ -213,8 +223,7 @@ export class ZonedDateTime implements Temporal.ZonedDateTime { let { year, month, day, hour, minute, second, millisecond, microsecond, nanosecond } = ES.InterpretTemporalDateTimeFields(calendar, fields, options); - const offsetNs = ES.ParseDateTimeUTCOffset(fields.offset); - const timeZone = GetSlot(this, TIME_ZONE); + const newOffsetNs = ES.ParseDateTimeUTCOffset(fields.offset); const epochNanoseconds = ES.InterpretISODateTimeOffset( year, month, @@ -226,7 +235,7 @@ export class ZonedDateTime implements Temporal.ZonedDateTime { microsecond, nanosecond, 'option', - offsetNs, + newOffsetNs, timeZone, disambiguation, offset, From 339afdf7ddd3f1ab7afe25245266b8b1bc73df6b Mon Sep 17 00:00:00 2001 From: Philip Chimento Date: Thu, 2 Mar 2023 15:22:31 -0800 Subject: [PATCH 24/39] Normative: Get receiver fields from a PlainDateTime in ZonedDateTime.with() Instead of calling PrepareTemporalFields on the receiver in ZonedDateTime.p.with() to get the values of the date units, first create a PlainDateTime from the ZonedDateTime's exact time and time zone, and call PrepareTemporalFields on that. This still calls the corresponding calendar method for each field, but avoids calling the time zone's getOffsetNanosecondsFor() method redundantly for each field. UPSTREAM_COMMIT=ec00d92822b6ffaef233417f0a83e7cd0a47f7e6 --- lib/zoneddatetime.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/zoneddatetime.ts b/lib/zoneddatetime.ts index 42d7bce0..0242178a 100644 --- a/lib/zoneddatetime.ts +++ b/lib/zoneddatetime.ts @@ -188,16 +188,16 @@ export class ZonedDateTime implements Temporal.ZonedDateTime { const options = ES.GetOptionsObject(optionsParam); const calendar = GetSlot(this, CALENDAR); + const timeZone = GetSlot(this, TIME_ZONE); + const offsetNs = ES.GetOffsetNanosecondsFor(timeZone, GetSlot(this, INSTANT)); + const dt = dateTime(this, offsetNs); const fieldNames: (keyof Temporal.ZonedDateTimeLike)[] = ES.CalendarFields(calendar, [ 'day', 'month', 'monthCode', 'year' ]); - let fields = ES.PrepareTemporalFields(this, fieldNames, []); - const timeZone = GetSlot(this, TIME_ZONE); - const offsetNs = ES.GetOffsetNanosecondsFor(timeZone, GetSlot(this, INSTANT)); - const dt = dateTime(this, offsetNs); + let fields = ES.PrepareTemporalFields(dt, fieldNames, []); fields.hour = GetSlot(dt, ISO_HOUR); fields.minute = GetSlot(dt, ISO_MINUTE); fields.second = GetSlot(dt, ISO_SECOND); From d6147f6411eb68eb81af0334c4fc845f08a10618 Mon Sep 17 00:00:00 2001 From: Philip Chimento Date: Thu, 2 Mar 2023 17:18:25 -0800 Subject: [PATCH 25/39] Normative: Copy options object in {Plain,Zoned}DateTime.{from,p.with} Following the precedent set in #2447, if we're going to pass the options object to a calendar method we should make a copy of it. Also flatten the 'options' property once it's read and converted to a string in InterpretTemporalDateTimeFields, so that it doesn't have to be observably converted to a string again in Calendar.p.dateFromFields(). In PlainDateTime.from, delay validation of the options until after validation of the ISO string, for consistency with ZonedDateTime.from and in accordance with our general principle of validating arguments in order. This affects the following APIs, which are all callers of InterpretTemporalDateTimeFields: - Temporal.PlainDateTime.from() - Temporal.PlainDateTime.prototype.with() - Temporal.ZonedDateTime.from() - Temporal.ZonedDateTime.prototype.with() It does not affect ToRelativeTemporalObject, even though that also calls InterpretTemporalDateTimeFields, because it does not take an options object from userland. UPSTREAM_COMMIT=c775f411df08b245f754ca63ea725f170e62349c --- lib/ecmascript.ts | 53 +++++++++++++++----------------------------- lib/plaindatetime.ts | 6 ++--- lib/zoneddatetime.ts | 10 ++++----- 3 files changed, 26 insertions(+), 43 deletions(-) diff --git a/lib/ecmascript.ts b/lib/ecmascript.ts index 3d0753d1..f19ff91c 100644 --- a/lib/ecmascript.ts +++ b/lib/ecmascript.ts @@ -1542,10 +1542,11 @@ export function ToTemporalDate( export function InterpretTemporalDateTimeFields( calendar: CalendarSlot, fields: PrimitiveFieldsOf & Parameters[1], - options?: Temporal.AssignmentOptions + options: Temporal.AssignmentOptions ) { let { hour, minute, second, millisecond, microsecond, nanosecond } = ToTemporalTimeRecord(fields); const overflow = ToTemporalOverflow(options); + options.overflow = overflow; // options is always an internal object, so not observable const date = CalendarDateFromFields(calendar, fields, options); const year = GetSlot(date, ISO_YEAR); const month = GetSlot(date, ISO_MONTH); @@ -1563,24 +1564,17 @@ export function InterpretTemporalDateTimeFields( } export function ToTemporalDateTime(item: PlainDateTimeParams['from'][0], options?: PlainDateTimeParams['from'][1]) { - let year: number, - month: number, - day: number, - hour: number, - minute: number, - second: number, - millisecond: number, - microsecond: number, - nanosecond: number, - calendar; + let year, month, day, hour, minute, second, millisecond, microsecond, nanosecond, calendar; + const resolvedOptions = SnapshotOwnProperties(GetOptionsObject(options), null); + if (IsObject(item)) { if (IsTemporalDateTime(item)) return item; if (IsTemporalZonedDateTime(item)) { - ToTemporalOverflow(options); // validate and ignore + ToTemporalOverflow(resolvedOptions); // validate and ignore return GetPlainDateTimeFor(GetSlot(item, TIME_ZONE), GetSlot(item, INSTANT), GetSlot(item, CALENDAR)); } if (IsTemporalDate(item)) { - ToTemporalOverflow(options); // validate and ignore + ToTemporalOverflow(resolvedOptions); // validate and ignore return CreateTemporalDateTime( GetSlot(item, ISO_YEAR), GetSlot(item, ISO_MONTH), @@ -1602,10 +1596,9 @@ export function ToTemporalDateTime(item: PlainDateTimeParams['from'][0], options ({ year, month, day, hour, minute, second, millisecond, microsecond, nanosecond } = InterpretTemporalDateTimeFields( calendar, fields, - options + resolvedOptions )); } else { - ToTemporalOverflow(options); // validate and ignore let z; ({ year, month, day, hour, minute, second, millisecond, microsecond, nanosecond, calendar, z } = ParseTemporalDateTimeString(RequireString(item))); @@ -1614,6 +1607,7 @@ export function ToTemporalDateTime(item: PlainDateTimeParams['from'][0], options if (!calendar) calendar = 'iso8601'; if (!IsBuiltinCalendar(calendar)) throw new RangeError(`invalid calendar identifier ${calendar}`); calendar = ASCIILowercase(calendar); + ToTemporalOverflow(resolvedOptions); // validate and ignore } return CreateTemporalDateTime(year, month, day, hour, minute, second, millisecond, microsecond, nanosecond, calendar); } @@ -1841,20 +1835,9 @@ export function ToTemporalZonedDateTime( item: ZonedDateTimeParams['from'][0], options?: ZonedDateTimeParams['from'][1] ) { - let year: number, - month: number, - day: number, - hour: number, - minute: number, - second: number, - millisecond: number, - microsecond: number, - nanosecond: number, - timeZone, - offset: string | undefined, - calendar: string | Temporal.CalendarProtocol | undefined; - let disambiguation: NonNullable; - let offsetOpt: NonNullable; + let year, month, day, hour, minute, second, millisecond, microsecond, nanosecond, timeZone, offset, calendar; + const resolvedOptions = SnapshotOwnProperties(GetOptionsObject(options), null); + let disambiguation, offsetOpt; let matchMinute = false; let offsetBehaviour: OffsetBehaviour = 'option'; if (IsObject(item)) { @@ -1882,12 +1865,12 @@ export function ToTemporalZonedDateTime( if (offset === undefined) { offsetBehaviour = 'wall'; } - disambiguation = ToTemporalDisambiguation(options); - offsetOpt = ToTemporalOffset(options, 'reject'); + disambiguation = ToTemporalDisambiguation(resolvedOptions); + offsetOpt = ToTemporalOffset(resolvedOptions, 'reject'); ({ year, month, day, hour, minute, second, millisecond, microsecond, nanosecond } = InterpretTemporalDateTimeFields( calendar, fields, - options + resolvedOptions )); } else { let tzAnnotation, z; @@ -1916,9 +1899,9 @@ export function ToTemporalZonedDateTime( if (!IsBuiltinCalendar(calendar)) throw new RangeError(`invalid calendar identifier ${calendar}`); calendar = ASCIILowercase(calendar); matchMinute = true; // ISO strings may specify offset with less precision - disambiguation = ToTemporalDisambiguation(options); - offsetOpt = ToTemporalOffset(options, 'reject'); - ToTemporalOverflow(options); // validate and ignore + disambiguation = ToTemporalDisambiguation(resolvedOptions); + offsetOpt = ToTemporalOffset(resolvedOptions, 'reject'); + ToTemporalOverflow(resolvedOptions); // validate and ignore } let offsetNs = 0; if (offsetBehaviour === 'option') offsetNs = ParseDateTimeUTCOffset(castExists(offset)); diff --git a/lib/plaindatetime.ts b/lib/plaindatetime.ts index d67baf47..52769f94 100644 --- a/lib/plaindatetime.ts +++ b/lib/plaindatetime.ts @@ -147,14 +147,14 @@ export class PlainDateTime implements Temporal.PlainDateTime { if (!ES.IsTemporalDateTime(this)) throw new TypeError('invalid receiver'); return ES.CalendarInLeapYear(GetSlot(this, CALENDAR), this); } - with(temporalDateTimeLike: Params['with'][0], optionsParam: Params['with'][1] = undefined): Return['with'] { + with(temporalDateTimeLike: Params['with'][0], options: Params['with'][1] = undefined): Return['with'] { if (!ES.IsTemporalDateTime(this)) throw new TypeError('invalid receiver'); if (!ES.IsObject(temporalDateTimeLike)) { throw new TypeError('invalid argument'); } ES.RejectTemporalLikeObject(temporalDateTimeLike); - const options = ES.GetOptionsObject(optionsParam); + const resolvedOptions = ES.SnapshotOwnProperties(ES.GetOptionsObject(options), null); const calendar = GetSlot(this, CALENDAR); const fieldNames = ES.CalendarFields(calendar, ['day', 'month', 'monthCode', 'year']); let fields = ES.PrepareTemporalFields(this, fieldNames, []); @@ -169,7 +169,7 @@ export class PlainDateTime implements Temporal.PlainDateTime { fields = ES.CalendarMergeFields(calendar, fields, partialDateTime); fields = ES.PrepareTemporalFields(fields, fieldNames, []); const { year, month, day, hour, minute, second, millisecond, microsecond, nanosecond } = - ES.InterpretTemporalDateTimeFields(calendar, fields, options); + ES.InterpretTemporalDateTimeFields(calendar, fields, resolvedOptions); return ES.CreateTemporalDateTime( year, diff --git a/lib/zoneddatetime.ts b/lib/zoneddatetime.ts index 0242178a..b8b84d3b 100644 --- a/lib/zoneddatetime.ts +++ b/lib/zoneddatetime.ts @@ -179,13 +179,13 @@ export class ZonedDateTime implements Temporal.ZonedDateTime { if (!ES.IsTemporalZonedDateTime(this)) throw new TypeError('invalid receiver'); return ES.GetOffsetNanosecondsFor(GetSlot(this, TIME_ZONE), GetSlot(this, INSTANT)); } - with(temporalZonedDateTimeLike: Params['with'][0], optionsParam: Params['with'][1] = undefined): Return['with'] { + with(temporalZonedDateTimeLike: Params['with'][0], options: Params['with'][1] = undefined): Return['with'] { if (!ES.IsTemporalZonedDateTime(this)) throw new TypeError('invalid receiver'); if (!ES.IsObject(temporalZonedDateTimeLike)) { throw new TypeError('invalid zoned-date-time-like'); } ES.RejectTemporalLikeObject(temporalZonedDateTimeLike); - const options = ES.GetOptionsObject(optionsParam); + const resolvedOptions = ES.SnapshotOwnProperties(ES.GetOptionsObject(options), null); const calendar = GetSlot(this, CALENDAR); const timeZone = GetSlot(this, TIME_ZONE); @@ -218,11 +218,11 @@ export class ZonedDateTime implements Temporal.ZonedDateTime { fields = ES.CalendarMergeFields(calendar, fields, partialZonedDateTime); fields = ES.PrepareTemporalFields(fields, fieldNames, ['offset']); - const disambiguation = ES.ToTemporalDisambiguation(options); - const offset = ES.ToTemporalOffset(options, 'prefer'); + const disambiguation = ES.ToTemporalDisambiguation(resolvedOptions); + const offset = ES.ToTemporalOffset(resolvedOptions, 'prefer'); let { year, month, day, hour, minute, second, millisecond, microsecond, nanosecond } = - ES.InterpretTemporalDateTimeFields(calendar, fields, options); + ES.InterpretTemporalDateTimeFields(calendar, fields, resolvedOptions); const newOffsetNs = ES.ParseDateTimeUTCOffset(fields.offset); const epochNanoseconds = ES.InterpretISODateTimeOffset( year, From 776a3a4423166b920cd3c6f046248545ded6468a Mon Sep 17 00:00:00 2001 From: Philip Chimento Date: Thu, 2 Mar 2023 17:45:48 -0800 Subject: [PATCH 26/39] Normative: Copy options object in Plain{Date,MonthDay,YearMonth}.{from,p.with} Also following the precedent set in #2447, this preserves consistency with analogous from/with operations in {Plain,Zoned}DateTime that need to read the overflow option twice in order to deal with time and date units separately. UPSTREAM_COMMIT=d92a1bee1c3ea4ae6d672ff6959b3b73b32fc7fb --- lib/ecmascript.ts | 18 ++++++++++++------ lib/plaindate.ts | 6 +++--- lib/plainmonthday.ts | 6 +++--- lib/plainyearmonth.ts | 6 +++--- 4 files changed, 21 insertions(+), 15 deletions(-) diff --git a/lib/ecmascript.ts b/lib/ecmascript.ts index f19ff91c..9d89995b 100644 --- a/lib/ecmascript.ts +++ b/lib/ecmascript.ts @@ -1507,8 +1507,10 @@ export function ToTemporalTimeRecord( export function ToTemporalDate( itemParam: PlainDateParams['from'][0], - options?: PlainDateParams['from'][1] + optionsParam?: PlainDateParams['from'][1] ): Temporal.PlainDate { + let options = optionsParam; + if (options !== undefined) options = SnapshotOwnProperties(GetOptionsObject(options), null); let item = itemParam; if (IsObject(item)) { if (IsTemporalDate(item)) return item; @@ -1530,12 +1532,12 @@ export function ToTemporalDate( const fields = PrepareTemporalFields(item, fieldNames, []); return CalendarDateFromFields(calendar, fields, options); } - ToTemporalOverflow(options); // validate and ignore let { year, month, day, calendar, z } = ParseTemporalDateString(RequireString(item)); if (z) throw new RangeError('Z designator not supported for PlainDate'); if (!calendar) calendar = 'iso8601'; if (!IsBuiltinCalendar(calendar)) throw new RangeError(`invalid calendar identifier ${calendar}`); calendar = ASCIILowercase(calendar); + ToTemporalOverflow(options); // validate and ignore return CreateTemporalDate(year, month, day, calendar); } @@ -1645,8 +1647,10 @@ export function ToTemporalInstant(itemParam: InstantParams['from'][0]) { export function ToTemporalMonthDay( itemParam: PlainMonthDayParams['from'][0], - options?: PlainMonthDayParams['from'][1] + optionsParam?: PlainMonthDayParams['from'][1] ) { + let options = optionsParam; + if (options !== undefined) options = SnapshotOwnProperties(GetOptionsObject(options), null); let item = itemParam; if (IsObject(item)) { if (IsTemporalMonthDay(item)) return item; @@ -1674,11 +1678,11 @@ export function ToTemporalMonthDay( return CalendarMonthDayFromFields(calendar, fields, options); } - ToTemporalOverflow(options); // validate and ignore let { month, day, referenceISOYear, calendar } = ParseTemporalMonthDayString(RequireString(item)); if (calendar === undefined) calendar = 'iso8601'; if (!IsBuiltinCalendar(calendar)) throw new RangeError(`invalid calendar identifier ${calendar}`); calendar = ASCIILowercase(calendar); + ToTemporalOverflow(options); // validate and ignore if (referenceISOYear === undefined) { RejectISODate(1972, month, day); @@ -1730,8 +1734,10 @@ export function ToTemporalTime( export function ToTemporalYearMonth( item: PlainYearMonthParams['from'][0], - options?: PlainYearMonthParams['from'][1] + optionsParam?: PlainYearMonthParams['from'][1] ): Temporal.PlainYearMonth { + let options = optionsParam; + if (options !== undefined) options = SnapshotOwnProperties(GetOptionsObject(options), null); if (IsObject(item)) { if (IsTemporalYearMonth(item)) return item; const calendar = GetTemporalCalendarSlotValueWithISODefault(item); @@ -1740,11 +1746,11 @@ export function ToTemporalYearMonth( return CalendarYearMonthFromFields(calendar, fields, options); } - ToTemporalOverflow(options); // validate and ignore let { year, month, referenceISODay, calendar } = ParseTemporalYearMonthString(RequireString(item)); if (calendar === undefined) calendar = 'iso8601'; if (!IsBuiltinCalendar(calendar)) throw new RangeError(`invalid calendar identifier ${calendar}`); calendar = ASCIILowercase(calendar); + ToTemporalOverflow(options); // validate and ignore if (referenceISODay === undefined) { RejectISODate(year, month, 1); diff --git a/lib/plaindate.ts b/lib/plaindate.ts index 1c4339f2..4609746c 100644 --- a/lib/plaindate.ts +++ b/lib/plaindate.ts @@ -96,13 +96,13 @@ export class PlainDate implements Temporal.PlainDate { if (!ES.IsTemporalDate(this)) throw new TypeError('invalid receiver'); return ES.CalendarInLeapYear(GetSlot(this, CALENDAR), this); } - with(temporalDateLike: Params['with'][0], optionsParam: Params['with'][1] = undefined): Return['with'] { + with(temporalDateLike: Params['with'][0], options: Params['with'][1] = undefined): Return['with'] { if (!ES.IsTemporalDate(this)) throw new TypeError('invalid receiver'); if (!ES.IsObject(temporalDateLike)) { throw new TypeError('invalid argument'); } ES.RejectTemporalLikeObject(temporalDateLike); - const options = ES.GetOptionsObject(optionsParam); + const resolvedOptions = ES.SnapshotOwnProperties(ES.GetOptionsObject(options), null); const calendar = GetSlot(this, CALENDAR); const fieldNames = ES.CalendarFields(calendar, ['day', 'month', 'monthCode', 'year'] as const); @@ -111,7 +111,7 @@ export class PlainDate implements Temporal.PlainDate { fields = ES.CalendarMergeFields(calendar, fields, partialDate); fields = ES.PrepareTemporalFields(fields, fieldNames, []); - return ES.CalendarDateFromFields(calendar, fields, options); + return ES.CalendarDateFromFields(calendar, fields, resolvedOptions); } withCalendar(calendarParam: Params['withCalendar'][0]): Return['withCalendar'] { if (!ES.IsTemporalDate(this)) throw new TypeError('invalid receiver'); diff --git a/lib/plainmonthday.ts b/lib/plainmonthday.ts index 69b4583f..2ef434aa 100644 --- a/lib/plainmonthday.ts +++ b/lib/plainmonthday.ts @@ -35,13 +35,13 @@ export class PlainMonthDay implements Temporal.PlainMonthDay { return ES.ToTemporalCalendarIdentifier(GetSlot(this, CALENDAR)); } - with(temporalMonthDayLike: Params['with'][0], optionsParam: Params['with'][1] = undefined): Return['with'] { + with(temporalMonthDayLike: Params['with'][0], options: Params['with'][1] = undefined): Return['with'] { if (!ES.IsTemporalMonthDay(this)) throw new TypeError('invalid receiver'); if (!ES.IsObject(temporalMonthDayLike)) { throw new TypeError('invalid argument'); } ES.RejectTemporalLikeObject(temporalMonthDayLike); - const options = ES.GetOptionsObject(optionsParam); + const resolvedOptions = ES.SnapshotOwnProperties(ES.GetOptionsObject(options), null); const calendar = GetSlot(this, CALENDAR); const fieldNames = ES.CalendarFields(calendar, ['day', 'month', 'monthCode', 'year'] as const); @@ -50,7 +50,7 @@ export class PlainMonthDay implements Temporal.PlainMonthDay { fields = ES.CalendarMergeFields(calendar, fields, partialMonthDay); fields = ES.PrepareTemporalFields(fields, fieldNames, []); - return ES.CalendarMonthDayFromFields(calendar, fields, options); + return ES.CalendarMonthDayFromFields(calendar, fields, resolvedOptions); } equals(otherParam: Params['equals'][0]): Return['equals'] { if (!ES.IsTemporalMonthDay(this)) throw new TypeError('invalid receiver'); diff --git a/lib/plainyearmonth.ts b/lib/plainyearmonth.ts index 52983197..22382dc5 100644 --- a/lib/plainyearmonth.ts +++ b/lib/plainyearmonth.ts @@ -61,13 +61,13 @@ export class PlainYearMonth implements Temporal.PlainYearMonth { if (!ES.IsTemporalYearMonth(this)) throw new TypeError('invalid receiver'); return ES.CalendarInLeapYear(GetSlot(this, CALENDAR), this); } - with(temporalYearMonthLike: Params['with'][0], optionsParam: Params['with'][1] = undefined): Return['with'] { + with(temporalYearMonthLike: Params['with'][0], options: Params['with'][1] = undefined): Return['with'] { if (!ES.IsTemporalYearMonth(this)) throw new TypeError('invalid receiver'); if (!ES.IsObject(temporalYearMonthLike)) { throw new TypeError('invalid argument'); } ES.RejectTemporalLikeObject(temporalYearMonthLike); - const options = ES.GetOptionsObject(optionsParam); + const resolvedOptions = ES.SnapshotOwnProperties(ES.GetOptionsObject(options), null); const calendar = GetSlot(this, CALENDAR); const fieldNames = ES.CalendarFields(calendar, ['month', 'monthCode', 'year'] as const); @@ -76,7 +76,7 @@ export class PlainYearMonth implements Temporal.PlainYearMonth { fields = ES.CalendarMergeFields(calendar, fields, partialYearMonth); fields = ES.PrepareTemporalFields(fields, fieldNames, []); - return ES.CalendarYearMonthFromFields(calendar, fields, options); + return ES.CalendarYearMonthFromFields(calendar, fields, resolvedOptions); } add(temporalDurationLike: Params['add'][0], options: Params['add'][1] = undefined): Return['add'] { if (!ES.IsTemporalYearMonth(this)) throw new TypeError('invalid receiver'); From bad2a5ee1ca9513c3a643daaf6d4cdea2c61537a Mon Sep 17 00:00:00 2001 From: Philip Chimento Date: Wed, 16 Aug 2023 14:52:49 -0700 Subject: [PATCH 27/39] Update test262 UPSTREAM_COMMIT=04f9544d02c46a4d5e2351d098e67b785769d75e --- test262 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test262 b/test262 index 0c87a86b..c30aff08 160000 --- a/test262 +++ b/test262 @@ -1 +1 @@ -Subproject commit 0c87a86b58391b40aa7623b919603d87d4b77a4d +Subproject commit c30aff08af165f3fbdc1de7653aaac97aede693a From d943b2a75f7be46ad7cdb10461e9a13dc15b6a64 Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Thu, 17 Aug 2023 14:15:23 -0400 Subject: [PATCH 28/39] Polyfill: Align time zone regular expressions with spec UPSTREAM_COMMIT=9378492cfba7bff3926679133b5b709bde1a1150 --- lib/regex.ts | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/lib/regex.ts b/lib/regex.ts index f70c2f9c..95c87712 100644 --- a/lib/regex.ts +++ b/lib/regex.ts @@ -1,18 +1,7 @@ -const tzComponent = /\.[-A-Za-z_]|\.\.[-A-Za-z._]{1,12}|\.[-A-Za-z_][-A-Za-z._]{0,12}|[A-Za-z_][-A-Za-z._]{0,13}/; -const offsetIdentifierNoCapture = /(?:[+\u2212-][0-2][0-9](?::?[0-5][0-9])?)/; +const offsetIdentifierNoCapture = /(?:[+\u2212-](?:[01][0-9]|2[0-3])(?::?[0-5][0-9])?)/; +const tzComponent = /[A-Za-z._][A-Za-z._0-9+-]*/; export const timeZoneID = new RegExp( - '(?:' + - [ - `(?:${tzComponent.source})(?:\\/(?:${tzComponent.source}))*`, - 'Etc/GMT(?:0|[-+]\\d{1,2})', - 'GMT[-+]?0', - 'EST5EDT', - 'CST6CDT', - 'MST7MDT', - 'PST8PDT', - offsetIdentifierNoCapture.source - ].join('|') + - ')' + `(?:${offsetIdentifierNoCapture.source}|(?:${tzComponent.source})(?:\\/(?:${tzComponent.source}))*)` ); const yearpart = /(?:[+\u2212-]\d{6}|\d{4})/; From 42ead262fa389a56dc30e98a7124439a21c8ce12 Mon Sep 17 00:00:00 2001 From: Justin Grant Date: Sun, 20 Aug 2023 14:14:06 -0700 Subject: [PATCH 29/39] Remove unreachable steps in DifferenceISODate Fixes #2649. See #issuecomment-1684907910 for an explanation of why it's unreachable. UPSTREAM_COMMIT=1275241afdcb116c439d6cdea7dd0f98aadc76d7 --- lib/ecmascript.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lib/ecmascript.ts b/lib/ecmascript.ts index 9d89995b..730cbb3c 100644 --- a/lib/ecmascript.ts +++ b/lib/ecmascript.ts @@ -4617,10 +4617,6 @@ export function DifferenceISODate( // The end date is later in the month than mid date (or earlier for // negative durations). Back up one month. months -= sign; - if (months === -sign) { - years -= sign; - months = 11 * sign; - } mid = AddISODate(y1, m1, d1, years, months, 0, 0, 'constrain'); } From 94318fc0d67e9fffdee53ac92dc5a5648cf4ea97 Mon Sep 17 00:00:00 2001 From: Aditi Date: Wed, 17 May 2023 19:53:20 +0530 Subject: [PATCH 30/39] Polyfill: Align with spec UPSTREAM_COMMIT=4c10c80f4f19d1f980185d8662bd0b149d69d5e9 --- lib/calendar.ts | 29 +++++++++++++++++++++-------- lib/ecmascript.ts | 34 +++++++++++++++++++++++++++++----- lib/plainmonthday.ts | 2 +- lib/plainyearmonth.ts | 2 +- 4 files changed, 52 insertions(+), 15 deletions(-) diff --git a/lib/calendar.ts b/lib/calendar.ts index e5b513dc..d0e3117e 100644 --- a/lib/calendar.ts +++ b/lib/calendar.ts @@ -29,6 +29,7 @@ import type { AnyTemporalKey, CalendarSlot } from './internaltypes'; +import type { CalendarFieldDescriptor } from './ecmascript'; const ArrayIncludes = Array.prototype.includes; const ArrayPrototypePush = Array.prototype.push; @@ -409,11 +410,10 @@ impl['iso8601'] = { if (fields.month !== undefined && fields.year === undefined && fields.monthCode === undefined) { throw new TypeError('either year or monthCode required with month'); } - const useYear = fields.monthCode === undefined; const referenceISOYear = 1972; fields = resolveNonLunisolarMonth(fields); let { month, day, year } = fields; - ({ month, day } = ES.RegulateISODate(useYear ? year : referenceISOYear, month, day, overflow)); + ({ month, day } = ES.RegulateISODate(year !== undefined ? year : referenceISOYear, month, day, overflow)); return ES.CreateTemporalMonthDay(month, day, calendarSlotValue, referenceISOYear); }, fields(fields) { @@ -2310,14 +2310,25 @@ class DangiHelper extends ChineseBaseHelper { */ class NonIsoCalendar implements CalendarImpl { constructor(private readonly helper: HelperBase) {} + CalendarFieldDescriptors(type: 'date' | 'month-day' | 'year-month'): CalendarFieldDescriptor[] { + let fieldDescriptors = [] as CalendarFieldDescriptor[]; + if (type !== 'month-day') { + fieldDescriptors = [ + { property: 'era', conversion: ES.ToString, required: false }, + { property: 'eraYear', conversion: ES.ToIntegerOrInfinity, required: false } + ]; + } + return fieldDescriptors; + } dateFromFields( fieldsParam: Params['dateFromFields'][0], options: NonNullable, calendarSlotValue: string ): Temporal.PlainDate { const cache = new OneObjectCache(); - const fieldNames = this.fields(['day', 'month', 'monthCode', 'year']) as AnyTemporalKey[]; - const fields = ES.PrepareTemporalFields(fieldsParam, fieldNames, []); + const fieldNames = ['day', 'month', 'monthCode', 'year'] as AnyTemporalKey[]; + const extraFieldDescriptors = this.CalendarFieldDescriptors('date'); + const fields = ES.PrepareTemporalFields(fieldsParam, fieldNames, [], extraFieldDescriptors); const overflow = ES.ToTemporalOverflow(options); const { year, month, day } = this.helper.calendarToIsoDate(fields, overflow, cache); const result = ES.CreateTemporalDate(year, month, day, calendarSlotValue); @@ -2330,8 +2341,9 @@ class NonIsoCalendar implements CalendarImpl { calendarSlotValue: CalendarSlot ): Temporal.PlainYearMonth { const cache = new OneObjectCache(); - const fieldNames = this.fields(['month', 'monthCode', 'year']) as AnyTemporalKey[]; - const fields = ES.PrepareTemporalFields(fieldsParam, fieldNames, []); + const fieldNames = ['month', 'monthCode', 'year'] as AnyTemporalKey[]; + const extraFieldDescriptors = this.CalendarFieldDescriptors('year-month'); + const fields = ES.PrepareTemporalFields(fieldsParam, fieldNames, [], extraFieldDescriptors); const overflow = ES.ToTemporalOverflow(options); const { year, month, day } = this.helper.calendarToIsoDate({ ...fields, day: 1 }, overflow, cache); const result = ES.CreateTemporalYearMonth(year, month, calendarSlotValue, /* referenceISODay = */ day); @@ -2346,8 +2358,9 @@ class NonIsoCalendar implements CalendarImpl { const cache = new OneObjectCache(); // For lunisolar calendars, either `monthCode` or `year` must be provided // because `month` is ambiguous without a year or a code. - const fieldNames = this.fields(['day', 'month', 'monthCode', 'year']) as AnyTemporalKey[]; - const fields = ES.PrepareTemporalFields(fieldsParam, fieldNames, []); + const fieldNames = ['day', 'month', 'monthCode', 'year'] as AnyTemporalKey[]; + const extraFieldDescriptors = this.CalendarFieldDescriptors('date'); + const fields = ES.PrepareTemporalFields(fieldsParam, fieldNames, [], extraFieldDescriptors); const overflow = ES.ToTemporalOverflow(options); const { year, month, day } = this.helper.monthDayFromFields(fields, overflow, cache); // `year` is a reference year where this month/day exists in this calendar diff --git a/lib/ecmascript.ts b/lib/ecmascript.ts index 730cbb3c..6035a624 100644 --- a/lib/ecmascript.ts +++ b/lib/ecmascript.ts @@ -1,5 +1,6 @@ const ArrayIncludes = Array.prototype.includes; const ArrayPrototypePush = Array.prototype.push; +const ArrayPrototypeFind = Array.prototype.find; const IntlDateTimeFormat = globalThis.Intl.DateTimeFormat; const IntlSupportedValuesOf: typeof globalThis.Intl.supportedValuesOf | undefined = globalThis.Intl.supportedValuesOf; const MathAbs = Math.abs; @@ -216,7 +217,7 @@ export function ToNumber(value: unknown): number { return NumberCtor(value); } -function ToIntegerOrInfinity(value: unknown) { +export function ToIntegerOrInfinity(value: unknown) { const number = ToNumber(value); if (NumberIsNaN(number) || number === 0) { return 0; @@ -356,8 +357,6 @@ const BUILTIN_CASTS = new Map([ ['milliseconds', ToIntegerIfIntegral], ['microseconds', ToIntegerIfIntegral], ['nanoseconds', ToIntegerIfIntegral], - ['era', ToPrimitiveAndRequireString], - ['eraYear', ToIntegerOrInfinity], ['offset', ToPrimitiveAndRequireString] ]); @@ -1413,6 +1412,12 @@ type FieldObjectFromOwners = Resolve< } >; +export interface CalendarFieldDescriptor { + property: string; + conversion: (value: unknown) => unknown; + required: boolean; +} + type PrepareTemporalFieldsReturn< FieldKeys extends AnyTemporalKey, RequiredFieldsOpt extends ReadonlyArray | FieldCompleteness, @@ -1430,11 +1435,21 @@ export function PrepareTemporalFields< bag: Partial>, fields: Array, requiredFields: RequiredFields, + extraFieldDescriptors: CalendarFieldDescriptor[] = [], duplicateBehaviour: 'throw' | 'ignore' = 'throw', { emptySourceErrorMessage }: FieldPrepareOptions = { emptySourceErrorMessage: 'no supported properties found' } ): PrepareTemporalFieldsReturn> { const result: Partial> = ObjectCreate(null); let any = false; + if (extraFieldDescriptors) { + for (let index = 0; index < extraFieldDescriptors.length; index++) { + let desc = extraFieldDescriptors[index]; + Call(ArrayPrototypePush, fields, [desc.property]); + if (desc.required === true && requiredFields !== 'partial') { + Call(ArrayPrototypePush, requiredFields, [desc.property]); + } + } + } fields.sort(); let previousProperty = undefined; for (const property of fields) { @@ -1447,6 +1462,14 @@ export function PrepareTemporalFields< any = true; if (BUILTIN_CASTS.has(property)) { value = castExists(BUILTIN_CASTS.get(property))(value); + } else if (extraFieldDescriptors) { + const matchingDescriptor = Call(ArrayPrototypeFind, extraFieldDescriptors, [ + (desc) => desc.property === property + ]); + if (matchingDescriptor) { + const convertor = matchingDescriptor.conversion; + value = convertor(value); + } } result[property] = value; } else if (requiredFields !== 'partial') { @@ -1490,11 +1513,12 @@ export function ToTemporalTimeRecord( ): Partial { // NOTE: Field order is sorted to make the sort in PrepareTemporalFields more efficient. const fields: (keyof TimeRecord)[] = ['hour', 'microsecond', 'millisecond', 'minute', 'nanosecond', 'second']; - const partial = PrepareTemporalFields(bag, fields, 'partial', undefined, { + const partial = PrepareTemporalFields(bag, fields, 'partial', undefined, undefined, { emptySourceErrorMessage: 'invalid time-like' }); const result: Partial = {}; - for (const field of fields) { + for (let index = 0; index < fields.length; index++) { + const field = fields[index]; const valueDesc = ObjectGetOwnPropertyDescriptor(partial, field); if (valueDesc !== undefined) { result[field] = valueDesc.value; diff --git a/lib/plainmonthday.ts b/lib/plainmonthday.ts index 2ef434aa..27723dd0 100644 --- a/lib/plainmonthday.ts +++ b/lib/plainmonthday.ts @@ -94,7 +94,7 @@ export class PlainMonthDay implements Temporal.PlainMonthDay { const inputFields = ES.PrepareTemporalFields(item, inputFieldNames, []); let mergedFields = ES.CalendarMergeFields(calendar, fields, inputFields); const concatenatedFieldNames = [...receiverFieldNames, ...inputFieldNames]; - mergedFields = ES.PrepareTemporalFields(mergedFields, concatenatedFieldNames, [], 'ignore'); + mergedFields = ES.PrepareTemporalFields(mergedFields, concatenatedFieldNames, [], [], 'ignore'); const options = ObjectCreate(null); options.overflow = 'reject'; return ES.CalendarDateFromFields(calendar, mergedFields, options); diff --git a/lib/plainyearmonth.ts b/lib/plainyearmonth.ts index 22382dc5..c85290fe 100644 --- a/lib/plainyearmonth.ts +++ b/lib/plainyearmonth.ts @@ -139,7 +139,7 @@ export class PlainYearMonth implements Temporal.PlainYearMonth { const inputFields = ES.PrepareTemporalFields(item, inputFieldNames, []); let mergedFields = ES.CalendarMergeFields(calendar, fields, inputFields); const mergedFieldNames = [...receiverFieldNames, ...inputFieldNames]; - mergedFields = ES.PrepareTemporalFields(mergedFields, mergedFieldNames, [], 'ignore'); + mergedFields = ES.PrepareTemporalFields(mergedFields, mergedFieldNames, [], [], 'ignore'); const options = ObjectCreate(null); options.overflow = 'reject'; return ES.CalendarDateFromFields(calendar, mergedFields, options); From 2e637d1e3936a20dfe09ba6c92da4c5d7ed21e70 Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Tue, 15 Aug 2023 19:59:43 -0400 Subject: [PATCH 31/39] Polyfill: Be more robust against late-run primordial manipulation UPSTREAM_COMMIT=e09bf4f7774cd8ae94dbe243b7356fc03589218f --- lib/calendar.ts | 69 +++++++++++++++++++++++++++++++------------------ 1 file changed, 44 insertions(+), 25 deletions(-) diff --git a/lib/calendar.ts b/lib/calendar.ts index d0e3117e..415fa27c 100644 --- a/lib/calendar.ts +++ b/lib/calendar.ts @@ -33,16 +33,29 @@ import type { CalendarFieldDescriptor } from './ecmascript'; const ArrayIncludes = Array.prototype.includes; const ArrayPrototypePush = Array.prototype.push; +const ArrayPrototypeSort = Array.prototype.sort; const IntlDateTimeFormat = globalThis.Intl.DateTimeFormat; -const ArraySort = Array.prototype.sort; const MathAbs = Math.abs; const MathFloor = Math.floor; const ObjectCreate = Object.create; const ObjectEntries = Object.entries; +const OriginalMap = Map; const OriginalSet = Set; +const OriginalWeakMap = WeakMap; const ReflectOwnKeys = Reflect.ownKeys; +const MapPrototypeEntries = Map.prototype.entries; +const MapPrototypeGet = Map.prototype.get; +const MapPrototypeSet = Map.prototype.set; const SetPrototypeAdd = Set.prototype.add; -const SetPrototypeValues = Set.prototype.values; +const WeakMapPrototypeGet = WeakMap.prototype.get; +const WeakMapPrototypeSet = WeakMap.prototype.set; + +const MapIterator = ES.Call(MapPrototypeEntries, new Map(), []); +const MapIteratorPrototypeNext = MapIterator.next; + +function arrayFromSet(src: Set): T[] { + return [...src]; +} /** * Shape of internal implementation of each built-in calendar. Note that @@ -420,7 +433,7 @@ impl['iso8601'] = { return fields; }, fieldKeysToIgnore(keys) { - const result = new OriginalSet(); + const result = new OriginalSet(); for (let ix = 0; ix < keys.length; ix++) { const key = keys[ix]; ES.Call(SetPrototypeAdd, result, [key]); @@ -430,7 +443,7 @@ impl['iso8601'] = { ES.Call(SetPrototypeAdd, result, ['month']); } } - return [...ES.Call(SetPrototypeValues, result, [])]; + return arrayFromSet(result); }, dateAdd(date, years, months, weeks, days, overflow, calendarSlotValue) { let year = GetSlot(date, ISO_YEAR); @@ -606,7 +619,7 @@ type CachedTypes = Temporal.PlainYearMonth | Temporal.PlainDate | Temporal.Plain * because each object's cache is thrown away when the object is GC-ed. */ class OneObjectCache { - map = new Map(); + map = new OriginalMap(); calls = 0; now: number; hits = 0; @@ -615,14 +628,17 @@ class OneObjectCache { this.now = globalThis.performance ? globalThis.performance.now() : Date.now(); if (cacheToClone !== undefined) { let i = 0; - for (const entry of cacheToClone.map.entries()) { + const entriesIterator = ES.Call(MapPrototypeEntries, cacheToClone.map, []); + for (;;) { + const iterResult = ES.Call(MapIteratorPrototypeNext, entriesIterator, []); + if (iterResult.done) break; if (++i > OneObjectCache.MAX_CACHE_ENTRIES) break; - this.map.set(...entry); + ES.Call(MapPrototypeSet, this.map, iterResult.value); } } } get(key: string) { - const result = this.map.get(key); + const result = ES.Call(MapPrototypeGet, this.map, [key]); if (result) { this.hits++; this.report(); @@ -631,7 +647,7 @@ class OneObjectCache { return result; } set(key: string, value: unknown) { - this.map.set(key, value); + ES.Call(MapPrototypeSet, this.map, [key, value]); this.misses++; this.report(); } @@ -644,12 +660,12 @@ class OneObjectCache { */ } setObject(obj: CachedTypes) { - if (OneObjectCache.objectMap.get(obj)) throw new RangeError('object already cached'); - OneObjectCache.objectMap.set(obj, this); + if (ES.Call(WeakMapPrototypeGet, OneObjectCache.objectMap, [obj])) throw new RangeError('object already cached'); + ES.Call(WeakMapPrototypeSet, OneObjectCache.objectMap, [obj, this]); this.report(); } - static objectMap = new WeakMap(); + static objectMap = new OriginalWeakMap(); static MAX_CACHE_ENTRIES = 1000; /** @@ -659,10 +675,10 @@ class OneObjectCache { * @param obj - object to associate with the cache */ static getCacheForObject(obj: CachedTypes) { - let cache = OneObjectCache.objectMap.get(obj); + let cache = ES.Call(WeakMapPrototypeGet, OneObjectCache.objectMap, [obj]); if (!cache) { cache = new OneObjectCache(); - OneObjectCache.objectMap.set(obj, cache); + ES.Call(WeakMapPrototypeSet, OneObjectCache.objectMap, [obj, cache]); } return cache; } @@ -1783,12 +1799,14 @@ function adjustEras(erasParam: InputEra[]): { eras: Era[]; anchorEra: Era } { // Ensure that the latest epoch is first in the array. This lets us try to // match eras in index order, with the last era getting the remaining older // years. Any reverse-signed era must be at the end. - ArraySort.call(eras, (e1, e2) => { - if (e1.reverseOf) return 1; - if (e2.reverseOf) return -1; - if (!e1.isoEpoch || !e2.isoEpoch) throw new RangeError('Invalid era data: missing ISO epoch'); - return e2.isoEpoch.year - e1.isoEpoch.year; - }); + ES.Call(ArrayPrototypeSort, eras, [ + (e1, e2) => { + if (e1.reverseOf) return 1; + if (e2.reverseOf) return -1; + if (!e1.isoEpoch || !e2.isoEpoch) throw new RangeError('Invalid era data: missing ISO epoch'); + return e2.isoEpoch.year - e1.isoEpoch.year; + } + ]); // If there's a reversed era, then the one before it must be the era that's // being reversed. @@ -2368,15 +2386,16 @@ class NonIsoCalendar implements CalendarImpl { cache.setObject(result); return result; } - fields(fieldsParam: string[]): string[] { - let fields = fieldsParam; - if (ArrayIncludes.call(fields, 'year')) fields = [...fields, 'era', 'eraYear']; + fields(fields: string[]): string[] { + if (ES.Call(ArrayIncludes, fields, ['year'])) { + ES.Call(ArrayPrototypePush, fields, ['era', 'eraYear']); + } return fields; } fieldKeysToIgnore( keys: Exclude[] ): Exclude[] { - const result = new OriginalSet(); + const result = new OriginalSet<(typeof keys)[number]>(); for (let ix = 0; ix < keys.length; ix++) { const key = keys[ix]; ES.Call(SetPrototypeAdd, result, [key]); @@ -2416,7 +2435,7 @@ class NonIsoCalendar implements CalendarImpl { break; } } - return [...ES.Call(SetPrototypeValues, result, [])]; + return arrayFromSet(result); } dateAdd( date: Temporal.PlainDate, From a12150429e87d0057113294b4a9509100df6a73c Mon Sep 17 00:00:00 2001 From: Philip Chimento Date: Mon, 21 Aug 2023 09:23:24 -0700 Subject: [PATCH 32/39] Update test262 UPSTREAM_COMMIT=4345a8cbe90bd140321d31549a0382d3cbf2546b --- test262 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test262 b/test262 index c30aff08..bdddd9e2 160000 --- a/test262 +++ b/test262 @@ -1 +1 @@ -Subproject commit c30aff08af165f3fbdc1de7653aaac97aede693a +Subproject commit bdddd9e2d286600462958c7bac6af10a2946134b From 27164a74b44545f706740bca118491bb9135ab98 Mon Sep 17 00:00:00 2001 From: Justin Grant Date: Mon, 26 Feb 2024 02:24:03 -0800 Subject: [PATCH 33/39] Prune unused exports --- lib/ecmascript.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/ecmascript.ts b/lib/ecmascript.ts index 6035a624..a0bd5dbb 100644 --- a/lib/ecmascript.ts +++ b/lib/ecmascript.ts @@ -735,7 +735,7 @@ export function ParseTemporalTimeZoneStringRaw(timeZoneString: string) { throwBadTimeZoneStringError(timeZoneString); } -export function ParseTemporalTimeZoneString(stringIdent: string) { +function ParseTemporalTimeZoneString(stringIdent: string) { const { tzAnnotation, offset, z } = ParseTemporalTimeZoneStringRaw(stringIdent); if (tzAnnotation) return ParseTimeZoneIdentifier(tzAnnotation); if (z) return ParseTimeZoneIdentifier('UTC'); @@ -2983,7 +2983,7 @@ export function ISODateTimePartString(part: number) { return ToZeroPaddedDecimalString(part, 2); } -export function FormatFractionalSeconds( +function FormatFractionalSeconds( subSecondNanoseconds: number, precision: Exclude ) { @@ -3369,7 +3369,7 @@ export function FormatOffsetTimeZoneIdentifier(offsetMinutes: number) { return `${sign}${timeString}`; } -export function FormatDateTimeUTCOffsetRounded(offsetNanosecondsParam: number) { +function FormatDateTimeUTCOffsetRounded(offsetNanosecondsParam: number) { const offsetNanoseconds = JSBI.toNumber( RoundNumberToIncrement(JSBI.BigInt(offsetNanosecondsParam), MINUTE_NANOS, 'halfExpand') ); From a0f8e926dbd008cd407634d741efa0b1efc777b5 Mon Sep 17 00:00:00 2001 From: Justin Grant Date: Mon, 26 Feb 2024 02:28:33 -0800 Subject: [PATCH 34/39] Fix validStrings.mjs tests --- test/validStrings.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/validStrings.mjs b/test/validStrings.mjs index 048daec5..ef3f0ac4 100644 --- a/test/validStrings.mjs +++ b/test/validStrings.mjs @@ -8,7 +8,7 @@ node --experimental-modules --experimental-specifier-resolution=node --no-warnin */ import assert from 'assert'; -import * as ES from '../lib/ecmascript.mjs'; +import * as ES from '../lib/ecmascript'; const timezoneNames = Intl.supportedValuesOf('timeZone'); const calendarNames = Intl.supportedValuesOf('calendar'); From 5ae5771044e3f514e2b730f0af5b29bc52232108 Mon Sep 17 00:00:00 2001 From: Justin Grant Date: Mon, 26 Feb 2024 02:38:33 -0800 Subject: [PATCH 35/39] Add expected failures for ES5 and Node 14/16 --- test/expected-failures-before-node18.txt | 5 +++++ test/expected-failures-es5.txt | 2 ++ 2 files changed, 7 insertions(+) diff --git a/test/expected-failures-before-node18.txt b/test/expected-failures-before-node18.txt index 4cd23a93..14fee904 100644 --- a/test/expected-failures-before-node18.txt +++ b/test/expected-failures-before-node18.txt @@ -3,6 +3,11 @@ intl402/DateTimeFormat/constructor-options-timeZoneName-valid.js # Intl.supportedValuesOf("timeZone") is only available starting in Node 18 intl402/Temporal/TimeZone/supported-values-of.js +intl402/Temporal/TimeZone/from/timezone-case-insensitive.js +intl402/Temporal/TimeZone/prototype/equals/canonical-not-equal.js +intl402/Temporal/TimeZone/prototype/equals/timezone-case-insensitive.js +intl402/Temporal/ZonedDateTime/from/timezone-case-insensitive.js +intl402/DateTimeFormat/timezone-case-insensitive.js intl402/Temporal/TimeZone/prototype/getNextTransition/transition-at-instant-boundaries.js intl402/Temporal/TimeZone/prototype/getPreviousTransition/transition-at-instant-boundaries.js diff --git a/test/expected-failures-es5.txt b/test/expected-failures-es5.txt index 39d7b1cc..6aa476af 100644 --- a/test/expected-failures-es5.txt +++ b/test/expected-failures-es5.txt @@ -304,6 +304,8 @@ built-ins/Temporal/TimeZone/prototype/toJSON/builtin.js built-ins/Temporal/TimeZone/prototype/toJSON/not-a-constructor.js built-ins/Temporal/TimeZone/prototype/toString/builtin.js built-ins/Temporal/TimeZone/prototype/toString/not-a-constructor.js +built-ins/Temporal/TimeZone/prototype/equals/not-a-constructor.js +built-ins/Temporal/TimeZone/prototype/equals/builtin.js built-ins/Temporal/ZonedDateTime/prototype/add/builtin.js built-ins/Temporal/ZonedDateTime/prototype/add/not-a-constructor.js built-ins/Temporal/ZonedDateTime/prototype/equals/builtin.js From 585a096375f4a715c243fc6a399ca1c3717ff431 Mon Sep 17 00:00:00 2001 From: Justin Grant Date: Mon, 4 Mar 2024 00:04:48 -0800 Subject: [PATCH 36/39] fixup! Editorial: DRY refactor of time formatting --- lib/ecmascript.ts | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/lib/ecmascript.ts b/lib/ecmascript.ts index a0bd5dbb..98cb52f8 100644 --- a/lib/ecmascript.ts +++ b/lib/ecmascript.ts @@ -3039,18 +3039,9 @@ interface ToStringOptions { roundingMode: ReturnType; } -// Because of JSBI, this helper function is quite a bit more complicated -// than in proposal-temporal. If we remove JSBI later, then we can simplify it -// to just the `typeof num === 'number'` branch. -const JSBI_NUMBER_MAX_SAFE_INTEGER = JSBI.BigInt(Number.MAX_SAFE_INTEGER); function formatAsDecimalNumber(num: number | JSBI) { - if (typeof num === 'number') { - if (num <= NumberMaxSafeInteger) return num.toString(10); - return JSBI.BigInt(num).toString(); - } else { - if (JSBI.lessThanOrEqual(num, JSBI_NUMBER_MAX_SAFE_INTEGER)) return JSBI.toNumber(num).toString(10); - return num.toString(); - } + if (typeof num === 'number' && num <= NumberMaxSafeInteger) return num.toString(10); + return JSBI.BigInt(num).toString(); } export function TemporalDurationToString( From 364ec9ee0de50ef57caa3e4a7a328bfeef6cac3c Mon Sep 17 00:00:00 2001 From: Justin Grant Date: Mon, 4 Mar 2024 00:24:12 -0800 Subject: [PATCH 37/39] fixup! chore: Ignore Test262 in ESLint and Prettier --- .eslintrc.yml | 16 +++------------- .prettierignore | 4 ++++ package.json | 10 +--------- 3 files changed, 8 insertions(+), 22 deletions(-) create mode 100644 .prettierignore diff --git a/.eslintrc.yml b/.eslintrc.yml index fe728843..f01ed8f6 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -18,9 +18,9 @@ globals: globalThis: readonly ignorePatterns: - node_modules/ - - /dist/ - - /tsc-out/ - - /test262/ + - dist/ + - tsc-out/ + - test262/ rules: array-element-newline: - error @@ -117,13 +117,3 @@ overrides: '@typescript-eslint/consistent-type-exports': error '@typescript-eslint/consistent-type-imports': error prefer-const: off - - files: - - polyfill/test262/** - rules: - quotes: - - error - - double - - avoidEscape: true - prettier/prettier: - - error - - singleQuote: false diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000..e7900581 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,4 @@ +node_modules/ +dist/ +tsc-out/ +test262/ diff --git a/package.json b/package.json index 0ff525bc..4ad39a78 100644 --- a/package.json +++ b/package.json @@ -124,15 +124,7 @@ "semi": true, "singleQuote": true, "bracketSpacing": true, - "arrowParens": "always", - "overrides": [ - { - "files": "polyfill/test262/**", - "options": { - "singleQuote": false - } - } - ] + "arrowParens": "always" }, "directories": { "lib": "lib", From 9260ee140594f9179f9e550eb7068ae3f6ef01c8 Mon Sep 17 00:00:00 2001 From: Justin Grant Date: Mon, 4 Mar 2024 17:43:09 -0800 Subject: [PATCH 38/39] fixup! Polyfill: Be more robust against late-run primordial manipulation --- lib/calendar.ts | 38 ++++++++++++++------------------------ 1 file changed, 14 insertions(+), 24 deletions(-) diff --git a/lib/calendar.ts b/lib/calendar.ts index 415fa27c..9c5e24ff 100644 --- a/lib/calendar.ts +++ b/lib/calendar.ts @@ -39,19 +39,9 @@ const MathAbs = Math.abs; const MathFloor = Math.floor; const ObjectCreate = Object.create; const ObjectEntries = Object.entries; -const OriginalMap = Map; const OriginalSet = Set; -const OriginalWeakMap = WeakMap; const ReflectOwnKeys = Reflect.ownKeys; -const MapPrototypeEntries = Map.prototype.entries; -const MapPrototypeGet = Map.prototype.get; -const MapPrototypeSet = Map.prototype.set; const SetPrototypeAdd = Set.prototype.add; -const WeakMapPrototypeGet = WeakMap.prototype.get; -const WeakMapPrototypeSet = WeakMap.prototype.set; - -const MapIterator = ES.Call(MapPrototypeEntries, new Map(), []); -const MapIteratorPrototypeNext = MapIterator.next; function arrayFromSet(src: Set): T[] { return [...src]; @@ -194,7 +184,7 @@ export class Calendar implements Temporal.Calendar { fields(fields: Params['fields'][0]): Return['fields'] { if (!ES.IsTemporalCalendar(this)) throw new TypeError('invalid receiver'); const fieldsArray = [] as string[]; - const allowed = new Set(['year', 'month', 'monthCode', 'day']); + const allowed = new OriginalSet(['year', 'month', 'monthCode', 'day']); for (const name of fields) { if (typeof name !== 'string') throw new TypeError('invalid fields'); if (!allowed.has(name)) throw new RangeError(`invalid field name ${name}`); @@ -619,7 +609,7 @@ type CachedTypes = Temporal.PlainYearMonth | Temporal.PlainDate | Temporal.Plain * because each object's cache is thrown away when the object is GC-ed. */ class OneObjectCache { - map = new OriginalMap(); + map = new Map(); calls = 0; now: number; hits = 0; @@ -628,17 +618,14 @@ class OneObjectCache { this.now = globalThis.performance ? globalThis.performance.now() : Date.now(); if (cacheToClone !== undefined) { let i = 0; - const entriesIterator = ES.Call(MapPrototypeEntries, cacheToClone.map, []); - for (;;) { - const iterResult = ES.Call(MapIteratorPrototypeNext, entriesIterator, []); - if (iterResult.done) break; + for (const entry of cacheToClone.map.entries()) { if (++i > OneObjectCache.MAX_CACHE_ENTRIES) break; - ES.Call(MapPrototypeSet, this.map, iterResult.value); + this.map.set(...entry); } } } get(key: string) { - const result = ES.Call(MapPrototypeGet, this.map, [key]); + const result = this.map.get(key); if (result) { this.hits++; this.report(); @@ -647,7 +634,7 @@ class OneObjectCache { return result; } set(key: string, value: unknown) { - ES.Call(MapPrototypeSet, this.map, [key, value]); + this.map.set(key, value); this.misses++; this.report(); } @@ -660,12 +647,12 @@ class OneObjectCache { */ } setObject(obj: CachedTypes) { - if (ES.Call(WeakMapPrototypeGet, OneObjectCache.objectMap, [obj])) throw new RangeError('object already cached'); - ES.Call(WeakMapPrototypeSet, OneObjectCache.objectMap, [obj, this]); + if (OneObjectCache.objectMap.get(obj)) throw new RangeError('object already cached'); + OneObjectCache.objectMap.set(obj, this); this.report(); } - static objectMap = new OriginalWeakMap(); + static objectMap = new WeakMap(); static MAX_CACHE_ENTRIES = 1000; /** @@ -675,10 +662,10 @@ class OneObjectCache { * @param obj - object to associate with the cache */ static getCacheForObject(obj: CachedTypes) { - let cache = ES.Call(WeakMapPrototypeGet, OneObjectCache.objectMap, [obj]); + let cache = OneObjectCache.objectMap.get(obj); if (!cache) { cache = new OneObjectCache(); - ES.Call(WeakMapPrototypeSet, OneObjectCache.objectMap, [obj, cache]); + OneObjectCache.objectMap.set(obj, cache); } return cache; } @@ -2387,6 +2374,9 @@ class NonIsoCalendar implements CalendarImpl { return result; } fields(fields: string[]): string[] { + // Note that `fields` is a new array created by the caller of this method, + // not the original input passed by the original caller. So it's safe to + // mutate it here because the mutation is not observable. if (ES.Call(ArrayIncludes, fields, ['year'])) { ES.Call(ArrayPrototypePush, fields, ['era', 'eraYear']); } From 100ac63310cae88c634e378b8ca958708923cae9 Mon Sep 17 00:00:00 2001 From: Justin Grant Date: Mon, 4 Mar 2024 18:54:12 -0800 Subject: [PATCH 39/39] Minor updates from code reviews * Change unnecessary `let` to `const`. * Add type annotations that were removed while rebasing, and add them for newly-added methods. --- lib/ecmascript.ts | 63 +++++++++++++++++++++++++++++++++-------------- 1 file changed, 45 insertions(+), 18 deletions(-) diff --git a/lib/ecmascript.ts b/lib/ecmascript.ts index 98cb52f8..3777632a 100644 --- a/lib/ecmascript.ts +++ b/lib/ecmascript.ts @@ -217,7 +217,7 @@ export function ToNumber(value: unknown): number { return NumberCtor(value); } -export function ToIntegerOrInfinity(value: unknown) { +export function ToIntegerOrInfinity(value: unknown): number { const number = ToNumber(value); if (NumberIsNaN(number) || number === 0) { return 0; @@ -308,7 +308,7 @@ function abs(x: JSBI): JSBI { } // This convenience function isn't in the spec, but is useful in the polyfill // for DRY and better error messages. -export function RequireString(value: unknown) { +export function RequireString(value: unknown): string { if (typeof value !== 'string') { // Use String() to ensure that Symbols won't throw throw new TypeError(`expected a string, not ${StringCtor(value)}`); @@ -318,7 +318,7 @@ export function RequireString(value: unknown) { // This function is an enum in the spec, but it's helpful to make it a // function in the polyfill. -function ToPrimitiveAndRequireString(valueParam: unknown) { +function ToPrimitiveAndRequireString(valueParam: unknown): string { const value = ToPrimitive(valueParam, StringCtor); return RequireString(value); } @@ -702,7 +702,9 @@ function throwBadTimeZoneStringError(timeZoneString: string): never { throw new RangeError(`${msg}: ${timeZoneString}`); } -export function ParseTimeZoneIdentifier(identifier: string): { tzName?: string; offsetMinutes?: number } { +export function ParseTimeZoneIdentifier( + identifier: string +): { tzName: string; offsetMinutes?: undefined } | { tzName?: undefined; offsetMinutes: number } { if (!TIMEZONE_IDENTIFIER.test(identifier)) { throwBadTimeZoneStringError(identifier); } @@ -719,7 +721,11 @@ export function ParseTimeZoneIdentifier(identifier: string): { tzName?: string; // ParseTemporalTimeZoneString so that parsing can be tested separately from the // logic of converting parsed values into a named or offset identifier. // ts-prune-ignore-next TODO: remove if test/validStrings is converted to TS. -export function ParseTemporalTimeZoneStringRaw(timeZoneString: string) { +export function ParseTemporalTimeZoneStringRaw(timeZoneString: string): { + tzAnnotation: string; + offset: string | undefined; + z: boolean; +} { if (TIMEZONE_IDENTIFIER.test(timeZoneString)) { return { tzAnnotation: timeZoneString, offset: undefined, z: false }; } @@ -735,7 +741,7 @@ export function ParseTemporalTimeZoneStringRaw(timeZoneString: string) { throwBadTimeZoneStringError(timeZoneString); } -function ParseTemporalTimeZoneString(stringIdent: string) { +function ParseTemporalTimeZoneString(stringIdent: string): ReturnType { const { tzAnnotation, offset, z } = ParseTemporalTimeZoneStringRaw(stringIdent); if (tzAnnotation) return ParseTimeZoneIdentifier(tzAnnotation); if (z) return ParseTimeZoneIdentifier('UTC'); @@ -1590,7 +1596,16 @@ export function InterpretTemporalDateTimeFields( } export function ToTemporalDateTime(item: PlainDateTimeParams['from'][0], options?: PlainDateTimeParams['from'][1]) { - let year, month, day, hour, minute, second, millisecond, microsecond, nanosecond, calendar; + let year: number, + month: number, + day: number, + hour: number, + minute: number, + second: number, + millisecond: number, + microsecond: number, + nanosecond: number, + calendar; const resolvedOptions = SnapshotOwnProperties(GetOptionsObject(options), null); if (IsObject(item)) { @@ -1865,9 +1880,21 @@ export function ToTemporalZonedDateTime( item: ZonedDateTimeParams['from'][0], options?: ZonedDateTimeParams['from'][1] ) { - let year, month, day, hour, minute, second, millisecond, microsecond, nanosecond, timeZone, offset, calendar; + let year: number, + month: number, + day: number, + hour: number, + minute: number, + second: number, + millisecond: number, + microsecond: number, + nanosecond: number, + timeZone, + offset: string | undefined, + calendar: string | Temporal.CalendarProtocol | undefined; const resolvedOptions = SnapshotOwnProperties(GetOptionsObject(options), null); - let disambiguation, offsetOpt; + let disambiguation: NonNullable; + let offsetOpt: NonNullable; let matchMinute = false; let offsetBehaviour: OffsetBehaviour = 'option'; if (IsObject(item)) { @@ -2771,7 +2798,7 @@ export function GetOffsetStringFor(timeZone: string | Temporal.TimeZoneProtocol, return FormatUTCOffsetNanoseconds(offsetNs); } -export function FormatUTCOffsetNanoseconds(offsetNs: number) { +export function FormatUTCOffsetNanoseconds(offsetNs: number): string { const sign = offsetNs < 0 ? '-' : '+'; const absoluteNs = MathAbs(offsetNs); const hour = MathFloor(absoluteNs / 3600e9); @@ -2970,8 +2997,8 @@ function GetPossibleInstantsFor( export function ISOYearString(year: number) { let yearString; if (year < 0 || year > 9999) { - let sign = year < 0 ? '-' : '+'; - let yearNumber = MathAbs(year); + const sign = year < 0 ? '-' : '+'; + const yearNumber = MathAbs(year); yearString = sign + ToZeroPaddedDecimalString(yearNumber, 6); } else { yearString = ToZeroPaddedDecimalString(year, 4); @@ -2986,7 +3013,7 @@ export function ISODateTimePartString(part: number) { function FormatFractionalSeconds( subSecondNanoseconds: number, precision: Exclude -) { +): string { let fraction; if (precision === 'auto') { if (subSecondNanoseconds === 0) return ''; @@ -3007,7 +3034,7 @@ export function FormatTimeString( second: number, subSecondNanoseconds: number, precision: SecondsStringPrecisionRecord['precision'] -) { +): string { let result = `${ISODateTimePartString(hour)}:${ISODateTimePartString(minute)}`; if (precision === 'minute') return result; @@ -3039,7 +3066,7 @@ interface ToStringOptions { roundingMode: ReturnType; } -function formatAsDecimalNumber(num: number | JSBI) { +function formatAsDecimalNumber(num: number | JSBI): string { if (typeof num === 'number' && num <= NumberMaxSafeInteger) return num.toString(10); return JSBI.BigInt(num).toString(); } @@ -3223,7 +3250,7 @@ export function IsOffsetTimeZoneIdentifier(string: string): boolean { return OFFSET.test(string); } -export function ParseDateTimeUTCOffset(string: string) { +export function ParseDateTimeUTCOffset(string: string): number { const match = OFFSET_WITH_PARTS.exec(string); if (!match) { throw new RangeError(`invalid time zone offset: ${string}`); @@ -3351,7 +3378,7 @@ export function GetNamedTimeZoneOffsetNanoseconds(id: string, epochNanoseconds: return JSBI.toNumber(JSBI.subtract(utc, epochNanoseconds)); } -export function FormatOffsetTimeZoneIdentifier(offsetMinutes: number) { +export function FormatOffsetTimeZoneIdentifier(offsetMinutes: number): string { const sign = offsetMinutes < 0 ? '-' : '+'; const absoluteMinutes = MathAbs(offsetMinutes); const hour = MathFloor(absoluteMinutes / 60); @@ -3360,7 +3387,7 @@ export function FormatOffsetTimeZoneIdentifier(offsetMinutes: number) { return `${sign}${timeString}`; } -function FormatDateTimeUTCOffsetRounded(offsetNanosecondsParam: number) { +function FormatDateTimeUTCOffsetRounded(offsetNanosecondsParam: number): string { const offsetNanoseconds = JSBI.toNumber( RoundNumberToIncrement(JSBI.BigInt(offsetNanosecondsParam), MINUTE_NANOS, 'halfExpand') );