Format Dates with Node's local time zone names

Date.prototype.toString, toDateString and toTimeString, String(date),
template literals and string concatenation print local time with the GMT
offset and the zone's long name, as Node does. The runtime identifies the
host zone from TZ or /etc/localtime, again after each write to
process.env.TZ, and looks its name up in a table generated from Node's
ICU data. Dates held in untyped values convert to strings with toString()
and to numbers with their time value.
This commit is contained in:
Chris Tate
2026-10-18 20:20:05 -05:00
parent 2d37d451e9
commit 705c197a71
21 changed files with 2476 additions and 17 deletions
@@ -1 +1 @@
- **Date:** Date unions, year/month constructors, setters, statically typed TypeScript identity comparisons, thrown Date values, and locale/string formatters are unsupported. Zero-argument and single number/string constructors, storage and passing, `getTime`, `valueOf`, `toISOString`, `toJSON` (a `string | null`: null for an invalid date, as in Node), calendar getters, `getTimezoneOffset`, and single-string `Date.parse` compile. JavaScript Dates retain identity through checked storage and support Date-copy construction and JSON serialization. A TypeScript Date converted to `unknown` becomes a Date with the same time value; each conversion creates a new Date, so two `unknown` values converted from one TypeScript Date are not `===`. See [Dates and timezones](#dates-and-timezones) for parsing restrictions.
- **Date:** Date unions, year/month constructors, setters, statically typed TypeScript identity comparisons, thrown Date values, `toUTCString`, and locale formatters are unsupported. Zero-argument and single number/string constructors, storage and passing, `getTime`, `valueOf`, `toISOString`, `toJSON` (a `string | null`: null for an invalid date, as in Node), `toString`, `toDateString`, `toTimeString`, `String(date)` and template literal conversion, calendar getters, `getTimezoneOffset`, and single-string `Date.parse` compile. JavaScript Dates retain identity through checked storage and support Date-copy construction and JSON serialization. A TypeScript Date converted to `unknown` becomes a Date with the same time value; each conversion creates a new Date, so two `unknown` values converted from one TypeScript Date are not `===`. See [Dates and timezones](#dates-and-timezones) for parsing restrictions.
@@ -1,3 +1,5 @@
String constructors and `Date.parse` accept ECMAScript date-only forms, date-times with an explicit `Z` or numeric offset, and GMT certificate-validity strings from the supported X509 APIs. Other V8-specific forms and offset-less local date-times produce an Invalid Date or `NaN`; `SC6005` reports string literals in those forms.
Local calendar getters use the operating system's timezone database. Historical results can differ from Node.js when timezone data differs. For valid extreme years outside a platform calendar API's range, scriptc uses a calendar-equivalent year in the 400-year Gregorian cycle to obtain the timezone rule.
`toString`, `toTimeString`, `String(date)` and template literals print the zone's long name after the GMT offset, as Node does (`GMT-0500 (Central Daylight Time)`). scriptc identifies the zone the way Node does, from `TZ` when it names a zone, else from `/etc/localtime`. Outside Windows, a write to `process.env.TZ` changes the zone for later conversions and getters. The names are the en-US names for the IANA zones; Node prints them in the language of the locale that `LC_ALL`, `LC_MESSAGES` or `LANG` selects, so with a non-English locale the name differs from Node's. A `TZ` value that is a file path or a POSIX rule string (such as `EST5EDT,M3.2.0,M11.1.0`) gives offsets and names that differ from Node's, which ignores those forms. On Windows, the name is the `GMT+hh:mm` form unless `TZ` names a zone. On `wasm32-wasi`, local time is UTC and the name is `Coordinated Universal Time`.
@@ -325,6 +325,9 @@ export const LIB_FN_SYMS: Record<string, string> = {
"date.getMilliseconds": "scr_date_get_milliseconds",
"date.getUTCMilliseconds": "scr_date_get_milliseconds",
"date.getTimezoneOffset": "scr_date_get_timezone_offset",
"date.toString": "scr_date_to_string",
"date.toDateString": "scr_date_to_date_string",
"date.toTimeString": "scr_date_to_time_string",
"fs.existsSync": "scr_fs_exists",
// ── the throwing slice (MAY_THROW_LIB_FNS members): the generic path
// emits the standard pending check after each — exprs.ts's finish.
@@ -33,8 +33,14 @@ const DATE_GETTER_FNS: Readonly<Record<string, IrLibFn>> = {
getTimezoneOffset: "date.getTimezoneOffset",
};
const DATE_TEXT_FNS: ReadonlyMap<string, IrLibFn> = new Map([
["toString", "date.toString"],
["toDateString", "date.toDateString"],
["toTimeString", "date.toTimeString"],
]);
const DATE_METHOD_HINT =
"getTime(), valueOf(), toISOString(), toJSON(), the local/UTC calendar getters, and getTimezoneOffset() are supported; Date setters and locale/string formatters have no lowering";
"getTime(), valueOf(), toISOString(), toJSON(), toString(), toDateString(), toTimeString(), the local/UTC calendar getters, and getTimezoneOffset() are supported; Date setters, toUTCString() and locale formatters have no lowering";
/** The Date slice: statics plus read-only Date values backed by one
* TimeClip'd millisecond scalar. Construction lives in lowerNew;
@@ -117,6 +123,10 @@ export function lowerDateCall(
if (fn !== undefined) {
return { kind: "libCall", fn, args: [receiver], type: F64, loc };
}
const textFn = DATE_TEXT_FNS.get(name);
if (textFn !== undefined) {
return { kind: "libCall", fn: textFn, args: [receiver], type: STRING, loc };
}
lowerer.noLowering(
`Date.prototype.${name}`,
call,
@@ -8863,6 +8863,10 @@ export function ensureString(lowerer: Lowerer, e: IrExpr, node: ts.Node): IrExpr
if (e.type.kind === "string") return e;
if (e.type.kind === "url")
return { kind: "libCall", fn: "url.href", args: [e], type: STRING, loc: e.loc };
// String(date) / `${date}` / "" + date: Date's ToPrimitive prefers
// toString().
if (e.type.kind === "date")
return { kind: "libCall", fn: "date.toString", args: [e], type: STRING, loc: e.loc };
if (e.type.kind === "regex") {
return {
kind: "regexIntrinsic",
@@ -2321,6 +2321,25 @@ export const AMBIENT_SURFACE_FNS: readonly AmbientSurfaceRow[] = [
name: "Date.prototype.getTimezoneOffset",
fns: ["date.getTimezoneOffset"],
},
{
id: "stdlib.date.toString",
kind: "stdlib",
name: "Date.prototype.toString",
fns: ["date.toString"],
note: "local time with the host zone's long English name; String(date) and template literals use it",
},
{
id: "stdlib.date.toDateString",
kind: "stdlib",
name: "Date.prototype.toDateString",
fns: ["date.toDateString"],
},
{
id: "stdlib.date.toTimeString",
kind: "stdlib",
name: "Date.prototype.toTimeString",
fns: ["date.toTimeString"],
},
// ── perf_hooks (lowerPerfHooksCall): the monotonic clock.
{
id: "node-builtin.perf_hooks.performance.now",
@@ -4208,6 +4208,12 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
"date.getMilliseconds": { argTypes: [DATE_T], result: F64 },
"date.getUTCMilliseconds": { argTypes: [DATE_T], result: F64 },
"date.getTimezoneOffset": { argTypes: [DATE_T], result: F64 },
/** Date.prototype.toString / toDateString / toTimeString and String(date):
* V8's local-time formats with the host zone's long name, "Invalid Date"
* for an invalid date. Owned (+1) string; never throws. */
"date.toString": { argTypes: [DATE_T], result: STRING },
"date.toDateString": { argTypes: [DATE_T], result: STRING },
"date.toTimeString": { argTypes: [DATE_T], result: STRING },
/** The composed `new Date(dateString).getTime()` read: one borrowed
* string, f64 milliseconds since epoch. The parsed grammar is BOUNDED
* (documented divergence — V8's parser accepts far more): the ASN.1
+19
View File
@@ -4209,6 +4209,12 @@
"status": "static",
"note": "one date string, using the bounded parser shared with new Date(dateString).getTime()"
},
{
"id": "stdlib.date.toDateString",
"kind": "stdlib",
"name": "Date.prototype.toDateString",
"status": "static"
},
{
"id": "stdlib.date.toISOString",
"kind": "stdlib",
@@ -4216,6 +4222,19 @@
"status": "static",
"note": "UTC ISO formatting over constructed and stored Date values"
},
{
"id": "stdlib.date.toString",
"kind": "stdlib",
"name": "Date.prototype.toString",
"status": "static",
"note": "local time with the host zone's long English name; String(date) and template literals use it"
},
{
"id": "stdlib.date.toTimeString",
"kind": "stdlib",
"name": "Date.prototype.toTimeString",
"status": "static"
},
{
"id": "stdlib.date.valueOf",
"kind": "stdlib",
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5963-date-to-string.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5964-date-dynamic-conversions.js"
],
"diags": []
}
+29 -6
View File
@@ -6805,6 +6805,15 @@ static bool scr_dyn_error_encoding(const ScrDyn *d) {
return d->kind == SCR_DYN_OBJ && scr_dyn_obj_get(d, "%error", 6) != NULL;
}
static bool scr_dyn_is_date(const ScrDyn *d) {
return d->kind == SCR_DYN_HANDLE && d->v.handle.tag == SCR_DYNH_DATE;
}
/* A Date's valueOf(): its time value as a number. +1. */
static ScrDyn *scr_dyn_date_value_of(const ScrDyn *d) {
return scr_dyn_handle_ops_of(d)->invoke(d->v.handle.ptr, (ScrDyn *)d, "valueOf", NULL, 0, "valueOf");
}
/* Receiver-kind-dispatched toString() on a checked-dynamic value (the
* dyn method surface — a stream's 'data'/for-await chunk is the common
* receiver): bytes decode per the encoding (Node's Buffer.toString,
@@ -6843,7 +6852,7 @@ ScrStr *scr_dyn_to_string(const ScrDyn *d, const ScrStr *enc) {
if (scr_dyn_error_encoding(d) && !scr_dyn_property_owner(d, "toString", 8)) return scr_dyn_error_string(d);
return scr_str_new("[object Object]", 15);
case SCR_DYN_HANDLE:
if (d->v.handle.tag == SCR_DYNH_REGEXP || d->v.handle.tag == SCR_DYNH_URL) {
if (d->v.handle.tag == SCR_DYNH_REGEXP || d->v.handle.tag == SCR_DYNH_URL || d->v.handle.tag == SCR_DYNH_DATE) {
ScrDyn *text = scr_dyn_handle_ops_of(d)->invoke(d->v.handle.ptr, (ScrDyn *)d, "toString", NULL, 0, "RegExp.toString");
if (!text) return NULL;
ScrStr *out = scr_str_retain(text->v.str);
@@ -7267,6 +7276,14 @@ bool scr_dyn_number_coerce_js(const ScrDyn *d, double *out) {
return false;
}
default: {
/* A Date's valueOf answers its time value. */
if (scr_dyn_is_date(d)) {
ScrDyn *time = scr_dyn_date_value_of(d);
if (!time) return false;
*out = time->v.num;
scr_dyn_release(time);
return true;
}
/* Arrays, byte views, functions, promises, and native handles inherit a
* valueOf that returns the receiver, then use their existing JS-exact
* string rendering for the numeric conversion. */
@@ -7287,8 +7304,14 @@ double scr_dyn_number_coerce(const ScrDyn *d) {
static ScrDyn *scr_dyn_add_primitive(const ScrDyn *d);
/* ToPrimitive with the number hint, which differs from the default hint
* only for a Date: its time value. */
static ScrDyn *scr_dyn_number_primitive(const ScrDyn *d) {
return scr_dyn_is_date(d) ? scr_dyn_date_value_of(d) : scr_dyn_add_primitive(d);
}
double scr_dyn_number_constructor(const ScrDyn *d) {
ScrDyn *primitive = scr_dyn_add_primitive(d);
ScrDyn *primitive = scr_dyn_number_primitive(d);
if (!primitive) return NAN;
double result = primitive->kind == SCR_DYN_BIGINT
? scr_bigint_to_f64(primitive->v.bigint) : scr_dyn_number_coerce(primitive);
@@ -7297,7 +7320,7 @@ double scr_dyn_number_constructor(const ScrDyn *d) {
}
static ScrBigInt *scr_dyn_bigint_convert(const ScrDyn *d, bool allow_number) {
ScrDyn *primitive = scr_dyn_add_primitive(d);
ScrDyn *primitive = scr_dyn_number_primitive(d);
if (!primitive) return NULL;
ScrBigInt *result = NULL;
switch (primitive->kind) {
@@ -7430,7 +7453,7 @@ ScrDyn *scr_dyn_add(const ScrDyn *left, const ScrDyn *right) {
}
ScrDyn *scr_dyn_to_numeric(const ScrDyn *value) {
ScrDyn *primitive = scr_dyn_add_primitive(value);
ScrDyn *primitive = scr_dyn_number_primitive(value);
if (!primitive) return NULL;
if (primitive->kind == SCR_DYN_BIGINT) return primitive;
double number;
@@ -7454,9 +7477,9 @@ ScrDyn *scr_dyn_increment(const ScrDyn *value, bool increment) {
bool scr_dyn_compare(const ScrDyn *left, const ScrDyn *right, const ScrStr *operation) {
// ToPrimitive is observable and always visits the original left operand
// first, including the > and <= spellings of relational comparison.
ScrDyn *l = scr_dyn_add_primitive(left);
ScrDyn *l = scr_dyn_number_primitive(left);
if (!l) return false;
ScrDyn *r = scr_dyn_add_primitive(right);
ScrDyn *r = scr_dyn_number_primitive(right);
if (!r) { scr_dyn_release(l); return false; }
double order = 2;
if (l->kind == SCR_DYN_STR && r->kind == SCR_DYN_STR) {
+380 -1
View File
@@ -10682,6 +10682,9 @@ double scr_date_utc(double y, double mo, double d,
typedef struct ScrDateParts {
long long year;
int month, date, day, hours, minutes, seconds, milliseconds;
/* Local fields: tm_isdst at the instant (or its surrogate). It fills the
* padding before timezone_offset, so the struct keeps its size. */
bool isdst;
double timezone_offset;
} ScrDateParts;
@@ -10718,6 +10721,7 @@ static void scr_date_utc_parts_unchecked(double t, ScrDateParts *out) {
out->seconds = (int)(msday / 1000 % 60);
out->milliseconds = (int)(msday % 1000);
out->timezone_offset = 0;
out->isdst = false;
}
static bool scr_date_utc_parts(double ms, ScrDateParts *out) {
@@ -10801,6 +10805,7 @@ static bool scr_date_local_parts_uncached(double clipped, ScrDateParts *out) {
scr_date_utc_parts_unchecked(clipped + local_offset * 1000.0, out);
double timezone_offset = trunc(-local_offset / 60.0);
out->timezone_offset = timezone_offset == 0 ? 0 : timezone_offset;
out->isdst = tmv.tm_isdst > 0;
return true;
}
@@ -10840,6 +10845,364 @@ double scr_date_get_timezone_offset(double ms) {
return scr_date_local_parts(ms, &p) ? p.timezone_offset : NAN;
}
/* ── Date time zone names ───────────────────────────────────────────────
* Long time zone names for Date.prototype.toString and toTimeString.
*
* Node prints "GMT-0500 (Central Daylight Time)": the name comes from ICU
* for the host zone, in standard or daylight form. ICU identifies the host
* zone from TZ when it looks like a zone ID, else from /etc/localtime (the
* symlink's target, or the zoneinfo file with the same contents), else
* from the libc zone abbreviation. The runtime follows the same steps and
* looks the ID up in the names Node prints for each tzdata zone
* (scr_timezone_data.h, generated). An ID without a row prints Node's
* fallback for a zone without a name, "GMT+hh:mm" with the zone's standard
* offset.
*
* Whether an instant is in daylight time follows V8: instants before 1970
* or after 2038-01-19 use the instant with the same month, day and time in
* an equivalent year between 2008 and 2037 (same leap-year status and
* weekday of January 1), and tm_isdst decides. A few zones carry Node's
* own daylight-time changes instead: those whose tzdata rules use a
* negative daylight saving (Europe/Dublin's winter time, Morocco's Ramadan
* time), where ICU's data calls the larger offset daylight time, and those
* Node shows in daylight time through 2038 (British Columbia's permanent
* UTC-7, which tzdata names standard time). */
#include "scr_timezone_data.h"
#define SCR_TZ_ROWS (sizeof scr_tz_names / sizeof scr_tz_names[0])
/* Text that no other code in this file uses lives in named arrays: string
* literals would join the merged read-only strings of every program that
* links this file. */
static const char scr_tz_posix_ids[4][8] = { "PST8PDT", "MST7MDT", "CST6CDT", "EST5EDT" };
static const char scr_tz_prefixes[2][7] = { "posix/", "right/" };
static const char scr_tz_env_name[] = "TZ";
static const char scr_tz_utc[] = "UTC";
static const char scr_tz_localtime_path[] = "/etc/localtime";
static const char scr_tz_zoneinfo_tail[] = "/zoneinfo/";
static const char scr_tz_zoneinfo_dir[] = "/usr/share/zoneinfo/";
static const char scr_tz_skipped[4][11] = { ".", "..", "posixrules", "localtime" };
static const char scr_tz_fallback_format[] = "GMT%c%02lld:%02lld";
static const char scr_date_invalid_text[] = "Invalid Date";
/* The thread's host zone, detected again after a write to process.env.TZ
* (scr_date_tz_generation). */
typedef struct {
unsigned generation;
const ScrTzNames *row; /* NULL: no row, use the fallback */
bool resolved;
bool fallback_ready;
char fallback[16]; /* "GMT+hh:mm" */
} ScrTzState;
static SCR_TL ScrTzState scr_tz_state;
/* The local UTC offset (seconds) and tm_isdst at an epoch second. */
static bool scr_tz_offset_at(double sec, double *offset, bool *isdst) {
struct tm tmv;
if (!scr_date_localtime(sec, &tmv)) return false;
double local =
scr_days_from_civil((long long)tmv.tm_year + 1900, tmv.tm_mon + 1, tmv.tm_mday) * 86400.0 +
tmv.tm_hour * 3600.0 + tmv.tm_min * 60.0 + tmv.tm_sec;
*offset = local - sec;
*isdst = tmv.tm_isdst > 0;
return true;
}
/* ICU's isValidOlsonID: letters and punctuation up to an optional run of at
* most two digits ending the string ("GMT+11"), or one of the four POSIX
* zone IDs tzdata also names. */
static bool scr_tz_valid_id(const char *id) {
size_t i = 0;
while (id[i] && !(id[i] >= '0' && id[i] <= '9') && id[i] != ',') i++;
size_t max = i + 2;
while (id[i] && id[i] >= '0' && id[i] <= '9' && i < max) i++;
if (id[i] == 0) return true;
for (size_t k = 0; k < 4; k++) {
if (!strcmp(id, scr_tz_posix_ids[k])) return true;
}
return false;
}
static const char *scr_tz_skip_prefix(const char *id) {
return !strncmp(id, scr_tz_prefixes[0], 6) || !strncmp(id, scr_tz_prefixes[1], 6) ? id + 6 : id;
}
static const ScrTzNames *scr_tz_find(const char *id) {
size_t lo = 0, hi = SCR_TZ_ROWS;
while (lo < hi) {
size_t mid = lo + (hi - lo) / 2;
int c = strcmp(id, scr_tz_ids + scr_tz_names[mid].id);
if (c == 0) return &scr_tz_names[mid];
if (c < 0) hi = mid;
else lo = mid + 1;
}
return NULL;
}
#if !defined(_WIN32) && !defined(__wasi__)
/* The bytes of a file, or NULL. */
static char *scr_tz_read_file(const char *path, size_t *len) {
FILE *f = fopen(path, "rb");
if (!f) return NULL;
size_t cap = 4096, n = 0;
char *data = malloc(cap);
while (data) {
n += fread(data + n, 1, cap - n, f);
if (n < cap) break;
char *grown = realloc(data, cap * 2);
if (!grown) {
free(data);
data = NULL;
break;
}
data = grown;
cap *= 2;
}
fclose(f);
*len = n;
return data;
}
static bool scr_tz_same_file(const char *path, const char *data, size_t len) {
struct stat st;
if (stat(path, &st) != 0 || !S_ISREG(st.st_mode) || (size_t)st.st_size != len) return false;
size_t other_len;
char *other = scr_tz_read_file(path, &other_len);
bool same = other && other_len == len && memcmp(other, data, len) == 0;
free(other);
return same;
}
/* ICU's search for a zoneinfo file with /etc/localtime's contents, in
* directory order, skipping "posixrules" and "localtime". Writes the
* path below the zoneinfo directory into `out`. */
static bool scr_tz_search(char *dir, size_t dir_len, const char *data, size_t len, char *out, size_t cap) {
DIR *d = opendir(dir);
if (!d) return false;
bool found = false;
struct dirent *entry;
while (!found && (entry = readdir(d)) != NULL) {
const char *name = entry->d_name;
bool skipped = false;
for (size_t k = 0; k < 4; k++) skipped = skipped || !strcmp(name, scr_tz_skipped[k]);
if (skipped) continue;
size_t name_len = strlen(name);
if (dir_len + name_len + 2 > PATH_MAX) continue;
memcpy(dir + dir_len, name, name_len + 1);
DIR *sub = opendir(dir);
if (sub) {
closedir(sub);
dir[dir_len + name_len] = '/';
dir[dir_len + name_len + 1] = 0;
found = scr_tz_search(dir, dir_len + name_len + 1, data, len, out, cap);
} else if (scr_tz_same_file(dir, data, len)) {
const char *id = scr_tz_skip_prefix(dir + strlen(scr_tz_zoneinfo_dir));
size_t id_len = strlen(id);
if (id_len < cap) {
memcpy(out, id, id_len + 1);
found = true;
}
}
dir[dir_len] = 0;
}
closedir(d);
return found;
}
#endif
/* The host zone's row, or NULL when ICU would not find a zone name. */
static const ScrTzNames *scr_tz_detect(void) {
#if defined(__wasi__)
/* WASI's libc keeps every local time in UTC. */
return scr_tz_find(scr_tz_utc);
#else
const char *tz = getenv(scr_tz_env_name);
if (tz && scr_tz_valid_id(tz)) {
if (tz[0] == ':') tz++;
return scr_tz_find(scr_tz_skip_prefix(tz));
}
#ifdef _WIN32
return NULL;
#else
/* The zone ID is the path below a zoneinfo directory of the
* /etc/localtime symlink's target, or of a zoneinfo file with the same
* contents when /etc/localtime is a regular file. */
char path[PATH_MAX];
if (realpath(scr_tz_localtime_path, path) && strcmp(path, scr_tz_localtime_path) != 0) {
const char *tail = strstr(path, scr_tz_zoneinfo_tail);
if (tail) {
tail = scr_tz_skip_prefix(tail + strlen(scr_tz_zoneinfo_tail));
if (scr_tz_valid_id(tail)) return scr_tz_find(tail);
}
} else {
size_t len;
DIR *zoneinfo = opendir(scr_tz_zoneinfo_dir);
char *data = zoneinfo ? scr_tz_read_file(scr_tz_localtime_path, &len) : NULL;
if (zoneinfo) closedir(zoneinfo);
if (data) {
char dir[PATH_MAX];
memcpy(dir, scr_tz_zoneinfo_dir, sizeof scr_tz_zoneinfo_dir);
char id[PATH_MAX];
bool found = scr_tz_search(dir, strlen(dir), data, len, id, sizeof id);
free(data);
if (found && scr_tz_valid_id(id)) return scr_tz_find(id);
}
}
/* Otherwise the standard-time abbreviation names the zone ("UTC" when
* there is no /etc/localtime). */
tzset();
return scr_tz_find(tzname[0]);
#endif
#endif
}
static ScrTzState *scr_tz_current(void) {
ScrTzState *s = &scr_tz_state;
unsigned generation = scr_date_tz_generation;
if (s->generation != generation || !s->resolved) {
s->generation = generation;
s->row = scr_tz_detect();
s->resolved = true;
s->fallback_ready = false;
}
return s;
}
/* "GMT+hh:mm" for the zone's current standard offset: the name Node prints
* for a zone ID it does not know. */
static const char *scr_tz_fallback(ScrTzState *s) {
if (!s->fallback_ready) {
double offset = 0;
bool isdst = false;
long long seconds = 0;
if (scr_tz_offset_at((double)time(NULL), &offset, &isdst)) seconds = (long long)offset - (isdst ? 3600 : 0);
long long minutes = (seconds < 0 ? -seconds : seconds) / 60;
snprintf(s->fallback, sizeof s->fallback, scr_tz_fallback_format, seconds < 0 ? '-' : '+',
minutes / 60, minutes % 60);
s->fallback_ready = true;
}
return s->fallback;
}
/* V8's EquivalentTime: the same month, day and time of day in a year
* between 2008 and 2037 with the same leap-year status and weekday of
* January 1. */
static double scr_tz_equivalent_time(double ms) {
ScrDateParts p;
scr_date_utc_parts_unchecked(ms, &p);
double within = ms - floor(ms / 86400000.0) * 86400000.0;
long long year = p.year;
bool leap = (year % 4 == 0 && year % 100 != 0) || year % 400 == 0;
long long week_day = ((long long)scr_days_from_civil(year, 1, 1) + 4) % 7;
if (week_day < 0) week_day += 7;
long long recent = (leap ? 1956 : 1967) + (week_day * 12) % 28;
long long equivalent = 2008 + (recent + 3 * 28 - 2008) % 28;
return (scr_days_from_civil(equivalent, p.month + 1, 1) + p.date - 1) * 86400000.0 + within;
}
static bool scr_tz_is_daylight(double ms, int isdst_hint, const ScrTzNames *row) {
/* V8 names instants in [0, 2^31 - 1] seconds directly. */
if (ms < 0 || (long long)ms > 2147483647000LL) {
ms = scr_tz_equivalent_time(ms);
isdst_hint = -1;
}
double sec = floor(ms / 1000.0);
if (row->change_count > 0) {
/* Daylight time after an odd number of changes. */
const int32_t *changes = scr_tz_daylight_changes + row->first_change;
size_t lo = 0, hi = row->change_count;
while (lo < hi) {
size_t mid = lo + (hi - lo) / 2;
if (changes[mid] <= sec) lo = mid + 1;
else hi = mid;
}
return (lo & 1) != 0;
}
if (isdst_hint >= 0) return isdst_hint != 0;
double offset;
bool isdst;
return scr_tz_offset_at(sec, &offset, &isdst) && isdst;
}
const char *scr_date_zone_name(double ms, int isdst) {
ScrTzState *s = scr_tz_current();
if (!s->row) return scr_tz_fallback(s);
bool daylight = scr_tz_is_daylight(ms, isdst, s->row);
return scr_tz_name_text + (daylight ? s->row->daylight : s->row->standard);
}
/* Date.prototype.toString, toDateString and toTimeString: V8's formats over
* the local calendar fields ("Sat Oct 18 2026 09:12:00 GMT-0500 (Central
* Daylight Time)"), with years before 1 as "-0001", and "Invalid Date" for
* an invalid date. */
enum { SCR_DATE_TEXT_FULL, SCR_DATE_TEXT_DATE, SCR_DATE_TEXT_TIME };
/* `value` (>= 0) in decimal, zero-padded to `width` digits. */
static char *scr_date_put_digits(char *p, long long value, int width) {
char digits[24];
int n = 0;
do {
digits[n++] = (char)('0' + value % 10);
value /= 10;
} while (value > 0);
while (n < width) digits[n++] = '0';
while (n > 0) *p++ = digits[--n];
return p;
}
static ScrStr *scr_date_text(double ms, int mode) {
static const char week_days[7][3] = { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" };
static const char months[12][3] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun",
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec" };
ScrDateParts p;
if (!scr_date_local_parts(ms, &p)) return scr_str_new(scr_date_invalid_text, sizeof scr_date_invalid_text - 1);
char buf[160];
char *o = buf;
if (mode != SCR_DATE_TEXT_TIME) {
memcpy(o, week_days[p.day], 3);
o[3] = ' ';
memcpy(o + 4, months[p.month], 3);
o[7] = ' ';
o = scr_date_put_digits(o + 8, p.date, 2);
*o++ = ' ';
/* "%04d", or "%05d" (a sign and four digits) before year 0. */
if (p.year < 0) *o++ = '-';
o = scr_date_put_digits(o, p.year < 0 ? -p.year : p.year, 4);
}
if (mode == SCR_DATE_TEXT_FULL) *o++ = ' ';
if (mode != SCR_DATE_TEXT_DATE) {
int offset = -(int)p.timezone_offset;
int magnitude = offset < 0 ? -offset : offset;
o = scr_date_put_digits(o, p.hours, 2);
*o++ = ':';
o = scr_date_put_digits(o, p.minutes, 2);
*o++ = ':';
o = scr_date_put_digits(o, p.seconds, 2);
*o++ = ' ';
*o++ = 'G';
*o++ = 'M';
*o++ = 'T';
*o++ = offset < 0 ? '-' : '+';
o = scr_date_put_digits(o, magnitude / 60, 2);
o = scr_date_put_digits(o, magnitude % 60, 2);
*o++ = ' ';
*o++ = '(';
/* Zone names are at most 64 bytes (scr_timezone_data.h). */
const char *zone = scr_date_zone_name(ms, p.isdst);
size_t zone_len = strlen(zone);
memcpy(o, zone, zone_len);
o += zone_len;
*o++ = ')';
}
return scr_str_new(buf, (size_t)(o - buf));
}
ScrStr *scr_date_to_string(double ms) { return scr_date_text(ms, SCR_DATE_TEXT_FULL); }
ScrStr *scr_date_to_date_string(double ms) { return scr_date_text(ms, SCR_DATE_TEXT_DATE); }
ScrStr *scr_date_to_time_string(double ms) { return scr_date_text(ms, SCR_DATE_TEXT_TIME); }
/* ── Number statics ────────────────────────────────────────────────────
* JS-exact: the ES2015 Number statics never coerce (unlike the global
* isNaN/isFinite), and the compiler routes only number-typed arguments
@@ -11176,11 +11539,25 @@ double scr_dyn_native_date_value(const ScrDyn *value) {
return ((ScrNativeDate *)value->v.handle.ptr)->milliseconds;
}
/* Method names without another use in this file, in named arrays (see
* scr_tz_posix_ids). */
static const char scr_date_to_date_string_name[] = "toDateString";
static const char scr_date_to_time_string_name[] = "toTimeString";
static ScrDyn *scr_native_date_invoke(void *ptr, ScrDyn *self, const char *method,
ScrDyn *const *args, size_t argc, const char *what) {
(void)self; (void)args; (void)argc; (void)what;
double ms = ((ScrNativeDate *)ptr)->milliseconds;
if (!strcmp(method, "getTime") || !strcmp(method, "valueOf")) return scr_dyn_new_num(ms);
if (!strcmp(method, "toString") || !strcmp(method, scr_date_to_date_string_name) ||
!strcmp(method, scr_date_to_time_string_name)) {
ScrStr *text = !strcmp(method, "toString") ? scr_date_to_string(ms)
: !strcmp(method, scr_date_to_date_string_name) ? scr_date_to_date_string(ms)
: scr_date_to_time_string(ms);
ScrDyn *result = scr_dyn_new_str(text);
scr_str_release(text);
return result;
}
if (!strcmp(method, "toISOString") || !strcmp(method, "toJSON")) {
if (!strcmp(method, "toJSON") && !isfinite(ms)) return scr_dyn_new_null();
ScrStr *text = scr_date_to_iso(ms);
@@ -11228,7 +11605,9 @@ ScrDyn *scr_dyn_native_date_new(const ScrDyn *arguments) {
/* The Date.prototype methods scr_native_date_invoke answers. */
static const char *const scr_native_date_methods[] = {
"getTime", "valueOf", "toISOString", "toJSON", "getFullYear", "getUTCFullYear", "getMonth",
"getTime", "valueOf", "toISOString", "toJSON", "toString", scr_date_to_date_string_name,
scr_date_to_time_string_name,
"getFullYear", "getUTCFullYear", "getMonth",
"getUTCMonth", "getDate", "getUTCDate", "getDay", "getUTCDay", "getHours", "getUTCHours",
"getMinutes", "getUTCMinutes", "getSeconds", "getUTCSeconds", "getMilliseconds",
"getUTCMilliseconds", "getTimezoneOffset",
+9
View File
@@ -120,6 +120,15 @@ double scr_date_get_minutes(double ms, bool utc);
double scr_date_get_seconds(double ms, bool utc);
double scr_date_get_milliseconds(double ms);
double scr_date_get_timezone_offset(double ms);
/* Date.prototype.toString, toDateString and toTimeString with V8's
* formats; "Invalid Date" for NaN. +1. */
ScrStr *scr_date_to_string(double ms);
ScrStr *scr_date_to_date_string(double ms);
ScrStr *scr_date_to_time_string(double ms);
/* The long time zone name Node prints after the GMT offset for the host
* zone at time value `ms`; `isdst` is tm_isdst at `ms`, or -1 when not
* known. Borrowed; valid until the next call on the same thread. */
const char *scr_date_zone_name(double ms, int isdst);
/* One-argument ABI wrappers used by the LLVM lib-call table. */
double scr_date_get_full_year_local(double ms);
double scr_date_get_full_year_utc(double ms);
File diff suppressed because it is too large Load Diff
+292
View File
@@ -0,0 +1,292 @@
#!/usr/bin/env node
// Long time zone names for Date.prototype.toString, toTimeString and
// String(date), derived from the pinned Node release's ICU.
//
// node scripts/gen-timezone-names.mjs [--tzdata <tzdata.zi>] regenerate the table
// node scripts/gen-timezone-names.mjs [--tzdata <tzdata.zi>] --check verify only
//
// Node prints the zone's long English name for standard or daylight time
// after the GMT offset ("GMT-0500 (Central Daylight Time)"). The name
// depends only on the zone and on whether the instant is in daylight time
// (V8 maps instants outside 1970 to 2038-01-19 into that range), so each
// zone ID in the IANA tzdata (zones and links, read from tzdata.zi) is
// sampled once a day over that range with TZ set to it, and the two names
// Node prints are recorded. Where the name changes together with the UTC
// offset, the name at the larger offset is the daylight name. Zones
// without a CLDR name print Node's "GMT+hh:mm" fallback, which is recorded
// as is.
//
// The runtime takes daylight time from tm_isdst, except for two kinds of
// zones, which get the exact instants at which Node's name changes:
// zones whose tzdata rules use a negative daylight saving (Europe/Dublin's
// winter time, Morocco's Ramadan time), where ICU's data calls the larger
// offset daylight time, and zones that Node shows in daylight time through
// 2038 (British Columbia's permanent UTC-7, which tzdata names standard
// time).
import { spawn } from "node:child_process";
import { readFileSync } from "node:fs";
import { writeFile } from "node:fs/promises";
import { availableParallelism } from "node:os";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
const self = fileURLToPath(import.meta.url);
const root = fileURLToPath(new URL("..", import.meta.url));
const out = join(root, "packages/runtime/src/scr_timezone_data.h");
// V8 names the zone of instants in [0, 2^31 - 1] seconds directly.
const FIRST_MS = 0;
const LAST_MS = 2147483647000;
const DAY_MS = 86400000;
const zoneName = (ms) => {
const text = new Date(ms).toString();
return text.slice(text.indexOf("(") + 1, -1);
};
if (process.argv.includes("--sample")) {
// One zone, selected by TZ: the first name printed, then every change of
// name with the exact instant (ms) and the UTC offsets (minutes) before
// and after it.
const changes = [];
let previous = null;
const visit = (ms) => {
const name = zoneName(ms);
const offset = -new Date(ms).getTimezoneOffset();
if (previous === null) {
changes.push({ at: ms, name, offset });
} else if (previous.name !== name) {
// The first second with the new name.
let lo = previous.ms / 1000;
let hi = ms / 1000;
while (hi - lo > 1) {
const mid = Math.floor((lo + hi) / 2);
if (zoneName(mid * 1000) === name) hi = mid;
else lo = mid;
}
changes.push({ at: hi * 1000, from: previous, name, offset });
}
previous = { ms, name, offset };
};
for (let ms = FIRST_MS; ms < LAST_MS; ms += DAY_MS) visit(ms);
visit(LAST_MS);
process.stdout.write(JSON.stringify(changes));
process.exit(0);
}
const tzdataIndex = process.argv.indexOf("--tzdata");
const tzdata = tzdataIndex >= 0 ? process.argv[tzdataIndex + 1] : "/usr/share/zoneinfo/tzdata.zi";
const checkOnly = process.argv.includes("--check");
/** Zone and link names in tzdata.zi, and the zones that save a negative
* amount at some time from 1970 on. Links name their target. */
function readTzdata(path) {
const text = readFileSync(path, "utf8");
const zones = new Map();
const negativeRules = new Set();
const negativeZones = new Set();
const year = (field) => (field === undefined ? Infinity : Number.parseInt(field, 10));
let zone = null;
for (const line of text.split("\n")) {
const fields = line.trim().split(/\s+/);
if (line.startsWith("R ")) {
// R NAME FROM TO - IN ON AT SAVE LETTER, TO "o" (only) or "ma" (max).
zone = null;
const to =
fields[3] === "o" ? year(fields[2]) : fields[3] === "ma" ? Infinity : year(fields[3]);
if (fields[8]?.startsWith("-") && to >= 1970) negativeRules.add(fields[1]);
continue;
}
if (line.startsWith("L ")) {
zone = null;
zones.set(fields[2], fields[1]);
continue;
}
// Z NAME STDOFF RULES FORMAT [UNTIL], continued by STDOFF RULES FORMAT [UNTIL].
let rest;
if (line.startsWith("Z ")) {
zone = fields[1];
zones.set(zone, zone);
rest = fields.slice(2);
} else if (zone && fields.length >= 3 && !line.startsWith("#")) {
rest = fields;
} else {
zone = null;
continue;
}
const [, rule, , until] = rest;
if (year(until) >= 1970 && (negativeRules.has(rule) || /^-\d/.test(rule)))
negativeZones.add(zone);
}
const version = /^# version (\S+)/.exec(text)?.[1] ?? "unknown";
return { zones, negativeZones, version };
}
/** The zone's [standard, daylight] names as Node prints them: at a change
* of name that also changes the UTC offset, the name printed at the larger
* offset is the daylight name; the name chosen at most such changes wins. */
function classify(id, changes) {
const names = new Set(changes.map((c) => c.name));
if (names.size > 2) throw new Error(`${id}: more than two names`);
if (names.size === 1) {
const [name] = names;
return [name, name];
}
// Zones that moved between offsets keep one daylight name, so the vote
// can include a transition that only changed the standard offset.
const votes = new Map();
for (const c of changes) {
if (!c.from || c.from.offset === c.offset) continue;
const name = c.offset > c.from.offset ? c.name : c.from.name;
votes.set(name, (votes.get(name) ?? 0) + 1);
}
const ranked = [...votes].sort((a, b) => b[1] - a[1]);
if (ranked.length === 0 || (ranked.length === 2 && ranked[0][1] === ranked[1][1]))
throw new Error(`${id}: no daylight name`);
const daylight = ranked[0][0];
const standard = [...names].find((n) => n !== daylight);
return [standard, daylight];
}
function sample(id) {
return new Promise((resolve, reject) => {
const env = { ...process.env, TZ: id, LANG: "en_US.UTF-8" };
delete env.LC_ALL;
delete env.LC_MESSAGES;
const child = spawn(process.execPath, [self, "--sample"], { env });
let text = "";
child.stdout.on("data", (chunk) => (text += chunk));
child.on("error", reject);
child.on("close", (code) =>
code === 0 ? resolve(JSON.parse(text)) : reject(new Error(`${id}: sampler exited ${code}`)),
);
});
}
const { zones, negativeZones, version } = readTzdata(tzdata);
const ids = [...zones.keys()]
.filter((id) => {
try {
new Intl.DateTimeFormat("en-US", { timeZone: id });
return true;
} catch {
return false;
}
})
.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
const samples = new Map();
let next = 0;
await Promise.all(
Array.from({ length: availableParallelism() }, async () => {
while (next < ids.length) {
const id = ids[next++];
samples.set(id, await sample(id));
}
}),
);
// Pools of NUL-terminated strings, addressed by byte offset.
function pool() {
const offsets = new Map();
let text = "";
return {
add(s) {
if (!offsets.has(s)) {
offsets.set(s, Buffer.byteLength(text));
text += s + "\0";
}
return offsets.get(s);
},
get text() {
return text;
},
};
}
const idPool = pool();
const namePool = pool();
const changeSeconds = [];
const rows = ids.map((id) => {
const changes = samples.get(id);
const [standard, daylight] = classify(id, changes);
// Exact daylight-time changes, as epoch seconds at which daylight time
// starts or ends; INT32_MIN first when 1970 starts in daylight time.
let first = 0;
let count = 0;
// Daylight time with no change in the last year sampled.
const last = changes.at(-1);
const permanent = last.name === daylight && LAST_MS - last.at > 366 * DAY_MS;
if (standard !== daylight && (negativeZones.has(zones.get(id)) || permanent)) {
first = changeSeconds.length;
if (changes[0].name === daylight) changeSeconds.push("INT32_MIN");
for (const c of changes.slice(1)) changeSeconds.push(String(c.at / 1000));
count = changeSeconds.length - first;
}
return ` { ${idPool.add(id)}, ${namePool.add(standard)}, ${namePool.add(daylight)}, ${first}, ${count} },`;
});
// The runtime formats into a fixed buffer.
for (const [standard, daylight] of ids.map((id) => classify(id, samples.get(id)))) {
if (Buffer.byteLength(standard) > 64 || Buffer.byteLength(daylight) > 64)
throw new Error(`zone name longer than 64 bytes: ${standard} / ${daylight}`);
}
for (const p of [idPool, namePool]) {
if (Buffer.byteLength(p.text) > 0xffff) throw new Error("string pool exceeds 16-bit offsets");
}
const cString = (text) =>
text
.split("\0")
.slice(0, -1)
.map((s) => ` "${s.replace(/[\\"]/g, "\\$&")}\\0"`)
.join("\n");
const wrap = (items) => {
const lines = [];
let line = " ";
for (const item of items) {
if (line.length + item.length + 2 > 100) {
lines.push(line);
line = " ";
}
line += ` ${item},`;
}
if (line.trim()) lines.push(line);
return lines.join("\n");
};
const header = `/* Generated by scripts/gen-timezone-names.mjs from Node ${process.version}
* (ICU ${process.versions.icu}, CLDR ${process.versions.cldr}) and tzdata ${version}. Do not edit.
* Each IANA zone or link ID maps to the long English names Node prints for
* its standard and daylight time. A row with change_count > 0 takes
* daylight time from scr_tz_daylight_changes[first_change ...]: the epoch
* seconds at which daylight time starts or ends between 1970 and
* 2038-01-19 (INT32_MIN first when 1970 starts in daylight time). Rows are
* sorted by ID. */
#ifndef SCR_TIMEZONE_DATA_H
#define SCR_TIMEZONE_DATA_H
#include <stdint.h>
typedef struct { uint16_t id, standard, daylight, first_change, change_count; } ScrTzNames;
static const char scr_tz_ids[] =
${cString(idPool.text)};
static const char scr_tz_name_text[] =
${cString(namePool.text)};
static const int32_t scr_tz_daylight_changes[] = {
${wrap(changeSeconds)}
};
static const ScrTzNames scr_tz_names[] = {
${rows.join("\n")}
};
#endif
`;
if (checkOnly) {
const current = readFileSync(out, "utf8");
if (current !== header) {
process.stderr.write(`${out} is out of date; run node scripts/gen-timezone-names.mjs\n`);
process.exit(1);
}
process.stdout.write(`${out} is up to date (${ids.length} zones)\n`);
} else {
await writeFile(out, header);
process.stdout.write(`wrote ${out} (${ids.length} zones)\n`);
}
+93
View File
@@ -0,0 +1,93 @@
// Date.prototype.toString, toDateString and toTimeString, String(date),
// template literals and string concatenation print local time with the
// GMT offset and the zone's long name, as Node does. Each write to
// process.env.TZ changes the zone for later conversions and getters.
function at(ms: number): Date {
return new Date(ms);
}
const winter = at(Date.UTC(2026, 0, 15, 12, 30, 45, 678));
const summer = at(Date.UTC(2026, 6, 15, 3, 4, 5));
const epoch = at(0);
const beforeEpoch = at(Date.UTC(1950, 5, 1, 18));
const farFuture = at(Date.UTC(2100, 6, 4, 9));
const yearZero = at(-62167219200000);
const maxDate = at(8.64e15);
const minDate = at(-8.64e15);
const invalid = new Date(NaN);
function show(zone: string): void {
process.env.TZ = zone;
console.log(`== ${zone}`);
console.log(winter.toString());
console.log(String(summer));
console.log(`${epoch} | ${beforeEpoch}`);
console.log("far: " + farFuture);
console.log(winter.toDateString(), "/", winter.toTimeString());
console.log(summer.toDateString(), "/", summer.toTimeString());
console.log(summer.getHours(), summer.getTimezoneOffset());
}
for (const zone of [
"America/Chicago",
"America/New_York",
"America/St_Johns",
"Europe/Dublin",
"Europe/London",
"Europe/Berlin",
"Asia/Kolkata",
"Asia/Kathmandu",
"Asia/Tokyo",
"Australia/Lord_Howe",
"Australia/Sydney",
"Pacific/Chatham",
"America/Sao_Paulo",
"UTC",
"Etc/GMT+5",
"Etc/GMT-14",
"US/Pacific",
":Asia/Seoul",
]) {
show(zone);
}
// Daylight-time edges: the instants before and at a change.
process.env.TZ = "America/Chicago";
const spring = Date.UTC(2026, 2, 8, 8);
const autumn = Date.UTC(2026, 10, 1, 7);
console.log(new Date(spring - 1000).toString(), "|", new Date(spring).toString());
console.log(new Date(autumn - 1000).toTimeString(), "|", new Date(autumn).toTimeString());
process.env.TZ = "Europe/Dublin";
console.log(new Date(Date.UTC(2026, 2, 29, 0, 59, 59)).toString());
console.log(new Date(Date.UTC(2026, 2, 29, 1)).toString());
process.env.TZ = "Australia/Sydney";
console.log(new Date(Date.UTC(2026, 3, 4, 15, 59, 59)).toString());
console.log(new Date(Date.UTC(2026, 3, 4, 16)).toString());
// Extreme and invalid time values.
process.env.TZ = "UTC";
console.log(maxDate.toString(), "|", minDate.toDateString());
console.log(yearZero.toString(), "|", at(Date.UTC(-1, 11, 31, 23)).toDateString());
console.log(at(Date.UTC(-12345, 0, 1)).toDateString(), "|", at(Date.UTC(12345, 0, 1)).toDateString());
console.log(invalid.toString(), invalid.toDateString(), invalid.toTimeString(), `${invalid}`, String(invalid));
// A Date held as unknown converts the same way.
process.env.TZ = "Asia/Kolkata";
const held: unknown = winter;
console.log(String(held), `${held}`);
// Values that are not zone IDs and unset TZ.
for (const zone of ["Not/A_Zone", ""]) {
process.env.TZ = zone;
console.log(JSON.stringify(zone), winter.toString());
}
delete process.env.TZ;
process.env.TZ = "UTC";
console.log(winter.toString());
// Formatting functions and fields.
function stamp(label: string, d: Date): string {
return `${label}: ${d.toDateString()} at ${d.toTimeString().slice(0, 8)}`;
}
const event = { name: "launch", when: summer };
process.env.TZ = "America/New_York";
console.log(stamp(event.name, event.when), "|", event.when.toString().length);
@@ -0,0 +1,29 @@
// Dates held in untyped JavaScript values convert like Node's: string
// conversions use toString() (String, templates, +, ==, join), number
// conversions use the time value (Number, BigInt, unary +, -, *, <, >=),
// and toString, toDateString and toTimeString are callable as methods and
// as function values.
process.env.TZ = "America/Chicago";
const date = new Date(Date.UTC(2026, 9, 18, 14, 12, 30));
const held = [date, new Date(NaN), new Date(0)];
const x = /** @type {any} */ (held[0]);
const bad = /** @type {any} */ (held[1]);
const zero = /** @type {any} */ (held[2]);
console.log(String(x));
console.log(`${x} / ${bad}`);
console.log(x.toString(), "|", x.toDateString(), "|", x.toTimeString());
console.log(held.join(" ; "));
console.log(x + 1, "|", 1 + x, "|", x + "");
console.log(+x, x - 0, x * 1, Number(x), Number(bad), BigInt(zero));
console.log(x > 5, x < zero, x >= x, x <= 1792332720000, bad < x, bad >= x);
console.log(x == x.toString(), x == 1792332750000, zero == "0");
const toTime = x.toTimeString;
const toDate = x.toDateString;
console.log(toTime.call(x), "|", toDate.call(zero));
process.env.TZ = "Asia/Kathmandu";
console.log(String(x), "|", [x, zero].toString());
process.env.TZ = "Europe/Dublin";
console.log(`${zero}`, "|", zero.toTimeString());
+1 -1
View File
@@ -1,5 +1,5 @@
// Unsupported formatters must reach the Date method diagnostic, including
// names inherited from Object.prototype by the compiler's dispatch table.
const date = new Date(0);
date.toString();
date.toUTCString();
date.toLocaleString();
@@ -1,17 +1,17 @@
date-formatters.ts:4:1 - error SC2020: 'Date.prototype.toString' is part of the standard library types but has no scriptc lowering yet
date-formatters.ts:4:1 - error SC2020: 'Date.prototype.toUTCString' is part of the standard library types but has no scriptc lowering yet
3 | const date = new Date(0);
4 | date.toString();
| ^~~~~~~~~~~~~~~
4 | date.toUTCString();
| ^~~~~~~~~~~~~~~~~~
5 | date.toLocaleString();
hint: getTime(), valueOf(), toISOString(), toJSON(), the local/UTC calendar getters, and getTimezoneOffset() are supported; Date setters and locale/string formatters have no lowering
hint: getTime(), valueOf(), toISOString(), toJSON(), toString(), toDateString(), toTimeString(), the local/UTC calendar getters, and getTimezoneOffset() are supported; Date setters, toUTCString() and locale formatters have no lowering
date-formatters.ts:5:1 - error SC2020: 'Date.prototype.toLocaleString' is part of the standard library types but has no scriptc lowering yet
4 | date.toString();
4 | date.toUTCString();
5 | date.toLocaleString();
| ^~~~~~~~~~~~~~~~~~~~~
6 |
hint: getTime(), valueOf(), toISOString(), toJSON(), the local/UTC calendar getters, and getTimezoneOffset() are supported; Date setters and locale/string formatters have no lowering
hint: getTime(), valueOf(), toISOString(), toJSON(), toString(), toDateString(), toTimeString(), the local/UTC calendar getters, and getTimezoneOffset() are supported; Date setters, toUTCString() and locale formatters have no lowering
@@ -66,7 +66,7 @@ stdlib-fence.ts:25:1 - error SC2020: 'Date.prototype.setUTCFullYear with argumen
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
26 | const sameDate = mutableDate === mutableDate; // scalar storage must not fake object identity
hint: getTime(), valueOf(), toISOString(), toJSON(), the local/UTC calendar getters, and getTimezoneOffset() are supported; Date setters and locale/string formatters have no lowering
hint: getTime(), valueOf(), toISOString(), toJSON(), toString(), toDateString(), toTimeString(), the local/UTC calendar getters, and getTimezoneOffset() are supported; Date setters, toUTCString() and locale formatters have no lowering
stdlib-fence.ts:26:18 - error SC1043: comparing non-number, non-string values are not supported yet
+4
View File
@@ -2190,6 +2190,8 @@
"5960-generic-mapped-types.ts": 2540,
"5961-frame-spread-updates.ts": 3260,
"5962-frame-tuples.ts": 3208,
"5963-date-to-string.ts": 2355,
"5964-date-dynamic-conversions.js": 2307,
"5970-net-connect-host-name.ts": 2400,
"5971-buffer-chunks-to-string.ts": 2700,
"5972-net-set-encoding-listeners.ts": 2550,
@@ -5249,6 +5251,8 @@
"5960-generic-mapped-types.ts": 3300,
"5961-frame-spread-updates.ts": 3732,
"5962-frame-tuples.ts": 3372,
"5963-date-to-string.ts": 3199,
"5964-date-dynamic-conversions.js": 3084,
"5970-net-connect-host-name.ts": 2800,
"5971-buffer-chunks-to-string.ts": 3100,
"5972-net-set-encoding-listeners.ts": 3000,