Skip to content

Commit 766e503

Browse files
12wrigjaptomato
andauthored
Port latest Normative changes from tc39/proposal-temporal. (#85)
Co-authored-by: Philip Chimento <[email protected]>
1 parent 960d9b7 commit 766e503

15 files changed

+239
-90
lines changed

lib/ecmascript.ts

Lines changed: 57 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,17 @@ export function IsTemporalMonthDay(item: unknown): item is Temporal.PlainMonthDa
268268
export function IsTemporalZonedDateTime(item: unknown): item is Temporal.ZonedDateTime {
269269
return HasSlot(item, EPOCHNANOSECONDS, TIME_ZONE, CALENDAR);
270270
}
271+
export function RejectObjectWithCalendarOrTimeZone(item: AnyTemporalLikeType) {
272+
if (HasSlot(item, CALENDAR) || HasSlot(item, TIME_ZONE)) {
273+
throw new TypeError('with() does not support a calendar or timeZone property');
274+
}
275+
if ((item as any).calendar !== undefined) {
276+
throw new TypeError('with() does not support a calendar property');
277+
}
278+
if ((item as any).timeZone !== undefined) {
279+
throw new TypeError('with() does not support a timeZone property');
280+
}
281+
}
271282
function TemporalTimeZoneFromString(stringIdent: string) {
272283
// TODO: why aren't these three variables destructured to include `undefined` as possible types?
273284
let { ianaName, offset, z } = ParseTemporalTimeZoneString(stringIdent);
@@ -917,6 +928,7 @@ export function ToRelativeTemporalObject(options: {
917928
if (relativeTo === undefined) return relativeTo as undefined;
918929

919930
let offsetBehaviour: OffsetBehaviour = 'option';
931+
let matchMinutes = false;
920932
let year, month, day, hour, minute, second, millisecond, microsecond, nanosecond, calendar, timeZone, offset;
921933
if (IsObject(relativeTo)) {
922934
if (IsTemporalZonedDateTime(relativeTo) || IsTemporalDateTime(relativeTo)) return relativeTo;
@@ -935,7 +947,18 @@ export function ToRelativeTemporalObject(options: {
935947
);
936948
}
937949
calendar = GetTemporalCalendarWithISODefault(relativeTo);
938-
const fieldNames = CalendarFields(calendar, ['day', 'month', 'monthCode', 'year'] as const);
950+
const fieldNames = CalendarFields(calendar, [
951+
'day',
952+
'hour',
953+
'microsecond',
954+
'millisecond',
955+
'minute',
956+
'month',
957+
'monthCode',
958+
'nanosecond',
959+
'second',
960+
'year'
961+
] as const);
939962
const fields = ToTemporalDateTimeFields(relativeTo, fieldNames);
940963
const dateOptions = ObjectCreate(null);
941964
dateOptions.overflow = 'constrain';
@@ -962,6 +985,7 @@ export function ToRelativeTemporalObject(options: {
962985
}
963986
if (!calendar) calendar = GetISO8601Calendar();
964987
calendar = ToTemporalCalendar(calendar);
988+
matchMinutes = true;
965989
}
966990
if (timeZone) {
967991
timeZone = ToTemporalTimeZone(timeZone);
@@ -981,7 +1005,8 @@ export function ToRelativeTemporalObject(options: {
9811005
offsetNs,
9821006
timeZone,
9831007
'compatible',
984-
'reject'
1008+
'reject',
1009+
matchMinutes
9851010
);
9861011
return CreateTemporalZonedDateTime(epochNanoseconds, timeZone, calendar);
9871012
}
@@ -1187,7 +1212,7 @@ export function ToTemporalYearMonthFields(
11871212
return PrepareTemporalFields(bag, entries);
11881213
}
11891214

1190-
export function ToTemporalZonedDateTimeFields(
1215+
function ToTemporalZonedDateTimeFields(
11911216
bag: Temporal.ZonedDateTimeLike,
11921217
fieldNames: readonly (keyof Temporal.ZonedDateTimeLike)[]
11931218
) {
@@ -1514,7 +1539,8 @@ export function InterpretISODateTimeOffset(
15141539
offsetNs: number,
15151540
timeZone: Temporal.TimeZoneProtocol,
15161541
disambiguation: Temporal.ToInstantOptions['disambiguation'],
1517-
offsetOpt: Temporal.OffsetDisambiguationOptions['offset']
1542+
offsetOpt: Temporal.OffsetDisambiguationOptions['offset'],
1543+
matchMinute: boolean
15181544
) {
15191545
const DateTime = GetIntrinsic('%Temporal.PlainDateTime%');
15201546
const dt = new DateTime(year, month, day, hour, minute, second, millisecond, microsecond, nanosecond);
@@ -1540,7 +1566,10 @@ export function InterpretISODateTimeOffset(
15401566
const possibleInstants = GetPossibleInstantsFor(timeZone, dt);
15411567
for (const candidate of possibleInstants) {
15421568
const candidateOffset = GetOffsetNanosecondsFor(timeZone, candidate);
1543-
if (candidateOffset === offsetNs) return GetSlot(candidate, EPOCHNANOSECONDS);
1569+
const roundedCandidateOffset = RoundNumberToIncrement(bigInt(candidateOffset), 60e9, 'halfExpand').toJSNumber();
1570+
if (candidateOffset === offsetNs || (matchMinute && roundedCandidateOffset === offsetNs)) {
1571+
return GetSlot(candidate, EPOCHNANOSECONDS);
1572+
}
15441573
}
15451574

15461575
// the user-provided offset doesn't match any instants for this time
@@ -1575,6 +1604,7 @@ export function ToTemporalZonedDateTime(
15751604
timeZone,
15761605
offset: string,
15771606
calendar: string | Temporal.CalendarProtocol;
1607+
let matchMinute = false;
15781608
let offsetBehaviour: OffsetBehaviour = 'option';
15791609
if (IsObject(item)) {
15801610
if (IsTemporalZonedDateTime(item)) return item;
@@ -1619,6 +1649,7 @@ export function ToTemporalZonedDateTime(
16191649
timeZone = new TemporalTimeZone(ianaName);
16201650
if (!calendar) calendar = GetISO8601Calendar();
16211651
calendar = ToTemporalCalendar(calendar);
1652+
matchMinute = true; // ISO strings may specify offset with less precision
16221653
}
16231654
let offsetNs = 0;
16241655
if (offsetBehaviour === 'option') offsetNs = ParseOffsetString(offset);
@@ -1638,7 +1669,8 @@ export function ToTemporalZonedDateTime(
16381669
offsetNs,
16391670
timeZone,
16401671
disambiguation,
1641-
offsetOpt
1672+
offsetOpt,
1673+
matchMinute
16421674
);
16431675
return CreateTemporalZonedDateTime(epochNanoseconds, timeZone, calendar);
16441676
}
@@ -2414,7 +2446,10 @@ export function TemporalInstantToString(
24142446
precision
24152447
);
24162448
let timeZoneString = 'Z';
2417-
if (timeZone !== undefined) timeZoneString = BuiltinTimeZoneGetOffsetStringFor(outputTimeZone, instant);
2449+
if (timeZone !== undefined) {
2450+
const offsetNs = GetOffsetNanosecondsFor(outputTimeZone, instant);
2451+
timeZoneString = FormatISOTimeZoneOffsetString(offsetNs);
2452+
}
24182453
return `${year}-${month}-${day}T${hour}:${minute}${seconds}${timeZoneString}`;
24192454
}
24202455

@@ -2629,7 +2664,10 @@ export function TemporalZonedDateTimeToString(
26292664
precision
26302665
);
26312666
let result = `${year}-${month}-${day}T${hour}:${minute}${seconds}`;
2632-
if (showOffset !== 'never') result += BuiltinTimeZoneGetOffsetStringFor(tz, instant);
2667+
if (showOffset !== 'never') {
2668+
const offsetNs = GetOffsetNanosecondsFor(tz, instant);
2669+
result += FormatISOTimeZoneOffsetString(offsetNs);
2670+
}
26332671
if (showTimeZone !== 'never') result += `[${tz}]`;
26342672
const calendarID = ToString(GetSlot(zdt, CALENDAR));
26352673
result += FormatCalendarAnnotation(calendarID, showCalendar);
@@ -2686,6 +2724,17 @@ function FormatTimeZoneOffsetString(offsetNanosecondsParam: number): string {
26862724
return `${sign}${hourString}:${minuteString}${post}`;
26872725
}
26882726

2727+
function FormatISOTimeZoneOffsetString(offsetNanosecondsParam: number): string {
2728+
let offsetNanoseconds = RoundNumberToIncrement(bigInt(offsetNanosecondsParam), 60e9, 'halfExpand').toJSNumber();
2729+
const sign = offsetNanoseconds < 0 ? '-' : '+';
2730+
offsetNanoseconds = MathAbs(offsetNanoseconds);
2731+
const minutes = (offsetNanoseconds / 60e9) % 60;
2732+
const hours = MathFloor(offsetNanoseconds / 3600e9);
2733+
2734+
const hourString = ISODateTimePartString(hours);
2735+
const minuteString = ISODateTimePartString(minutes);
2736+
return `${sign}${hourString}:${minuteString}`;
2737+
}
26892738
export function GetEpochFromISOParts(
26902739
year: number,
26912740
month: number,

lib/plaindate.ts

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,8 @@ import {
1111
ISO_MICROSECOND,
1212
ISO_NANOSECOND,
1313
CALENDAR,
14-
TIME_ZONE,
1514
EPOCHNANOSECONDS,
16-
GetSlot,
17-
HasSlot
15+
GetSlot
1816
} from './slots';
1917
import { Temporal } from '..';
2018
import { DateTimeFormat } from './intl';
@@ -109,15 +107,7 @@ export class PlainDate implements Temporal.PlainDate {
109107
if (!ES.IsObject(temporalDateLike)) {
110108
throw new TypeError('invalid argument');
111109
}
112-
if (HasSlot(temporalDateLike, CALENDAR) || HasSlot(temporalDateLike, TIME_ZONE)) {
113-
throw new TypeError('with() does not support a calendar or timeZone property');
114-
}
115-
if (temporalDateLike.calendar !== undefined) {
116-
throw new TypeError('with() does not support a calendar property');
117-
}
118-
if ((temporalDateLike as Temporal.ZonedDateTimeLike).timeZone !== undefined) {
119-
throw new TypeError('with() does not support a timeZone property');
120-
}
110+
ES.RejectObjectWithCalendarOrTimeZone(temporalDateLike);
121111

122112
const calendar = GetSlot(this, CALENDAR);
123113
const fieldNames = ES.CalendarFields(calendar, ['day', 'month', 'monthCode', 'year'] as const);

lib/plaindatetime.ts

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,8 @@ import {
1212
ISO_MICROSECOND,
1313
ISO_NANOSECOND,
1414
CALENDAR,
15-
TIME_ZONE,
1615
EPOCHNANOSECONDS,
17-
GetSlot,
18-
HasSlot
16+
GetSlot
1917
} from './slots';
2018
import { Temporal } from '..';
2119
import { DateTimeFormat } from './intl';
@@ -156,15 +154,7 @@ export class PlainDateTime implements Temporal.PlainDateTime {
156154
if (!ES.IsObject(temporalDateTimeLike)) {
157155
throw new TypeError('invalid argument');
158156
}
159-
if (HasSlot(temporalDateTimeLike, CALENDAR) || HasSlot(temporalDateTimeLike, TIME_ZONE)) {
160-
throw new TypeError('with() does not support a calendar or timeZone property');
161-
}
162-
if (temporalDateTimeLike.calendar !== undefined) {
163-
throw new TypeError('with() does not support a calendar property');
164-
}
165-
if ((temporalDateTimeLike as Temporal.ZonedDateTimeLike).timeZone !== undefined) {
166-
throw new TypeError('with() does not support a timeZone property');
167-
}
157+
ES.RejectObjectWithCalendarOrTimeZone(temporalDateTimeLike);
168158

169159
const options = ES.GetOptionsObject(optionsParam);
170160
const calendar = GetSlot(this, CALENDAR);

lib/plainmonthday.ts

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as ES from './ecmascript';
22
import { MakeIntrinsicClass } from './intrinsicclass';
3-
import { ISO_MONTH, ISO_DAY, ISO_YEAR, CALENDAR, TIME_ZONE, GetSlot, HasSlot } from './slots';
3+
import { ISO_MONTH, ISO_DAY, ISO_YEAR, CALENDAR, GetSlot } from './slots';
44
import { Temporal } from '..';
55
import { DateTimeFormat } from './intl';
66
import type { FieldRecord, PlainMonthDayParams as Params, PlainMonthDayReturn as Return } from './internaltypes';
@@ -48,15 +48,7 @@ export class PlainMonthDay implements Temporal.PlainMonthDay {
4848
if (!ES.IsObject(temporalMonthDayLike)) {
4949
throw new TypeError('invalid argument');
5050
}
51-
if (HasSlot(temporalMonthDayLike, CALENDAR) || HasSlot(temporalMonthDayLike, TIME_ZONE)) {
52-
throw new TypeError('with() does not support a calendar or timeZone property');
53-
}
54-
if (temporalMonthDayLike.calendar !== undefined) {
55-
throw new TypeError('with() does not support a calendar property');
56-
}
57-
if ((temporalMonthDayLike as Temporal.ZonedDateTimeLike).timeZone !== undefined) {
58-
throw new TypeError('with() does not support a timeZone property');
59-
}
51+
ES.RejectObjectWithCalendarOrTimeZone(temporalMonthDayLike);
6052

6153
const calendar = GetSlot(this, CALENDAR);
6254
const fieldNames = ES.CalendarFields(calendar, ['day', 'month', 'monthCode', 'year'] as const);
@@ -129,6 +121,7 @@ export class PlainMonthDay implements Temporal.PlainMonthDay {
129121
});
130122
mergedFields = ES.PrepareTemporalFields(mergedFields, mergedEntries);
131123
const options = ObjectCreate(null);
124+
options.overflow = 'reject';
132125
return ES.DateFromFields(calendar, mergedFields, options);
133126
}
134127
getISOFields(): Return['getISOFields'] {

lib/plaintime.ts

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,9 @@ import {
1313
ISO_MICROSECOND,
1414
ISO_NANOSECOND,
1515
CALENDAR,
16-
TIME_ZONE,
1716
EPOCHNANOSECONDS,
1817
CreateSlots,
1918
GetSlot,
20-
HasSlot,
2119
SetSlot
2220
} from './slots';
2321
import { Temporal } from '..';
@@ -146,15 +144,7 @@ export class PlainTime implements Temporal.PlainTime {
146144
if (!ES.IsObject(temporalTimeLike)) {
147145
throw new TypeError('invalid argument');
148146
}
149-
if (HasSlot(temporalTimeLike, CALENDAR) || HasSlot(temporalTimeLike, TIME_ZONE)) {
150-
throw new TypeError('with() does not support a calendar or timeZone property');
151-
}
152-
if (temporalTimeLike.calendar !== undefined) {
153-
throw new TypeError('with() does not support a calendar property');
154-
}
155-
if ((temporalTimeLike as Temporal.ZonedDateTimeLike).timeZone !== undefined) {
156-
throw new TypeError('with() does not support a timeZone property');
157-
}
147+
ES.RejectObjectWithCalendarOrTimeZone(temporalTimeLike);
158148

159149
const options = ES.GetOptionsObject(optionsParam);
160150
const overflow = ES.ToTemporalOverflow(options);

lib/plainyearmonth.ts

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as ES from './ecmascript';
22
import { GetIntrinsic, MakeIntrinsicClass } from './intrinsicclass';
3-
import { ISO_YEAR, ISO_MONTH, ISO_DAY, CALENDAR, TIME_ZONE, GetSlot, HasSlot } from './slots';
3+
import { ISO_YEAR, ISO_MONTH, ISO_DAY, CALENDAR, GetSlot } from './slots';
44
import { Temporal } from '..';
55
import { DateTimeFormat } from './intl';
66
import type { FieldRecord, PlainYearMonthParams as Params, PlainYearMonthReturn as Return } from './internaltypes';
@@ -85,15 +85,7 @@ export class PlainYearMonth implements Temporal.PlainYearMonth {
8585
if (!ES.IsObject(temporalYearMonthLike)) {
8686
throw new TypeError('invalid argument');
8787
}
88-
if (HasSlot(temporalYearMonthLike, CALENDAR) || HasSlot(temporalYearMonthLike, TIME_ZONE)) {
89-
throw new TypeError('with() does not support a calendar or timeZone property');
90-
}
91-
if (temporalYearMonthLike.calendar !== undefined) {
92-
throw new TypeError('with() does not support a calendar property');
93-
}
94-
if ((temporalYearMonthLike as Temporal.ZonedDateTimeLike).timeZone !== undefined) {
95-
throw new TypeError('with() does not support a timeZone property');
96-
}
88+
ES.RejectObjectWithCalendarOrTimeZone(temporalYearMonthLike);
9789

9890
const calendar = GetSlot(this, CALENDAR);
9991
const fieldNames = ES.CalendarFields(calendar, ['month', 'monthCode', 'year'] as const);

lib/zoneddatetime.ts

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,7 @@ import {
1414
ISO_NANOSECOND,
1515
ISO_SECOND,
1616
TIME_ZONE,
17-
GetSlot,
18-
HasSlot
17+
GetSlot
1918
} from './slots';
2019
import { Temporal } from '..';
2120
import { DateTimeFormat } from './intl';
@@ -179,15 +178,7 @@ export class ZonedDateTime implements Temporal.ZonedDateTime {
179178
if (!ES.IsObject(temporalZonedDateTimeLike)) {
180179
throw new TypeError('invalid zoned-date-time-like');
181180
}
182-
if (HasSlot(temporalZonedDateTimeLike, CALENDAR) || HasSlot(temporalZonedDateTimeLike, TIME_ZONE)) {
183-
throw new TypeError('with() does not support a calendar or timeZone property');
184-
}
185-
if (temporalZonedDateTimeLike.calendar !== undefined) {
186-
throw new TypeError('calendar invalid for with(). use withCalendar()');
187-
}
188-
if (temporalZonedDateTimeLike.timeZone !== undefined) {
189-
throw new TypeError('timeZone invalid for with(). use withTimeZone()');
190-
}
181+
ES.RejectObjectWithCalendarOrTimeZone(temporalZonedDateTimeLike);
191182

192183
const options = ES.GetOptionsObject(optionsParam);
193184
const disambiguation = ES.ToTemporalDisambiguation(options);
@@ -212,9 +203,30 @@ export class ZonedDateTime implements Temporal.ZonedDateTime {
212203
if (!props) {
213204
throw new TypeError('invalid zoned-date-time-like');
214205
}
215-
let fields = ES.ToTemporalZonedDateTimeFields(this, fieldNames) as any;
206+
// Unlike ToTemporalZonedDateTimeFields, the offset property will be required.
207+
const entries: ([keyof Temporal.ZonedDateTimeLike, 0 | undefined] | ['timeZone'] | ['offset'])[] = [
208+
['day', undefined],
209+
['hour', 0],
210+
['microsecond', 0],
211+
['millisecond', 0],
212+
['minute', 0],
213+
['month', undefined],
214+
['monthCode', undefined],
215+
['nanosecond', 0],
216+
['second', 0],
217+
['year', undefined],
218+
['offset'],
219+
['timeZone']
220+
];
221+
// Add extra fields from the calendar at the end
222+
fieldNames.forEach((fieldName) => {
223+
if (!entries.some(([name]) => name === fieldName)) {
224+
entries.push([fieldName, undefined]);
225+
}
226+
});
227+
let fields = ES.PrepareTemporalFields(this, entries as any);
216228
fields = ES.CalendarMergeFields(calendar, fields, props);
217-
fields = ES.ToTemporalZonedDateTimeFields(fields, fieldNames);
229+
fields = ES.PrepareTemporalFields(fields, entries as any);
218230
const { year, month, day, hour, minute, second, millisecond, microsecond, nanosecond } =
219231
ES.InterpretTemporalDateTimeFields(calendar, fields, options);
220232
const offsetNs = ES.ParseOffsetString(fields.offset);
@@ -232,7 +244,8 @@ export class ZonedDateTime implements Temporal.ZonedDateTime {
232244
offsetNs,
233245
timeZone,
234246
disambiguation,
235-
offset
247+
offset,
248+
/* matchMinute = */ false
236249
);
237250

238251
return ES.CreateTemporalZonedDateTime(epochNanoseconds, GetSlot(this, TIME_ZONE), calendar);
@@ -648,7 +661,8 @@ export class ZonedDateTime implements Temporal.ZonedDateTime {
648661
offsetNs,
649662
timeZone,
650663
'compatible',
651-
'prefer'
664+
'prefer',
665+
/* matchMinute = */ false
652666
);
653667

654668
return ES.CreateTemporalZonedDateTime(epochNanoseconds, timeZone, GetSlot(this, CALENDAR));

0 commit comments

Comments
 (0)