-
Notifications
You must be signed in to change notification settings - Fork 1
Temporal API
The engine implements the TC39 Temporal proposal - a modern date/time API with nanosecond precision and proper timezone support.
flowchart TB
subgraph Temporal["Temporal Namespace"]
Now((Temporal.Now))
Instant((Instant))
Duration((Duration))
PlainDate((PlainDate))
PlainTime((PlainTime))
PlainDateTime((PlainDateTime))
ZonedDateTime((ZonedDateTime))
PlainYearMonth((PlainYearMonth))
PlainMonthDay((PlainMonthDay))
end
Now --> Instant
Now --> PlainDate
Now --> PlainTime
Now --> PlainDateTime
Now --> ZonedDateTime
PlainDate --> PlainDateTime
PlainTime --> PlainDateTime
PlainDateTime --> ZonedDateTime
Instant --> ZonedDateTime
| File | Purpose |
|---|---|
StdLib/Temporal/TemporalHelper.cs |
Temporal namespace and constructors |
JsTypes/JsTemporalInstant.cs |
Exact moment in time (nanoseconds) |
JsTypes/JsTemporalDuration.cs |
Time span representation |
JsTypes/JsTemporalPlainDate.cs |
Calendar date without time |
JsTypes/JsTemporalPlainTime.cs |
Wall-clock time without date |
JsTypes/JsTemporalPlainDateTime.cs |
Combined date and time |
JsTypes/JsTemporalZonedDateTime.cs |
Date/time with timezone |
JsTypes/JsTemporalPlainYearMonth.cs |
Year and month only |
JsTypes/JsTemporalPlainMonthDay.cs |
Month and day only |
JavaScript Date only has millisecond precision. Temporal requires nanosecond precision (10^-9 seconds).
public sealed class JsTemporalInstant(BigInteger epochNanoseconds)
: IEquatable<JsTemporalInstant>, IComparable<JsTemporalInstant>
{
private const long NanosecondsPerMillisecond = 1_000_000L;
private const long NanosecondsPerSecond = 1_000_000_000L;
public BigInteger EpochNanoseconds { get; } = epochNanoseconds;
public long EpochMilliseconds => (long)(EpochNanoseconds / NanosecondsPerMillisecond);
public long EpochSeconds => (long)(EpochNanoseconds / NanosecondsPerSecond);
}flowchart LR
subgraph Temporal["Temporal (nanoseconds)"]
BigInt["BigInteger\n(nanoseconds)"]
end
subgraph DotNet[".NET"]
DTO["DateTimeOffset\n(100ns ticks)"]
end
BigInt -->|"/ 100"| DTO
DTO -->|"* 100"| BigInt
private static BigInteger DateTimeOffsetToNanoseconds(DateTimeOffset dto)
{
var ticks = (dto - UnixEpoch).Ticks;
// 1 tick = 100 nanoseconds
return new BigInteger(ticks) * 100;
}
public DateTimeOffset ToDateTimeOffset()
{
// Convert nanoseconds to ticks (1 tick = 100ns)
// Note: Loses precision beyond 100ns
var ticks = (long)(EpochNanoseconds / 100);
return UnixEpoch.AddTicks(ticks);
}Provides current time in various formats:
private static JsObject CreateTemporalNow(RealmState realm, TemporalPrototypes prototypes)
{
var now = new JsObject(realm.ObjectPrototype);
// Temporal.Now.instant()
now.DefineProperty("instant", CreateFunction(realm, "instant", 0, (_, _) =>
{
var instant = JsTemporalInstant.Now();
return WrapInstant(instant, realm, prototypes.InstantPrototype);
}));
// Temporal.Now.timeZoneId()
now.DefineProperty("timeZoneId", CreateFunction(realm, "timeZoneId", 0, (_, _) =>
{
var tzId = TimeZoneInfo.Local.Id;
// Convert Windows timezone ID to IANA if needed
if (OperatingSystem.IsWindows() &&
TimeZoneInfo.TryConvertWindowsIdToIanaId(tzId, out var ianaId))
{
tzId = ianaId;
}
return new JsValue(tzId);
}));
// Temporal.Now.plainDateISO()
// Temporal.Now.plainTimeISO()
// Temporal.Now.plainDateTimeISO()
// Temporal.Now.zonedDateTimeISO()
// ...
}Represents an exact moment in time (UTC).
// From BigInteger nanoseconds
public JsTemporalInstant(BigInteger epochNanoseconds) { ... }
// From milliseconds (Date.now() compatible)
public static JsTemporalInstant FromEpochMilliseconds(long epochMilliseconds)
{
return new JsTemporalInstant(epochMilliseconds);
}
// Current time
public static JsTemporalInstant Now()
{
return new JsTemporalInstant(DateTimeOffset.UtcNow);
}public override string ToString()
{
var dto = ToDateTimeOffset();
var nanosPart = (int)(EpochNanoseconds % NanosecondsPerSecond);
if (nanosPart < 0)
nanosPart += (int)NanosecondsPerSecond;
if (nanosPart == 0)
return dto.ToString("yyyy-MM-dd'T'HH:mm:ss'Z'", CultureInfo.InvariantCulture);
var secondsPart = dto.ToString("yyyy-MM-dd'T'HH:mm:ss", CultureInfo.InvariantCulture);
var nanosStr = nanosPart.ToString("D9", CultureInfo.InvariantCulture).TrimEnd('0');
return $"{secondsPart}.{nanosStr}Z";
}| Method | Description |
|---|---|
add(duration) |
Add time-only duration components |
subtract(duration) |
Subtract time-only duration components |
until(other) |
Duration between instants |
since(other) |
Duration since other instant |
round(options) |
Round to unit boundary |
equals(other) |
Check equality |
toZonedDateTimeISO(timeZone) |
Convert to zoned |
Represents a span of time with multiple components.
flowchart LR
subgraph Date["Date Components"]
Y["years"]
M["months"]
W["weeks"]
D["days"]
end
subgraph Time["Time Components"]
H["hours"]
Min["minutes"]
S["seconds"]
MS["milliseconds"]
US["microseconds"]
NS["nanoseconds"]
end
public sealed class JsTemporalDuration
{
public double Years { get; }
public double Months { get; }
public double Weeks { get; }
public double Days { get; }
public double Hours { get; }
public double Minutes { get; }
public double Seconds { get; }
public double Milliseconds { get; }
public double Microseconds { get; }
public double Nanoseconds { get; }
public int Sign => ComputeSign();
public bool Blank => Sign == 0;
}public double Total(string unit)
{
// Convert all components to the specified unit
return unit switch
{
"years" => TotalYears(),
"months" => TotalMonths(),
"weeks" => TotalWeeks(),
"days" => TotalDays(),
"hours" => TotalHours(),
"minutes" => TotalMinutes(),
"seconds" => TotalSeconds(),
"milliseconds" => TotalMilliseconds(),
"microseconds" => TotalMicroseconds(),
"nanoseconds" => TotalNanoseconds(),
_ => throw new ArgumentException($"Unknown unit: {unit}")
};
}Calendar date without time or timezone.
| Property | Type | Description |
|---|---|---|
year |
number | ISO year |
month |
number | Month (1-12) |
day |
number | Day of month |
monthCode |
string | "M01"-"M12" |
dayOfWeek |
number | 1=Monday, 7=Sunday |
dayOfYear |
number | 1-366 |
weekOfYear |
number | ISO week number |
daysInMonth |
number | 28-31 |
daysInYear |
number | 365 or 366 |
inLeapYear |
boolean | Leap year check |
calendarId |
string | "iso8601" |
public JsTemporalPlainDate Add(JsTemporalDuration duration)
{
var date = new DateTime(Year, Month, Day);
// Add in order: years, months, weeks, days
date = date.AddYears((int)duration.Years);
date = date.AddMonths((int)duration.Months);
date = date.AddDays((int)duration.Weeks * 7);
date = date.AddDays((int)duration.Days);
return new JsTemporalPlainDate(date.Year, date.Month, date.Day, Calendar);
}Wall-clock time without date or timezone.
public sealed class JsTemporalPlainTime
{
public int Hour { get; } // 0-23
public int Minute { get; } // 0-59
public int Second { get; } // 0-59
public int Millisecond { get; } // 0-999
public int Microsecond { get; } // 0-999
public int Nanosecond { get; } // 0-999
}public JsTemporalPlainTime Round(string smallestUnit)
{
var totalNanos = TotalNanoseconds();
var divisor = smallestUnit switch
{
"hour" => 3600_000_000_000L,
"minute" => 60_000_000_000L,
"second" => 1_000_000_000L,
"millisecond" => 1_000_000L,
"microsecond" => 1_000L,
_ => 1L
};
var rounded = (totalNanos / divisor) * divisor;
return FromNanoseconds(rounded);
}Complete date/time with timezone - the most complete representation.
flowchart TB
ZDT((ZonedDateTime))
subgraph Components
Date["Date (Y/M/D)"]
Time["Time (H:M:S.ns)"]
TZ["Timezone"]
Cal["Calendar"]
end
ZDT --> Date
ZDT --> Time
ZDT --> TZ
ZDT --> Cal
subgraph Conversions
Instant2["toInstant()"]
PlainDT["toPlainDateTime()"]
PlainD["toPlainDate()"]
PlainT["toPlainTime()"]
end
ZDT --> Instant2
ZDT --> PlainDT
ZDT --> PlainD
ZDT --> PlainT
// Windows to IANA conversion
if (OperatingSystem.IsWindows() &&
TimeZoneInfo.TryConvertWindowsIdToIanaId(tzId, out var ianaId))
{
tzId = ianaId;
}
// IANA to Windows conversion for .NET operations
if (!OperatingSystem.IsWindows() &&
TimeZoneInfo.TryConvertIanaIdToWindowsId(tzId, out var windowsId))
{
// Use windowsId for TimeZoneInfo.FindSystemTimeZoneById
}Each realm maintains its own set of Temporal prototypes:
internal sealed class TemporalPrototypes
{
public JsObject InstantPrototype { get; set; } = null!;
public JsObject DurationPrototype { get; set; } = null!;
public JsObject PlainDatePrototype { get; set; } = null!;
public JsObject PlainTimePrototype { get; set; } = null!;
public JsObject PlainDateTimePrototype { get; set; } = null!;
public JsObject ZonedDateTimePrototype { get; set; } = null!;
public JsObject PlainYearMonthPrototype { get; set; } = null!;
public JsObject PlainMonthDayPrototype { get; set; } = null!;
}
private static readonly ConditionalWeakTable<RealmState, TemporalPrototypes>
_prototypeCache = new();private static HostFunction CreateInstantConstructor(RealmState realm, TemporalPrototypes prototypes)
{
var prototype = new JsObject(realm.ObjectPrototype);
prototypes.InstantPrototype = prototype;
prototype.DefineProperty(SymbolKeys.ToStringTag,
new PropertyDescriptor { Value = "Temporal.Instant", ... });
// Add getters
AddPrototypeGetter(prototype, realm, "epochMilliseconds", thisValue =>
{
var instant = GetInstant(thisValue);
return new JsValue((double)instant.EpochMilliseconds);
});
AddPrototypeGetter(prototype, realm, "epochNanoseconds", thisValue =>
{
var instant = GetInstant(thisValue);
return JsValue.FromObjectUnsafe(new JsBigInt(instant.EpochNanoseconds));
});
// Add methods
AddPrototypeMethod(prototype, realm, "toString", 0, (thisValue, _) =>
{
var instant = GetInstant(thisValue);
return new JsValue(instant.ToString());
});
// ...
}Temporal objects store their data in internal slots:
private const string TemporalInstantSlot = "[[TemporalInstant]]";
private const string TemporalDurationSlot = "[[TemporalDuration]]";
private const string TemporalPlainDateSlot = "[[TemporalPlainDate]]";
// ...
private static JsValue WrapInstant(JsTemporalInstant instant, RealmState realm, JsObject prototype)
{
var obj = new JsObject(prototype);
obj.SetInternalSlot(TemporalInstantSlot, instant);
return JsValue.FromJsObject(obj);
}
private static JsTemporalInstant GetInstant(JsValue thisValue)
{
if (thisValue.TryGetObject<JsObject>(out var obj) &&
obj.TryGetInternalSlot(TemporalInstantSlot, out var slot) &&
slot is JsTemporalInstant instant)
{
return instant;
}
throw StandardLibrary.ThrowTypeError("Invalid Temporal.Instant");
}private static JsTemporalInstant ToTemporalInstant(JsValue arg, RealmState realm)
{
// Already an Instant
if (arg.TryGetObject<JsObject>(out var obj) &&
obj.TryGetInternalSlot(TemporalInstantSlot, out var slot) &&
slot is JsTemporalInstant instant)
{
return instant;
}
// Parse from string
if (arg.IsString)
{
var str = arg.AsString();
return JsTemporalInstant.Parse(str);
}
throw StandardLibrary.ThrowTypeError("Cannot convert to Temporal.Instant");
}private static JsTemporalDuration ToTemporalDuration(JsValue arg, RealmState realm)
{
// Already a Duration
if (arg.TryGetObject<JsObject>(out var obj) &&
obj.TryGetInternalSlot(TemporalDurationSlot, out var slot) &&
slot is JsTemporalDuration duration)
{
return duration;
}
// Parse from string (ISO 8601 duration)
if (arg.IsString)
{
return JsTemporalDuration.Parse(arg.AsString());
}
// Build from property bag
if (arg.TryGetObject<IJsPropertyAccessor>(out var accessor))
{
return DurationFromPropertyBag(accessor);
}
throw StandardLibrary.ThrowTypeError("Cannot convert to Temporal.Duration");
}All Temporal types implement comparison:
var compare = CreateFunction(realm, "compare", 2, (_, args) =>
{
var i1 = ToTemporalInstant(args.GetArgument(0), realm);
var i2 = ToTemporalInstant(args.GetArgument(1), realm);
return new JsValue(i1.CompareTo(i2)); // -1, 0, or 1
});public bool Equals(JsTemporalInstant? other)
{
return other is not null && EpochNanoseconds.Equals(other.EpochNanoseconds);
}Temporal types reject implicit conversion to prevent bugs:
AddPrototypeMethod(prototype, realm, "valueOf", 0, (_, _) =>
throw StandardLibrary.ThrowTypeError(
"Temporal.Instant.prototype.valueOf does not support implicit conversion",
realm: realm));This prevents accidental comparisons like instant1 < instant2 (use Temporal.Instant.compare() instead).
The default calendar is ISO 8601:
AddPrototypeGetter(prototype, realm, "calendarId", tv =>
new JsValue(GetPlainDate(tv).Calendar)); // "iso8601"
// ISO 8601 always has 7 days per week
AddPrototypeGetter(prototype, realm, "daysInWeek", _ => new JsValue(7));
// ISO 8601 calendar has no era
AddPrototypeGetter(prototype, realm, "era", _ => JsValue.Undefined);
AddPrototypeGetter(prototype, realm, "eraYear", _ => JsValue.Undefined);- JsValue System - How Temporal values are represented
- Standard Library Architecture - How Temporal constructors are defined
- Promise & Microtasks - Async operations with Temporal