mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-04 08:04:37 +08:00
Convert a Date to local time once for its local getters and follow TZ changes (#1313)
This commit is contained in:
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5683-date-local-fields-tz.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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);
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user