@barefootjs/client 0.24.1 → 0.26.0
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/dist/format-date.d.ts +18 -10
- package/dist/format-date.d.ts.map +1 -1
- package/dist/index.js +44 -4
- package/dist/runtime/index.js +44 -4
- package/dist/runtime/standalone.js +44 -4
- package/package.json +2 -2
- package/src/format-date.ts +81 -15
package/dist/format-date.d.ts
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `formatDate` — format a `Date` (or ISO-8601 string) with an explicit pattern
|
|
3
|
-
* and an explicit
|
|
3
|
+
* and an explicit timezone. The pure-function date formatter of #2324: every
|
|
4
4
|
* input is explicit, so the output is a deterministic function of its
|
|
5
|
-
* arguments — no host locale, no host timezone, no ICU/CLDR data.
|
|
6
|
-
* lets the SSR adapters lower a `formatDate(...)` call to their
|
|
7
|
-
* `format_date` runtime helper (spec/template-helpers.md) with
|
|
8
|
-
* output on every backend.
|
|
5
|
+
* arguments — no host locale, no *implicit* host timezone, no ICU/CLDR data.
|
|
6
|
+
* That is what lets the SSR adapters lower a `formatDate(...)` call to their
|
|
7
|
+
* native `format_date` runtime helper (spec/template-helpers.md) with
|
|
8
|
+
* byte-identical output on every backend.
|
|
9
9
|
*
|
|
10
10
|
* ```tsx
|
|
11
11
|
* <time>{formatDate(createdAt, 'YYYY/M/D', '+09:00')}</time>
|
|
12
|
+
* <time>{formatDate(createdAt, 'YYYY/M/D', 'Asia/Tokyo')}</time>
|
|
12
13
|
* <time>{formatDate(createdAt, 'MMMM D, YYYY', 'UTC', names)}</time>
|
|
13
14
|
* ```
|
|
14
15
|
*
|
|
@@ -27,11 +28,18 @@
|
|
|
27
28
|
* same normative zero-value discipline as an unparseable date, byte-identical
|
|
28
29
|
* on every backend.
|
|
29
30
|
*
|
|
30
|
-
* `timeZone` is `'UTC'
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
31
|
+
* `timeZone` (#2344) is `'UTC'`, a fixed offset `±HH:MM` within ECMA-402's
|
|
32
|
+
* valid range (`'+09:00'`, `'-05:30'` — hours 00–23, minutes 00–59), or a
|
|
33
|
+
* **canonical IANA zone ID** (`'Asia/Tokyo'`) resolved through tzdata at the
|
|
34
|
+
* instant being formatted — DST-aware, historical-transition-aware, at up to
|
|
35
|
+
* seconds precision (pre-standard LMT offsets count). Any other value — an
|
|
36
|
+
* unknown zone, a non-canonical spelling (`'asia/tokyo'`, a link name the
|
|
37
|
+
* host canonicalizes away), an out-of-range offset (`'+25:00'`) — **throws a
|
|
38
|
+
* `RangeError`**, mirroring `Intl`: loud, never a silent fallback to UTC (a
|
|
39
|
+
* silently substituted timezone is the one failure mode this helper must not
|
|
40
|
+
* have — the pre-#2344 total-function normalization is gone). The receiver
|
|
41
|
+
* contract still comes first: an unset or unparseable `date` renders `''`
|
|
42
|
+
* before `timeZone` is ever inspected, on every backend identically.
|
|
35
43
|
*/
|
|
36
44
|
export declare function formatDate(date: Date | string, pattern: string, timeZone?: string, names?: readonly string[]): string;
|
|
37
45
|
//# sourceMappingURL=format-date.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"format-date.d.ts","sourceRoot":"","sources":["../src/format-date.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"format-date.d.ts","sourceRoot":"","sources":["../src/format-date.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AAoEH,wBAAgB,UAAU,CACxB,IAAI,EAAE,IAAI,GAAG,MAAM,EACnB,OAAO,EAAE,MAAM,EACf,QAAQ,SAAQ,EAChB,KAAK,GAAE,SAAS,MAAM,EAAO,GAC5B,MAAM,CAiDR"}
|
package/dist/index.js
CHANGED
|
@@ -163,12 +163,49 @@ function queryHref(base, params) {
|
|
|
163
163
|
return qs ? `${base}?${qs}` : base;
|
|
164
164
|
}
|
|
165
165
|
// src/format-date.ts
|
|
166
|
-
var OFFSET_RE = /^([+-])(\d
|
|
166
|
+
var OFFSET_RE = /^([+-])([01]\d|2[0-3]):([0-5]\d)$/;
|
|
167
167
|
var TOKEN_RE = /YYYY|MMMM|MMM|MM|DD|dddd|ddd|M|D/g;
|
|
168
168
|
var MONTHS_WIDE = 0;
|
|
169
169
|
var MONTHS_ABBR = 12;
|
|
170
170
|
var WEEKDAYS_WIDE = 24;
|
|
171
171
|
var WEEKDAYS_ABBR = 31;
|
|
172
|
+
var dtfCache = new Map;
|
|
173
|
+
function invalidTimeZone(timeZone) {
|
|
174
|
+
return new RangeError(`formatDate: unresolvable timeZone ${JSON.stringify(timeZone)} — expected 'UTC', a fixed ±HH:MM offset, or a canonical IANA zone ID`);
|
|
175
|
+
}
|
|
176
|
+
function zoneOffsetMs(timeZone, t) {
|
|
177
|
+
let dtf = dtfCache.get(timeZone);
|
|
178
|
+
if (!dtf) {
|
|
179
|
+
try {
|
|
180
|
+
dtf = new Intl.DateTimeFormat("en-US", {
|
|
181
|
+
timeZone,
|
|
182
|
+
hourCycle: "h23",
|
|
183
|
+
era: "short",
|
|
184
|
+
year: "numeric",
|
|
185
|
+
month: "2-digit",
|
|
186
|
+
day: "2-digit",
|
|
187
|
+
hour: "2-digit",
|
|
188
|
+
minute: "2-digit",
|
|
189
|
+
second: "2-digit"
|
|
190
|
+
});
|
|
191
|
+
} catch {
|
|
192
|
+
throw invalidTimeZone(timeZone);
|
|
193
|
+
}
|
|
194
|
+
if (dtf.resolvedOptions().timeZone !== timeZone)
|
|
195
|
+
throw invalidTimeZone(timeZone);
|
|
196
|
+
dtfCache.set(timeZone, dtf);
|
|
197
|
+
}
|
|
198
|
+
const field = {};
|
|
199
|
+
for (const part of dtf.formatToParts(t))
|
|
200
|
+
field[part.type] = part.value;
|
|
201
|
+
let year = Number(field.year);
|
|
202
|
+
if (field.era && field.era.startsWith("B"))
|
|
203
|
+
year = 1 - year;
|
|
204
|
+
const wall = new Date(0);
|
|
205
|
+
wall.setUTCFullYear(year, Number(field.month) - 1, Number(field.day));
|
|
206
|
+
wall.setUTCHours(Number(field.hour), Number(field.minute), Number(field.second), 0);
|
|
207
|
+
return wall.getTime() - Math.floor(t / 1000) * 1000;
|
|
208
|
+
}
|
|
172
209
|
function pad2(n) {
|
|
173
210
|
return String(n).padStart(2, "0");
|
|
174
211
|
}
|
|
@@ -179,9 +216,12 @@ function formatDate(date, pattern, timeZone = "UTC", names = []) {
|
|
|
179
216
|
const t = d.getTime();
|
|
180
217
|
if (Number.isNaN(t))
|
|
181
218
|
return "";
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
219
|
+
let offsetMs = 0;
|
|
220
|
+
if (timeZone !== "UTC") {
|
|
221
|
+
const m = OFFSET_RE.exec(timeZone);
|
|
222
|
+
offsetMs = m ? (m[1] === "-" ? -1 : 1) * (Number(m[2]) * 60 + Number(m[3])) * 60000 : zoneOffsetMs(timeZone, t);
|
|
223
|
+
}
|
|
224
|
+
const s = new Date(t + offsetMs);
|
|
185
225
|
const year = s.getUTCFullYear();
|
|
186
226
|
const yyyy = (year < 0 ? "-" : "") + String(Math.abs(year)).padStart(4, "0");
|
|
187
227
|
const month = s.getUTCMonth() + 1;
|
package/dist/runtime/index.js
CHANGED
|
@@ -163,12 +163,49 @@ function queryHref(base, params) {
|
|
|
163
163
|
return qs ? `${base}?${qs}` : base;
|
|
164
164
|
}
|
|
165
165
|
// src/format-date.ts
|
|
166
|
-
var OFFSET_RE = /^([+-])(\d
|
|
166
|
+
var OFFSET_RE = /^([+-])([01]\d|2[0-3]):([0-5]\d)$/;
|
|
167
167
|
var TOKEN_RE = /YYYY|MMMM|MMM|MM|DD|dddd|ddd|M|D/g;
|
|
168
168
|
var MONTHS_WIDE = 0;
|
|
169
169
|
var MONTHS_ABBR = 12;
|
|
170
170
|
var WEEKDAYS_WIDE = 24;
|
|
171
171
|
var WEEKDAYS_ABBR = 31;
|
|
172
|
+
var dtfCache = new Map;
|
|
173
|
+
function invalidTimeZone(timeZone) {
|
|
174
|
+
return new RangeError(`formatDate: unresolvable timeZone ${JSON.stringify(timeZone)} — expected 'UTC', a fixed ±HH:MM offset, or a canonical IANA zone ID`);
|
|
175
|
+
}
|
|
176
|
+
function zoneOffsetMs(timeZone, t) {
|
|
177
|
+
let dtf = dtfCache.get(timeZone);
|
|
178
|
+
if (!dtf) {
|
|
179
|
+
try {
|
|
180
|
+
dtf = new Intl.DateTimeFormat("en-US", {
|
|
181
|
+
timeZone,
|
|
182
|
+
hourCycle: "h23",
|
|
183
|
+
era: "short",
|
|
184
|
+
year: "numeric",
|
|
185
|
+
month: "2-digit",
|
|
186
|
+
day: "2-digit",
|
|
187
|
+
hour: "2-digit",
|
|
188
|
+
minute: "2-digit",
|
|
189
|
+
second: "2-digit"
|
|
190
|
+
});
|
|
191
|
+
} catch {
|
|
192
|
+
throw invalidTimeZone(timeZone);
|
|
193
|
+
}
|
|
194
|
+
if (dtf.resolvedOptions().timeZone !== timeZone)
|
|
195
|
+
throw invalidTimeZone(timeZone);
|
|
196
|
+
dtfCache.set(timeZone, dtf);
|
|
197
|
+
}
|
|
198
|
+
const field = {};
|
|
199
|
+
for (const part of dtf.formatToParts(t))
|
|
200
|
+
field[part.type] = part.value;
|
|
201
|
+
let year = Number(field.year);
|
|
202
|
+
if (field.era && field.era.startsWith("B"))
|
|
203
|
+
year = 1 - year;
|
|
204
|
+
const wall = new Date(0);
|
|
205
|
+
wall.setUTCFullYear(year, Number(field.month) - 1, Number(field.day));
|
|
206
|
+
wall.setUTCHours(Number(field.hour), Number(field.minute), Number(field.second), 0);
|
|
207
|
+
return wall.getTime() - Math.floor(t / 1000) * 1000;
|
|
208
|
+
}
|
|
172
209
|
function pad2(n) {
|
|
173
210
|
return String(n).padStart(2, "0");
|
|
174
211
|
}
|
|
@@ -179,9 +216,12 @@ function formatDate(date, pattern, timeZone = "UTC", names = []) {
|
|
|
179
216
|
const t = d.getTime();
|
|
180
217
|
if (Number.isNaN(t))
|
|
181
218
|
return "";
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
219
|
+
let offsetMs = 0;
|
|
220
|
+
if (timeZone !== "UTC") {
|
|
221
|
+
const m = OFFSET_RE.exec(timeZone);
|
|
222
|
+
offsetMs = m ? (m[1] === "-" ? -1 : 1) * (Number(m[2]) * 60 + Number(m[3])) * 60000 : zoneOffsetMs(timeZone, t);
|
|
223
|
+
}
|
|
224
|
+
const s = new Date(t + offsetMs);
|
|
185
225
|
const year = s.getUTCFullYear();
|
|
186
226
|
const yyyy = (year < 0 ? "-" : "") + String(Math.abs(year)).padStart(4, "0");
|
|
187
227
|
const month = s.getUTCMonth() + 1;
|
|
@@ -816,12 +816,49 @@ function queryHref(base, params) {
|
|
|
816
816
|
return qs ? `${base}?${qs}` : base;
|
|
817
817
|
}
|
|
818
818
|
// src/format-date.ts
|
|
819
|
-
var OFFSET_RE = /^([+-])(\d
|
|
819
|
+
var OFFSET_RE = /^([+-])([01]\d|2[0-3]):([0-5]\d)$/;
|
|
820
820
|
var TOKEN_RE = /YYYY|MMMM|MMM|MM|DD|dddd|ddd|M|D/g;
|
|
821
821
|
var MONTHS_WIDE = 0;
|
|
822
822
|
var MONTHS_ABBR = 12;
|
|
823
823
|
var WEEKDAYS_WIDE = 24;
|
|
824
824
|
var WEEKDAYS_ABBR = 31;
|
|
825
|
+
var dtfCache = new Map;
|
|
826
|
+
function invalidTimeZone(timeZone) {
|
|
827
|
+
return new RangeError(`formatDate: unresolvable timeZone ${JSON.stringify(timeZone)} — expected 'UTC', a fixed ±HH:MM offset, or a canonical IANA zone ID`);
|
|
828
|
+
}
|
|
829
|
+
function zoneOffsetMs(timeZone, t) {
|
|
830
|
+
let dtf = dtfCache.get(timeZone);
|
|
831
|
+
if (!dtf) {
|
|
832
|
+
try {
|
|
833
|
+
dtf = new Intl.DateTimeFormat("en-US", {
|
|
834
|
+
timeZone,
|
|
835
|
+
hourCycle: "h23",
|
|
836
|
+
era: "short",
|
|
837
|
+
year: "numeric",
|
|
838
|
+
month: "2-digit",
|
|
839
|
+
day: "2-digit",
|
|
840
|
+
hour: "2-digit",
|
|
841
|
+
minute: "2-digit",
|
|
842
|
+
second: "2-digit"
|
|
843
|
+
});
|
|
844
|
+
} catch {
|
|
845
|
+
throw invalidTimeZone(timeZone);
|
|
846
|
+
}
|
|
847
|
+
if (dtf.resolvedOptions().timeZone !== timeZone)
|
|
848
|
+
throw invalidTimeZone(timeZone);
|
|
849
|
+
dtfCache.set(timeZone, dtf);
|
|
850
|
+
}
|
|
851
|
+
const field = {};
|
|
852
|
+
for (const part of dtf.formatToParts(t))
|
|
853
|
+
field[part.type] = part.value;
|
|
854
|
+
let year = Number(field.year);
|
|
855
|
+
if (field.era && field.era.startsWith("B"))
|
|
856
|
+
year = 1 - year;
|
|
857
|
+
const wall = new Date(0);
|
|
858
|
+
wall.setUTCFullYear(year, Number(field.month) - 1, Number(field.day));
|
|
859
|
+
wall.setUTCHours(Number(field.hour), Number(field.minute), Number(field.second), 0);
|
|
860
|
+
return wall.getTime() - Math.floor(t / 1000) * 1000;
|
|
861
|
+
}
|
|
825
862
|
function pad2(n) {
|
|
826
863
|
return String(n).padStart(2, "0");
|
|
827
864
|
}
|
|
@@ -832,9 +869,12 @@ function formatDate(date, pattern, timeZone = "UTC", names = []) {
|
|
|
832
869
|
const t = d.getTime();
|
|
833
870
|
if (Number.isNaN(t))
|
|
834
871
|
return "";
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
872
|
+
let offsetMs = 0;
|
|
873
|
+
if (timeZone !== "UTC") {
|
|
874
|
+
const m = OFFSET_RE.exec(timeZone);
|
|
875
|
+
offsetMs = m ? (m[1] === "-" ? -1 : 1) * (Number(m[2]) * 60 + Number(m[3])) * 60000 : zoneOffsetMs(timeZone, t);
|
|
876
|
+
}
|
|
877
|
+
const s = new Date(t + offsetMs);
|
|
838
878
|
const year = s.getUTCFullYear();
|
|
839
879
|
const yyyy = (year < 0 ? "-" : "") + String(Math.abs(year)).padStart(4, "0");
|
|
840
880
|
const month = s.getUTCMonth() + 1;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@barefootjs/client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.0",
|
|
4
4
|
"description": "BarefootJS client package: reactive primitives (SSR-safe) plus browser runtime under the `/runtime` subpath (compiler target)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"directory": "packages/client"
|
|
56
56
|
},
|
|
57
57
|
"dependencies": {
|
|
58
|
-
"@barefootjs/shared": "0.
|
|
58
|
+
"@barefootjs/shared": "0.26.0"
|
|
59
59
|
},
|
|
60
60
|
"peerDependencies": {
|
|
61
61
|
"@barefootjs/jsx": ">=0.2.0"
|
package/src/format-date.ts
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `formatDate` — format a `Date` (or ISO-8601 string) with an explicit pattern
|
|
3
|
-
* and an explicit
|
|
3
|
+
* and an explicit timezone. The pure-function date formatter of #2324: every
|
|
4
4
|
* input is explicit, so the output is a deterministic function of its
|
|
5
|
-
* arguments — no host locale, no host timezone, no ICU/CLDR data.
|
|
6
|
-
* lets the SSR adapters lower a `formatDate(...)` call to their
|
|
7
|
-
* `format_date` runtime helper (spec/template-helpers.md) with
|
|
8
|
-
* output on every backend.
|
|
5
|
+
* arguments — no host locale, no *implicit* host timezone, no ICU/CLDR data.
|
|
6
|
+
* That is what lets the SSR adapters lower a `formatDate(...)` call to their
|
|
7
|
+
* native `format_date` runtime helper (spec/template-helpers.md) with
|
|
8
|
+
* byte-identical output on every backend.
|
|
9
9
|
*
|
|
10
10
|
* ```tsx
|
|
11
11
|
* <time>{formatDate(createdAt, 'YYYY/M/D', '+09:00')}</time>
|
|
12
|
+
* <time>{formatDate(createdAt, 'YYYY/M/D', 'Asia/Tokyo')}</time>
|
|
12
13
|
* <time>{formatDate(createdAt, 'MMMM D, YYYY', 'UTC', names)}</time>
|
|
13
14
|
* ```
|
|
14
15
|
*
|
|
@@ -27,14 +28,21 @@
|
|
|
27
28
|
* same normative zero-value discipline as an unparseable date, byte-identical
|
|
28
29
|
* on every backend.
|
|
29
30
|
*
|
|
30
|
-
* `timeZone` is `'UTC'
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
31
|
+
* `timeZone` (#2344) is `'UTC'`, a fixed offset `±HH:MM` within ECMA-402's
|
|
32
|
+
* valid range (`'+09:00'`, `'-05:30'` — hours 00–23, minutes 00–59), or a
|
|
33
|
+
* **canonical IANA zone ID** (`'Asia/Tokyo'`) resolved through tzdata at the
|
|
34
|
+
* instant being formatted — DST-aware, historical-transition-aware, at up to
|
|
35
|
+
* seconds precision (pre-standard LMT offsets count). Any other value — an
|
|
36
|
+
* unknown zone, a non-canonical spelling (`'asia/tokyo'`, a link name the
|
|
37
|
+
* host canonicalizes away), an out-of-range offset (`'+25:00'`) — **throws a
|
|
38
|
+
* `RangeError`**, mirroring `Intl`: loud, never a silent fallback to UTC (a
|
|
39
|
+
* silently substituted timezone is the one failure mode this helper must not
|
|
40
|
+
* have — the pre-#2344 total-function normalization is gone). The receiver
|
|
41
|
+
* contract still comes first: an unset or unparseable `date` renders `''`
|
|
42
|
+
* before `timeZone` is ever inspected, on every backend identically.
|
|
35
43
|
*/
|
|
36
44
|
|
|
37
|
-
const OFFSET_RE = /^([+-])(\d
|
|
45
|
+
const OFFSET_RE = /^([+-])([01]\d|2[0-3]):([0-5]\d)$/
|
|
38
46
|
const TOKEN_RE = /YYYY|MMMM|MMM|MM|DD|dddd|ddd|M|D/g
|
|
39
47
|
|
|
40
48
|
/** `names`-table section offsets (spec/template-helpers.md "format_date"). */
|
|
@@ -43,6 +51,59 @@ const MONTHS_ABBR = 12
|
|
|
43
51
|
const WEEKDAYS_WIDE = 24
|
|
44
52
|
const WEEKDAYS_ABBR = 31
|
|
45
53
|
|
|
54
|
+
/** Probe formatter per named zone — construction is the expensive step. */
|
|
55
|
+
const dtfCache = new Map<string, Intl.DateTimeFormat>()
|
|
56
|
+
|
|
57
|
+
function invalidTimeZone(timeZone: string): RangeError {
|
|
58
|
+
return new RangeError(
|
|
59
|
+
`formatDate: unresolvable timeZone ${JSON.stringify(timeZone)} — expected 'UTC', a fixed ±HH:MM offset, or a canonical IANA zone ID`,
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Resolve a canonical IANA zone ID's UTC offset (in ms, always a whole
|
|
65
|
+
* multiple of 1000 — tz offsets are second-granular) at instant `t`, via the
|
|
66
|
+
* environment's own tzdata as ECMA-402 exposes it. The wall-clock fields the
|
|
67
|
+
* zone formats for `t` are re-encoded as-if-UTC and diffed against `t` — no
|
|
68
|
+
* dependency on `timeZoneName: 'longOffset'` support, and seconds-precision
|
|
69
|
+
* LMT offsets fall out naturally. Non-canonical spellings and link names
|
|
70
|
+
* (anything `resolvedOptions().timeZone` does not echo verbatim) throw: the
|
|
71
|
+
* canonical primary ID is the only spelling every backend's tzdata resolves
|
|
72
|
+
* identically, so the portable contract admits exactly that.
|
|
73
|
+
*/
|
|
74
|
+
function zoneOffsetMs(timeZone: string, t: number): number {
|
|
75
|
+
let dtf = dtfCache.get(timeZone)
|
|
76
|
+
if (!dtf) {
|
|
77
|
+
try {
|
|
78
|
+
dtf = new Intl.DateTimeFormat('en-US', {
|
|
79
|
+
timeZone,
|
|
80
|
+
hourCycle: 'h23',
|
|
81
|
+
era: 'short',
|
|
82
|
+
year: 'numeric',
|
|
83
|
+
month: '2-digit',
|
|
84
|
+
day: '2-digit',
|
|
85
|
+
hour: '2-digit',
|
|
86
|
+
minute: '2-digit',
|
|
87
|
+
second: '2-digit',
|
|
88
|
+
})
|
|
89
|
+
} catch {
|
|
90
|
+
throw invalidTimeZone(timeZone)
|
|
91
|
+
}
|
|
92
|
+
if (dtf.resolvedOptions().timeZone !== timeZone) throw invalidTimeZone(timeZone)
|
|
93
|
+
dtfCache.set(timeZone, dtf)
|
|
94
|
+
}
|
|
95
|
+
const field: Record<string, string> = {}
|
|
96
|
+
for (const part of dtf.formatToParts(t)) field[part.type] = part.value
|
|
97
|
+
// era 'BC' marks proleptic years <= 0: ISO year = 1 - displayed year.
|
|
98
|
+
let year = Number(field.year)
|
|
99
|
+
if (field.era && field.era.startsWith('B')) year = 1 - year
|
|
100
|
+
// setUTCFullYear, not Date.UTC — Date.UTC maps years 0..99 to 1900+y.
|
|
101
|
+
const wall = new Date(0)
|
|
102
|
+
wall.setUTCFullYear(year, Number(field.month) - 1, Number(field.day))
|
|
103
|
+
wall.setUTCHours(Number(field.hour), Number(field.minute), Number(field.second), 0)
|
|
104
|
+
return wall.getTime() - Math.floor(t / 1000) * 1000
|
|
105
|
+
}
|
|
106
|
+
|
|
46
107
|
function pad2(n: number): string {
|
|
47
108
|
return String(n).padStart(2, '0')
|
|
48
109
|
}
|
|
@@ -61,11 +122,16 @@ export function formatDate(
|
|
|
61
122
|
const d = date instanceof Date ? date : new Date(date)
|
|
62
123
|
const t = d.getTime()
|
|
63
124
|
if (Number.isNaN(t)) return ''
|
|
64
|
-
|
|
65
|
-
|
|
125
|
+
let offsetMs = 0
|
|
126
|
+
if (timeZone !== 'UTC') {
|
|
127
|
+
const m = OFFSET_RE.exec(timeZone)
|
|
128
|
+
offsetMs = m
|
|
129
|
+
? (m[1] === '-' ? -1 : 1) * (Number(m[2]) * 60 + Number(m[3])) * 60_000
|
|
130
|
+
: zoneOffsetMs(timeZone, t)
|
|
131
|
+
}
|
|
66
132
|
// Shift the instant by the offset, then read UTC fields: the shifted UTC
|
|
67
|
-
// clock face IS the local clock face at that
|
|
68
|
-
const s = new Date(t +
|
|
133
|
+
// clock face IS the local clock face in that zone at that instant.
|
|
134
|
+
const s = new Date(t + offsetMs)
|
|
69
135
|
const year = s.getUTCFullYear()
|
|
70
136
|
const yyyy = (year < 0 ? '-' : '') + String(Math.abs(year)).padStart(4, '0')
|
|
71
137
|
const month = s.getUTCMonth() + 1
|