Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions ext/date/lib/timelib.h
Original file line number Diff line number Diff line change
Expand Up @@ -641,6 +641,7 @@ void timelib_strtointerval(const char *s, size_t len,
* calculate the right timestamp.
*/
void timelib_update_ts(timelib_time* time, timelib_tzinfo* tzi);
void timelib_update_ts_first(timelib_time* time, timelib_tzinfo* tzi);

/**
* Returns the number of days from the y/m/d fields of 'time' since the Unix
Expand Down
100 changes: 78 additions & 22 deletions ext/date/lib/tm2unixtime.c
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,50 @@ static void do_adjust_special_early(timelib_time* time)
timelib_do_normalize(time);
}

static void do_adjust_timezone(timelib_time *tz, timelib_tzinfo *tzi)
static bool timelib_adjust_timezone_for_overlap(timelib_time *tz, timelib_tzinfo *tzi, int32_t preferred_offset)
{
timelib_sll earlier_sse = INT64_MAX;
timelib_sll second_earliest_sse = INT64_MAX;
timelib_sll matching_offset_sse = INT64_MAX;
uint64_t type_count = tzi->bit64.typecnt;

for (uint64_t i = 0; i < type_count; i++) {
int32_t offset = tzi->type[i].offset;
int32_t actual_offset;

if ((offset > 0 && tz->sse < INT64_MIN + offset) || (offset < 0 && tz->sse > INT64_MAX + offset)) {
continue;
}

timelib_sll candidate_sse = tz->sse - offset;
if (!timelib_get_time_zone_offset_info(candidate_sse, tzi, &actual_offset, NULL, NULL) || actual_offset != offset) {
continue;
}

if (candidate_sse < earlier_sse) {
second_earliest_sse = earlier_sse;
earlier_sse = candidate_sse;
} else if (candidate_sse > earlier_sse && candidate_sse < second_earliest_sse) {
second_earliest_sse = candidate_sse;
}

if (offset == preferred_offset && candidate_sse < matching_offset_sse) {
matching_offset_sse = candidate_sse;
}
}

if (second_earliest_sse == INT64_MAX) {
return false;
}

/* Keep the previous offset if it is valid for the new local time. */
tz->sse = matching_offset_sse != INT64_MAX ? matching_offset_sse : earlier_sse;
tz->is_localtime = 1;
timelib_set_timezone(tz, tzi);
return true;
}

