@formatjs/intl-datetimeformat 7.2.0 → 7.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@formatjs/intl-datetimeformat",
3
3
  "description": "Intl.DateTimeFormat polyfill",
4
- "version": "7.2.0",
4
+ "version": "7.2.1",
5
5
  "license": "MIT",
6
6
  "author": "Long Ho <holevietlong@gmail.com>",
7
7
  "type": "module",
@@ -18,12 +18,12 @@
18
18
  "dependencies": {
19
19
  "decimal.js": "^10.6.0",
20
20
  "tslib": "^2.8.1",
21
- "@formatjs/ecma402-abstract": "3.1.0",
22
- "@formatjs/intl-localematcher": "0.8.0"
21
+ "@formatjs/ecma402-abstract": "3.1.1",
22
+ "@formatjs/intl-localematcher": "0.8.1"
23
23
  },
24
24
  "devDependencies": {
25
- "@formatjs/intl-getcanonicallocales": "3.2.0",
26
- "@formatjs/intl-locale": "5.2.0"
25
+ "@formatjs/intl-getcanonicallocales": "3.2.1",
26
+ "@formatjs/intl-locale": "5.2.1"
27
27
  },
28
28
  "bugs": "https://github.com/formatjs/formatjs/issues",
29
29
  "homepage": "https://github.com/formatjs/formatjs#readme",
package/polyfill.iife.js CHANGED
@@ -6816,7 +6816,8 @@
6816
6816
  getInternalSlots: getInternalSlots2,
6817
6817
  localeData,
6818
6818
  getDefaultTimeZone,
6819
- tzData
6819
+ tzData,
6820
+ rangeFormatOptions
6820
6821
  }) {
6821
6822
  x = TimeClip(x);
6822
6823
  const internalSlots = getInternalSlots2(dtf);
@@ -6913,7 +6914,7 @@
6913
6914
  }
6914
6915
  }
6915
6916
  if (p === "hour" && hourCycle === "h24") {
6916
- if (v === 0) {
6917
+ if (v === 0 && !(rangeFormatOptions == null ? void 0 : rangeFormatOptions.isDifferentDate)) {
6917
6918
  v = 24;
6918
6919
  }
6919
6920
  }
@@ -7040,6 +7041,7 @@
7040
7041
  let rangePattern;
7041
7042
  let dateFieldsPracticallyEqual = true;
7042
7043
  let patternContainsLargerDateField = false;
7044
+ let firstDifferingField;
7043
7045
  for (const fieldName of TABLE_2_FIELDS) {
7044
7046
  if (dateFieldsPracticallyEqual && !patternContainsLargerDateField) {
7045
7047
  let rp = fieldName in rangePatterns ? rangePatterns[fieldName] : void 0;
@@ -7052,6 +7054,7 @@
7052
7054
  let v2 = tm2.hour;
7053
7055
  if (v1 > 11 && v2 < 11 || v1 < 11 && v2 > 11) {
7054
7056
  dateFieldsPracticallyEqual = false;
7057
+ firstDifferingField = fieldName;
7055
7058
  }
7056
7059
  } else if (fieldName === "dayPeriod") {
7057
7060
  } else if (fieldName === "fractionalSecondDigits") {
@@ -7067,12 +7070,14 @@
7067
7070
  );
7068
7071
  if (!SameValue(v1, v2)) {
7069
7072
  dateFieldsPracticallyEqual = false;
7073
+ firstDifferingField = fieldName;
7070
7074
  }
7071
7075
  } else {
7072
7076
  let v1 = tm1[fieldName];
7073
7077
  let v2 = tm2[fieldName];
7074
7078
  if (!SameValue(v1, v2)) {
7075
7079
  dateFieldsPracticallyEqual = false;
7080
+ firstDifferingField = fieldName;
7076
7081
  }
7077
7082
  }
7078
7083
  }
@@ -7090,6 +7095,10 @@
7090
7095
  }
7091
7096
  return result2;
7092
7097
  }
7098
+ if (rangePattern === void 0 && firstDifferingField === "ampm") {
7099
+ rangePattern = "hour" in rangePatterns ? rangePatterns["hour"] : void 0;
7100
+ }
7101
+ const datesDiffer = tm1.year !== tm2.year || tm1.month !== tm2.month || tm1.day !== tm2.day;
7093
7102
  let result = [];
7094
7103
  if (rangePattern === void 0) {
7095
7104
  rangePattern = rangePatterns.default;
@@ -7125,7 +7134,9 @@
7125
7134
  z = y;
7126
7135
  }
