@@ -268,6 +268,17 @@ export function IsTemporalMonthDay(item: unknown): item is Temporal.PlainMonthDa
268
268
export function IsTemporalZonedDateTime(item: unknown): item is Temporal.ZonedDateTime {
269
269
return HasSlot(item, EPOCHNANOSECONDS, TIME_ZONE, CALENDAR);
270
270
}
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
+ }
271
282
function TemporalTimeZoneFromString(stringIdent: string) {
272
283
// TODO: why aren't these three variables destructured to include `undefined` as possible types?
273
284
let { ianaName, offset, z } = ParseTemporalTimeZoneString(stringIdent);
@@ -917,6 +928,7 @@ export function ToRelativeTemporalObject(options: {
917
928
if (relativeTo === undefined) return relativeTo as undefined;
918
929
919
930
let offsetBehaviour: OffsetBehaviour = 'option';
931
+ let matchMinutes = false;
920
932
let year, month, day, hour, minute, second, millisecond, microsecond, nanosecond, calendar, timeZone, offset;
921
933
if (IsObject(relativeTo)) {
922
934
if (IsTemporalZonedDateTime(relativeTo) || IsTemporalDateTime(relativeTo)) return relativeTo;
@@ -935,7 +947,18 @@ export function ToRelativeTemporalObject(options: {
935
947
);
936
948
}
937
949
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);
939
962
const fields = ToTemporalDateTimeFields(relativeTo, fieldNames);
940
963
const dateOptions = ObjectCreate(null);
941
964
dateOptions.overflow = 'constrain';
@@ -962,6 +985,7 @@ export function ToRelativeTemporalObject(options: {
962
985
}
963
986
if (!calendar) calendar = GetISO8601Calendar();
964
987
calendar = ToTemporalCalendar(calendar);
988
+ matchMinutes = true;
965
989
}
966
990
if (timeZone) {
967
991
timeZone = ToTemporalTimeZone(timeZone);
@@ -981,7 +1005,8 @@ export function ToRelativeTemporalObject(options: {
981
1005
offsetNs,
982
1006
timeZone,
983
1007
'compatible',
984
- 'reject'
1008
+ 'reject',
1009
+ matchMinutes
985
1010
);
986
1011
return CreateTemporalZonedDateTime(epochNanoseconds, timeZone, calendar);
987
1012
}
@@ -1187,7 +1212,7 @@ export function ToTemporalYearMonthFields(
1187
1212
return PrepareTemporalFields(bag, entries);
1188
1213
}
1189
1214
1190
- export function ToTemporalZonedDateTimeFields(
1215
+ function ToTemporalZonedDateTimeFields(
1191
1216
bag: Temporal.ZonedDateTimeLike,
1192
1217
fieldNames: readonly (keyof Temporal.ZonedDateTimeLike)[]
1193
1218
) {
@@ -1514,7 +1539,8 @@ export function InterpretISODateTimeOffset(
1514
1539
offsetNs: number,
1515
1540
timeZone: Temporal.TimeZoneProtocol,
1516
1541
disambiguation: Temporal.ToInstantOptions['disambiguation'],
1517
- offsetOpt: Temporal.OffsetDisambiguationOptions['offset']
1542
+ offsetOpt: Temporal.OffsetDisambiguationOptions['offset'],
1543
+ matchMinute: boolean
1518
1544
) {
1519
1545
const DateTime = GetIntrinsic('%Temporal.PlainDateTime%');
1520
1546
const dt = new DateTime(year, month, day, hour, minute, second, millisecond, microsecond, nanosecond);
@@ -1540,7 +1566,10 @@ export function InterpretISODateTimeOffset(
1540
1566
const possibleInstants = GetPossibleInstantsFor(timeZone, dt);
1541
1567
for (const candidate of possibleInstants) {
1542
1568
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
+ }
1544
1573
}
1545
1574
1546
1575
// the user-provided offset doesn't match any instants for this time
@@ -1575,6 +1604,7 @@ export function ToTemporalZonedDateTime(
1575
1604
timeZone,
1576
1605
offset: string,
1577
1606
calendar: string | Temporal.CalendarProtocol;
1607
+ let matchMinute = false;
1578
1608
let offsetBehaviour: OffsetBehaviour = 'option';
1579
1609
if (IsObject(item)) {
1580
1610
if (IsTemporalZonedDateTime(item)) return item;
@@ -1619,6 +1649,7 @@ export function ToTemporalZonedDateTime(
1619
1649
timeZone = new TemporalTimeZone(ianaName);
1620
1650
if (!calendar) calendar = GetISO8601Calendar();
1621
1651
calendar = ToTemporalCalendar(calendar);
1652
+ matchMinute = true; // ISO strings may specify offset with less precision
1622
1653
}
1623
1654
let offsetNs = 0;
1624
1655
if (offsetBehaviour === 'option') offsetNs = ParseOffsetString(offset);
@@ -1638,7 +1669,8 @@ export function ToTemporalZonedDateTime(
1638
1669
offsetNs,
1639
1670
timeZone,
1640
1671
disambiguation,
1641
- offsetOpt
1672
+ offsetOpt,
1673
+ matchMinute
1642
1674
);
1643
1675
return CreateTemporalZonedDateTime(epochNanoseconds, timeZone, calendar);
1644
1676
}
@@ -2414,7 +2446,10 @@ export function TemporalInstantToString(
2414
2446
precision
2415
2447
);
2416
2448
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
+ }
2418
2453
return `${year}-${month}-${day}T${hour}:${minute}${seconds}${timeZoneString}`;
2419
2454
}
2420
2455
@@ -2629,7 +2664,10 @@ export function TemporalZonedDateTimeToString(
2629
2664
precision
2630
2665
);
2631
2666
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
+ }
2633
2671
if (showTimeZone !== 'never') result += `[${tz}]`;
2634
2672
const calendarID = ToString(GetSlot(zdt, CALENDAR));
2635
2673
result += FormatCalendarAnnotation(calendarID, showCalendar);
@@ -2686,6 +2724,17 @@ function FormatTimeZoneOffsetString(offsetNanosecondsParam: number): string {
2686
2724
return `${sign}${hourString}:${minuteString}${post}`;
2687
2725
}
2688
2726
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
+ }
2689
2738
export function GetEpochFromISOParts(
2690
2739
year: number,
2691
2740
month: number,
0 commit comments