@fable-org/fable-library-js 2.6.0 → 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 +14 -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/TimeOnlyTemporal.js +142 -0
- package/TimeSpanTemporal.js +259 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
last_commit_released:
|
|
2
|
+
last_commit_released: c49d1ec5d1a491bfff65fe4bd6c71014cac9b321
|
|
3
3
|
updaters:
|
|
4
4
|
- package.json:
|
|
5
5
|
file: package.json
|
|
@@ -15,6 +15,19 @@ All notable changes to this project will be documented in this file.
|
|
|
15
15
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
16
16
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
17
17
|
|
|
18
|
+
## 2.7.0 - 2026-09-08
|
|
19
|
+
|
|
20
|
+
### 🚀 Features
|
|
21
|
+
|
|
22
|
+
* Add Temporal dates and times (opt-in) (#4759) ([3e8f56f7](https://github.com/fable-compiler/Fable/commit/3e8f56f78345fbee8a4394c5452eb212264701f9))
|
|
23
|
+
|
|
24
|
+
### 🐞 Bug Fixes
|
|
25
|
+
|
|
26
|
+
* *(js/ts)* Match .NET for G, E and custom numeric formats ([6fb03090](https://github.com/fable-compiler/Fable/commit/6fb03090ce328b8c5d6a36422f120dd641aefe3a))
|
|
27
|
+
* *(js/ts)* Allow the E format specifier on Int64 and UInt64 ([c49d1ec5](https://github.com/fable-compiler/Fable/commit/c49d1ec5d1a491bfff65fe4bd6c71014cac9b321))
|
|
28
|
+
|
|
29
|
+
<strong><small>[View changes on Github](https://github.com/fable-compiler/Fable/compare/bf694309ae5dbc6bb546d9acc4d091f5e35fe330..c49d1ec5d1a491bfff65fe4bd6c71014cac9b321)</small></strong>
|
|
30
|
+
|
|
18
31
|
## 2.6.0 - 2026-09-03
|
|
19
32
|
|
|
20
33
|
### 🚀 Features
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { dateTime as DateTime_stamp } from "./DateTimeTemporal.js";
|
|
2
|
+
import { fromDate as DateTimeOffset_fromDate } from "./DateTimeOffsetTemporal.js";
|
|
3
|
+
import { Exception, DateTimeKind, padWithZeros } from "./Util.js";
|
|
4
|
+
export const PlainDate = Temporal.PlainDate;
|
|
5
|
+
// The generic equality/comparison/hashing helpers in Util.ts dispatch at runtime
|
|
6
|
+
// on .NET-style Equals/CompareTo/GetHashCode methods (with a special case for JS
|
|
7
|
+
// Date, which Temporal.PlainDate cannot benefit from). Attach them so DateOnly
|
|
8
|
+
// also works in generic contexts: records, tuples, erased generics, etc.
|
|
9
|
+
const proto = Temporal.PlainDate.prototype;
|
|
10
|
+
proto.Equals = function (other) { return this.equals(other); };
|
|
11
|
+
proto.CompareTo = function (other) { return Temporal.PlainDate.compare(this, other); };
|
|
12
|
+
proto.GetHashCode = function () { return dayNumber(this); };
|
|
13
|
+
// Lets String.Format format this value without String.ts importing this module (see String.ts)
|
|
14
|
+
proto[Symbol.for("Fable.DateTimeFormattable")] = function (format) { return toString(this, format); };
|
|
15
|
+
export function create(year, month, day) {
|
|
16
|
+
return new Temporal.PlainDate(year, month, day);
|
|
17
|
+
}
|
|
18
|
+
export function maxValue() {
|
|
19
|
+
return new Temporal.PlainDate(9999, 12, 31);
|
|
20
|
+
}
|
|
21
|
+
export function minValue() {
|
|
22
|
+
return new Temporal.PlainDate(1, 1, 1);
|
|
23
|
+
}
|
|
24
|
+
export function dayNumber(d) {
|
|
25
|
+
return minValue().until(d).days;
|
|
26
|
+
}
|
|
27
|
+
export function fromDayNumber(dayNumber) {
|
|
28
|
+
return minValue().add({ days: dayNumber });
|
|
29
|
+
}
|
|
30
|
+
export function fromDateTime(d) {
|
|
31
|
+
// Under the Temporal representation a DateTime is a PlainDateTime (kind-agnostic wall-clock)
|
|
32
|
+
return new Temporal.PlainDate(d.year, d.month, d.day);
|
|
33
|
+
}
|
|
34
|
+
export function dayOfWeek(d) {
|
|
35
|
+
// Temporal: Monday = 1 ... Sunday = 7, .NET: Sunday = 0 ... Saturday = 6
|
|
36
|
+
return d.dayOfWeek % 7;
|
|
37
|
+
}
|
|
38
|
+
export function toDateTime(d, time, kind = DateTimeKind.Unspecified) {
|
|
39
|
+
return DateTime_stamp(d.toPlainDateTime(time), kind);
|
|
40
|
+
}
|
|
41
|
+
export function toDateTimeOffset(d, time, offset) {
|
|
42
|
+
return DateTimeOffset_fromDate(DateTime_stamp(d.toPlainDateTime(time), DateTimeKind.Unspecified), offset);
|
|
43
|
+
}
|
|
44
|
+
export function addDays(d, v) {
|
|
45
|
+
return d.add({ days: v });
|
|
46
|
+
}
|
|
47
|
+
export function addMonths(d, v) {
|
|
48
|
+
return d.add({ months: v });
|
|
49
|
+
}
|
|
50
|
+
export function addYears(d, v) {
|
|
51
|
+
return d.add({ years: v });
|
|
52
|
+
}
|
|
53
|
+
export function equals(x, y) {
|
|
54
|
+
return x.equals(y);
|
|
55
|
+
}
|
|
56
|
+
export function compare(x, y) {
|
|
57
|
+
return Temporal.PlainDate.compare(x, y);
|
|
58
|
+
}
|
|
59
|
+
export function hash(d) {
|
|
60
|
+
return dayNumber(d);
|
|
61
|
+
}
|
|
62
|
+
export function toString(d, format = "d", _provider) {
|
|
63
|
+
switch (format) {
|
|
64
|
+
case "d":
|
|
65
|
+
return `${padWithZeros(d.month, 2)}/${padWithZeros(d.day, 2)}/${padWithZeros(d.year, 4)}`;
|
|
66
|
+
case "o":
|
|
67
|
+
case "O":
|
|
68
|
+
// PlainDate.toString() is the ISO yyyy-MM-dd round-trip format
|
|
69
|
+
return d.toString();
|
|
70
|
+
default:
|
|
71
|
+
throw new Exception("Custom formats are not supported");
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
export function parse(str) {
|
|
75
|
+
function fail() {
|
|
76
|
+
throw new Exception(`String '${str}' was not recognized as a valid DateOnly.`);
|
|
77
|
+
}
|
|
78
|
+
// Allowed separators: . , / -
|
|
79
|
+
// TODO whitespace alone as the separator
|
|
80
|
+
//
|
|
81
|
+
// Whitespace around separators
|
|
82
|
+
//
|
|
83
|
+
// Allowed format types:
|
|
84
|
+
// yyyy/mm/dd
|
|
85
|
+
// mm/dd/yyyy
|
|
86
|
+
// mm/dd
|
|
87
|
+
// mm/yyyy
|
|
88
|
+
// yyyy/mm
|
|
89
|
+
const r = /^\s*(\d{1,4})(?:\s*[.,-\/]\s*(\d{1,2}))?\s*[.,-\/]\s*(\d{1,4})\s*$/.exec(str);
|
|
90
|
+
if (r != null) {
|
|
91
|
+
let y = 0;
|
|
92
|
+
let m = 0;
|
|
93
|
+
let d = 1;
|
|
94
|
+
if (r[2] == null) {
|
|
95
|
+
if (r[1].length < 3) {
|
|
96
|
+
if (r[3].length < 3) {
|
|
97
|
+
// 12/30 = December 30, {CurrentYear}
|
|
98
|
+
y = Temporal.Now.plainDateISO().year;
|
|
99
|
+
m = +r[1];
|
|
100
|
+
d = +r[3];
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
// 12/2000 = December 1, 2000
|
|
104
|
+
m = +r[1];
|
|
105
|
+
y = +r[3];
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
if (r[3].length > 2)
|
|
110
|
+
fail();
|
|
111
|
+
// 2000/12 = December 1, 2000
|
|
112
|
+
y = +r[1];
|
|
113
|
+
m = +r[3];
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
// 2000/1/30 or 1/30/2000
|
|
118
|
+
const yearFirst = r[1].length > 2;
|
|
119
|
+
const yTmp = r[yearFirst ? 1 : 3];
|
|
120
|
+
y = +yTmp;
|
|
121
|
+
// year 0-29 is 2000-2029, 30-99 is 1930-1999
|
|
122
|
+
if (yTmp.length < 3)
|
|
123
|
+
y += y >= 30 ? 1900 : 2000;
|
|
124
|
+
m = +r[yearFirst ? 2 : 1];
|
|
125
|
+
d = +r[yearFirst ? 3 : 2];
|
|
126
|
+
}
|
|
127
|
+
if (y > 0) {
|
|
128
|
+
try {
|
|
129
|
+
return new Temporal.PlainDate(y, m, d);
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
return fail();
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return fail();
|
|
137
|
+
}
|
|
138
|
+
export function tryParse(v, defValue) {
|
|
139
|
+
try {
|
|
140
|
+
defValue.contents = parse(v);
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
@@ -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
|
+
}
|