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