@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,283 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DateTimeOffset as an offset-only Temporal.ZonedDateTime.
|
|
3
|
+
*
|
|
4
|
+
* .NET DateTimeOffset is a wall-clock date+time plus an explicit UTC offset (a
|
|
5
|
+
* TimeSpan of whole minutes). A ZonedDateTime whose time zone is a fixed offset
|
|
6
|
+
* (e.g. "-08:00") carries the wall-clock, the offset, and the absolute instant
|
|
7
|
+
* natively — so there is no extra metadata to stamp and no re-stamping after
|
|
8
|
+
* operations (arithmetic returns a ZonedDateTime that keeps the offset).
|
|
9
|
+
*
|
|
10
|
+
* The offset is exchanged with the rest of the runtime as a TimeSpan (Temporal
|
|
11
|
+
* .Duration), matching how .NET types it. Equality/comparison are by instant
|
|
12
|
+
* (offset-independent), matching .NET '=='; EqualsExact also compares the offset.
|
|
13
|
+
*
|
|
14
|
+
* Formatting and parsing run on the Temporal value's own fields (see
|
|
15
|
+
* DateTimeFormat.ts) rather than through a JS Date, which would truncate every
|
|
16
|
+
* value to milliseconds.
|
|
17
|
+
*/
|
|
18
|
+
import { fromFloat64 } from "./BigInt.js";
|
|
19
|
+
import { Exception, DateTimeKind, padWithZeros } from "./Util.js";
|
|
20
|
+
import { fromTicks as TimeSpan_fromTicks, fromUnits as TimeSpan_fromUnits, totalNanoseconds as TimeSpan_totalNanoseconds, } from "./TimeSpanTemporal.js";
|
|
21
|
+
import { dateTime, create as createDateTime, getTicks as DateTime_getTicks } from "./DateTimeTemporal.js";
|
|
22
|
+
import * as Format from "./DateTimeFormat.js";
|
|
23
|
+
export const ZonedDateTime = Temporal.ZonedDateTime;
|
|
24
|
+
// Generic equality/comparison/hashing dispatch (Util.ts) uses .NET-style methods.
|
|
25
|
+
const proto = Temporal.ZonedDateTime.prototype;
|
|
26
|
+
proto.Equals = function (other) { return equals(this, other); };
|
|
27
|
+
proto.CompareTo = function (other) { return compare(this, other); };
|
|
28
|
+
proto.GetHashCode = function () { return hash(this); };
|
|
29
|
+
// Lets String.Format format this value without String.ts importing this module (see String.ts)
|
|
30
|
+
proto[Symbol.for("Fable.DateTimeFormattable")] = function (format) { return toString(this, format); };
|
|
31
|
+
const nsPerMinute = 60000000000n;
|
|
32
|
+
// Nanoseconds between the Unix epoch (1970-01-01) and DateTime.MinValue (0001-01-01)
|
|
33
|
+
const epochToMinValueNs = 62135596800000n * 1000000n;
|
|
34
|
+
function hostTimeZone() {
|
|
35
|
+
return Temporal.Now.timeZoneId();
|
|
36
|
+
}
|
|
37
|
+
// .NET DateTimeOffset has 100ns (tick) precision; Temporal instants are
|
|
38
|
+
// nanosecond-precise. Floors, so the tick count from year 1 is floored too.
|
|
39
|
+
function truncateToTicks(epochNs) {
|
|
40
|
+
const remainder = epochNs % 100n;
|
|
41
|
+
return remainder >= 0n ? epochNs - remainder : epochNs - remainder - 100n;
|
|
42
|
+
}
|
|
43
|
+
function checkOffsetInRange(offsetNs) {
|
|
44
|
+
if (offsetNs % nsPerMinute !== 0n) {
|
|
45
|
+
throw new Exception("Offset must be specified in whole minutes.");
|
|
46
|
+
}
|
|
47
|
+
if (offsetNs > 14n * 60n * nsPerMinute || offsetNs < -14n * 60n * nsPerMinute) {
|
|
48
|
+
throw new Exception("Offset must be within plus or minus 14 hours.");
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function offsetNanosecondsToZoneString(offsetNs) {
|
|
52
|
+
const sign = offsetNs < 0n ? "-" : "+";
|
|
53
|
+
const abs = offsetNs < 0n ? -offsetNs : offsetNs;
|
|
54
|
+
const totalMinutes = Number(abs / nsPerMinute);
|
|
55
|
+
return `${sign}${padWithZeros(Math.trunc(totalMinutes / 60), 2)}:${padWithZeros(totalMinutes % 60, 2)}`;
|
|
56
|
+
}
|
|
57
|
+
function offsetToNanoseconds(offset) {
|
|
58
|
+
return TimeSpan_totalNanoseconds(offset);
|
|
59
|
+
}
|
|
60
|
+
function fromEpoch(epochNs, offsetNs = 0n) {
|
|
61
|
+
if (offsetNs === 0n)
|
|
62
|
+
return new Temporal.ZonedDateTime(epochNs, "UTC");
|
|
63
|
+
checkOffsetInRange(offsetNs);
|
|
64
|
+
return new Temporal.ZonedDateTime(epochNs, offsetNanosecondsToZoneString(offsetNs));
|
|
65
|
+
}
|
|
66
|
+
export function offset(d) {
|
|
67
|
+
return TimeSpan_fromTicks(BigInt(d.offsetNanoseconds) / 100n);
|
|
68
|
+
}
|
|
69
|
+
export function create(year, month, day, h, m, s, ms, offset) {
|
|
70
|
+
// Overload without milliseconds: the 7th argument is the offset
|
|
71
|
+
if (offset == null) {
|
|
72
|
+
offset = ms;
|
|
73
|
+
ms = 0;
|
|
74
|
+
}
|
|
75
|
+
const offsetNs = offsetToNanoseconds(offset);
|
|
76
|
+
checkOffsetInRange(offsetNs);
|
|
77
|
+
return Temporal.ZonedDateTime.from({
|
|
78
|
+
year, month, day, hour: h, minute: m, second: s, millisecond: ms,
|
|
79
|
+
timeZone: offsetNanosecondsToZoneString(offsetNs),
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
// Offset (ns) of the host time zone for a given wall-clock, DST-aware. Resolved
|
|
83
|
+
// through Temporal rather than `new Date(year, ...)`, which maps years 0-99 into
|
|
84
|
+
// 1900-1999 and would pick that era's rules.
|
|
85
|
+
function hostOffsetNanoseconds(d) {
|
|
86
|
+
return BigInt(d.toZonedDateTime(hostTimeZone()).offsetNanoseconds);
|
|
87
|
+
}
|
|
88
|
+
export function fromDate(date, offset) {
|
|
89
|
+
const kind = date.kind ?? DateTimeKind.Unspecified;
|
|
90
|
+
let offsetNs;
|
|
91
|
+
switch (kind) {
|
|
92
|
+
case DateTimeKind.Utc:
|
|
93
|
+
if (offset != null && offsetToNanoseconds(offset) !== 0n) {
|
|
94
|
+
throw new Exception("The UTC Offset for Utc DateTime instances must be 0.");
|
|
95
|
+
}
|
|
96
|
+
offsetNs = 0n;
|
|
97
|
+
break;
|
|
98
|
+
case DateTimeKind.Local:
|
|
99
|
+
offsetNs = hostOffsetNanoseconds(date);
|
|
100
|
+
if (offset != null && offsetToNanoseconds(offset) !== offsetNs) {
|
|
101
|
+
throw new Exception("The UTC Offset of the local dateTime parameter does not match the offset argument.");
|
|
102
|
+
}
|
|
103
|
+
break;
|
|
104
|
+
default:
|
|
105
|
+
offsetNs = offset != null ? offsetToNanoseconds(offset) : hostOffsetNanoseconds(date);
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
checkOffsetInRange(offsetNs);
|
|
109
|
+
return Temporal.ZonedDateTime.from({
|
|
110
|
+
year: date.year, month: date.month, day: date.day,
|
|
111
|
+
hour: date.hour, minute: date.minute, second: date.second,
|
|
112
|
+
millisecond: date.millisecond, microsecond: date.microsecond, nanosecond: date.nanosecond,
|
|
113
|
+
timeZone: offsetNanosecondsToZoneString(offsetNs),
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
export function fromTicks(ticks, offset) {
|
|
117
|
+
const offsetNs = offsetToNanoseconds(offset);
|
|
118
|
+
// ticks are the local wall-clock ticks; the instant is that minus the offset
|
|
119
|
+
const localNs = BigInt(ticks) * 100n - epochToMinValueNs;
|
|
120
|
+
return fromEpoch(localNs - offsetNs, offsetNs);
|
|
121
|
+
}
|
|
122
|
+
export function fromUnixTimeMilliseconds(ms) {
|
|
123
|
+
return fromEpoch(BigInt(ms) * 1000000n);
|
|
124
|
+
}
|
|
125
|
+
export function fromUnixTimeSeconds(seconds) {
|
|
126
|
+
return fromEpoch(BigInt(seconds) * 1000000000n);
|
|
127
|
+
}
|
|
128
|
+
export function minValue() {
|
|
129
|
+
return fromEpoch(-epochToMinValueNs);
|
|
130
|
+
}
|
|
131
|
+
export function maxValue() {
|
|
132
|
+
// 9999-12-31T23:59:59.9999999 UTC
|
|
133
|
+
return fromEpoch(253402300799999999900n, 0n);
|
|
134
|
+
}
|
|
135
|
+
export function dayOfWeek(d) {
|
|
136
|
+
return d.dayOfWeek % 7;
|
|
137
|
+
}
|
|
138
|
+
export function timeOfDay(d) {
|
|
139
|
+
// Elapsed since midnight (local wall-clock). DateTimeOffset values are tick-aligned, so this is too.
|
|
140
|
+
return d.startOfDay().until(d);
|
|
141
|
+
}
|
|
142
|
+
export function date(d) {
|
|
143
|
+
return createDateTime(d.year, d.month, d.day, 0, 0, 0, 0, DateTimeKind.Unspecified);
|
|
144
|
+
}
|
|
145
|
+
export function dateTimeProp(d) {
|
|
146
|
+
return dateTime(d.toPlainDateTime(), DateTimeKind.Unspecified);
|
|
147
|
+
}
|
|
148
|
+
export function toUniversalTime(d) {
|
|
149
|
+
return fromEpoch(d.epochNanoseconds);
|
|
150
|
+
}
|
|
151
|
+
export function toLocalTime(d) {
|
|
152
|
+
const local = d.withTimeZone(hostTimeZone());
|
|
153
|
+
return fromEpoch(local.epochNanoseconds, BigInt(local.offsetNanoseconds));
|
|
154
|
+
}
|
|
155
|
+
export function utcDateTime(d) {
|
|
156
|
+
return dateTime(d.withTimeZone("UTC").toPlainDateTime(), DateTimeKind.Utc);
|
|
157
|
+
}
|
|
158
|
+
export function localDateTime(d) {
|
|
159
|
+
return dateTime(d.withTimeZone(hostTimeZone()).toPlainDateTime(), DateTimeKind.Local);
|
|
160
|
+
}
|
|
161
|
+
export function add(d, ts) {
|
|
162
|
+
return d.add(ts);
|
|
163
|
+
}
|
|
164
|
+
export function addYears(d, v) {
|
|
165
|
+
return d.add({ years: v });
|
|
166
|
+
}
|
|
167
|
+
export function addMonths(d, v) {
|
|
168
|
+
return d.add({ months: v });
|
|
169
|
+
}
|
|
170
|
+
export function addDays(d, v) {
|
|
171
|
+
return d.add(TimeSpan_fromUnits(v, "days"));
|
|
172
|
+
}
|
|
173
|
+
export function addHours(d, v) {
|
|
174
|
+
return d.add(TimeSpan_fromUnits(v, "hours"));
|
|
175
|
+
}
|
|
176
|
+
export function addMinutes(d, v) {
|
|
177
|
+
return d.add(TimeSpan_fromUnits(v, "minutes"));
|
|
178
|
+
}
|
|
179
|
+
export function addSeconds(d, v) {
|
|
180
|
+
return d.add(TimeSpan_fromUnits(v, "seconds"));
|
|
181
|
+
}
|
|
182
|
+
export function addMilliseconds(d, v) {
|
|
183
|
+
return d.add(TimeSpan_fromUnits(v, "milliseconds"));
|
|
184
|
+
}
|
|
185
|
+
export function addTicks(d, v) {
|
|
186
|
+
return d.add(TimeSpan_fromTicks(v));
|
|
187
|
+
}
|
|
188
|
+
// Split rather than returning a union — see the note in DateTimeTemporal.ts.
|
|
189
|
+
export function subtractDate(d, that) {
|
|
190
|
+
return TimeSpan_fromTicks((d.epochNanoseconds - that.epochNanoseconds) / 100n); // instant difference
|
|
191
|
+
}
|
|
192
|
+
export function subtractTimeSpan(d, ts) {
|
|
193
|
+
return d.subtract(ts);
|
|
194
|
+
}
|
|
195
|
+
export function equals(d1, d2) {
|
|
196
|
+
return d1.epochNanoseconds === d2.epochNanoseconds;
|
|
197
|
+
}
|
|
198
|
+
export function equalsExact(d1, d2) {
|
|
199
|
+
return d1.epochNanoseconds === d2.epochNanoseconds && d1.offsetNanoseconds === d2.offsetNanoseconds;
|
|
200
|
+
}
|
|
201
|
+
export function compare(d1, d2) {
|
|
202
|
+
const a = d1.epochNanoseconds, b = d2.epochNanoseconds;
|
|
203
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
204
|
+
}
|
|
205
|
+
export const compareTo = compare;
|
|
206
|
+
export function op_Addition(x, y) {
|
|
207
|
+
return add(x, y);
|
|
208
|
+
}
|
|
209
|
+
export function hash(d) {
|
|
210
|
+
return Number(d.epochNanoseconds % 2147483647n);
|
|
211
|
+
}
|
|
212
|
+
export function toOffset(d, offset) {
|
|
213
|
+
return fromEpoch(d.epochNanoseconds, offsetToNanoseconds(offset));
|
|
214
|
+
}
|
|
215
|
+
export function getUtcTicks(d) {
|
|
216
|
+
return DateTime_getTicks(utcDateTime(d));
|
|
217
|
+
}
|
|
218
|
+
export function getTicks(d) {
|
|
219
|
+
return DateTime_getTicks(dateTimeProp(d));
|
|
220
|
+
}
|
|
221
|
+
export function toUnixTimeMilliseconds(d) {
|
|
222
|
+
return fromFloat64(d.epochMilliseconds);
|
|
223
|
+
}
|
|
224
|
+
export function toUnixTimeSeconds(d) {
|
|
225
|
+
return fromFloat64(Math.floor(d.epochMilliseconds / 1000));
|
|
226
|
+
}
|
|
227
|
+
export function now() {
|
|
228
|
+
const z = Temporal.Now.zonedDateTimeISO(hostTimeZone());
|
|
229
|
+
return fromEpoch(truncateToTicks(z.epochNanoseconds), BigInt(z.offsetNanoseconds));
|
|
230
|
+
}
|
|
231
|
+
export function utcNow() {
|
|
232
|
+
return fromEpoch(truncateToTicks(Temporal.Now.instant().epochNanoseconds));
|
|
233
|
+
}
|
|
234
|
+
// --- Formatting and parsing ---
|
|
235
|
+
// Wall-clock fields for the formatter, with the sub-second component in .NET
|
|
236
|
+
// ticks so "O" and the f/F specifiers can print all 7 digits.
|
|
237
|
+
function toDateInfo(z) {
|
|
238
|
+
return {
|
|
239
|
+
year: z.year,
|
|
240
|
+
month: z.month,
|
|
241
|
+
day: z.day,
|
|
242
|
+
hour: z.hour,
|
|
243
|
+
minute: z.minute,
|
|
244
|
+
second: z.second,
|
|
245
|
+
tick: z.millisecond * 10000 + z.microsecond * 10 + Math.trunc(z.nanosecond / 100),
|
|
246
|
+
dayOfWeek: z.dayOfWeek % 7,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
export function toString(d, format, _provider) {
|
|
250
|
+
const offsetString = () => offsetNanosecondsToZoneString(BigInt(d.offsetNanoseconds));
|
|
251
|
+
return Format.dateToString({
|
|
252
|
+
info: toDateInfo(d),
|
|
253
|
+
utcInfo: () => toDateInfo(d.withTimeZone("UTC")),
|
|
254
|
+
// A fixed-offset zone renders as .NET's "O" exactly: ISO wall-clock plus
|
|
255
|
+
// the numeric offset, with no [zone] annotation.
|
|
256
|
+
roundTrip: () => d.toString({ fractionalSecondDigits: 7, timeZoneName: "never" }),
|
|
257
|
+
sortable: () => d.toPlainDateTime().toString({ smallestUnit: "second" }),
|
|
258
|
+
defaultSuffix: " " + offsetString(),
|
|
259
|
+
// K/z print the value's own offset, never the host zone's, so Local is
|
|
260
|
+
// passed as the kind and that offset stands in for the host one.
|
|
261
|
+
kind: DateTimeKind.Local,
|
|
262
|
+
hostOffsetString: offsetString,
|
|
263
|
+
}, format);
|
|
264
|
+
}
|
|
265
|
+
export function parse(str) {
|
|
266
|
+
const [parsed, offsetMatch] = Format.parseRaw(str);
|
|
267
|
+
// parseRaw resolves the instant through JS Date, which truncates at the
|
|
268
|
+
// millisecond, so the digits below that are recovered from the input separately.
|
|
269
|
+
const epochNs = BigInt(parsed.getTime()) * 1000000n + BigInt(Format.subMillisecondTicks(str)) * 100n;
|
|
270
|
+
const offsetNs = offsetMatch == null
|
|
271
|
+
? BigInt(new Temporal.Instant(epochNs).toZonedDateTimeISO(hostTimeZone()).offsetNanoseconds)
|
|
272
|
+
: (offsetMatch === "Z" ? 0n : BigInt(offsetMatch) * nsPerMinute);
|
|
273
|
+
return fromEpoch(epochNs, offsetNs);
|
|
274
|
+
}
|
|
275
|
+
export function tryParse(v, defValue) {
|
|
276
|
+
try {
|
|
277
|
+
defValue.contents = parse(v);
|
|
278
|
+
return true;
|
|
279
|
+
}
|
|
280
|
+
catch {
|
|
281
|
+
return false;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DateTime as an extended Temporal.PlainDateTime.
|
|
3
|
+
*
|
|
4
|
+
* .NET DateTime is a wall-clock date+time (no offset) plus a Kind (Utc | Local |
|
|
5
|
+
* Unspecified) that is metadata only. PlainDateTime models the wall-clock exactly
|
|
6
|
+
* and, unlike the JS Date representation, is tick (100ns) precise and DST-agnostic
|
|
7
|
+
* for arithmetic — which matches .NET, where DateTime math operates on the raw
|
|
8
|
+
* wall-clock ticks.
|
|
9
|
+
*
|
|
10
|
+
* Kind has no Temporal home, so it is attached to the instance as a `kind` property
|
|
11
|
+
* (Temporal objects are extensible). Because Temporal operations return fresh,
|
|
12
|
+
* un-stamped instances, every operation must funnel through `dateTime(...)` to
|
|
13
|
+
* re-stamp the kind.
|
|
14
|
+
*
|
|
15
|
+
* Formatting, parsing and time-zone conversion are done on the Temporal value
|
|
16
|
+
* itself (see DateTimeFormat.ts), never by round-tripping through a JS Date —
|
|
17
|
+
* that would silently truncate every value to milliseconds. The one exception is
|
|
18
|
+
* the four ToLong/ShortDate/TimeString members, which are host-locale display
|
|
19
|
+
* strings with no .NET-defined layout and no sub-second component.
|
|
20
|
+
*/
|
|
21
|
+
import { toInt64 } from "./BigInt.js";
|
|
22
|
+
import { DateTimeKind } from "./Util.js";
|
|
23
|
+
import { fromTicks as TimeSpan_fromTicks, fromUnits as TimeSpan_fromUnits, totalNanoseconds as TimeSpan_totalNanoseconds, } from "./TimeSpanTemporal.js";
|
|
24
|
+
import * as Format from "./DateTimeFormat.js";
|
|
25
|
+
export const PlainDateTime = Temporal.PlainDateTime;
|
|
26
|
+
// Generic equality/comparison/hashing dispatch (Util.ts) uses .NET-style methods.
|
|
27
|
+
const proto = Temporal.PlainDateTime.prototype;
|
|
28
|
+
proto.Equals = function (other) { return Temporal.PlainDateTime.compare(this, other) === 0; };
|
|
29
|
+
proto.CompareTo = function (other) { return Temporal.PlainDateTime.compare(this, other); };
|
|
30
|
+
proto.GetHashCode = function () { return hash(this); };
|
|
31
|
+
// Lets String.Format format this value without String.ts importing this module (see String.ts)
|
|
32
|
+
proto[Symbol.for("Fable.DateTimeFormattable")] = function (format) { return toString(this, format); };
|
|
33
|
+
const minDateTime = new Temporal.PlainDateTime(1, 1, 1);
|
|
34
|
+
export function getKind(value) {
|
|
35
|
+
return value.kind ?? DateTimeKind.Unspecified;
|
|
36
|
+
}
|
|
37
|
+
export function dateTime(value, kind = DateTimeKind.Unspecified) {
|
|
38
|
+
const d = value;
|
|
39
|
+
d.kind = kind;
|
|
40
|
+
return d;
|
|
41
|
+
}
|
|
42
|
+
// `dateTime` stamps in place, so anything handing out a value derived from a
|
|
43
|
+
// shared instance (a module constant, or the argument of SpecifyKind) has to
|
|
44
|
+
// copy first, or it would mutate the caller's value.
|
|
45
|
+
function copy(d) {
|
|
46
|
+
return new Temporal.PlainDateTime(d.year, d.month, d.day, d.hour, d.minute, d.second, d.millisecond, d.microsecond, d.nanosecond);
|
|
47
|
+
}
|
|
48
|
+
function hostTimeZone() {
|
|
49
|
+
return Temporal.Now.timeZoneId();
|
|
50
|
+
}
|
|
51
|
+
// Host-zone offset at this wall-clock, e.g. "+01:00". Unlike building a JS Date
|
|
52
|
+
// from the fields, this is correct for years 0-99 (which JS maps to 1900-1999).
|
|
53
|
+
function hostOffsetString(d) {
|
|
54
|
+
return d.toZonedDateTime(hostTimeZone()).offset;
|
|
55
|
+
}
|
|
56
|
+
// Wall-clock fields for the formatter. The sub-second component is carried in
|
|
57
|
+
// .NET ticks so that "O" and the f/F specifiers can print all 7 digits.
|
|
58
|
+
function toDateInfo(d) {
|
|
59
|
+
return {
|
|
60
|
+
year: d.year,
|
|
61
|
+
month: d.month,
|
|
62
|
+
day: d.day,
|
|
63
|
+
hour: d.hour,
|
|
64
|
+
minute: d.minute,
|
|
65
|
+
second: d.second,
|
|
66
|
+
tick: d.millisecond * 10000 + d.microsecond * 10 + Math.trunc(d.nanosecond / 100),
|
|
67
|
+
dayOfWeek: dayOfWeek(d),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
// Display-only bridge for the host-locale strings below. Sub-second precision is
|
|
71
|
+
// irrelevant to all four, but years 0-99 still need correcting.
|
|
72
|
+
function toJsDate(d) {
|
|
73
|
+
const jsDate = new Date(d.year, d.month - 1, d.day, d.hour, d.minute, d.second, d.millisecond);
|
|
74
|
+
jsDate.setFullYear(d.year, d.month - 1, d.day);
|
|
75
|
+
return jsDate;
|
|
76
|
+
}
|
|
77
|
+
export function create(year, month, day, h = 0, m = 0, s = 0, ms = 0, kind) {
|
|
78
|
+
return dateTime(new Temporal.PlainDateTime(year, month, day, h, m, s, ms), kind);
|
|
79
|
+
}
|
|
80
|
+
export function fromTicks(ticks, kind) {
|
|
81
|
+
return dateTime(minDateTime.add(TimeSpan_fromTicks(ticks)), kind);
|
|
82
|
+
}
|
|
83
|
+
export function getTicks(date) {
|
|
84
|
+
return toInt64(TimeSpan_totalNanoseconds(minDateTime.until(date, { largestUnit: "day" })) / 100n);
|
|
85
|
+
}
|
|
86
|
+
export function minValue() {
|
|
87
|
+
return dateTime(copy(minDateTime), DateTimeKind.Unspecified);
|
|
88
|
+
}
|
|
89
|
+
export function maxValue() {
|
|
90
|
+
return dateTime(new Temporal.PlainDateTime(9999, 12, 31, 23, 59, 59, 999, 999, 900), DateTimeKind.Unspecified);
|
|
91
|
+
}
|
|
92
|
+
// .NET DateTime has 100ns (tick) precision; Temporal.Now is nanosecond-precise.
|
|
93
|
+
function truncateToTicks(d) {
|
|
94
|
+
return d.round({ smallestUnit: "nanosecond", roundingIncrement: 100, roundingMode: "trunc" });
|
|
95
|
+
}
|
|
96
|
+
export function now() {
|
|
97
|
+
return dateTime(truncateToTicks(Temporal.Now.plainDateTimeISO()), DateTimeKind.Local);
|
|
98
|
+
}
|
|
99
|
+
export function utcNow() {
|
|
100
|
+
return dateTime(truncateToTicks(Temporal.Now.plainDateTimeISO("UTC")), DateTimeKind.Utc);
|
|
101
|
+
}
|
|
102
|
+
export function today() {
|
|
103
|
+
return date(now());
|
|
104
|
+
}
|
|
105
|
+
export function specifyKind(d, kind) {
|
|
106
|
+
// Kind is metadata: every tick of the wall-clock is preserved.
|
|
107
|
+
return dateTime(copy(d), kind);
|
|
108
|
+
}
|
|
109
|
+
export function dayOfWeek(d) {
|
|
110
|
+
// Temporal: Monday = 1 ... Sunday = 7, .NET: Sunday = 0 ... Saturday = 6
|
|
111
|
+
return d.dayOfWeek % 7;
|
|
112
|
+
}
|
|
113
|
+
export function date(d) {
|
|
114
|
+
return dateTime(d.with({ hour: 0, minute: 0, second: 0, millisecond: 0, microsecond: 0, nanosecond: 0 }), getKind(d));
|
|
115
|
+
}
|
|
116
|
+
export function timeOfDay(d) {
|
|
117
|
+
// Elapsed since midnight. DateTime values are tick-aligned, so this is too.
|
|
118
|
+
return d.with({ hour: 0, minute: 0, second: 0, millisecond: 0, microsecond: 0, nanosecond: 0 }).until(d);
|
|
119
|
+
}
|
|
120
|
+
export function add(d, ts) {
|
|
121
|
+
return dateTime(d.add(ts), getKind(d));
|
|
122
|
+
}
|
|
123
|
+
export function addYears(d, v) {
|
|
124
|
+
return dateTime(d.add({ years: v }), getKind(d));
|
|
125
|
+
}
|
|
126
|
+
export function addMonths(d, v) {
|
|
127
|
+
return dateTime(d.add({ months: v }), getKind(d));
|
|
128
|
+
}
|
|
129
|
+
export function addDays(d, v) {
|
|
130
|
+
return dateTime(d.add(TimeSpan_fromUnits(v, "days")), getKind(d));
|
|
131
|
+
}
|
|
132
|
+
export function addHours(d, v) {
|
|
133
|
+
return dateTime(d.add(TimeSpan_fromUnits(v, "hours")), getKind(d));
|
|
134
|
+
}
|
|
135
|
+
export function addMinutes(d, v) {
|
|
136
|
+
return dateTime(d.add(TimeSpan_fromUnits(v, "minutes")), getKind(d));
|
|
137
|
+
}
|
|
138
|
+
export function addSeconds(d, v) {
|
|
139
|
+
return dateTime(d.add(TimeSpan_fromUnits(v, "seconds")), getKind(d));
|
|
140
|
+
}
|
|
141
|
+
export function addMilliseconds(d, v) {
|
|
142
|
+
return dateTime(d.add(TimeSpan_fromUnits(v, "milliseconds")), getKind(d));
|
|
143
|
+
}
|
|
144
|
+
export function addTicks(d, v) {
|
|
145
|
+
return dateTime(d.add(TimeSpan_fromTicks(v)), getKind(d));
|
|
146
|
+
}
|
|
147
|
+
export function subtractDate(d, that) {
|
|
148
|
+
return that.until(d, { largestUnit: "day" });
|
|
149
|
+
}
|
|
150
|
+
export function subtractTimeSpan(d, ts) {
|
|
151
|
+
return dateTime(d.subtract(ts), getKind(d));
|
|
152
|
+
}
|
|
153
|
+
export function equals(d1, d2) {
|
|
154
|
+
return Temporal.PlainDateTime.compare(d1, d2) === 0;
|
|
155
|
+
}
|
|
156
|
+
export function compare(d1, d2) {
|
|
157
|
+
return Temporal.PlainDateTime.compare(d1, d2);
|
|
158
|
+
}
|
|
159
|
+
export const compareTo = compare;
|
|
160
|
+
export function op_Addition(x, y) {
|
|
161
|
+
return add(x, y);
|
|
162
|
+
}
|
|
163
|
+
export function hash(d) {
|
|
164
|
+
return Number(getTicks(d) % 2147483647n);
|
|
165
|
+
}
|
|
166
|
+
export function isLeapYear(year) {
|
|
167
|
+
return year % 4 === 0 && year % 100 !== 0 || year % 400 === 0;
|
|
168
|
+
}
|
|
169
|
+
export function daysInMonth(year, month) {
|
|
170
|
+
return month === 2
|
|
171
|
+
? (isLeapYear(year) ? 29 : 28)
|
|
172
|
+
: (month >= 8 ? (month % 2 === 0 ? 31 : 30) : (month % 2 === 0 ? 30 : 31));
|
|
173
|
+
}
|
|
174
|
+
// --- Host time zone ---
|
|
175
|
+
export function toUniversalTime(d) {
|
|
176
|
+
// .NET reads an Unspecified value as local time here.
|
|
177
|
+
return getKind(d) === DateTimeKind.Utc
|
|
178
|
+
? d
|
|
179
|
+
: dateTime(d.toZonedDateTime(hostTimeZone()).withTimeZone("UTC").toPlainDateTime(), DateTimeKind.Utc);
|
|
180
|
+
}
|
|
181
|
+
export function toLocalTime(d) {
|
|
182
|
+
// .NET reads an Unspecified value as UTC here — the mirror image of the above.
|
|
183
|
+
return getKind(d) === DateTimeKind.Local
|
|
184
|
+
? d
|
|
185
|
+
: dateTime(d.toZonedDateTime("UTC").withTimeZone(hostTimeZone()).toPlainDateTime(), DateTimeKind.Local);
|
|
186
|
+
}
|
|
187
|
+
export function isDaylightSavingTime(d) {
|
|
188
|
+
const tz = hostTimeZone();
|
|
189
|
+
const offsetAt = (month) => new Temporal.PlainDateTime(d.year, month, 1).toZonedDateTime(tz).offsetNanoseconds;
|
|
190
|
+
// The larger of the two mid-season offsets is the daylight-saving one.
|
|
191
|
+
return Math.max(offsetAt(1), offsetAt(7)) === d.toZonedDateTime(tz).offsetNanoseconds;
|
|
192
|
+
}
|
|
193
|
+
// --- Host-locale display strings (no .NET-defined layout) ---
|
|
194
|
+
export function toLongDateString(d) {
|
|
195
|
+
return toJsDate(d).toDateString();
|
|
196
|
+
}
|
|
197
|
+
export function toShortDateString(d) {
|
|
198
|
+
return toJsDate(d).toLocaleDateString();
|
|
199
|
+
}
|
|
200
|
+
export function toLongTimeString(d) {
|
|
201
|
+
return toJsDate(d).toLocaleTimeString();
|
|
202
|
+
}
|
|
203
|
+
export function toShortTimeString(d) {
|
|
204
|
+
return toJsDate(d).toLocaleTimeString().replace(/:\d\d(?!:)/, "");
|
|
205
|
+
}
|
|
206
|
+
// --- Formatting and parsing ---
|
|
207
|
+
export function toString(d, format, _provider) {
|
|
208
|
+
const kind = getKind(d);
|
|
209
|
+
return Format.dateToString({
|
|
210
|
+
info: toDateInfo(d),
|
|
211
|
+
utcInfo: () => toDateInfo(toUniversalTime(d)),
|
|
212
|
+
// An Unspecified DateTime prints no zone at all, which is what makes "O"
|
|
213
|
+
// round-trip it back to Unspecified.
|
|
214
|
+
roundTrip: () => d.toString({ fractionalSecondDigits: 7 })
|
|
215
|
+
+ (kind === DateTimeKind.Utc ? "Z" : kind === DateTimeKind.Local ? hostOffsetString(d) : ""),
|
|
216
|
+
sortable: () => d.toString({ smallestUnit: "second" }),
|
|
217
|
+
defaultSuffix: "",
|
|
218
|
+
kind,
|
|
219
|
+
hostOffsetString: () => hostOffsetString(d),
|
|
220
|
+
}, format);
|
|
221
|
+
}
|
|
222
|
+
export function parse(str, detectUTC = false) {
|
|
223
|
+
const [parsed, offset] = Format.parseRaw(str);
|
|
224
|
+
// .NET always parses DateTime as Local if there's offset info (even "Z")
|
|
225
|
+
// Newtonsoft.Json uses UTC if the offset is "Z"
|
|
226
|
+
const kind = offset != null
|
|
227
|
+
? (detectUTC && offset === "Z" ? DateTimeKind.Utc : DateTimeKind.Local)
|
|
228
|
+
: DateTimeKind.Unspecified;
|
|
229
|
+
// parseRaw resolves the instant through JS Date, which truncates at the
|
|
230
|
+
// millisecond, so the digits below that are recovered from the input separately.
|
|
231
|
+
const epochNs = BigInt(parsed.getTime()) * 1000000n + BigInt(Format.subMillisecondTicks(str)) * 100n;
|
|
232
|
+
const zone = kind === DateTimeKind.Utc ? "UTC" : hostTimeZone();
|
|
233
|
+
return dateTime(new Temporal.Instant(epochNs).toZonedDateTimeISO(zone).toPlainDateTime(), kind);
|
|
234
|
+
}
|
|
235
|
+
export function tryParse(v, defValue) {
|
|
236
|
+
try {
|
|
237
|
+
defValue.contents = parse(v);
|
|
238
|
+
return true;
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
243
|
+
}
|
package/Guid.js
CHANGED
|
@@ -66,7 +66,9 @@ export function newGuid() {
|
|
|
66
66
|
}
|
|
67
67
|
// RFC 9562 UUID v7
|
|
68
68
|
export function createVersion7(timestamp) {
|
|
69
|
-
|
|
69
|
+
// A number/bigint timestamp is Unix milliseconds (used by the Temporal representation,
|
|
70
|
+
// whose DateTimeOffset is not a JS Date); a Date uses getTime().
|
|
71
|
+
const ms = timestamp == null ? Date.now() : typeof timestamp === "object" ? timestamp.getTime() : Number(timestamp);
|
|
70
72
|
// 48-bit timestamp as hex
|
|
71
73
|
const msHex = Math.floor(ms).toString(16).padStart(12, "0");
|
|
72
74
|
// random bits
|
package/Numeric.js
CHANGED
|
@@ -28,6 +28,18 @@ export function multiply(x, y) {
|
|
|
28
28
|
return x[symbol]().multiply(y);
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
|
+
export function divide(x, y) {
|
|
32
|
+
if (typeof x === "number") {
|
|
33
|
+
return x / y;
|
|
34
|
+
}
|
|
35
|
+
else if (typeof x === "bigint") {
|
|
36
|
+
// Keep the fractional part so the caller can round it like .NET does
|
|
37
|
+
return Number(x) / y;
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
return x[symbol]().multiply(1 / y);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
31
43
|
export function toFixed(x, dp) {
|
|
32
44
|
if (typeof x === "number") {
|
|
33
45
|
return x.toFixed(dp);
|
|
@@ -50,12 +62,37 @@ export function toPrecision(x, sd) {
|
|
|
50
62
|
return x[symbol]().toPrecision(sd);
|
|
51
63
|
}
|
|
52
64
|
}
|
|
65
|
+
// A bigint can hold more significant digits than a double, so the mantissa is rounded on the
|
|
66
|
+
// decimal digits themselves instead of going through Number
|
|
67
|
+
function bigintToExponential(x, dp) {
|
|
68
|
+
const sign = x < 0n ? "-" : "";
|
|
69
|
+
let digits = (x < 0n ? -x : x).toString();
|
|
70
|
+
if (x === 0n) {
|
|
71
|
+
return sign + (dp ? "0." + "0".repeat(dp) : "0") + "e+0";
|
|
72
|
+
}
|
|
73
|
+
dp = dp ?? digits.length - 1;
|
|
74
|
+
let exponent = digits.length - 1;
|
|
75
|
+
if (digits.length > dp + 1) {
|
|
76
|
+
// .NET rounds the discarded digits away from zero
|
|
77
|
+
const roundUp = digits.charCodeAt(dp + 1) >= 53;
|
|
78
|
+
digits = (BigInt(digits.slice(0, dp + 1)) + (roundUp ? 1n : 0n)).toString();
|
|
79
|
+
if (digits.length > dp + 1) {
|
|
80
|
+
digits = digits.slice(0, dp + 1);
|
|
81
|
+
exponent += 1;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
digits = digits.padEnd(dp + 1, "0");
|
|
86
|
+
}
|
|
87
|
+
const mantissa = dp > 0 ? digits[0] + "." + digits.slice(1) : digits[0];
|
|
88
|
+
return sign + mantissa + "e+" + exponent;
|
|
89
|
+
}
|
|
53
90
|
export function toExponential(x, dp) {
|
|
54
91
|
if (typeof x === "number") {
|
|
55
92
|
return x.toExponential(dp);
|
|
56
93
|
}
|
|
57
94
|
else if (typeof x === "bigint") {
|
|
58
|
-
return x;
|
|
95
|
+
return bigintToExponential(x, dp);
|
|
59
96
|
}
|
|
60
97
|
else {
|
|
61
98
|
return x[symbol]().toExponential(dp);
|
package/String.js
CHANGED
|
@@ -1,8 +1,19 @@
|
|
|
1
1
|
import { toString as dateToString } from "./Date.js";
|
|
2
|
-
import { compare as numericCompare, isNumeric, isIntegral, multiply, toExponential, toFixed, toHex, toPrecision } from "./Numeric.js";
|
|
2
|
+
import { compare as numericCompare, divide, isNumeric, isIntegral, multiply, toExponential, toFixed, toHex, toPrecision } from "./Numeric.js";
|
|
3
3
|
import { escape } from "./RegExp.js";
|
|
4
4
|
import { toString } from "./Types.js";
|
|
5
5
|
import { Exception } from "./Util.js";
|
|
6
|
+
// Temporal date/time values (only present under --test:js-temporal) are not JS Dates. To avoid
|
|
7
|
+
// importing the side-effecting Temporal runtime modules into this core file — which would pull
|
|
8
|
+
// them into every bundle — they advertise a .NET-style formatter through this well-known symbol,
|
|
9
|
+
// attached to their prototype when (and only when) those modules are loaded.
|
|
10
|
+
const dateTimeFormattableSymbol = Symbol.for("Fable.DateTimeFormattable");
|
|
11
|
+
function isNetDateFormattable(rep) {
|
|
12
|
+
return rep != null && typeof rep[dateTimeFormattableSymbol] === "function";
|
|
13
|
+
}
|
|
14
|
+
function formatDateLike(rep, format) {
|
|
15
|
+
return rep instanceof Date ? dateToString(rep, format) : rep[dateTimeFormattableSymbol](format);
|
|
16
|
+
}
|
|
6
17
|
const fsFormatRegExp = /(^|[^%])%([0+\- ]*)(\*|\d+)?(?:\.(\d+))?(\w)/g;
|
|
7
18
|
const interpolateRegExp = /(?:(^|[^%])%([0+\- ]*)(\d+)?(?:\.(\d+))?(\w))?%P\(\)/g;
|
|
8
19
|
const formatRegExp = /\{(\d+)(,-?\d+)?(?:\:([a-zA-Z])(\d{0,2})|\:(.+?))?\}/g;
|
|
@@ -236,8 +247,8 @@ function formatReplacement(rep, flags, padLength, precision, format) {
|
|
|
236
247
|
break;
|
|
237
248
|
}
|
|
238
249
|
}
|
|
239
|
-
else if (rep instanceof Date) {
|
|
240
|
-
rep =
|
|
250
|
+
else if (rep instanceof Date || isNetDateFormattable(rep)) {
|
|
251
|
+
rep = formatDateLike(rep, undefined);
|
|
241
252
|
}
|
|
242
253
|
else if (format === "A" && typeof rep === "string") {
|
|
243
254
|
rep = "\"" + rep + "\"";
|
|
@@ -396,9 +407,14 @@ export function format(str, ...args) {
|
|
|
396
407
|
}
|
|
397
408
|
break;
|
|
398
409
|
case "e":
|
|
399
|
-
case "E":
|
|
400
|
-
|
|
410
|
+
case "E": {
|
|
411
|
+
precision = precision != null ? precision : 6;
|
|
412
|
+
rep = String(toExponential(rep, precision));
|
|
413
|
+
// .NET always signs the exponent and pads it to at least three digits
|
|
414
|
+
const eIdx = rep.indexOf("e");
|
|
415
|
+
rep = rep.slice(0, eIdx) + format + rep[eIdx + 1] + padLeft(rep.slice(eIdx + 2), 3, "0");
|
|
401
416
|
break;
|
|
417
|
+
}
|
|
402
418
|
case "f":
|
|
403
419
|
case "F":
|
|
404
420
|
precision = precision != null ? precision : 2;
|
|
@@ -422,7 +438,7 @@ export function format(str, ...args) {
|
|
|
422
438
|
const eChar = format === "G" ? "E" : "e";
|
|
423
439
|
rep = mantissa + eChar + expSign + paddedExpDigits;
|
|
424
440
|
}
|
|
425
|
-
else {
|
|
441
|
+
else if (rep.indexOf(".") >= 0) {
|
|
426
442
|
rep = trimEnd(trimEnd(rep, "0"), ".");
|
|
427
443
|
}
|
|
428
444
|
break;
|
|
@@ -473,7 +489,21 @@ export function format(str, ...args) {
|
|
|
473
489
|
}
|
|
474
490
|
if (pattern) {
|
|
475
491
|
let sign = "";
|
|
476
|
-
|
|
492
|
+
const patternStr = pattern;
|
|
493
|
+
// Each `%` scales the value by 100 and is kept as a literal in the output
|
|
494
|
+
const percents = (patternStr.match(/%/g) ?? []).length;
|
|
495
|
+
if (percents > 0) {
|
|
496
|
+
rep = multiply(rep, Math.pow(100, percents));
|
|
497
|
+
}
|
|
498
|
+
// .NET ignores commas placed after the decimal placeholders, hence the trailing group
|
|
499
|
+
rep = patternStr.replace(/([0#,]+)(\.[0#]+)?(,*)/, (_, intPart, decimalPart) => {
|
|
500
|
+
// Commas between the last integer placeholder and the decimal point scale the
|
|
501
|
+
// value down by 1000 each; commas anywhere else only turn on digit grouping
|
|
502
|
+
const scaleCommas = /,*$/.exec(intPart)[0].length;
|
|
503
|
+
if (scaleCommas > 0) {
|
|
504
|
+
intPart = intPart.substring(0, intPart.length - scaleCommas);
|
|
505
|
+
rep = divide(rep, Math.pow(1000, scaleCommas));
|
|
506
|
+
}
|
|
477
507
|
if (isLessThan(rep, 0)) {
|
|
478
508
|
rep = multiply(rep, -1);
|
|
479
509
|
sign = "-";
|
|
@@ -507,8 +537,8 @@ export function format(str, ...args) {
|
|
|
507
537
|
}
|
|
508
538
|
}
|
|
509
539
|
}
|
|
510
|
-
else if (rep instanceof Date) {
|
|
511
|
-
rep =
|
|
540
|
+
else if (rep instanceof Date || isNetDateFormattable(rep)) {
|
|
541
|
+
rep = formatDateLike(rep, pattern || format);
|
|
512
542
|
}
|
|
513
543
|
else {
|
|
514
544
|
rep = toString(rep);
|