Decide a Date's zone name at the instant, not from the offset cache

- Date toString and toTimeString now decide the zone name's daylight flag at the instant itself, as V8's LocalTimezone does. The offset cache's segments hold offsets only, as V8's do. Before, a merged segment kept the DST flag of the read that created it, so a zone that left daylight time without changing its offset could print the summer name after the change: Africa/Algiers on 21 October 1977, at +0100 on both sides.
- Corpus date-zone-intervals.ts reads Algiers across that day in three orders; the names match Node.
This commit is contained in:
Chris Tate
2026-10-21 00:11:47 -05:00
parent 99c63271e9
commit acfa46a919
2 changed files with 34 additions and 26 deletions
+15 -26
View File
@@ -10746,9 +10746,6 @@ 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;
@@ -10785,7 +10782,6 @@ 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;
}
/* The thread's last UTC decomposition: reading several UTC fields of one
@@ -10843,14 +10839,13 @@ static bool scr_date_local_parts_uncached(double clipped, ScrDateParts *out);
* twice within 19 days (America/Argentina/Tucuman in June 2004) breaks that
* assumption, and Node's answers between the two changes then depend on the
* order of the reads; this cache gives the same answers for the same order
* (corpus date-zone-intervals.ts). Segments compare offsets only, as V8's do;
* each keeps the DST flag of the read that set its offset. */
* (corpus date-zone-intervals.ts). Segments hold offsets only, as V8's do;
* zone names decide daylight time at the instant (scr_date_zone_name). */
#define SCR_DATE_ZONE_DELTA_MS (19LL * 86400000LL)
#define SCR_DATE_ZONE_SEGMENTS 32
typedef struct {
long long start_ms, end_ms; /* empty when start_ms > end_ms */
double offset; /* local minus UTC, seconds */
bool isdst;
int last_used;
} ScrDateZoneSegment;
typedef struct {
@@ -10868,7 +10863,6 @@ static void scr_date_zone_clear(ScrDateZoneSegment *segment) {
segment->start_ms = 0;
segment->end_ms = -1;
segment->offset = 0;
segment->isdst = false;
segment->last_used = 0;
}
@@ -10918,7 +10912,7 @@ static void scr_date_zone_probe(ScrDateZoneCache *z, long long t) {
}
/* V8's ExtendTheAfterSegment. */
static void scr_date_zone_extend_after(ScrDateZoneCache *z, long long t, double offset, bool isdst) {
static void scr_date_zone_extend_after(ScrDateZoneCache *z, long long t, double offset) {
ScrDateZoneSegment *after = z->after;
if (!scr_date_zone_empty(after) && after->offset == offset &&
after->start_ms - SCR_DATE_ZONE_DELTA_MS <= t && t <= after->end_ms) {
@@ -10928,11 +10922,11 @@ static void scr_date_zone_extend_after(ScrDateZoneCache *z, long long t, double
if (!scr_date_zone_empty(after)) after = z->after = scr_date_zone_lru(z, z->before);
after->start_ms = after->end_ms = t;
after->offset = offset;
after->isdst = isdst;
after->last_used = ++z->usage;
}
static bool scr_date_zone_lookup(double clipped, double *offset, bool *isdst) {
static bool scr_date_zone_lookup(double clipped, double *offset) {
bool isdst; /* scr_date_zone_offset's flag, unused here */
ScrDateZoneCache *z = &scr_date_zone_cache;
unsigned generation = scr_date_tz_generation;
if (!z->valid || z->generation != generation) scr_date_zone_reset(z, generation);
@@ -10945,28 +10939,25 @@ static bool scr_date_zone_lookup(double clipped, double *offset, bool *isdst) {
if (before->start_ms <= t && t <= before->end_ms) {
before->last_used = ++z->usage;
*offset = before->offset;
*isdst = before->isdst;
return true;
}
scr_date_zone_probe(z, t);
before = z->before;
if (scr_date_zone_empty(before)) {
if (!scr_date_zone_offset(clipped, offset, isdst)) return false;
if (!scr_date_zone_offset(clipped, offset, &isdst)) return false;
before->start_ms = before->end_ms = t;
before->offset = *offset;
before->isdst = *isdst;
before->last_used = ++z->usage;
return true;
}
if (t <= before->end_ms) {
before->last_used = ++z->usage;
*offset = before->offset;
*isdst = before->isdst;
return true;
}
if (t - SCR_DATE_ZONE_DELTA_MS > before->end_ms) {
if (!scr_date_zone_offset(clipped, offset, isdst)) return false;
scr_date_zone_extend_after(z, t, *offset, *isdst);
if (!scr_date_zone_offset(clipped, offset, &isdst)) return false;
scr_date_zone_extend_after(z, t, *offset);
ScrDateZoneSegment *swap = z->before;
z->before = z->after;
z->after = swap;
@@ -10976,9 +10967,8 @@ static bool scr_date_zone_lookup(double clipped, double *offset, bool *isdst) {
long long ahead = before->end_ms + SCR_DATE_ZONE_DELTA_MS;
if (scr_date_zone_empty(z->after) || ahead <= z->after->start_ms) {
double ahead_offset;
bool ahead_dst;
if (!scr_date_zone_offset((double)ahead, &ahead_offset, &ahead_dst)) return false;
scr_date_zone_extend_after(z, ahead, ahead_offset, ahead_dst);
if (!scr_date_zone_offset((double)ahead, &ahead_offset, &isdst)) return false;
scr_date_zone_extend_after(z, ahead, ahead_offset);
} else {
z->after->last_used = ++z->usage;
}
@@ -10987,7 +10977,6 @@ static bool scr_date_zone_lookup(double clipped, double *offset, bool *isdst) {
before->end_ms = after->end_ms;
scr_date_zone_clear(after);
*offset = before->offset;
*isdst = before->isdst;
return true;
}
/* Binary search for the change point, giving up after five probes (the
@@ -10995,7 +10984,7 @@ static bool scr_date_zone_lookup(double clipped, double *offset, bool *isdst) {
for (int i = 4; i >= 0; i--) {
long long delta = after->start_ms - before->end_ms;
long long middle = i == 0 ? t : before->end_ms + delta / 2;
if (!scr_date_zone_offset((double)middle, offset, isdst)) return false;
if (!scr_date_zone_offset((double)middle, offset, &isdst)) return false;
if (before->offset == *offset) {
before->end_ms = middle;
if (t <= before->end_ms) return true;
@@ -11031,12 +11020,10 @@ static bool scr_date_local_parts(double ms, ScrDateParts *out) {
static bool scr_date_local_parts_uncached(double clipped, ScrDateParts *out) {
double local_offset;
bool isdst;
if (!scr_date_zone_lookup(clipped, &local_offset, &isdst)) return false;
if (!scr_date_zone_lookup(clipped, &local_offset)) return false;
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 = isdst;
return true;
}
@@ -11459,7 +11446,9 @@ static ScrStr *scr_date_text(double ms, int mode) {
*o++ = ' ';
*o++ = '(';
/* Zone names are at most 64 bytes (scr_timezone_data.h). */
const char *zone = scr_date_zone_name(ms, p.isdst);
/* The name's daylight flag is decided at the instant itself, as V8's
* LocalTimezone does, not from the offset cache. */
const char *zone = scr_date_zone_name(ms, -1);
size_t zone_len = strlen(zone);
memcpy(o, zone, zone_len);
o += zone_len;
+19
View File
@@ -79,3 +79,22 @@ for (const zone of ["America/Argentina/Tucuman", "America/New_York", "Australia/
for (let order = 0; order < 10; order++) sums.push(orderChecksum(zone, Date.UTC(2003, 0, 1), 3 * 365, 60));
console.log(zone, sums.join(","));
}
// Africa/Algiers kept its +1 offset on 21 October 1977 but left daylight
// time: the zone name changes while the offset does not, in every order of
// reads.
const algiers: number[] = [];
for (let day = 15; day <= 31; day++) algiers.push(Date.UTC(1977, 9, day, 12));
for (const order of ["ascending", "descending", "mixed"]) {
process.env.TZ = "UTC";
process.env.TZ = "Africa/Algiers";
const reads =
order === "ascending"
? algiers
: order === "descending"
? algiers.slice().reverse()
: [algiers[16]!, algiers[0]!, algiers[8]!, ...algiers];
const names = new Map<number, string>();
for (const t of reads) names.set(t, new Date(t).toString().slice(16));
console.log(order, algiers.map((t) => names.get(t)).join(" | "));
}