static void do_adjust_timezone(timelib_time *tz, timelib_tzinfo *tzi, bool resolve_overlap, int32_t preferred_offset)
{
switch (tz->zone_type) {
case TIMELIB_ZONETYPE_OFFSET:
Expand Down Expand Up @@ -373,33 +416,31 @@ static void do_adjust_timezone(timelib_time *tz, timelib_tzinfo *tzi)
if (!tzi) {
return;
}
if (resolve_overlap && timelib_adjust_timezone_for_overlap(tz, tzi, preferred_offset)) {
return;
}

timelib_get_time_zone_offset_info(tz->sse, tzi, &current_offset, &current_transition_time, &current_is_dst);
timelib_get_time_zone_offset_info(tz->sse - current_offset, tzi, &after_offset, &after_transition_time, NULL);
actual_offset = after_offset;
actual_transition_time = after_transition_time;
if (current_offset == after_offset && tz->have_zone) {
/* Make sure we're not missing a DST change because we don't know the actual offset yet */
if (current_offset >= 0 && tz->dst && !current_is_dst) {
/* Timezone or its DST at or east of UTC, so the local time, interpreted as UTC, leaves DST (as defined in the actual timezone) before the actual local time */
int32_t earlier_offset;
timelib_sll earlier_transition_time;
timelib_get_time_zone_offset_info(tz->sse - current_offset - 7200, tzi, &earlier_offset, &earlier_transition_time, NULL);
if ((earlier_offset != after_offset) && (tz->sse - earlier_offset < after_transition_time)) {
/* Looking behind a bit clarified the actual offset to use */
actual_offset = earlier_offset;
actual_transition_time = earlier_transition_time;
}
int32_t earlier_offset;
timelib_sll earlier_transition_time;
timelib_get_time_zone_offset_info(tz->sse - current_offset - 7200, tzi, &earlier_offset, &earlier_transition_time, NULL);
if ((earlier_offset != after_offset) && (tz->sse - earlier_offset < after_transition_time)) {
actual_offset = earlier_offset;
actual_transition_time = earlier_transition_time;
}
} else if (current_offset <= 0 && current_is_dst && !tz->dst) {
/* Timezone west of UTC, so the local time, interpreted as UTC, leaves DST (as defined in the actual timezone) after the actual local time */
int32_t later_offset;
timelib_sll later_transition_time;
timelib_get_time_zone_offset_info(tz->sse - current_offset + 7200, tzi, &later_offset, &later_transition_time, NULL);
if ((later_offset != after_offset) && (tz->sse - later_offset >= later_transition_time)) {
/* Looking ahead a bit clarified the actual offset to use */
actual_offset = later_offset;
actual_transition_time = later_transition_time;
}
int32_t later_offset;
timelib_sll later_transition_time;
timelib_get_time_zone_offset_info(tz->sse - current_offset + 7200, tzi, &later_offset, &later_transition_time, NULL);
if ((later_offset != after_offset) && (tz->sse - later_offset >= later_transition_time)) {
actual_offset = later_offset;
actual_transition_time = later_transition_time;
}
}
}

Expand Down Expand Up @@ -439,8 +480,13 @@ timelib_sll timelib_epoch_days_from_time(timelib_time *time)
return era * DAYS_PER_ERA + day_of_era - HINNANT_EPOCH_SHIFT;
}

void timelib_update_ts(timelib_time* time, timelib_tzinfo* tzi)
static void timelib_update_ts_internal(timelib_time* time, timelib_tzinfo* tzi, bool resolve_overlap)
{
int32_t preferred_offset = time->z;
if (time->sse_uptodate && time->zone_type == TIMELIB_ZONETYPE_ID && time->tz_info) {
timelib_get_time_zone_offset_info(time->sse, time->tz_info, &preferred_offset, NULL, NULL);
}

do_adjust_special_early(time);
do_adjust_relative(time);
do_adjust_special(time);
Expand All @@ -456,12 +502,22 @@ void timelib_update_ts(timelib_time* time, timelib_tzinfo* tzi)
time->sse += timelib_epoch_days_from_time(time) * (SECS_PER_DAY / 2);

// This modifies time->sse, if needed
do_adjust_timezone(time, tzi);
do_adjust_timezone(time, tzi, resolve_overlap, preferred_offset);

time->sse_uptodate = 1;
time->have_relative = time->relative.have_weekday_relative = time->relative.have_special_relative = time->relative.first_last_day_of = 0;
}

void timelib_update_ts(timelib_time* time, timelib_tzinfo* tzi)
{
timelib_update_ts_internal(time, tzi, false);
}

void timelib_update_ts_first(timelib_time* time, timelib_tzinfo* tzi)
{
timelib_update_ts_internal(time, tzi, true);
}

#if 0
int main(void)
{
Expand Down
7 changes: 5 additions & 2 deletions ext/date/php_date.c
Original file line number Diff line number Diff line change
Expand Up @@ -3289,6 +3289,9 @@ static bool php_date_modify(zval *object, char *modify, size_t modify_len) /* {{

memcpy(&dateobj->time->relative, &tmp_time->relative, sizeof(timelib_rel_time));
dateobj->time->have_relative = tmp_time->have_relative;
if (dateobj->time->sse_uptodate && dateobj->time->zone_type == TIMELIB_ZONETYPE_ID && dateobj->time->tz_info) {
timelib_get_time_zone_offset_info(dateobj->time->sse, dateobj->time->tz_info, &dateobj->time->z, NULL, NULL);
}
dateobj->time->sse_uptodate = 0;

if (tmp_time->y != TIMELIB_UNSET) {
Expand Down Expand Up @@ -3332,7 +3335,7 @@ static bool php_date_modify(zval *object, char *modify, size_t modify_len) /* {{

timelib_time_dtor(tmp_time);

timelib_update_ts(dateobj->time, NULL);
timelib_update_ts_first(dateobj->time, NULL);
timelib_update_from_sse(dateobj->time);
dateobj->time->have_relative = 0;
memset(&dateobj->time->relative, 0, sizeof(dateobj->time->relative));
Expand Down Expand Up @@ -3691,7 +3694,7 @@ static void php_date_time_set(zval *object, zend_long h, zend_long i, zend_long
dateobj->time->i = i;
dateobj->time->s = s;
dateobj->time->us = ms;
timelib_update_ts(dateobj->time, NULL);
timelib_update_ts_first(dateobj->time, NULL);
timelib_update_from_sse(dateobj->time);
} /* }}} */

Expand Down
49 changes: 49 additions & 0 deletions ext/date/tests/DateTime_setTime_dst_overlap.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
--TEST--
DateTime::setTime() preserves the offset in an overlapping local time
--FILE--
<?php

foreach ([
['Europe/Dublin', '2026-10-25 00:10:30 UTC', 1, 10],
['Europe/London', '2026-10-25 00:10:30 UTC', 1, 10],
['Europe/Amsterdam', '2026-10-25 00:10:30 UTC', 2, 10],
['America/New_York', '2026-11-01 05:10:30 UTC', 1, 10],
['Australia/Lord_Howe', '2026-04-04 14:45:30 UTC', 1, 45],
] as [$timezone, $instant, $hour, $minute]) {
$date = new DateTimeImmutable($instant);
$date = $date->setTimezone(new DateTimeZone($timezone));
$result = $date->setTime($hour, $minute, 59);
printf("%s %s %+d\n", $timezone, $result->format('H:i:s T'), $result->getTimestamp() - $date->getTimestamp());
}

$date = new DateTime('2026-10-25 01:10:30 UTC');
$date->setTimezone(new DateTimeZone('Europe/Dublin'));
$date->setTime(1, 10, 59);
printf("%s %+d\n", $date->format('H:i:s T'), $date->getTimestamp() - strtotime('2026-10-25 01:10:30 UTC'));

$date = new DateTimeImmutable('2026-10-25 01:10:30 UTC');
$date = $date->setTimezone(new DateTimeZone('Europe/Dublin'));
$result = $date->setTime(1, 10, 59);
printf("%s %+d\n", $result->format('H:i:s T'), $result->getTimestamp() - $date->getTimestamp());

$date = new DateTimeImmutable('2026-10-25 01:10:30 UTC');
$date = $date->setTimezone(new DateTimeZone('Europe/London'));
$result = $date->modify('+29 seconds');
printf("%s %+d\n", $result->format('H:i:s T'), $result->getTimestamp() - $date->getTimestamp());

$date = new DateTimeImmutable('2026-10-25 00:10:30 UTC');
$date = $date->setTimezone(new DateTimeZone('Europe/Dublin'));
$result = $date->modify('+29 seconds');
printf("%s %+d\n", $result->format('H:i:s T'), $result->getTimestamp() - $date->getTimestamp());

?>
--EXPECT--
Europe/Dublin 01:10:59 IST +29
Europe/London 01:10:59 BST +29
Europe/Amsterdam 02:10:59 CEST +29
America/New_York 01:10:59 EDT +29
Australia/Lord_Howe 01:45:59 +11 +29
01:10:59 GMT +29
01:10:59 GMT +29
01:10:59 IST +29
01:10:59 IST +29
Loading