@pbkware/dot-net-date-number-formatting 0.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/LICENSE +21 -0
- package/README.md +143 -0
- package/dist/code/datetime-formatter.js +604 -0
- package/dist/code/datetime-formatter.js.map +1 -0
- package/dist/code/datetime-style.js +101 -0
- package/dist/code/datetime-style.js.map +1 -0
- package/dist/code/index.js +6 -0
- package/dist/code/index.js.map +1 -0
- package/dist/code/locale-settings.js +404 -0
- package/dist/code/locale-settings.js.map +1 -0
- package/dist/code/number-formatter.js +916 -0
- package/dist/code/number-formatter.js.map +1 -0
- package/dist/code/number-style.js +211 -0
- package/dist/code/number-style.js.map +1 -0
- package/dist/types/dot-net-date-number-formatting-untrimmed.d.ts +795 -0
- package/dist/types/public-api.d.ts +714 -0
- package/dist/types/tsdoc-metadata.json +11 -0
- package/package.json +63 -0
- package/src/code/datetime-formatter.ts +718 -0
- package/src/code/datetime-style.ts +128 -0
- package/src/code/index.ts +5 -0
- package/src/code/locale-settings.ts +453 -0
- package/src/code/number-formatter.ts +1108 -0
- package/src/code/number-style.ts +247 -0
|
@@ -0,0 +1,718 @@
|
|
|
1
|
+
import { Err, Ok, Result } from "@pbkware/js-utils";
|
|
2
|
+
import {
|
|
3
|
+
DotNetDateTimeStyleId,
|
|
4
|
+
DotNetDateTimeStyleSet,
|
|
5
|
+
DotNetDateTimeStyles,
|
|
6
|
+
} from "./datetime-style.js";
|
|
7
|
+
import { DotNetLocaleSettings } from "./locale-settings.js";
|
|
8
|
+
|
|
9
|
+
type ElementType =
|
|
10
|
+
| "standard"
|
|
11
|
+
| "day"
|
|
12
|
+
| "day2"
|
|
13
|
+
| "dayNameShort"
|
|
14
|
+
| "dayNameLong"
|
|
15
|
+
| "month"
|
|
16
|
+
| "month2"
|
|
17
|
+
| "monthNameShort"
|
|
18
|
+
| "monthNameLong"
|
|
19
|
+
| "year2"
|
|
20
|
+
| "year3"
|
|
21
|
+
| "year4"
|
|
22
|
+
| "year5"
|
|
23
|
+
| "hour12"
|
|
24
|
+
| "hour12_2"
|
|
25
|
+
| "hour24"
|
|
26
|
+
| "hour24_2"
|
|
27
|
+
| "minute"
|
|
28
|
+
| "minute2"
|
|
29
|
+
| "second"
|
|
30
|
+
| "second2"
|
|
31
|
+
| "fraction"
|
|
32
|
+
| "fractionTrim"
|
|
33
|
+
| "amPm1"
|
|
34
|
+
| "amPm2"
|
|
35
|
+
| "dateSep"
|
|
36
|
+
| "timeSep"
|
|
37
|
+
| "literal";
|
|
38
|
+
|
|
39
|
+
interface Element {
|
|
40
|
+
type: ElementType;
|
|
41
|
+
len?: number;
|
|
42
|
+
text?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const dayNamesShort = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
|
46
|
+
const dayNamesLong = [
|
|
47
|
+
"Sunday",
|
|
48
|
+
"Monday",
|
|
49
|
+
"Tuesday",
|
|
50
|
+
"Wednesday",
|
|
51
|
+
"Thursday",
|
|
52
|
+
"Friday",
|
|
53
|
+
"Saturday",
|
|
54
|
+
];
|
|
55
|
+
const monthNamesShort = [
|
|
56
|
+
"Jan",
|
|
57
|
+
"Feb",
|
|
58
|
+
"Mar",
|
|
59
|
+
"Apr",
|
|
60
|
+
"May",
|
|
61
|
+
"Jun",
|
|
62
|
+
"Jul",
|
|
63
|
+
"Aug",
|
|
64
|
+
"Sep",
|
|
65
|
+
"Oct",
|
|
66
|
+
"Nov",
|
|
67
|
+
"Dec",
|
|
68
|
+
];
|
|
69
|
+
const monthNamesLong = [
|
|
70
|
+
"January",
|
|
71
|
+
"February",
|
|
72
|
+
"March",
|
|
73
|
+
"April",
|
|
74
|
+
"May",
|
|
75
|
+
"June",
|
|
76
|
+
"July",
|
|
77
|
+
"August",
|
|
78
|
+
"September",
|
|
79
|
+
"October",
|
|
80
|
+
"November",
|
|
81
|
+
"December",
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
function pad(value: number, len: number): string {
|
|
85
|
+
return value.toString().padStart(len, "0");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function formatStandard(token: string, date: Date, locale: string): string {
|
|
89
|
+
switch (token) {
|
|
90
|
+
case "d":
|
|
91
|
+
return new Intl.DateTimeFormat(locale, { dateStyle: "short" }).format(
|
|
92
|
+
date,
|
|
93
|
+
);
|
|
94
|
+
case "D":
|
|
95
|
+
return new Intl.DateTimeFormat(locale, { dateStyle: "full" }).format(
|
|
96
|
+
date,
|
|
97
|
+
);
|
|
98
|
+
case "f":
|
|
99
|
+
return new Intl.DateTimeFormat(locale, {
|
|
100
|
+
dateStyle: "full",
|
|
101
|
+
timeStyle: "short",
|
|
102
|
+
}).format(date);
|
|
103
|
+
case "F":
|
|
104
|
+
return new Intl.DateTimeFormat(locale, {
|
|
105
|
+
dateStyle: "full",
|
|
106
|
+
timeStyle: "medium",
|
|
107
|
+
}).format(date);
|
|
108
|
+
case "g":
|
|
109
|
+
return new Intl.DateTimeFormat(locale, {
|
|
110
|
+
dateStyle: "short",
|
|
111
|
+
timeStyle: "short",
|
|
112
|
+
}).format(date);
|
|
113
|
+
case "G":
|
|
114
|
+
return new Intl.DateTimeFormat(locale, {
|
|
115
|
+
dateStyle: "short",
|
|
116
|
+
timeStyle: "medium",
|
|
117
|
+
}).format(date);
|
|
118
|
+
case "M":
|
|
119
|
+
case "m":
|
|
120
|
+
return `${monthNamesLong[date.getMonth()]} ${date.getDate()}`;
|
|
121
|
+
case "O":
|
|
122
|
+
case "o":
|
|
123
|
+
return date.toISOString();
|
|
124
|
+
case "s":
|
|
125
|
+
return `${pad(date.getFullYear(), 4)}-${pad(date.getMonth() + 1, 2)}-${pad(date.getDate(), 2)}T${pad(date.getHours(), 2)}:${pad(date.getMinutes(), 2)}:${pad(date.getSeconds(), 2)}`;
|
|
126
|
+
case "t":
|
|
127
|
+
return new Intl.DateTimeFormat(locale, { timeStyle: "short" }).format(
|
|
128
|
+
date,
|
|
129
|
+
);
|
|
130
|
+
case "T":
|
|
131
|
+
return new Intl.DateTimeFormat(locale, { timeStyle: "medium" }).format(
|
|
132
|
+
date,
|
|
133
|
+
);
|
|
134
|
+
case "u":
|
|
135
|
+
return date.toISOString().replace("T", " ").replace("Z", "Z");
|
|
136
|
+
case "Y":
|
|
137
|
+
case "y":
|
|
138
|
+
return `${pad(date.getFullYear(), 4)} ${monthNamesLong[date.getMonth()]}`;
|
|
139
|
+
default:
|
|
140
|
+
return "?";
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function tokenizeCustom(format: string): Result<Element[]> {
|
|
145
|
+
const elements: Element[] = [];
|
|
146
|
+
let index = 0;
|
|
147
|
+
|
|
148
|
+
const pushLiteral = (text: string) => {
|
|
149
|
+
if (text.length > 0) {
|
|
150
|
+
elements.push({ type: "literal", text });
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
while (index < format.length) {
|
|
155
|
+
const current = format[index];
|
|
156
|
+
|
|
157
|
+
if (current === "'" || current === '"') {
|
|
158
|
+
const quote = current;
|
|
159
|
+
index += 1;
|
|
160
|
+
let literal = "";
|
|
161
|
+
while (index < format.length && format[index] !== quote) {
|
|
162
|
+
literal += format[index];
|
|
163
|
+
index += 1;
|
|
164
|
+
}
|
|
165
|
+
if (index >= format.length) {
|
|
166
|
+
return new Err("Unterminated quoted literal in format");
|
|
167
|
+
}
|
|
168
|
+
index += 1;
|
|
169
|
+
pushLiteral(literal);
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (current === "\\") {
|
|
174
|
+
index += 1;
|
|
175
|
+
if (index >= format.length) {
|
|
176
|
+
return new Err("Single-char literal escape at end of format");
|
|
177
|
+
}
|
|
178
|
+
pushLiteral(format[index]);
|
|
179
|
+
index += 1;
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (current === "%") {
|
|
184
|
+
index += 1;
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
let runLength = 1;
|
|
189
|
+
while (
|
|
190
|
+
index + runLength < format.length &&
|
|
191
|
+
format[index + runLength] === current
|
|
192
|
+
) {
|
|
193
|
+
runLength += 1;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const consume = () => {
|
|
197
|
+
index += runLength;
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
switch (current) {
|
|
201
|
+
case "d":
|
|
202
|
+
if (runLength === 1) elements.push({ type: "day" });
|
|
203
|
+
else if (runLength === 2) elements.push({ type: "day2" });
|
|
204
|
+
else if (runLength === 3) elements.push({ type: "dayNameShort" });
|
|
205
|
+
else if (runLength === 4) elements.push({ type: "dayNameLong" });
|
|
206
|
+
else return new Err("Too many repeated d characters");
|
|
207
|
+
consume();
|
|
208
|
+
break;
|
|
209
|
+
case "M":
|
|
210
|
+
if (runLength === 1) elements.push({ type: "month" });
|
|
211
|
+
else if (runLength === 2) elements.push({ type: "month2" });
|
|
212
|
+
else if (runLength === 3) elements.push({ type: "monthNameShort" });
|
|
213
|
+
else if (runLength === 4) elements.push({ type: "monthNameLong" });
|
|
214
|
+
else return new Err("Too many repeated M characters");
|
|
215
|
+
consume();
|
|
216
|
+
break;
|
|
217
|
+
case "y":
|
|
218
|
+
if (runLength === 1 || runLength === 2)
|
|
219
|
+
elements.push({ type: "year2" });
|
|
220
|
+
else if (runLength === 3) elements.push({ type: "year3" });
|
|
221
|
+
else if (runLength === 4) elements.push({ type: "year4" });
|
|
222
|
+
else if (runLength === 5) elements.push({ type: "year5" });
|
|
223
|
+
else return new Err("Too many repeated y characters");
|
|
224
|
+
consume();
|
|
225
|
+
break;
|
|
226
|
+
case "h":
|
|
227
|
+
elements.push({ type: runLength === 1 ? "hour12" : "hour12_2" });
|
|
228
|
+
consume();
|
|
229
|
+
break;
|
|
230
|
+
case "H":
|
|
231
|
+
elements.push({ type: runLength === 1 ? "hour24" : "hour24_2" });
|
|
232
|
+
consume();
|
|
233
|
+
break;
|
|
234
|
+
case "m":
|
|
235
|
+
elements.push({ type: runLength === 1 ? "minute" : "minute2" });
|
|
236
|
+
consume();
|
|
237
|
+
break;
|
|
238
|
+
case "s":
|
|
239
|
+
elements.push({ type: runLength === 1 ? "second" : "second2" });
|
|
240
|
+
consume();
|
|
241
|
+
break;
|
|
242
|
+
case "f":
|
|
243
|
+
elements.push({ type: "fraction", len: Math.min(runLength, 7) });
|
|
244
|
+
consume();
|
|
245
|
+
break;
|
|
246
|
+
case "F":
|
|
247
|
+
elements.push({ type: "fractionTrim", len: Math.min(runLength, 7) });
|
|
248
|
+
consume();
|
|
249
|
+
break;
|
|
250
|
+
case "t":
|
|
251
|
+
elements.push({ type: runLength === 1 ? "amPm1" : "amPm2" });
|
|
252
|
+
consume();
|
|
253
|
+
break;
|
|
254
|
+
case "/":
|
|
255
|
+
elements.push({ type: "dateSep" });
|
|
256
|
+
consume();
|
|
257
|
+
break;
|
|
258
|
+
case ":":
|
|
259
|
+
elements.push({ type: "timeSep" });
|
|
260
|
+
consume();
|
|
261
|
+
break;
|
|
262
|
+
default:
|
|
263
|
+
pushLiteral(current.repeat(runLength));
|
|
264
|
+
consume();
|
|
265
|
+
break;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return new Ok(elements);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Formatter for dates and times using .NET-compatible format strings.
|
|
274
|
+
*
|
|
275
|
+
* Supports both standard format strings (d, D, f, F, g, G, M, O, s, t, T, u, Y) and
|
|
276
|
+
* custom format strings with specific date/time components (yyyy, MM, dd, HH, mm, ss, etc.).
|
|
277
|
+
*
|
|
278
|
+
* @example
|
|
279
|
+
* ```typescript
|
|
280
|
+
* const formatter = new DotNetDateTimeFormatter();
|
|
281
|
+
* formatter.localeSettings = DotNetLocaleSettings.createInvariant();
|
|
282
|
+
*
|
|
283
|
+
* // Standard format strings
|
|
284
|
+
* formatter.trySetFormat('d'); // Short date
|
|
285
|
+
* console.log(formatter.toString(new Date(2024, 6, 5))); // "7/5/2024"
|
|
286
|
+
*
|
|
287
|
+
* formatter.trySetFormat('F'); // Full date/time
|
|
288
|
+
* console.log(formatter.toString(new Date(2024, 6, 5, 14, 30, 0)));
|
|
289
|
+
* // "Friday, July 5, 2024 2:30:00 PM"
|
|
290
|
+
*
|
|
291
|
+
* // Custom format strings
|
|
292
|
+
* formatter.trySetFormat('yyyy-MM-dd');
|
|
293
|
+
* console.log(formatter.toString(new Date(2024, 6, 5))); // "2024-07-05"
|
|
294
|
+
*
|
|
295
|
+
* formatter.trySetFormat("dddd, MMMM d 'at' h:mm tt");
|
|
296
|
+
* console.log(formatter.toString(new Date(2024, 6, 5, 14, 30, 0)));
|
|
297
|
+
* // "Friday, July 5 at 2:30 PM"
|
|
298
|
+
* ```
|
|
299
|
+
*
|
|
300
|
+
* @public
|
|
301
|
+
* @category DateTime Formatting
|
|
302
|
+
*/
|
|
303
|
+
export class DotNetDateTimeFormatter {
|
|
304
|
+
/**
|
|
305
|
+
* Set of date/time style flags that are not currently supported by this implementation.
|
|
306
|
+
* Operations using these styles will fail.
|
|
307
|
+
*/
|
|
308
|
+
static readonly unsupportedStyles = new Set<DotNetDateTimeStyleId>([
|
|
309
|
+
DotNetDateTimeStyleId.AdjustToUniversal,
|
|
310
|
+
DotNetDateTimeStyleId.AssumeLocal,
|
|
311
|
+
DotNetDateTimeStyleId.AssumeUniversal,
|
|
312
|
+
DotNetDateTimeStyleId.RoundTripKind,
|
|
313
|
+
]);
|
|
314
|
+
|
|
315
|
+
private format = "";
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* The set of {@link DotNetDateTimeStyleId} flags that control date/time parsing behavior.
|
|
319
|
+
* Currently only used for future parsing functionality.
|
|
320
|
+
*/
|
|
321
|
+
styles: DotNetDateTimeStyleSet = new Set(DotNetDateTimeStyles.none);
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* The locale settings that determine date/time separators and formatting conventions.
|
|
325
|
+
*/
|
|
326
|
+
localeSettings = DotNetLocaleSettings.current;
|
|
327
|
+
|
|
328
|
+
private formatIsStandard = false;
|
|
329
|
+
private elements: Element[] = [];
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Contains the error message from the last failed operation.
|
|
333
|
+
* Check this property if {@link DotNetDateTimeFormatter.trySetFormat} returns an error.
|
|
334
|
+
*/
|
|
335
|
+
parseErrorText = "";
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Sets the format string to use for formatting dates and times.
|
|
339
|
+
*
|
|
340
|
+
* The format string is tokenized to confirm validity and to optimize formatting/parsing operations.
|
|
341
|
+
*
|
|
342
|
+
* @param value - A standard format string (single character like 'd', 'F', 'o') or
|
|
343
|
+
* custom format string (e.g., "yyyy-MM-dd HH:mm:ss").
|
|
344
|
+
* @returns A Result indicating success or containing an error message if the format string is invalid.
|
|
345
|
+
*
|
|
346
|
+
* @example
|
|
347
|
+
* ```typescript
|
|
348
|
+
* // Standard format
|
|
349
|
+
* formatter.trySetFormat('d'); // Short date
|
|
350
|
+
*
|
|
351
|
+
* // Custom format
|
|
352
|
+
* formatter.trySetFormat('yyyy-MM-dd HH:mm:ss');
|
|
353
|
+
*
|
|
354
|
+
* // Check for errors
|
|
355
|
+
* const result = formatter.trySetFormat('');
|
|
356
|
+
* if (result.isErr()) {
|
|
357
|
+
* console.error(result.error); // "Format text cannot be empty"
|
|
358
|
+
* }
|
|
359
|
+
* ```
|
|
360
|
+
*/
|
|
361
|
+
trySetFormat(value: string): Result<void> {
|
|
362
|
+
if (value.length === 0) {
|
|
363
|
+
return new Err("Format text cannot be empty");
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
this.format = value;
|
|
367
|
+
this.formatIsStandard = value.length === 1;
|
|
368
|
+
|
|
369
|
+
if (this.formatIsStandard) {
|
|
370
|
+
this.elements = [{ type: "standard", text: value }];
|
|
371
|
+
return new Ok(undefined);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const parsed = tokenizeCustom(value);
|
|
375
|
+
if (parsed.isErr()) {
|
|
376
|
+
return parsed.createType<void>();
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
this.elements = parsed.value;
|
|
380
|
+
return new Ok(undefined);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Formats a Date using the current format string.
|
|
385
|
+
*
|
|
386
|
+
* @param value - The Date to format.
|
|
387
|
+
* @returns The formatted date/time string.
|
|
388
|
+
*
|
|
389
|
+
* @example
|
|
390
|
+
* ```typescript
|
|
391
|
+
* const date = new Date(2024, 6, 5, 14, 30, 45);
|
|
392
|
+
*
|
|
393
|
+
* formatter.trySetFormat('d');
|
|
394
|
+
* console.log(formatter.toString(date)); // "7/5/2024"
|
|
395
|
+
*
|
|
396
|
+
* formatter.trySetFormat('yyyy-MM-dd HH:mm:ss');
|
|
397
|
+
* console.log(formatter.toString(date)); // "2024-07-05 14:30:45"
|
|
398
|
+
* ```
|
|
399
|
+
*/
|
|
400
|
+
toString(value: Date): string {
|
|
401
|
+
if (this.formatIsStandard) {
|
|
402
|
+
return formatStandard(this.format, value, this.localeSettings.name);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
let result = "";
|
|
406
|
+
const hour = value.getHours();
|
|
407
|
+
const hour12 = hour % 12 === 0 ? 12 : hour % 12;
|
|
408
|
+
|
|
409
|
+
for (const element of this.elements) {
|
|
410
|
+
switch (element.type) {
|
|
411
|
+
case "day":
|
|
412
|
+
result += value.getDate().toString();
|
|
413
|
+
break;
|
|
414
|
+
case "day2":
|
|
415
|
+
result += pad(value.getDate(), 2);
|
|
416
|
+
break;
|
|
417
|
+
case "dayNameShort":
|
|
418
|
+
result += dayNamesShort[value.getDay()];
|
|
419
|
+
break;
|
|
420
|
+
case "dayNameLong":
|
|
421
|
+
result += dayNamesLong[value.getDay()];
|
|
422
|
+
break;
|
|
423
|
+
case "month":
|
|
424
|
+
result += (value.getMonth() + 1).toString();
|
|
425
|
+
break;
|
|
426
|
+
case "month2":
|
|
427
|
+
result += pad(value.getMonth() + 1, 2);
|
|
428
|
+
break;
|
|
429
|
+
case "monthNameShort":
|
|
430
|
+
result += monthNamesShort[value.getMonth()];
|
|
431
|
+
break;
|
|
432
|
+
case "monthNameLong":
|
|
433
|
+
result += monthNamesLong[value.getMonth()];
|
|
434
|
+
break;
|
|
435
|
+
case "year2":
|
|
436
|
+
result += (value.getFullYear() % 100).toString();
|
|
437
|
+
break;
|
|
438
|
+
case "year3":
|
|
439
|
+
result += pad(value.getFullYear(), 3);
|
|
440
|
+
break;
|
|
441
|
+
case "year4":
|
|
442
|
+
result += pad(value.getFullYear(), 4);
|
|
443
|
+
break;
|
|
444
|
+
case "year5":
|
|
445
|
+
result += pad(value.getFullYear(), 5);
|
|
446
|
+
break;
|
|
447
|
+
case "hour12":
|
|
448
|
+
result += hour12.toString();
|
|
449
|
+
break;
|
|
450
|
+
case "hour12_2":
|
|
451
|
+
result += pad(hour12, 2);
|
|
452
|
+
break;
|
|
453
|
+
case "hour24":
|
|
454
|
+
result += hour.toString();
|
|
455
|
+
break;
|
|
456
|
+
case "hour24_2":
|
|
457
|
+
result += pad(hour, 2);
|
|
458
|
+
break;
|
|
459
|
+
case "minute":
|
|
460
|
+
result += value.getMinutes().toString();
|
|
461
|
+
break;
|
|
462
|
+
case "minute2":
|
|
463
|
+
result += pad(value.getMinutes(), 2);
|
|
464
|
+
break;
|
|
465
|
+
case "second":
|
|
466
|
+
result += value.getSeconds().toString();
|
|
467
|
+
break;
|
|
468
|
+
case "second2":
|
|
469
|
+
result += pad(value.getSeconds(), 2);
|
|
470
|
+
break;
|
|
471
|
+
case "fraction": {
|
|
472
|
+
const ms = pad(value.getMilliseconds(), 3);
|
|
473
|
+
result += (ms + "0000000").slice(0, element.len ?? 3);
|
|
474
|
+
break;
|
|
475
|
+
}
|
|
476
|
+
case "fractionTrim": {
|
|
477
|
+
const ms = pad(value.getMilliseconds(), 3);
|
|
478
|
+
result += (ms + "0000000")
|
|
479
|
+
.slice(0, element.len ?? 3)
|
|
480
|
+
.replace(/0+$/, "");
|
|
481
|
+
break;
|
|
482
|
+
}
|
|
483
|
+
case "amPm1":
|
|
484
|
+
result += hour < 12 ? "A" : "P";
|
|
485
|
+
break;
|
|
486
|
+
case "amPm2":
|
|
487
|
+
result += hour < 12 ? "AM" : "PM";
|
|
488
|
+
break;
|
|
489
|
+
case "dateSep":
|
|
490
|
+
result += this.localeSettings.dateSeparator;
|
|
491
|
+
break;
|
|
492
|
+
case "timeSep":
|
|
493
|
+
result += this.localeSettings.timeSeparator;
|
|
494
|
+
break;
|
|
495
|
+
case "literal":
|
|
496
|
+
result += element.text ?? "";
|
|
497
|
+
break;
|
|
498
|
+
case "standard":
|
|
499
|
+
result += formatStandard(
|
|
500
|
+
element.text ?? "",
|
|
501
|
+
value,
|
|
502
|
+
this.localeSettings.name,
|
|
503
|
+
);
|
|
504
|
+
break;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
return result;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
tryFromString(strValue: string): Result<Date> {
|
|
512
|
+
let parseText = strValue;
|
|
513
|
+
|
|
514
|
+
if (
|
|
515
|
+
this.styles.has(DotNetDateTimeStyleId.AllowLeadingWhite) &&
|
|
516
|
+
this.styles.has(DotNetDateTimeStyleId.AllowTrailingWhite)
|
|
517
|
+
) {
|
|
518
|
+
parseText = parseText.trim();
|
|
519
|
+
} else if (this.styles.has(DotNetDateTimeStyleId.AllowLeadingWhite)) {
|
|
520
|
+
parseText = parseText.trimStart();
|
|
521
|
+
} else if (this.styles.has(DotNetDateTimeStyleId.AllowTrailingWhite)) {
|
|
522
|
+
parseText = parseText.trimEnd();
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
if (parseText.length === 0) {
|
|
526
|
+
return new Err("DateTime string is empty");
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
if (this.formatIsStandard) {
|
|
530
|
+
const parsed = new Date(parseText);
|
|
531
|
+
if (Number.isNaN(parsed.getTime())) {
|
|
532
|
+
return new Err("Invalid Date string");
|
|
533
|
+
}
|
|
534
|
+
return new Ok(parsed);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
const regexParts: string[] = ["^"];
|
|
538
|
+
const setters: Array<
|
|
539
|
+
(
|
|
540
|
+
match: string,
|
|
541
|
+
state: {
|
|
542
|
+
y: number;
|
|
543
|
+
m: number;
|
|
544
|
+
d: number;
|
|
545
|
+
hh: number;
|
|
546
|
+
mm: number;
|
|
547
|
+
ss: number;
|
|
548
|
+
ms: number;
|
|
549
|
+
ampm: number;
|
|
550
|
+
},
|
|
551
|
+
) => void
|
|
552
|
+
> = [];
|
|
553
|
+
|
|
554
|
+
for (const element of this.elements) {
|
|
555
|
+
switch (element.type) {
|
|
556
|
+
case "day":
|
|
557
|
+
case "day2":
|
|
558
|
+
regexParts.push("(\\d{1,2})");
|
|
559
|
+
setters.push((v, s) => {
|
|
560
|
+
s.d = Number.parseInt(v, 10);
|
|
561
|
+
});
|
|
562
|
+
break;
|
|
563
|
+
case "month":
|
|
564
|
+
case "month2":
|
|
565
|
+
regexParts.push("(\\d{1,2})");
|
|
566
|
+
setters.push((v, s) => {
|
|
567
|
+
s.m = Number.parseInt(v, 10);
|
|
568
|
+
});
|
|
569
|
+
break;
|
|
570
|
+
case "year2":
|
|
571
|
+
regexParts.push("(\\d{1,2})");
|
|
572
|
+
setters.push((v, s) => {
|
|
573
|
+
const yr = Number.parseInt(v, 10);
|
|
574
|
+
s.y = yr < 50 ? 2000 + yr : 1900 + yr;
|
|
575
|
+
});
|
|
576
|
+
break;
|
|
577
|
+
case "year3":
|
|
578
|
+
case "year4":
|
|
579
|
+
case "year5":
|
|
580
|
+
regexParts.push("(\\d{3,5})");
|
|
581
|
+
setters.push((v, s) => {
|
|
582
|
+
s.y = Number.parseInt(v, 10);
|
|
583
|
+
});
|
|
584
|
+
break;
|
|
585
|
+
case "hour12":
|
|
586
|
+
case "hour12_2":
|
|
587
|
+
regexParts.push("(\\d{1,2})");
|
|
588
|
+
setters.push((v, s) => {
|
|
589
|
+
s.hh = Number.parseInt(v, 10);
|
|
590
|
+
});
|
|
591
|
+
break;
|
|
592
|
+
case "hour24":
|
|
593
|
+
case "hour24_2":
|
|
594
|
+
regexParts.push("(\\d{1,2})");
|
|
595
|
+
setters.push((v, s) => {
|
|
596
|
+
s.hh = Number.parseInt(v, 10);
|
|
597
|
+
});
|
|
598
|
+
break;
|
|
599
|
+
case "minute":
|
|
600
|
+
case "minute2":
|
|
601
|
+
regexParts.push("(\\d{1,2})");
|
|
602
|
+
setters.push((v, s) => {
|
|
603
|
+
s.mm = Number.parseInt(v, 10);
|
|
604
|
+
});
|
|
605
|
+
break;
|
|
606
|
+
case "second":
|
|
607
|
+
case "second2":
|
|
608
|
+
regexParts.push("(\\d{1,2})");
|
|
609
|
+
setters.push((v, s) => {
|
|
610
|
+
s.ss = Number.parseInt(v, 10);
|
|
611
|
+
});
|
|
612
|
+
break;
|
|
613
|
+
case "fraction":
|
|
614
|
+
case "fractionTrim":
|
|
615
|
+
regexParts.push("(\\d{1,7})");
|
|
616
|
+
setters.push((v, s) => {
|
|
617
|
+
s.ms = Number.parseInt((v + "000").slice(0, 3), 10);
|
|
618
|
+
});
|
|
619
|
+
break;
|
|
620
|
+
case "amPm1":
|
|
621
|
+
regexParts.push("([APap])");
|
|
622
|
+
setters.push((v, s) => {
|
|
623
|
+
s.ampm = v.toUpperCase() === "P" ? 2 : 1;
|
|
624
|
+
});
|
|
625
|
+
break;
|
|
626
|
+
case "amPm2":
|
|
627
|
+
regexParts.push("(AM|PM|am|pm)");
|
|
628
|
+
setters.push((v, s) => {
|
|
629
|
+
s.ampm = v.toUpperCase() === "PM" ? 2 : 1;
|
|
630
|
+
});
|
|
631
|
+
break;
|
|
632
|
+
case "dayNameShort":
|
|
633
|
+
regexParts.push(`(${dayNamesShort.join("|")})`);
|
|
634
|
+
setters.push(() => undefined);
|
|
635
|
+
break;
|
|
636
|
+
case "dayNameLong":
|
|
637
|
+
regexParts.push(`(${dayNamesLong.join("|")})`);
|
|
638
|
+
setters.push(() => undefined);
|
|
639
|
+
break;
|
|
640
|
+
case "monthNameShort":
|
|
641
|
+
regexParts.push(`(${monthNamesShort.join("|")})`);
|
|
642
|
+
setters.push((v, s) => {
|
|
643
|
+
s.m =
|
|
644
|
+
monthNamesShort.findIndex(
|
|
645
|
+
(x) => x.toLowerCase() === v.toLowerCase(),
|
|
646
|
+
) + 1;
|
|
647
|
+
});
|
|
648
|
+
break;
|
|
649
|
+
case "monthNameLong":
|
|
650
|
+
regexParts.push(`(${monthNamesLong.join("|")})`);
|
|
651
|
+
setters.push((v, s) => {
|
|
652
|
+
s.m =
|
|
653
|
+
monthNamesLong.findIndex(
|
|
654
|
+
(x) => x.toLowerCase() === v.toLowerCase(),
|
|
655
|
+
) + 1;
|
|
656
|
+
});
|
|
657
|
+
break;
|
|
658
|
+
case "dateSep":
|
|
659
|
+
regexParts.push(
|
|
660
|
+
this.localeSettings.dateSeparator.replace(
|
|
661
|
+
/[.*+?^${}()|[\]\\]/g,
|
|
662
|
+
"\\$&",
|
|
663
|
+
),
|
|
664
|
+
);
|
|
665
|
+
break;
|
|
666
|
+
case "timeSep":
|
|
667
|
+
regexParts.push(
|
|
668
|
+
this.localeSettings.timeSeparator.replace(
|
|
669
|
+
/[.*+?^${}()|[\]\\]/g,
|
|
670
|
+
"\\$&",
|
|
671
|
+
),
|
|
672
|
+
);
|
|
673
|
+
break;
|
|
674
|
+
case "literal":
|
|
675
|
+
regexParts.push(
|
|
676
|
+
(element.text ?? "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&"),
|
|
677
|
+
);
|
|
678
|
+
break;
|
|
679
|
+
case "standard":
|
|
680
|
+
break;
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
regexParts.push("$");
|
|
685
|
+
const re = new RegExp(regexParts.join(""));
|
|
686
|
+
const match = re.exec(parseText);
|
|
687
|
+
if (match === null) {
|
|
688
|
+
return new Err("DateTime string does not match all Format specifiers");
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
const state = { y: 1, m: 1, d: 1, hh: 0, mm: 0, ss: 0, ms: 0, ampm: 0 };
|
|
692
|
+
let group = 1;
|
|
693
|
+
for (const setter of setters) {
|
|
694
|
+
setter(match[group], state);
|
|
695
|
+
group += 1;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
if (state.ampm > 0) {
|
|
699
|
+
if (state.hh === 12) state.hh = 0;
|
|
700
|
+
if (state.ampm === 2) state.hh += 12;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
const value = new Date(
|
|
704
|
+
state.y,
|
|
705
|
+
state.m - 1,
|
|
706
|
+
state.d,
|
|
707
|
+
state.hh,
|
|
708
|
+
state.mm,
|
|
709
|
+
state.ss,
|
|
710
|
+
state.ms,
|
|
711
|
+
);
|
|
712
|
+
if (Number.isNaN(value.getTime())) {
|
|
713
|
+
return new Err("Year, Month or Day is invalid");
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
return new Ok(value);
|
|
717
|
+
}
|
|
718
|
+
}
|