@fable-org/fable-library-ts 1.1.0 → 1.2.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 +8 -0
- package/Date.ts +354 -25
- package/Util.ts +2 -13
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## Unreleased
|
|
9
9
|
|
|
10
|
+
## 1.2.0 - 2024-03-01
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
* [GH-3772](https://github.com/fable-compiler/Fable/pull/3772) [JS/TS] Re-implement `DateTime.ToString` custom format handling (by @MangelMaxime)
|
|
15
|
+
|
|
16
|
+
It now supports all custom format specifiers, and behave as if `CultureInfo.InvariantCulture` was used (Fable does not support Globalization).
|
|
17
|
+
|
|
10
18
|
## 1.1.0 - 2024-02-20
|
|
11
19
|
|
|
12
20
|
### Added
|
package/Date.ts
CHANGED
|
@@ -15,6 +15,360 @@ import { compareDates, DateKind, dateOffset, IDateTime, IDateTimeOffset, padWith
|
|
|
15
15
|
export type OffsetInMinutes = number;
|
|
16
16
|
export type Offset = "Z" | OffsetInMinutes | null;
|
|
17
17
|
|
|
18
|
+
const shortDays =
|
|
19
|
+
[
|
|
20
|
+
"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
const longDays =
|
|
24
|
+
[
|
|
25
|
+
"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
const shortMonths =
|
|
29
|
+
[
|
|
30
|
+
"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
const longMonths =
|
|
34
|
+
[
|
|
35
|
+
"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
function parseRepeatToken(format: string, pos: number, patternChar: string) {
|
|
40
|
+
let tokenLength = 0;
|
|
41
|
+
let internalPos = pos;
|
|
42
|
+
while (internalPos < format.length && format[internalPos] === patternChar) {
|
|
43
|
+
internalPos++;
|
|
44
|
+
tokenLength++;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return tokenLength;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function parseNextChar(format: string, pos: number) {
|
|
51
|
+
if (pos >= format.length - 1) {
|
|
52
|
+
return -1
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return format.charCodeAt(pos + 1);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function parseQuotedString(format: string, pos: number) : [string, number] {
|
|
59
|
+
let beginPos = pos;
|
|
60
|
+
// Get the character used to quote the string
|
|
61
|
+
const quoteChar = format[pos];
|
|
62
|
+
|
|
63
|
+
let result = "";
|
|
64
|
+
let foundQuote = false;
|
|
65
|
+
|
|
66
|
+
while (pos < format.length) {
|
|
67
|
+
pos++;
|
|
68
|
+
const currentChar = format[pos];
|
|
69
|
+
if (currentChar === quoteChar) {
|
|
70
|
+
foundQuote = true;
|
|
71
|
+
break;
|
|
72
|
+
} else if (currentChar === "\\") {
|
|
73
|
+
if (pos < format.length) {
|
|
74
|
+
pos++;
|
|
75
|
+
result += format[pos];
|
|
76
|
+
} else {
|
|
77
|
+
// This means that '\' is the last character in the string.
|
|
78
|
+
throw new Error("Invalid string format");
|
|
79
|
+
}
|
|
80
|
+
} else {
|
|
81
|
+
result += currentChar;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (!foundQuote) {
|
|
86
|
+
// We could not find the matching quote
|
|
87
|
+
throw new Error(`Invalid string format could not find matching quote for ${quoteChar}`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return [result, pos - beginPos + 1];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function dateToStringWithCustomFormat(date: Date, format: string, utc: boolean) {
|
|
94
|
+
let cursorPos = 0;
|
|
95
|
+
let tokenLength = 0;
|
|
96
|
+
let result = "";
|
|
97
|
+
const localizedDate = utc ? DateTime(date.getTime(), DateKind.UTC) : date;
|
|
98
|
+
|
|
99
|
+
while (cursorPos < format.length) {
|
|
100
|
+
const token = format[cursorPos];
|
|
101
|
+
|
|
102
|
+
switch (token) {
|
|
103
|
+
case "d":
|
|
104
|
+
tokenLength = parseRepeatToken(format, cursorPos, "d");
|
|
105
|
+
cursorPos += tokenLength;
|
|
106
|
+
switch (tokenLength) {
|
|
107
|
+
case 1:
|
|
108
|
+
result += localizedDate.getDate();
|
|
109
|
+
break;
|
|
110
|
+
case 2:
|
|
111
|
+
result += padWithZeros(localizedDate.getDate(), 2);
|
|
112
|
+
break;
|
|
113
|
+
case 3:
|
|
114
|
+
result += shortDays[dayOfWeek(localizedDate)];
|
|
115
|
+
break;
|
|
116
|
+
case 4:
|
|
117
|
+
result += longDays[dayOfWeek(localizedDate)];
|
|
118
|
+
break;
|
|
119
|
+
default:
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
break;
|
|
123
|
+
case "f":
|
|
124
|
+
tokenLength = parseRepeatToken(format, cursorPos, "f");
|
|
125
|
+
cursorPos += tokenLength;
|
|
126
|
+
if (tokenLength <= 3) {
|
|
127
|
+
const precision = 10 ** (3 - tokenLength);
|
|
128
|
+
result += padWithZeros(Math.floor(millisecond(localizedDate) / precision), tokenLength);
|
|
129
|
+
} else if (tokenLength <= 7) {
|
|
130
|
+
// JavaScript Date only support precision to the millisecond
|
|
131
|
+
// so we fill the rest of the precision with 0 as if the date didn't have
|
|
132
|
+
// milliseconds provided to it.
|
|
133
|
+
// This is to have the same behavior as .NET when doing:
|
|
134
|
+
// DateTime(1, 2, 3, 4, 5, 6, DateTimeKind.Utc).ToString("fffff") => 00000
|
|
135
|
+
result += (""+millisecond(localizedDate)).padEnd(tokenLength, "0");
|
|
136
|
+
}
|
|
137
|
+
break;
|
|
138
|
+
case "F":
|
|
139
|
+
tokenLength = parseRepeatToken(format, cursorPos, "F");
|
|
140
|
+
cursorPos += tokenLength;
|
|
141
|
+
if (tokenLength <= 3) {
|
|
142
|
+
const precision = 10 ** (3 - tokenLength);
|
|
143
|
+
const value = Math.floor(millisecond(localizedDate) / precision)
|
|
144
|
+
if (value != 0) {
|
|
145
|
+
result += padWithZeros(value, tokenLength);
|
|
146
|
+
}
|
|
147
|
+
} else if (tokenLength <= 7) {
|
|
148
|
+
// JavaScript Date only support precision to the millisecond
|
|
149
|
+
// so we can't go beyond that.
|
|
150
|
+
// We also need to pad start with 0 if the value is not 0
|
|
151
|
+
const value = millisecond(localizedDate);
|
|
152
|
+
if (value != 0) {
|
|
153
|
+
result += padWithZeros(value, 3);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
break;
|
|
157
|
+
case "g":
|
|
158
|
+
tokenLength = parseRepeatToken(format, cursorPos, "g");
|
|
159
|
+
cursorPos += tokenLength;
|
|
160
|
+
if (tokenLength <= 2) {
|
|
161
|
+
result += "A.D.";
|
|
162
|
+
}
|
|
163
|
+
break;
|
|
164
|
+
case "h":
|
|
165
|
+
tokenLength = parseRepeatToken(format, cursorPos, "h");
|
|
166
|
+
cursorPos += tokenLength;
|
|
167
|
+
switch (tokenLength) {
|
|
168
|
+
case 1:
|
|
169
|
+
result += hour(localizedDate) % 12
|
|
170
|
+
break;
|
|
171
|
+
case 2:
|
|
172
|
+
result += padWithZeros(hour(localizedDate) % 12, 2);
|
|
173
|
+
break;
|
|
174
|
+
default:
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
break;
|
|
178
|
+
case "H":
|
|
179
|
+
tokenLength = parseRepeatToken(format, cursorPos, "H");
|
|
180
|
+
cursorPos += tokenLength;
|
|
181
|
+
switch (tokenLength) {
|
|
182
|
+
case 1:
|
|
183
|
+
result += hour(localizedDate);
|
|
184
|
+
break;
|
|
185
|
+
case 2:
|
|
186
|
+
result += padWithZeros(hour(localizedDate), 2);
|
|
187
|
+
break;
|
|
188
|
+
default:
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
break;
|
|
192
|
+
case "K":
|
|
193
|
+
tokenLength = parseRepeatToken(format, cursorPos, "K");
|
|
194
|
+
cursorPos += tokenLength;
|
|
195
|
+
switch (tokenLength) {
|
|
196
|
+
case 1:
|
|
197
|
+
switch (kind(localizedDate)) {
|
|
198
|
+
case DateKind.UTC:
|
|
199
|
+
result += "Z";
|
|
200
|
+
break;
|
|
201
|
+
case DateKind.Local:
|
|
202
|
+
result += dateOffsetToString(localizedDate.getTimezoneOffset() * -60000);
|
|
203
|
+
break;
|
|
204
|
+
case DateKind.Unspecified:
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
break;
|
|
208
|
+
default:
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
break;
|
|
212
|
+
case "m":
|
|
213
|
+
tokenLength = parseRepeatToken(format, cursorPos, "m");
|
|
214
|
+
cursorPos += tokenLength;
|
|
215
|
+
switch (tokenLength) {
|
|
216
|
+
case 1:
|
|
217
|
+
result += minute(localizedDate);
|
|
218
|
+
break;
|
|
219
|
+
case 2:
|
|
220
|
+
result += padWithZeros(minute(localizedDate), 2);
|
|
221
|
+
break;
|
|
222
|
+
default:
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
break;
|
|
226
|
+
case "M":
|
|
227
|
+
tokenLength = parseRepeatToken(format, cursorPos, "M");
|
|
228
|
+
cursorPos += tokenLength;
|
|
229
|
+
switch (tokenLength) {
|
|
230
|
+
case 1:
|
|
231
|
+
result += month(localizedDate);
|
|
232
|
+
break;
|
|
233
|
+
case 2:
|
|
234
|
+
result += padWithZeros(month(localizedDate), 2);
|
|
235
|
+
break;
|
|
236
|
+
case 3:
|
|
237
|
+
result += shortMonths[month(localizedDate) - 1];
|
|
238
|
+
break;
|
|
239
|
+
case 4:
|
|
240
|
+
result += longMonths[month(localizedDate) - 1];
|
|
241
|
+
break;
|
|
242
|
+
default:
|
|
243
|
+
break;
|
|
244
|
+
}
|
|
245
|
+
break;
|
|
246
|
+
case "s":
|
|
247
|
+
tokenLength = parseRepeatToken(format, cursorPos, "s");
|
|
248
|
+
cursorPos += tokenLength;
|
|
249
|
+
switch (tokenLength) {
|
|
250
|
+
case 1:
|
|
251
|
+
result += second(localizedDate);
|
|
252
|
+
break;
|
|
253
|
+
case 2:
|
|
254
|
+
result += padWithZeros(second(localizedDate), 2);
|
|
255
|
+
break;
|
|
256
|
+
default:
|
|
257
|
+
break;
|
|
258
|
+
}
|
|
259
|
+
break;
|
|
260
|
+
case "t":
|
|
261
|
+
tokenLength = parseRepeatToken(format, cursorPos, "t");
|
|
262
|
+
cursorPos += tokenLength;
|
|
263
|
+
switch (tokenLength) {
|
|
264
|
+
case 1:
|
|
265
|
+
result += localizedDate.getHours() < 12 ? "A" : "P";
|
|
266
|
+
break;
|
|
267
|
+
case 2:
|
|
268
|
+
result += localizedDate.getHours() < 12 ? "AM" : "PM";
|
|
269
|
+
break;
|
|
270
|
+
default:
|
|
271
|
+
break;
|
|
272
|
+
}
|
|
273
|
+
break;
|
|
274
|
+
case "y":
|
|
275
|
+
tokenLength = parseRepeatToken(format, cursorPos, "y");
|
|
276
|
+
cursorPos += tokenLength;
|
|
277
|
+
switch (tokenLength) {
|
|
278
|
+
case 1:
|
|
279
|
+
result += localizedDate.getFullYear() % 100;
|
|
280
|
+
break;
|
|
281
|
+
case 2:
|
|
282
|
+
result += padWithZeros(localizedDate.getFullYear() % 100, 2);
|
|
283
|
+
break;
|
|
284
|
+
case 3:
|
|
285
|
+
result += padWithZeros(localizedDate.getFullYear(), 3);
|
|
286
|
+
break;
|
|
287
|
+
case 4:
|
|
288
|
+
result += padWithZeros(localizedDate.getFullYear(), 4);
|
|
289
|
+
break;
|
|
290
|
+
case 5:
|
|
291
|
+
result += padWithZeros(localizedDate.getFullYear(), 5);
|
|
292
|
+
break;
|
|
293
|
+
default:
|
|
294
|
+
break;
|
|
295
|
+
}
|
|
296
|
+
break;
|
|
297
|
+
case "z":
|
|
298
|
+
tokenLength = parseRepeatToken(format, cursorPos, "z");
|
|
299
|
+
cursorPos += tokenLength;
|
|
300
|
+
let utcOffsetText = "";
|
|
301
|
+
|
|
302
|
+
switch (kind(localizedDate)) {
|
|
303
|
+
case DateKind.UTC:
|
|
304
|
+
utcOffsetText = "+00:00";
|
|
305
|
+
break;
|
|
306
|
+
case DateKind.Local:
|
|
307
|
+
utcOffsetText = dateOffsetToString(localizedDate.getTimezoneOffset() * -60000);
|
|
308
|
+
break;
|
|
309
|
+
case DateKind.Unspecified:
|
|
310
|
+
utcOffsetText = dateOffsetToString(toLocalTime(localizedDate).getTimezoneOffset() * -60000);
|
|
311
|
+
break;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const sign = utcOffsetText[0] === "-" ? "-" : "+";
|
|
315
|
+
const hours = parseInt(utcOffsetText.substring(1, 3), 10);
|
|
316
|
+
const minutes = parseInt(utcOffsetText.substring(4, 6), 10);
|
|
317
|
+
|
|
318
|
+
switch (tokenLength) {
|
|
319
|
+
case 1:
|
|
320
|
+
result += `${sign}${hours}`;
|
|
321
|
+
break;
|
|
322
|
+
case 2:
|
|
323
|
+
result += `${sign}${padWithZeros(hours, 2)}`;
|
|
324
|
+
break;
|
|
325
|
+
default:
|
|
326
|
+
result += `${sign}${padWithZeros(hours, 2)}:${padWithZeros(minutes, 2)}`;
|
|
327
|
+
break;
|
|
328
|
+
}
|
|
329
|
+
break;
|
|
330
|
+
case ":":
|
|
331
|
+
result += ":";
|
|
332
|
+
cursorPos++;
|
|
333
|
+
break;
|
|
334
|
+
case "/":
|
|
335
|
+
result += "/";
|
|
336
|
+
cursorPos++;
|
|
337
|
+
break;
|
|
338
|
+
case "'":
|
|
339
|
+
case '"':
|
|
340
|
+
const [quotedString, quotedStringLenght] = parseQuotedString(format, cursorPos);
|
|
341
|
+
result += quotedString;
|
|
342
|
+
cursorPos += quotedStringLenght;
|
|
343
|
+
break;
|
|
344
|
+
case "%":
|
|
345
|
+
const nextChar = parseNextChar(format, cursorPos);
|
|
346
|
+
if (nextChar >= 0 && nextChar !== "%".charCodeAt(0)) {
|
|
347
|
+
cursorPos += 2;
|
|
348
|
+
result += dateToStringWithCustomFormat(localizedDate, String.fromCharCode(nextChar), utc);
|
|
349
|
+
} else {
|
|
350
|
+
throw new Error("Invalid format string");
|
|
351
|
+
}
|
|
352
|
+
break;
|
|
353
|
+
case "\\":
|
|
354
|
+
const nextChar2 = parseNextChar(format, cursorPos);
|
|
355
|
+
if (nextChar2 >= 0) {
|
|
356
|
+
cursorPos += 2;
|
|
357
|
+
result += String.fromCharCode(nextChar2);
|
|
358
|
+
} else {
|
|
359
|
+
throw new Error("Invalid format string");
|
|
360
|
+
}
|
|
361
|
+
break;
|
|
362
|
+
default:
|
|
363
|
+
cursorPos++;
|
|
364
|
+
result += token;
|
|
365
|
+
break;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
return result;
|
|
370
|
+
}
|
|
371
|
+
|
|
18
372
|
export function kind(value: IDateTime): number {
|
|
19
373
|
return value.kind || 0;
|
|
20
374
|
}
|
|
@@ -66,31 +420,6 @@ function dateToISOStringWithOffset(dateWithOffset: Date, offset: number) {
|
|
|
66
420
|
return str.substring(0, str.length - 1) + dateOffsetToString(offset);
|
|
67
421
|
}
|
|
68
422
|
|
|
69
|
-
function dateToStringWithCustomFormat(date: Date, format: string, utc: boolean) {
|
|
70
|
-
return format.replace(/(\w)\1*/g, (match: string) => {
|
|
71
|
-
let rep = Number.NaN;
|
|
72
|
-
switch (match.substring(0, 1)) {
|
|
73
|
-
case "y":
|
|
74
|
-
const y = utc ? date.getUTCFullYear() : date.getFullYear();
|
|
75
|
-
rep = match.length < 4 ? y % 100 : y; break;
|
|
76
|
-
case "M": rep = (utc ? date.getUTCMonth() : date.getMonth()) + 1; break;
|
|
77
|
-
case "d": rep = utc ? date.getUTCDate() : date.getDate(); break;
|
|
78
|
-
case "H": rep = utc ? date.getUTCHours() : date.getHours(); break;
|
|
79
|
-
case "h":
|
|
80
|
-
const h = utc ? date.getUTCHours() : date.getHours();
|
|
81
|
-
rep = h > 12 ? h % 12 : h; break;
|
|
82
|
-
case "m": rep = utc ? date.getUTCMinutes() : date.getMinutes(); break;
|
|
83
|
-
case "s": rep = utc ? date.getUTCSeconds() : date.getSeconds(); break;
|
|
84
|
-
case "f": rep = utc ? date.getUTCMilliseconds() : date.getMilliseconds(); break;
|
|
85
|
-
}
|
|
86
|
-
if (Number.isNaN(rep)) {
|
|
87
|
-
return match;
|
|
88
|
-
} else {
|
|
89
|
-
return padWithZeros(rep, match.length);
|
|
90
|
-
}
|
|
91
|
-
});
|
|
92
|
-
}
|
|
93
|
-
|
|
94
423
|
function dateToStringWithOffset(date: IDateTimeOffset, format?: string) {
|
|
95
424
|
const d = new Date(date.getTime() + (date.offset ?? 0));
|
|
96
425
|
if (typeof format !== "string") {
|
package/Util.ts
CHANGED
|
@@ -270,22 +270,11 @@ export function lazyFromValue<T>(v: T) {
|
|
|
270
270
|
}
|
|
271
271
|
|
|
272
272
|
export function padWithZeros(i: number, length: number) {
|
|
273
|
-
|
|
274
|
-
while (str.length < length) {
|
|
275
|
-
str = "0" + str;
|
|
276
|
-
}
|
|
277
|
-
return str;
|
|
273
|
+
return i.toString(10).padStart(length, "0");
|
|
278
274
|
}
|
|
279
275
|
|
|
280
276
|
export function padLeftAndRightWithZeros(i: number, lengthLeft: number, lengthRight: number) {
|
|
281
|
-
|
|
282
|
-
while (str.length < lengthLeft) {
|
|
283
|
-
str = "0" + str;
|
|
284
|
-
}
|
|
285
|
-
while (str.length < lengthRight) {
|
|
286
|
-
str = str + "0";
|
|
287
|
-
}
|
|
288
|
-
return str;
|
|
277
|
+
return i.toString(10).padStart(lengthLeft, "0").padEnd(lengthRight, "0");
|
|
289
278
|
}
|
|
290
279
|
|
|
291
280
|
export function dateOffset(date: IDateTime | IDateTimeOffset): number {
|
package/package.json
CHANGED