@fable-org/fable-library-js 2.6.0 → 2.7.1

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.
@@ -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
- const ms = timestamp != null ? timestamp.getTime() : Date.now();
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 = dateToString(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
- rep = precision != null ? toExponential(rep, precision) : toExponential(rep);
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
- rep = pattern.replace(/([0#,]+)(\.[0#]+)?/, (_, intPart, decimalPart) => {
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 = dateToString(rep, pattern || format);
540
+ else if (rep instanceof Date || isNetDateFormattable(rep)) {
541
+ rep = formatDateLike(rep, pattern || format);
512
542
  }
513
543
  else {
514
544
  rep = toString(rep);