Convert a Date to local time once for its local getters and follow TZ changes (#1313)

This commit is contained in:
Chris Tate
2026-10-18 13:57:42 -05:00
committed by GitHub
4 changed files with 164 additions and 5 deletions
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5683-date-local-fields-tz.ts"
],
"diags": []
}
+61 -5
View File
@@ -824,6 +824,26 @@ static void scr_node_debug_capture(const ScrStr *name) {
if (name->len == 10 && memcmp(name->data, "NODE_DEBUG", 10) == 0) (void)scr_node_debug_initial();
}
/* Counts writes and deletes of process.env.TZ. The local-time getters keep
* their last conversion for the generation it was made in. */
#ifdef SCR_WORKERS
static atomic_uint scr_date_tz_generation;
#else
static unsigned scr_date_tz_generation;
#endif
/* After a write or delete of the process's TZ, local time follows the new
* value, as in Node: tzset(3) rereads it for later localtime calls. */
static void scr_env_tz_changed(const ScrStr *name) {
/* Compared by character: a string literal would join the merged
* read-only strings of every program that links this file. */
if (name->len != 2 || name->data[0] != 'T' || name->data[1] != 'Z') return;
#if !defined(_WIN32) && !defined(__wasi__)
tzset();
#endif
scr_date_tz_generation++;
}
/* process.env.NAME = v — setenv(3): later scr_env_get reads and spawned
* children (posix_spawn inherits environ) observe the write, like Node.
* Both args borrowed (NUL-terminated ScrStr data); setenv copies. */
@@ -842,6 +862,7 @@ void scr_env_set(const ScrStr *name, const ScrStr *value) {
#else
setenv(k, v, 1);
#endif
scr_env_tz_changed(name);
#ifdef SCR_WORKERS
done:
#endif
@@ -861,6 +882,7 @@ void scr_env_unset(const ScrStr *name) {
#else
unsetenv(name->data);
#endif
scr_env_tz_changed(name);
}
static ScrStr *scr_fs_read_file_core(ScrStr *path, bool env_file, size_t *ascii);
@@ -10475,9 +10497,14 @@ typedef struct ScrDateParts {
* endpoint. The checked wrapper is the public UTC-getter gate; the inner
* walk accepts those bounded local-time values too. */
static void scr_date_utc_parts_unchecked(double t, ScrDateParts *out) {
double dayd = floor(t / 86400000.0);
long long msday = (long long)(t - dayd * 86400000.0);
long long z = (long long)dayd + 719468;
/* t is an integral number of milliseconds within about 2^53. */
long long tms = (long long)t;
long long day = tms / 86400000, msday = tms % 86400000;
if (msday < 0) {
msday += 86400000;
day--;
}
long long z = day + 719468;
long long era = (z >= 0 ? z : z - 146096) / 146097;
unsigned long long doe = (unsigned long long)(z - era * 146097);
unsigned long long yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
@@ -10487,7 +10514,7 @@ static void scr_date_utc_parts_unchecked(double t, ScrDateParts *out) {
unsigned long long d = doy - (153 * mp + 2) / 5 + 1;
unsigned long long m = mp < 10 ? mp + 3 : mp - 9;
if (m <= 2) y++;
long long wday = ((long long)dayd + 4) % 7;
long long wday = (day + 4) % 7;
if (wday < 0) wday += 7;
out->year = y;
out->month = (int)m - 1;
@@ -10516,10 +10543,39 @@ static bool scr_date_localtime(double secd, struct tm *out) {
#endif
}
/* The thread's last local conversion: reading several local fields of one
* Date (getFullYear, getMonth, getDate, getHours, ...) converts it once.
* A change of process.env.TZ starts a new generation. */
typedef struct {
bool valid;
unsigned generation;
double ms;
ScrDateParts parts;
} ScrDateLocalCache;
static SCR_TL ScrDateLocalCache scr_date_local_cache;
static bool scr_date_local_parts_uncached(double clipped, ScrDateParts *out);
static bool scr_date_local_parts(double ms, ScrDateParts *out) {
if (!isfinite(ms) || fabs(ms) > 8640000000000000.0) return false;
double clipped = trunc(ms);
double secd = floor(clipped / 1000.0);
ScrDateLocalCache *c = &scr_date_local_cache;
unsigned generation = scr_date_tz_generation;
if (c->valid && c->ms == clipped && c->generation == generation) {
*out = c->parts;
return true;
}
if (!scr_date_local_parts_uncached(clipped, out)) return false;
c->valid = true;
c->generation = generation;
c->ms = clipped;
c->parts = *out;
return true;
}
static bool scr_date_local_parts_uncached(double clipped, ScrDateParts *out) {
long long cms = (long long)clipped;
double secd = (double)(cms / 1000 - (cms % 1000 < 0));
struct tm tmv;
double basis_secd = secd;
if (!scr_date_localtime(basis_secd, &tmv)) {
+95
View File
@@ -0,0 +1,95 @@
// Local-time getters read repeatedly on one Date and alternately on two,
// on Dates parsed from strings, and across process.env.TZ writes and
// deletes: every read answers the fields of the zone in effect at that
// moment, as in Node.
function fields(d: Date): string {
return [
d.getFullYear(),
d.getMonth(),
d.getDate(),
d.getDay(),
d.getHours(),
d.getMinutes(),
d.getSeconds(),
d.getMilliseconds(),
d.getTimezoneOffset(),
].join(",");
}
const a = new Date(Date.UTC(2024, 2, 10, 6, 59, 59, 999));
const b = new Date(Date.UTC(2024, 2, 10, 7, 0, 0, 0));
const c = new Date(Date.UTC(2024, 10, 3, 5, 30, 0, 250));
function report(label: string): void {
console.log(label, fields(a), fields(a));
console.log(label, fields(b), fields(c), fields(b));
let mixed = 0;
for (let i = 0; i < 6; i++) mixed += a.getHours() * 100 + b.getHours() + c.getDate();
console.log(label, "alternating", mixed);
}
report("start");
process.env.TZ = "America/New_York";
report("new york");
process.env.TZ = "Asia/Kolkata";
report("kolkata");
process.env.TZ = "Australia/Lord_Howe";
report("lord howe");
// Dates parsed from strings with an explicit offset, read in local time.
const d = new Date("2024-06-01T23:45:00+05:30");
console.log("offset string", fields(d), d.getTime());
const e = new Date("2024-07-11T00:15:30.001Z");
console.log("offset string", fields(e), e.getTime());
console.log("first again", fields(d));
delete process.env.TZ;
report("deleted");
process.env.TZ = "UTC";
report("utc");
// Many instants read field by field.
process.env.TZ = "Europe/Berlin";
let sum = 0;
for (let i = 0; i < 2000; i++) {
const t = new Date(1700000000000 + i * 3_599_999);
sum += t.getHours() + t.getDate() * 3 + t.getMonth() * 7 + t.getTimezoneOffset();
}
console.log("berlin sum", sum);
const invalid = new Date(Number.NaN);
console.log("invalid", fields(invalid), fields(invalid));
// Instants around day and second boundaries, before the epoch and at the
// ends of the Date range, in UTC and local fields; then 2000-2030 in a zone
// with DST.
function utcFields(d: Date): string {
return [
d.getUTCFullYear(),
d.getUTCMonth(),
d.getUTCDate(),
d.getUTCDay(),
d.getUTCHours(),
d.getUTCMinutes(),
d.getUTCSeconds(),
d.getUTCMilliseconds(),
].join(",");
}
const edges = [
0, -0, -0.5, 0.5, -1, 1, -999, -1000, -1001, 999.9, -86_400_000, -86_400_001, 86_399_999,
-62_198_755_200_000, -62_198_755_200_001, 8_640_000_000_000_000, -8_640_000_000_000_000,
8_639_999_999_999_999, -8_639_999_999_999_999, 951_782_400_000, 4_107_542_399_999,
];
process.env.TZ = "UTC";
for (const t of edges) {
const x = new Date(t);
console.log(t, utcFields(x), fields(x));
}
process.env.TZ = "America/Sao_Paulo";
let walk = 0;
for (let t = 946_684_800_000; t < 1_893_456_000_000; t += 77_777_777) {
const x = new Date(t);
walk = (walk * 31 + x.getUTCDate() + x.getUTCHours() * 7 + x.getDate() + x.getMinutes() + x.getDay()) % 1_000_003;
}
console.log("walk", walk);
+2
View File
@@ -1997,6 +1997,7 @@
"5680-array-growth-after-copy.ts": 3791,
"5681-array-pop-and-length.ts": 3754,
"5682-publish-wide-objects.ts": 3326,
"5683-date-local-fields-tz.ts": 3848,
"5700-cycle-release-retained.ts": 2156,
"5701-cycle-release-old-edges.ts": 2601,
"5702-publish-candidate-buffers.ts": 3469,
@@ -5026,6 +5027,7 @@
"5680-array-growth-after-copy.ts": 4581,
"5681-array-pop-and-length.ts": 3216,
"5682-publish-wide-objects.ts": 3376,
"5683-date-local-fields-tz.ts": 3911,
"5700-cycle-release-retained.ts": 3414,
"5701-cycle-release-old-edges.ts": 4210,
"5702-publish-candidate-buffers.ts": 3848,