@fable-org/fable-library-js 2.5.1 → 2.7.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/CHANGELOG.md +26 -1
- package/DateOnlyTemporal.js +146 -0
- package/DateTimeFormat.js +523 -0
- package/DateTimeOffsetTemporal.js +283 -0
- package/DateTimeTemporal.js +243 -0
- package/Guid.js +3 -1
- package/Numeric.js +38 -1
- package/String.js +39 -9
- package/System.Text.js +9 -9
- package/TimeOnlyTemporal.js +142 -0
- package/TimeSpanTemporal.js +259 -0
- package/package.json +1 -1
- package/quotation.js +47 -0
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Field-based .NET date/time formatting and lenient parsing, shared by the
|
|
3
|
+
* Temporal DateTime / DateTimeOffset representations.
|
|
4
|
+
*
|
|
5
|
+
* The formatters operate on a plain `DateInfo` (wall-clock fields), which the
|
|
6
|
+
* Temporal types expose directly (`d.year`, `d.hour`, `d.dayOfWeek % 7`, ...),
|
|
7
|
+
* so no JS-Date intermediate is needed there.
|
|
8
|
+
*
|
|
9
|
+
* Parsing is the exception. .NET accepts far more than ISO 8601 ("12/30/2009",
|
|
10
|
+
* "June 15, 2009 1:45 PM"), and the JS `Date` built-in is the only lenient date
|
|
11
|
+
* parser the platform offers — Temporal's `from` is strict ISO and rejects all of
|
|
12
|
+
* it. So `parseRaw` uses `Date` as a parsing engine and returns the instant it
|
|
13
|
+
* resolved, which callers convert into the Temporal value they need. That instant
|
|
14
|
+
* is only millisecond-precise, hence `subMillisecondTicks`: the finer digits are
|
|
15
|
+
* read straight off the input, because `Date` has already discarded them.
|
|
16
|
+
*
|
|
17
|
+
* This is a port of the equivalent logic in Date.ts / DateOffset.ts, kept
|
|
18
|
+
* separate so the Temporal modules do not depend on the JS-Date representation.
|
|
19
|
+
*/
|
|
20
|
+
import { Exception, DateTimeKind, padWithZeros } from "./Util.js";
|
|
21
|
+
// The 7 fractional-second digits .NET works with, most significant first.
|
|
22
|
+
function fractionDigits(info) {
|
|
23
|
+
return padWithZeros(info.tick, 7);
|
|
24
|
+
}
|
|
25
|
+
const shortDays = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
|
26
|
+
const longDays = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
|
|
27
|
+
const shortMonths = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
|
28
|
+
const longMonths = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
|
|
29
|
+
function parseRepeatToken(format, pos, patternChar) {
|
|
30
|
+
let tokenLength = 0;
|
|
31
|
+
let internalPos = pos;
|
|
32
|
+
while (internalPos < format.length && format[internalPos] === patternChar) {
|
|
33
|
+
internalPos++;
|
|
34
|
+
tokenLength++;
|
|
35
|
+
}
|
|
36
|
+
return tokenLength;
|
|
37
|
+
}
|
|
38
|
+
function parseNextChar(format, pos) {
|
|
39
|
+
if (pos >= format.length - 1) {
|
|
40
|
+
return -1;
|
|
41
|
+
}
|
|
42
|
+
return format.charCodeAt(pos + 1);
|
|
43
|
+
}
|
|
44
|
+
function parseQuotedString(format, pos) {
|
|
45
|
+
let beginPos = pos;
|
|
46
|
+
// Get the character used to quote the string
|
|
47
|
+
const quoteChar = format[pos];
|
|
48
|
+
let result = "";
|
|
49
|
+
let foundQuote = false;
|
|
50
|
+
while (pos < format.length) {
|
|
51
|
+
pos++;
|
|
52
|
+
const currentChar = format[pos];
|
|
53
|
+
if (currentChar === quoteChar) {
|
|
54
|
+
foundQuote = true;
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
else if (currentChar === "\\") {
|
|
58
|
+
if (pos < format.length) {
|
|
59
|
+
pos++;
|
|
60
|
+
result += format[pos];
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
// This means that '\' is the last character in the string.
|
|
64
|
+
throw new Exception("Invalid string format");
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
result += currentChar;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (!foundQuote) {
|
|
72
|
+
// We could not find the matching quote
|
|
73
|
+
throw new Exception(`Invalid string format could not find matching quote for ${quoteChar}`);
|
|
74
|
+
}
|
|
75
|
+
return [result, pos - beginPos + 1];
|
|
76
|
+
}
|
|
77
|
+
// Custom format string. `kind` decides the K/z tokens; `hostOffsetString`
|
|
78
|
+
// (e.g. "+01:00") is the host-zone offset at this wall-clock, used for the
|
|
79
|
+
// Local / Unspecified cases.
|
|
80
|
+
export function dateToStringWithCustomFormat(info, format, kind, hostOffsetString) {
|
|
81
|
+
let cursorPos = 0;
|
|
82
|
+
let tokenLength = 0;
|
|
83
|
+
let result = "";
|
|
84
|
+
while (cursorPos < format.length) {
|
|
85
|
+
const token = format[cursorPos];
|
|
86
|
+
switch (token) {
|
|
87
|
+
case "d":
|
|
88
|
+
tokenLength = parseRepeatToken(format, cursorPos, "d");
|
|
89
|
+
cursorPos += tokenLength;
|
|
90
|
+
switch (tokenLength) {
|
|
91
|
+
case 1:
|
|
92
|
+
result += info.day;
|
|
93
|
+
break;
|
|
94
|
+
case 2:
|
|
95
|
+
result += padWithZeros(info.day, 2);
|
|
96
|
+
break;
|
|
97
|
+
case 3:
|
|
98
|
+
result += shortDays[info.dayOfWeek];
|
|
99
|
+
break;
|
|
100
|
+
case 4:
|
|
101
|
+
default:
|
|
102
|
+
result += longDays[info.dayOfWeek];
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
break;
|
|
106
|
+
case "f":
|
|
107
|
+
// The first `tokenLength` fractional-second digits, always padded.
|
|
108
|
+
tokenLength = parseRepeatToken(format, cursorPos, "f");
|
|
109
|
+
cursorPos += tokenLength;
|
|
110
|
+
if (tokenLength <= 7) {
|
|
111
|
+
result += fractionDigits(info).substring(0, tokenLength);
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
throw "Input string was not in a correct format.";
|
|
115
|
+
}
|
|
116
|
+
break;
|
|
117
|
+
case "F":
|
|
118
|
+
// As "f", but with trailing zeros dropped — and nothing at all when the
|
|
119
|
+
// requested digits are all zero.
|
|
120
|
+
tokenLength = parseRepeatToken(format, cursorPos, "F");
|
|
121
|
+
cursorPos += tokenLength;
|
|
122
|
+
if (tokenLength <= 7) {
|
|
123
|
+
result += fractionDigits(info).substring(0, tokenLength).replace(/0+$/, "");
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
throw "Input string was not in a correct format.";
|
|
127
|
+
}
|
|
128
|
+
break;
|
|
129
|
+
case "g":
|
|
130
|
+
tokenLength = parseRepeatToken(format, cursorPos, "g");
|
|
131
|
+
cursorPos += tokenLength;
|
|
132
|
+
result += "A.D.";
|
|
133
|
+
break;
|
|
134
|
+
case "h":
|
|
135
|
+
tokenLength = parseRepeatToken(format, cursorPos, "h");
|
|
136
|
+
cursorPos += tokenLength;
|
|
137
|
+
switch (tokenLength) {
|
|
138
|
+
case 1:
|
|
139
|
+
const h1Value = info.hour % 12;
|
|
140
|
+
result += h1Value ? h1Value : 12;
|
|
141
|
+
break;
|
|
142
|
+
case 2:
|
|
143
|
+
default:
|
|
144
|
+
const h2Value = info.hour % 12;
|
|
145
|
+
result += padWithZeros(h2Value ? h2Value : 12, 2);
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
break;
|
|
149
|
+
case "H":
|
|
150
|
+
tokenLength = parseRepeatToken(format, cursorPos, "H");
|
|
151
|
+
cursorPos += tokenLength;
|
|
152
|
+
switch (tokenLength) {
|
|
153
|
+
case 1:
|
|
154
|
+
result += info.hour;
|
|
155
|
+
break;
|
|
156
|
+
case 2:
|
|
157
|
+
default:
|
|
158
|
+
result += padWithZeros(info.hour, 2);
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
break;
|
|
162
|
+
case "K":
|
|
163
|
+
tokenLength = parseRepeatToken(format, cursorPos, "K");
|
|
164
|
+
cursorPos += tokenLength;
|
|
165
|
+
switch (tokenLength) {
|
|
166
|
+
case 1:
|
|
167
|
+
switch (kind) {
|
|
168
|
+
case DateTimeKind.Utc:
|
|
169
|
+
result += "Z";
|
|
170
|
+
break;
|
|
171
|
+
case DateTimeKind.Local:
|
|
172
|
+
result += hostOffsetString;
|
|
173
|
+
break;
|
|
174
|
+
case DateTimeKind.Unspecified:
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
break;
|
|
178
|
+
default:
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
break;
|
|
182
|
+
case "m":
|
|
183
|
+
tokenLength = parseRepeatToken(format, cursorPos, "m");
|
|
184
|
+
cursorPos += tokenLength;
|
|
185
|
+
switch (tokenLength) {
|
|
186
|
+
case 1:
|
|
187
|
+
result += info.minute;
|
|
188
|
+
break;
|
|
189
|
+
case 2:
|
|
190
|
+
default:
|
|
191
|
+
result += padWithZeros(info.minute, 2);
|
|
192
|
+
break;
|
|
193
|
+
}
|
|
194
|
+
break;
|
|
195
|
+
case "M":
|
|
196
|
+
tokenLength = parseRepeatToken(format, cursorPos, "M");
|
|
197
|
+
cursorPos += tokenLength;
|
|
198
|
+
switch (tokenLength) {
|
|
199
|
+
case 1:
|
|
200
|
+
result += info.month;
|
|
201
|
+
break;
|
|
202
|
+
case 2:
|
|
203
|
+
result += padWithZeros(info.month, 2);
|
|
204
|
+
break;
|
|
205
|
+
case 3:
|
|
206
|
+
result += shortMonths[info.month - 1];
|
|
207
|
+
break;
|
|
208
|
+
case 4:
|
|
209
|
+
default:
|
|
210
|
+
result += longMonths[info.month - 1];
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
break;
|
|
214
|
+
case "s":
|
|
215
|
+
tokenLength = parseRepeatToken(format, cursorPos, "s");
|
|
216
|
+
cursorPos += tokenLength;
|
|
217
|
+
switch (tokenLength) {
|
|
218
|
+
case 1:
|
|
219
|
+
result += info.second;
|
|
220
|
+
break;
|
|
221
|
+
case 2:
|
|
222
|
+
default:
|
|
223
|
+
result += padWithZeros(info.second, 2);
|
|
224
|
+
break;
|
|
225
|
+
}
|
|
226
|
+
break;
|
|
227
|
+
case "t":
|
|
228
|
+
tokenLength = parseRepeatToken(format, cursorPos, "t");
|
|
229
|
+
cursorPos += tokenLength;
|
|
230
|
+
switch (tokenLength) {
|
|
231
|
+
case 1:
|
|
232
|
+
result += info.hour < 12 ? "A" : "P";
|
|
233
|
+
break;
|
|
234
|
+
case 2:
|
|
235
|
+
default:
|
|
236
|
+
result += info.hour < 12 ? "AM" : "PM";
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
239
|
+
break;
|
|
240
|
+
case "y":
|
|
241
|
+
tokenLength = parseRepeatToken(format, cursorPos, "y");
|
|
242
|
+
cursorPos += tokenLength;
|
|
243
|
+
switch (tokenLength) {
|
|
244
|
+
case 1:
|
|
245
|
+
result += info.year % 100;
|
|
246
|
+
break;
|
|
247
|
+
case 2:
|
|
248
|
+
result += padWithZeros(info.year % 100, 2);
|
|
249
|
+
break;
|
|
250
|
+
default:
|
|
251
|
+
result += padWithZeros(info.year, tokenLength);
|
|
252
|
+
break;
|
|
253
|
+
}
|
|
254
|
+
break;
|
|
255
|
+
case "z":
|
|
256
|
+
tokenLength = parseRepeatToken(format, cursorPos, "z");
|
|
257
|
+
cursorPos += tokenLength;
|
|
258
|
+
const utcOffsetText = kind === DateTimeKind.Utc ? "+00:00" : hostOffsetString;
|
|
259
|
+
const sign = utcOffsetText[0] === "-" ? "-" : "+";
|
|
260
|
+
const hours = parseInt(utcOffsetText.substring(1, 3), 10);
|
|
261
|
+
const minutes = parseInt(utcOffsetText.substring(4, 6), 10);
|
|
262
|
+
switch (tokenLength) {
|
|
263
|
+
case 1:
|
|
264
|
+
result += `${sign}${hours}`;
|
|
265
|
+
break;
|
|
266
|
+
case 2:
|
|
267
|
+
result += `${sign}${padWithZeros(hours, 2)}`;
|
|
268
|
+
break;
|
|
269
|
+
default:
|
|
270
|
+
result += `${sign}${padWithZeros(hours, 2)}:${padWithZeros(minutes, 2)}`;
|
|
271
|
+
break;
|
|
272
|
+
}
|
|
273
|
+
break;
|
|
274
|
+
case ":":
|
|
275
|
+
result += ":";
|
|
276
|
+
cursorPos++;
|
|
277
|
+
break;
|
|
278
|
+
case "/":
|
|
279
|
+
result += "/";
|
|
280
|
+
cursorPos++;
|
|
281
|
+
break;
|
|
282
|
+
case "'":
|
|
283
|
+
case '"':
|
|
284
|
+
const [quotedString, quotedStringLenght] = parseQuotedString(format, cursorPos);
|
|
285
|
+
result += quotedString;
|
|
286
|
+
cursorPos += quotedStringLenght;
|
|
287
|
+
break;
|
|
288
|
+
case "%":
|
|
289
|
+
const nextChar = parseNextChar(format, cursorPos);
|
|
290
|
+
if (nextChar >= 0 && nextChar !== "%".charCodeAt(0)) {
|
|
291
|
+
cursorPos += 2;
|
|
292
|
+
result += dateToStringWithCustomFormat(info, String.fromCharCode(nextChar), kind, hostOffsetString);
|
|
293
|
+
}
|
|
294
|
+
else {
|
|
295
|
+
throw new Exception("Invalid format string");
|
|
296
|
+
}
|
|
297
|
+
break;
|
|
298
|
+
case "\\":
|
|
299
|
+
const nextChar2 = parseNextChar(format, cursorPos);
|
|
300
|
+
if (nextChar2 >= 0) {
|
|
301
|
+
cursorPos += 2;
|
|
302
|
+
result += String.fromCharCode(nextChar2);
|
|
303
|
+
}
|
|
304
|
+
else {
|
|
305
|
+
throw new Exception("Invalid format string");
|
|
306
|
+
}
|
|
307
|
+
break;
|
|
308
|
+
default:
|
|
309
|
+
cursorPos++;
|
|
310
|
+
result += token;
|
|
311
|
+
break;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return result;
|
|
315
|
+
}
|
|
316
|
+
// --- Standard single-letter format building blocks (InvariantCulture) ---
|
|
317
|
+
export function dateToString_D(info) {
|
|
318
|
+
return longDays[info.dayOfWeek]
|
|
319
|
+
+ ", " + padWithZeros(info.day, 2)
|
|
320
|
+
+ " " + longMonths[info.month - 1]
|
|
321
|
+
+ " " + info.year;
|
|
322
|
+
}
|
|
323
|
+
export function dateToString_d(info) {
|
|
324
|
+
return padWithZeros(info.month, 2)
|
|
325
|
+
+ "/" + padWithZeros(info.day, 2)
|
|
326
|
+
+ "/" + info.year;
|
|
327
|
+
}
|
|
328
|
+
export function dateToString_T(info) {
|
|
329
|
+
return padWithZeros(info.hour, 2)
|
|
330
|
+
+ ":" + padWithZeros(info.minute, 2)
|
|
331
|
+
+ ":" + padWithZeros(info.second, 2);
|
|
332
|
+
}
|
|
333
|
+
export function dateToString_t(info) {
|
|
334
|
+
return padWithZeros(info.hour, 2)
|
|
335
|
+
+ ":" + padWithZeros(info.minute, 2);
|
|
336
|
+
}
|
|
337
|
+
// RFC 1123: "Thu, 01 Jan 2009 00:00:00 GMT". `info` must already be UTC.
|
|
338
|
+
export function dateToString_R(info) {
|
|
339
|
+
return shortDays[info.dayOfWeek] + ", "
|
|
340
|
+
+ padWithZeros(info.day, 2) + " "
|
|
341
|
+
+ shortMonths[info.month - 1] + " "
|
|
342
|
+
+ info.year + " "
|
|
343
|
+
+ padWithZeros(info.hour, 2) + ":"
|
|
344
|
+
+ padWithZeros(info.minute, 2) + ":"
|
|
345
|
+
+ padWithZeros(info.second, 2) + " GMT";
|
|
346
|
+
}
|
|
347
|
+
// Universal sortable: "2009-06-15 13:45:30Z". `info` must already be UTC.
|
|
348
|
+
export function dateToString_u(info) {
|
|
349
|
+
return padWithZeros(info.year, 4) + "-"
|
|
350
|
+
+ padWithZeros(info.month, 2) + "-"
|
|
351
|
+
+ padWithZeros(info.day, 2) + " "
|
|
352
|
+
+ padWithZeros(info.hour, 2) + ":"
|
|
353
|
+
+ padWithZeros(info.minute, 2) + ":"
|
|
354
|
+
+ padWithZeros(info.second, 2) + "Z";
|
|
355
|
+
}
|
|
356
|
+
// Month/day (InvariantCulture "MMMM dd"): "June 15"
|
|
357
|
+
export function dateToString_M(info) {
|
|
358
|
+
return longMonths[info.month - 1] + " " + padWithZeros(info.day, 2);
|
|
359
|
+
}
|
|
360
|
+
// Year/month (InvariantCulture "yyyy MMMM"): "2009 June"
|
|
361
|
+
export function dateToString_Y(info) {
|
|
362
|
+
return info.year + " " + longMonths[info.month - 1];
|
|
363
|
+
}
|
|
364
|
+
// .NET's standard date/time format specifiers, InvariantCulture. Anything that is
|
|
365
|
+
// not a single recognised letter is treated as a custom format string.
|
|
366
|
+
export function dateToString(ctx, format) {
|
|
367
|
+
const info = ctx.info;
|
|
368
|
+
if (typeof format !== "string") {
|
|
369
|
+
return dateToString_d(info) + " " + dateToString_T(info) + ctx.defaultSuffix;
|
|
370
|
+
}
|
|
371
|
+
if (format.length === 1) {
|
|
372
|
+
switch (format) {
|
|
373
|
+
case "D": return dateToString_D(info);
|
|
374
|
+
case "d": return dateToString_d(info);
|
|
375
|
+
case "F": return dateToString_D(info) + " " + dateToString_T(info);
|
|
376
|
+
case "f": return dateToString_D(info) + " " + dateToString_t(info);
|
|
377
|
+
case "G": return dateToString_d(info) + " " + dateToString_T(info);
|
|
378
|
+
case "g": return dateToString_d(info) + " " + dateToString_t(info);
|
|
379
|
+
case "M":
|
|
380
|
+
case "m": return dateToString_M(info);
|
|
381
|
+
case "O":
|
|
382
|
+
case "o": return ctx.roundTrip();
|
|
383
|
+
case "R":
|
|
384
|
+
case "r": return dateToString_R(ctx.utcInfo());
|
|
385
|
+
case "s": return ctx.sortable();
|
|
386
|
+
case "T": return dateToString_T(info);
|
|
387
|
+
case "t": return dateToString_t(info);
|
|
388
|
+
case "u": return dateToString_u(ctx.utcInfo());
|
|
389
|
+
case "U": {
|
|
390
|
+
const utc = ctx.utcInfo();
|
|
391
|
+
return dateToString_D(utc) + " " + dateToString_T(utc);
|
|
392
|
+
}
|
|
393
|
+
case "Y":
|
|
394
|
+
case "y": return dateToString_Y(info);
|
|
395
|
+
default: throw new Exception("Unrecognized Date print format");
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
return dateToStringWithCustomFormat(info, format, ctx.kind, ctx.hostOffsetString());
|
|
399
|
+
}
|
|
400
|
+
// The only date words .NET's invariant parser recognises: month names, weekday names,
|
|
401
|
+
// meridiem designators and zone markers (plus the ISO "T" separator). Anything else is
|
|
402
|
+
// rejected. Used to reject JS-permissive inputs (see `parseRaw`).
|
|
403
|
+
const recognizedDateWords = new Set([
|
|
404
|
+
"january", "february", "march", "april", "may", "june",
|
|
405
|
+
"july", "august", "september", "october", "november", "december",
|
|
406
|
+
"jan", "feb", "mar", "apr", "jun", "jul", "aug", "sep", "sept", "oct", "nov", "dec",
|
|
407
|
+
"monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday",
|
|
408
|
+
"mon", "tue", "wed", "thu", "fri", "sat", "sun",
|
|
409
|
+
"am", "pm", "gmt", "utc", "ut", "t", "z",
|
|
410
|
+
]);
|
|
411
|
+
// The ticks below the millisecond (0..9999) written in the seconds fraction of
|
|
412
|
+
// `input`, e.g. "…:30.6175425" -> 5425.
|
|
413
|
+
//
|
|
414
|
+
// `parseRaw` resolves the instant through the JS `Date` built-in, which is
|
|
415
|
+
// millisecond-precise, so those digits would otherwise be lost. Anchoring on
|
|
416
|
+
// ":ss." keeps date separators (as in "15.06.2009") from matching. Digits past
|
|
417
|
+
// the 7th are truncated, matching how TimeOnly/TimeSpan parsing treats them.
|
|
418
|
+
export function subMillisecondTicks(input) {
|
|
419
|
+
const m = /:\d{1,2}\.(\d+)/.exec(input);
|
|
420
|
+
return m == null ? 0 : +m[1].padEnd(7, "0").substring(3, 7);
|
|
421
|
+
}
|
|
422
|
+
// A trailing UTC designator, if the input carries one. The JS `Date` built-in
|
|
423
|
+
// applies the offset but does not report whether there was one, and callers need
|
|
424
|
+
// that: .NET gives a parsed value Kind=Local when an offset is present and
|
|
425
|
+
// Kind=Unspecified when it is not.
|
|
426
|
+
//
|
|
427
|
+
// The offset is only recognised after a time, so that the trailing "-11" of a
|
|
428
|
+
// date like "2014-09-11" is not mistaken for one.
|
|
429
|
+
function detectOffset(input) {
|
|
430
|
+
const m = /\d\d:\d\d(?::\d\d)?(?:\.\d+)?\s*(Z|[+-]\d\d:?(?:\d\d)?)\s*$/i.exec(input);
|
|
431
|
+
if (m == null) {
|
|
432
|
+
return null;
|
|
433
|
+
}
|
|
434
|
+
const designator = m[1];
|
|
435
|
+
if (designator.toUpperCase() === "Z") {
|
|
436
|
+
return "Z";
|
|
437
|
+
}
|
|
438
|
+
const digits = designator.substring(1).replace(":", "");
|
|
439
|
+
const minutes = +digits.substring(0, 2) * 60 + (digits.length > 2 ? +digits.substring(2) : 0);
|
|
440
|
+
return designator[0] === "-" ? -minutes : minutes;
|
|
441
|
+
}
|
|
442
|
+
// Lenient .NET-compatible parse. Returns a JS Date (the parsed instant) and the
|
|
443
|
+
// detected offset, if any. Callers read the wall-clock fields and build the
|
|
444
|
+
// appropriate Temporal value.
|
|
445
|
+
export function parseRaw(input) {
|
|
446
|
+
function fail() {
|
|
447
|
+
throw new Exception(`The string is not a valid Date: ${input}`);
|
|
448
|
+
}
|
|
449
|
+
if (input == null || input.trim() === "") {
|
|
450
|
+
fail();
|
|
451
|
+
}
|
|
452
|
+
if ((input.match(/[a-z]+/gi) ?? []).some(word => !recognizedDateWords.has(word.toLowerCase()))) {
|
|
453
|
+
fail();
|
|
454
|
+
}
|
|
455
|
+
// ISO dates without TZ are parsed as UTC. Adding time without TZ keeps them local.
|
|
456
|
+
if (input.length === 10 && input[4] === "-" && input[7] === "-") {
|
|
457
|
+
input += "T00:00:00";
|
|
458
|
+
}
|
|
459
|
+
let date = new Date(input);
|
|
460
|
+
let offset = detectOffset(input);
|
|
461
|
+
if (isNaN(date.getTime())) {
|
|
462
|
+
// Try to check strings JS Date cannot parse (see #1045, #1422)
|
|
463
|
+
const m = /^\s*(\d+[^\w\s:]\d+[^\w\s:]\d+)?\s*(\d+:\d+(?::\d+(?:\.\d+)?)?)?\s*([AaPp][Mm])?\s*(Z|[+-]([01]?\d):?([0-5]?\d)?)?\s*$/.exec(input);
|
|
464
|
+
if (m != null) {
|
|
465
|
+
let baseDate;
|
|
466
|
+
let timeInSeconds = 0;
|
|
467
|
+
if (m[2] != null) {
|
|
468
|
+
const timeParts = m[2].split(":");
|
|
469
|
+
const hourPart = parseInt(timeParts[0], 10);
|
|
470
|
+
timeInSeconds =
|
|
471
|
+
hourPart * 3600 +
|
|
472
|
+
parseInt(timeParts[1] || "0", 10) * 60 +
|
|
473
|
+
parseFloat(timeParts[2] || "0");
|
|
474
|
+
if (m[3] != null && m[3].toUpperCase() === "PM" && hourPart < 12) {
|
|
475
|
+
timeInSeconds += 12 * 3600;
|
|
476
|
+
}
|
|
477
|
+
else if (m[3] != null && m[3].toUpperCase() === "AM" && hourPart === 12) {
|
|
478
|
+
timeInSeconds -= 12 * 3600;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
if (m[4] != null) { // There's an offset, parse as UTC
|
|
482
|
+
if (m[1] != null) {
|
|
483
|
+
baseDate = new Date(m[1] + " UTC");
|
|
484
|
+
}
|
|
485
|
+
else {
|
|
486
|
+
const d = new Date();
|
|
487
|
+
baseDate = new Date(d.getUTCFullYear() + "/" + (d.getUTCMonth() + 1) + "/" + d.getUTCDate());
|
|
488
|
+
}
|
|
489
|
+
if (m[4] === "Z") {
|
|
490
|
+
offset = "Z";
|
|
491
|
+
}
|
|
492
|
+
else {
|
|
493
|
+
let offsetInMinutes = parseInt(m[5], 10) * 60 + parseInt(m[6] || "0", 10);
|
|
494
|
+
if (m[4][0] === "-") {
|
|
495
|
+
offsetInMinutes *= -1;
|
|
496
|
+
}
|
|
497
|
+
offset = offsetInMinutes;
|
|
498
|
+
timeInSeconds -= offsetInMinutes * 60;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
else {
|
|
502
|
+
if (m[1] != null) {
|
|
503
|
+
baseDate = new Date(m[1]);
|
|
504
|
+
}
|
|
505
|
+
else {
|
|
506
|
+
const d = new Date();
|
|
507
|
+
baseDate = new Date(d.getFullYear() + "/" + (d.getMonth() + 1) + "/" + d.getDate());
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
date = new Date(baseDate.getTime() + timeInSeconds * 1000);
|
|
511
|
+
// correct for daylight savings time
|
|
512
|
+
date = new Date(date.getTime() + (date.getTimezoneOffset() - baseDate.getTimezoneOffset()) * 60000);
|
|
513
|
+
}
|
|
514
|
+
else {
|
|
515
|
+
fail();
|
|
516
|
+
}
|
|
517
|
+
// Check again the date is valid after transformations, see #2229
|
|
518
|
+
if (isNaN(date.getTime())) {
|
|
519
|
+
fail();
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
return [date, offset];
|
|
523
|
+
}
|