7127
7136
  const patternParts = PartitionPattern(pattern2);
7128
- let partResult = FormatDateTimePattern(dtf, patternParts, z, implDetails);
7137
+ let partResult = FormatDateTimePattern(dtf, patternParts, z, __spreadProps(__spreadValues({}, implDetails), {
7138
+ rangeFormatOptions: { isDifferentDate: datesDiffer }
7139
+ }));
7129
7140
  for (const r of partResult) {
7130
7141
  r.source = source;
7131
7142
  }
@@ -5,10 +5,13 @@ export interface FormatDateTimePatternImplDetails {
5
5
  getInternalSlots(dtf: Intl.DateTimeFormat | DateTimeFormat): IntlDateTimeFormatInternal;
6
6
  localeData: Record<string, DateTimeFormatLocaleInternalData>;
7
7
  getDefaultTimeZone(): string;
8
+ rangeFormatOptions?: {
9
+ isDifferentDate?: boolean;
10
+ };
8
11
  }
9
12
  /**
10
13
  * https://tc39.es/ecma402/#sec-partitiondatetimepattern
11
14
  * @param dtf
12
15
  * @param x
13
16
  */
14
- export declare function FormatDateTimePattern(dtf: Intl.DateTimeFormat | DateTimeFormat, patternParts: IntlDateTimeFormatPart[], x: Decimal, { getInternalSlots, localeData, getDefaultTimeZone, tzData }: FormatDateTimePatternImplDetails & ToLocalTimeImplDetails): IntlDateTimeFormatPart[];
17
+ export declare function FormatDateTimePattern(dtf: Intl.DateTimeFormat | DateTimeFormat, patternParts: IntlDateTimeFormatPart[], x: Decimal, { getInternalSlots, localeData, getDefaultTimeZone, tzData, rangeFormatOptions }: FormatDateTimePatternImplDetails & ToLocalTimeImplDetails): IntlDateTimeFormatPart[];
@@ -30,7 +30,7 @@ function offsetToGmtString(gmtFormat, hourFormat, offsetInMs, style) {
30
30
  * @param dtf
31
31
  * @param x
32
32
  */
33
- export function FormatDateTimePattern(dtf, patternParts, x, { getInternalSlots, localeData, getDefaultTimeZone, tzData }) {
33
+ export function FormatDateTimePattern(dtf, patternParts, x, { getInternalSlots, localeData, getDefaultTimeZone, tzData, rangeFormatOptions }) {
34
34
  x = TimeClip(x);
35
35
  /** IMPL START */
36
36
  const internalSlots = getInternalSlots(dtf);
@@ -130,8 +130,21 @@ export function FormatDateTimePattern(dtf, patternParts, x, { getInternalSlots,
130
130
  v = 12;
131
131
  }
132
132
  }
133
+ // GH #4535: In h24 format, midnight is typically shown as 24:00 (end of day).
134
+ // However, in date ranges where dates differ (e.g., "May 3, 22:00 – May 4, 00:00"),
135
+ // showing "May 4, 24:00" is semantically incorrect because 24:00 of May 4 would
136
+ // actually be May 5, 00:00. In this case, keep midnight as 00:00 for clarity.
137
+ //
138
+ // LDML Spec (UTS #35): "Tuesday 24:00 = Wednesday 00:00" - they represent the same
139
+ // instant. The 'k' symbol (1-24) means 24:00 represents the END of day, not the start.
140
+ // See: https://unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table
141
+ //
142
+ // Note: ICU4J's SimpleDateFormat always converts 0→24 for 'k' pattern without this
143
+ // range-aware check. Our fix is more semantically correct for date range formatting.
144
+ // See: https://github.com/unicode-org/icu/blob/main/icu4j/main/core/src/main/java/com/ibm/icu/text/SimpleDateFormat.java
133
145
  if (p === "hour" && hourCycle === "h24") {
134
- if (v === 0) {
146
+ if (v === 0 && !rangeFormatOptions?.isDifferentDate) {
147
+ // Only convert 0 to 24 when NOT in a range with different dates
135
148
  v = 24;
136
149
  }
137
150
  }
@@ -46,6 +46,8 @@ export function PartitionDateTimeRangePattern(dtf, x, y, implDetails) {
46
46
  let rangePattern;
47
47
  let dateFieldsPracticallyEqual = true;
48
48
  let patternContainsLargerDateField = false;
49
+ // Track the first field that differs between the two dates
50
+ let firstDifferingField;
49
51
  for (const fieldName of TABLE_2_FIELDS) {
50
52
  if (dateFieldsPracticallyEqual && !patternContainsLargerDateField) {
51
53
  let rp = fieldName in rangePatterns ? rangePatterns[fieldName] : undefined;
@@ -58,6 +60,7 @@ export function PartitionDateTimeRangePattern(dtf, x, y, implDetails) {
58
60
  let v2 = tm2.hour;
59
61
  if (v1 > 11 && v2 < 11 || v1 < 11 && v2 > 11) {
60
62
  dateFieldsPracticallyEqual = false;
63
+ firstDifferingField = fieldName;
61
64
  }
62
65
  } else if (fieldName === "dayPeriod") {} else if (fieldName === "fractionalSecondDigits") {
63
66
  let fractionalSecondDigits = internalSlots.fractionalSecondDigits;
@@ -68,12 +71,14 @@ export function PartitionDateTimeRangePattern(dtf, x, y, implDetails) {
68
71
  let v2 = Math.floor(tm2.millisecond * 10 ** (fractionalSecondDigits - 3));
69
72
  if (!SameValue(v1, v2)) {
70
73
  dateFieldsPracticallyEqual = false;
74
+ firstDifferingField = fieldName;
71
75
  }
72
76
  } else {
73
77
  let v1 = tm1[fieldName];
74
78
  let v2 = tm2[fieldName];
75
79
  if (!SameValue(v1, v2)) {
76
80
  dateFieldsPracticallyEqual = false;
81
+ firstDifferingField = fieldName;
77
82
  }
78
83
  }
79
84
  }
@@ -86,6 +91,25 @@ export function PartitionDateTimeRangePattern(dtf, x, y, implDetails) {
86
91
  }
87
92
  return result;
88
93
  }
94
+ // GH #4535: ICU4J-style AM_PM → HOUR fallback.
95
+ // When AM_PM differs but no AM_PM pattern exists, try the HOUR pattern.
96
+ // This handles cases where CLDR provides 'H' (24-hour) patterns but not 'h' (12-hour),
97
+ // or the skeleton uses hour12:true but CLDR only has 24-hour interval patterns.
98
+ //
99
+ // ICU4J DateIntervalFormat.java genIntervalPattern() method (~line 1825):
100
+ // // for 24 hour system, interval patterns in resource file
101
+ // // might not include pattern when am_pm differ,
102
+ // // which should be the same as hour differ.
103
+ // if (field == Calendar.AM_PM) {
104
+ // pattern = fInfo.getIntervalPattern(bestSkeleton, Calendar.HOUR);
105
+ // }
106
+ // See: https://github.com/unicode-org/icu/blob/main/icu4j/main/core/src/main/java/com/ibm/icu/text/DateIntervalFormat.java#L2018
107
+ // LDML Spec: https://unicode.org/reports/tr35/tr35-dates.html#intervalFormats
108
+ if (rangePattern === undefined && firstDifferingField === "ampm") {
109
+ rangePattern = "hour" in rangePatterns ? rangePatterns["hour"] : undefined;
110
+ }
111
+ // GH #4535: Check if dates (year/month/day) differ for h24 midnight handling
112
+ const datesDiffer = tm1.year !== tm2.year || tm1.month !== tm2.month || tm1.day !== tm2.day;
89
113
  let result = [];
90
114
  if (rangePattern === undefined) {
91
115
  rangePattern = rangePatterns.default;
@@ -161,7 +185,10 @@ export function PartitionDateTimeRangePattern(dtf, x, y, implDetails) {
161
185
  z = y;
162
186
  }
163
187
  const patternParts = PartitionPattern(pattern);
164
- let partResult = FormatDateTimePattern(dtf, patternParts, z, implDetails);
188
+ let partResult = FormatDateTimePattern(dtf, patternParts, z, {
189
+ ...implDetails,
190
+ rangeFormatOptions: { isDifferentDate: datesDiffer }
191
+ });
165
192
  for (const r of partResult) {
166
193
  r.source = source;
167
194
  }
package/src/packer.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type UnpackedData, type PackedData } from "./types.js";
1
+ import { type UnpackedData, type PackedData } from "./types.ts";
2
2
  import { type UnpackedZoneData } from "@formatjs/ecma402-abstract";
3
3
  export declare function pack(data: UnpackedData): PackedData;
4
4
  export declare function unpack(data: PackedData): Record<string, UnpackedZoneData[]>;