Skip to content

Commit 4be69d3

Browse files
committed
feat: Temporal TimeSpan, DateTime, DateTimeOffset
1 parent 238f613 commit 4be69d3

3 files changed

Lines changed: 27 additions & 39 deletions

File tree

src/fable-library-ts/DateTimeFormat.ts

Lines changed: 16 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,15 @@
44
*
55
* The formatters operate on a plain `DateInfo` (wall-clock fields), which the
66
* Temporal types expose directly (`d.year`, `d.hour`, `d.dayOfWeek % 7`, ...),
7-
* so no JS-Date intermediate is needed. `parseRaw` still leans on the JS `Date`
8-
* built-in for .NET-compatible lenient parsing (Temporal has only strict ISO
9-
* parsers); it returns wall-clock fields the callers turn into Temporal values.
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.
1016
*
1117
* This is a port of the equivalent logic in Date.ts / DateOffset.ts, kept
1218
* separate so the Temporal modules do not depend on the JS-Date representation.
@@ -378,16 +384,6 @@ export function dateToString_R(info: DateInfo): string {
378384
+ padWithZeros(info.second, 2) + " GMT";
379385
}
380386

381-
// Sortable ISO 8601, no timezone: "2009-06-15T13:45:30"
382-
export function dateToString_s(info: DateInfo): string {
383-
return padWithZeros(info.year, 4) + "-"
384-
+ padWithZeros(info.month, 2) + "-"
385-
+ padWithZeros(info.day, 2) + "T"
386-
+ padWithZeros(info.hour, 2) + ":"
387-
+ padWithZeros(info.minute, 2) + ":"
388-
+ padWithZeros(info.second, 2);
389-
}
390-
391387
// Universal sortable: "2009-06-15 13:45:30Z". `info` must already be UTC.
392388
export function dateToString_u(info: DateInfo): string {
393389
return padWithZeros(info.year, 4) + "-"
@@ -408,29 +404,18 @@ export function dateToString_Y(info: DateInfo): string {
408404
return info.year + " " + longMonths[info.month - 1];
409405
}
410406

411-
// Round-trip "O"/"o": "2009-06-15T13:45:30.1234567" + an offset suffix (a bare
412-
// "Z" for UTC, an offset like "+01:00", or "" when unspecified). .NET prints all
413-
// 7 tick digits here — that is what makes the format round-trip.
414-
export function dateToString_O(info: DateInfo, suffix: string): string {
415-
return padWithZeros(info.year, 4) + "-"
416-
+ padWithZeros(info.month, 2) + "-"
417-
+ padWithZeros(info.day, 2) + "T"
418-
+ padWithZeros(info.hour, 2) + ":"
419-
+ padWithZeros(info.minute, 2) + ":"
420-
+ padWithZeros(info.second, 2) + "."
421-
+ fractionDigits(info) + suffix;
422-
}
423-
424407
// Everything a standard format specifier needs beyond the wall-clock fields.
425408
// DateTime and DateTimeOffset differ only in these, so they share the table below.
426409
// Everything past `info` is a thunk: most specifiers need none of them, and each
427410
// costs a time-zone conversion.
428411
export interface FormatContext {
429412
info: DateInfo;
430413
utcInfo: () => DateInfo;
431-
// Trails the round-trip "O": "Z" for UTC, an offset like "+01:00", or "" when
432-
// the value carries no zone information at all.
433-
roundTripSuffix: () => string;
414+
// "O"/"o" and "s" are ISO 8601, which Temporal renders itself — and renders
415+
// tick-exactly — so the owning module supplies them rather than this file
416+
// reassembling the fields by hand.
417+
roundTrip: () => string;
418+
sortable: () => string;
434419
// Trails the no-format rendering. A DateTimeOffset shows its offset there; a
435420
// DateTime has none to show.
436421
defaultSuffix: string;
@@ -457,9 +442,9 @@ export function dateToString(ctx: FormatContext, format?: string): string {
457442
case "G": return dateToString_d(info) + " " + dateToString_T(info);
458443
case "g": return dateToString_d(info) + " " + dateToString_t(info);
459444
case "M": case "m": return dateToString_M(info);
460-
case "O": case "o": return dateToString_O(info, ctx.roundTripSuffix());
445+
case "O": case "o": return ctx.roundTrip();
461446
case "R": case "r": return dateToString_R(ctx.utcInfo());
462-
case "s": return dateToString_s(info);
447+
case "s": return ctx.sortable();
463448
case "T": return dateToString_T(info);
464449
case "t": return dateToString_t(info);
465450
case "u": return dateToString_u(ctx.utcInfo());

src/fable-library-ts/DateTimeOffsetTemporal.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -319,7 +319,10 @@ export function toString(d: DateTimeOffset, format?: string, _provider?: any): s
319319
{
320320
info: toDateInfo(d),
321321
utcInfo: () => toDateInfo(d.withTimeZone("UTC")),
322-
roundTripSuffix: offsetString,
322+
// A fixed-offset zone renders as .NET's "O" exactly: ISO wall-clock plus
323+
// the numeric offset, with no [zone] annotation.
324+
roundTrip: () => d.toString({ fractionalSecondDigits: 7, timeZoneName: "never" }),
325+
sortable: () => d.toPlainDateTime().toString({ smallestUnit: "second" }),
323326
defaultSuffix: " " + offsetString(),
324327
// K/z print the value's own offset, never the host zone's, so Local is
325328
// passed as the kind and that offset stands in for the host one.
@@ -331,8 +334,8 @@ export function toString(d: DateTimeOffset, format?: string, _provider?: any): s
331334

332335
export function parse(str: string): DateTimeOffset {
333336
const [parsed, offsetMatch] = Format.parseRaw(str);
334-
// parseRaw resolves the instant through JS Date, so anything below the
335-
// millisecond has to be recovered from the input separately.
337+
// parseRaw resolves the instant through JS Date, which truncates at the
338+
// millisecond, so the digits below that are recovered from the input separately.
336339
const epochNs = BigInt(parsed.getTime()) * 1_000_000n + BigInt(Format.subMillisecondTicks(str)) * 100n;
337340

338341
const offsetNs = offsetMatch == null

src/fable-library-ts/DateTimeTemporal.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -277,9 +277,9 @@ export function toString(d: DateTime, format?: string, _provider?: any): string
277277
utcInfo: () => toDateInfo(toUniversalTime(d)),
278278
// An Unspecified DateTime prints no zone at all, which is what makes "O"
279279
// round-trip it back to Unspecified.
280-
roundTripSuffix: () => kind === DateTimeKind.Utc
281-
? "Z"
282-
: kind === DateTimeKind.Local ? hostOffsetString(d) : "",
280+
roundTrip: () => d.toString({ fractionalSecondDigits: 7 })
281+
+ (kind === DateTimeKind.Utc ? "Z" : kind === DateTimeKind.Local ? hostOffsetString(d) : ""),
282+
sortable: () => d.toString({ smallestUnit: "second" }),
283283
defaultSuffix: "",
284284
kind,
285285
hostOffsetString: () => hostOffsetString(d),
@@ -295,8 +295,8 @@ export function parse(str: string, detectUTC = false): DateTime {
295295
? (detectUTC && offset === "Z" ? DateTimeKind.Utc : DateTimeKind.Local)
296296
: DateTimeKind.Unspecified;
297297

298-
// parseRaw resolves the instant through JS Date, so anything below the
299-
// millisecond has to be recovered from the input separately.
298+
// parseRaw resolves the instant through JS Date, which truncates at the
299+
// millisecond, so the digits below that are recovered from the input separately.
300300
const epochNs = BigInt(parsed.getTime()) * 1_000_000n + BigInt(Format.subMillisecondTicks(str)) * 100n;
301301
const zone = kind === DateTimeKind.Utc ? "UTC" : hostTimeZone();
302302

0 commit comments

Comments
 (0)