diff --git a/ext/date/lib/timelib.h b/ext/date/lib/timelib.h index d08aa77ea5e5..0a10e41e51eb 100644 --- a/ext/date/lib/timelib.h +++ b/ext/date/lib/timelib.h @@ -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 diff --git a/ext/date/lib/tm2unixtime.c b/ext/date/lib/tm2unixtime.c index 4c0252e244dd..adf8595ba113 100644 --- a/ext/date/lib/tm2unixtime.c +++ b/ext/date/lib/tm2unixtime.c @@ -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: @@ -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, ¤t_offset, ¤t_transition_time, ¤t_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; + } } } @@ -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); @@ -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) { diff --git a/ext/date/php_date.c b/ext/date/php_date.c index 33a6c5b3ccfc..7d62d69614e7 100644 --- a/ext/date/php_date.c +++ b/ext/date/php_date.c @@ -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) { @@ -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)); @@ -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); } /* }}} */ diff --git a/ext/date/tests/DateTime_setTime_dst_overlap.phpt b/ext/date/tests/DateTime_setTime_dst_overlap.phpt new file mode 100644 index 000000000000..ecf3d3f62034 --- /dev/null +++ b/ext/date/tests/DateTime_setTime_dst_overlap.phpt @@ -0,0 +1,49 @@ +--TEST-- +DateTime::setTime() preserves the offset in an overlapping local time +--FILE-- +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