Carbon, timestamp и timezone

В Laravel для работы со временем используется удобное расширение Carbon. Тут мы столкнулись с его не очевидным поведением при применении timestamp. Unix timestamp всегда в часовом поясе UTC, поэтому логично предположить, что если передаю в Carbon timestamp он будет работать с часовым поясом UTC. Например:


        $timestamp = 1595552400; // 2020-07-24T01:00:00+00:00
        $carbon = Carbon::createFromTimestamp($timestamp);
        echo $carbon->format(Carbon::ATOM); // 2020-07-24T01:00:00+00:00

Еще в Carbon есть замечательная функция startOfDay, которая возвращает начало дня. В нашем примере она ожидаемо работает так:


        $start_of_day_timestamp = $carbon->startOfDay()->timestamp;
        $carbon = Carbon::createFromTimestamp($start_of_day_timestamp);
        echo $start_of_day_timestamp; //1595548800
        echo $carbon->format(Carbon::ATOM); //2020-07-24T00:00:00+00:00

Дальше мы можем работать с другими часовыми поясами, например, так:


        echo $carbon->timezone('America/New_York')->startOfDay()->timestamp; // 1595476800
        echo $carbon->timezone('America/New_York')->format(Carbon::ATOM); // 2020-07-23T20:00:00-04:00

Обратите внимание что startOfDay возвращает разный timestamp для разных часовых поясов: 1595548800 != 1595476800. Это тоже ожидаемо, начало дня в разных часовых поясах отличается относительно Гринвича.

Каково же было наше удивление, когда мы получили такое:


        $timestamp = 1595552400; // 2020-07-24T01:00:00+00:00
        $carbon = Carbon::createFromTimestamp($timestamp);
        echo $carbon->startOfDay()->timestamp; // 1595476800
        echo $carbon->format(Carbon::ATOM); ; // 2020-07-23T20:00:00-04:00

Т.е. мы нигде явно не прописывали часовой пояс, мы передаем timestamp, поэтому ожидаем что работаем в UTC, а на выходе получаем начало дня в NYC. В результате в тестах мы смогли повторить ошибку, выглядит это так:


        $timestamp = 1595552400; // 2020-07-24T01:00:00+00:00

    date_default_timezone_set('UTC');
    $carbon = Carbon::createFromTimestamp($timestamp);
    echo $carbon->startOfDay()->format(Carbon::ATOM); // 2020-07-24T00:00:00+00:00

    date_default_timezone_set('America/New_York');
    $carbon = Carbon::createFromTimestamp($timestamp);
    echo $carbon->startOfDay()->format(Carbon::ATOM); // 2020-07-23T00:00:00-04:00

Это означает что при использовании timestamp Carbon использует часовой пояс по умолчанию, а не UTC. Фикс выглядит так:


    date_default_timezone_set('UTC');
    $carbon = Carbon::createFromTimestamp($timestamp, 'UTC');
    echo $carbon->startOfDay()->format(Carbon::ATOM); // 2020-07-24T00:00:00+00:00

    date_default_timezone_set('America/New_York');
    $carbon = Carbon::createFromTimestamp($timestamp, 'UTC');
    echo $carbon->startOfDay()->format(Carbon::ATOM); // 2020-07-24T00:00:00+00:00

Keep calm and use UTC timezone.

23.07.2020
Tikhon Kozyrev

Вот тут (carbon.nesbot.com/docs) пишут, что createFromTimestamp "will create a Carbon instance equal to the given timestamp and will set the timezone as well or default it to the current timezone". Так что это нормальное поведение , а если надо, чтобы таймзона не обрабатывалась там же говорят, что есть createFromTimestampUTC, который "is different in that the timezone will remain UTC").

23.07.2020 15:46

Tikhon Kozyrev


Carbon::createFromTimestamp($timestamp, 'UTC');
Carbon::createFromTimestampUTC($timestamp); 

Из клиентского кода оба варианта выглядят примерно одинаково, и я даже подумал, что один из методов использует другой, но исходники показывают следующее:


public static function createFromTimestamp($timestamp, $tz = null) { 
 $date = new DateTime('@'.((int) $timestamp)); 
 $tz = static::safeCreateDateTimeZone($tz); 
 if ($tz) { 
 $date->setTimezone($tz); 
 } 
 return (new static($date->format(DateTime::ATOM)))->tz($tz); 
 } 

public static function createFromTimestampUTC($timestamp){ 
 return new static('@'.$timestamp); 
 }

То есть createFromTimestampUTC должен работать быстрее. Если по кейсам преобразование выполняется часто и много - разница в производительности может быть заметной. С другой стороны, если это преобразование выполняется близко к клиентской части, то вероятность того, что преобразование в какой-то момент времени будет в пользовательскую таймзону достаточно высока, чтобы использовать createFromTimestamp с переменной или параметром, хранящими таймзону.

Можно реализовать и компромиссное решение:


function Timstamp2Carbon($timestamp, $timezone=null){
if ($timezone){
return Carbon::createFromTimestamp($timestamp, $timezone);
} else {
return Carbon::createFromTimestampUTC($timestamp);
}

Какой вариант использовать - сильно зависит от кода, в котром это преобразование выполняется...

24.07.2020 05:12

Написать комментарий