Записки программиста
Часовые пояса обычно ассоциируются с чем-то невероятно сложным. Однако, как это часто бывает, если сесть и спокойно во всем разобраться, то проблема оказывается не такой уж большой. Особенно если положиться на проверенные решения, где уже реализована вся логика.
PostgreSQL предлагает два типа для хранения даты и времени — timestamp и timestamptz. Внутреннее представление данных типов абсолютно одинаковое. Это 64-х битные знаковые целые, хранящие время в микросекундах относительно 1-го января 2000-го года 00:00:00 UTC.
Отличие только в том, как они отображаются:
С timestamp все понятно, это же просто время в UTC. Однако timestamptz отображается в часовом поясе текущей сессии:
=# show time zone;
TimeZone
—————
Europe/Moscow
=# select ‘2021-10-01 12:34:56 Europe/Moscow’ :: timestamptz;
timestamptz
————————
2021-10-01 12:34:56+03
=# set time zone ‘+00’;
SET
=# select ‘2021-10-01 12:34:56 Europe/Moscow’ :: timestamptz;
timestamptz
————————
2021-10-01 09:34:56+00
Еще раз, ключевой момент — значение одно и то же! Но отображается оно по-разному в зависимости от параметров сессии.
Часовой пояс, используемый по умолчанию, определяется параметром timezone в файле postgresql.conf. Этот параметр прописывается утилитой initdb во время инициализации СУБД. Значение параметра определяется окружением, в котором был запущен initdb. Если параметр не указан в postgresql.conf, используется часовой пояс GMT.
Получить информацию о доступных именах часовых поясов можно так:
=# select * from pg_timezone_names where abbrev = ‘MSK’;
name | abbrev | utc_offset | is_dst
——————-+———+————+———
Europe/Moscow | MSK | 03:00:00 | f
Europe/Simferopol | MSK | 03:00:00 | f
W-SU | MSK | 03:00:00 | f
Отсюда мы узнаем, что для Europe/Moscow есть сокращение MSK. Сокращения можно использовать при преобразовании строки в timestamptz:
=# select ‘2021-10-01 12:34:56 MSK’ :: timestamptz;
timestamptz
————————
2021-10-01 12:34:56+03
Но команда set time zone принимает только полное имя часового пояса:
=# set time zone ‘MSK’;
ERROR: invalid value for parameter «TimeZone»: «MSK»
=# set time zone ‘Europe/Moscow’;
SET
Иногда нужно преобразовать timestamptz в timestamp или наоборот. Если делать это в лоб, то результат может получиться не тот, который вы ожидаете:
=# set time zone ‘+00’;
SET
=# select (‘2021-10-01 12:34:56 MSK’ :: timestamptz) :: timestamp;
timestamp
———————
2021-10-01 09:34:56
Как же так? Ведь я хотел отрезать информацию о таймзоне и получить 12:34:56! Но вспомним, что внутри timestamptz хранится просто как время в UTC. В момент, когда timestamptz кастуется в timestamp у PostgreSQL нет никакой возможности узнать, что изначально время было в Europe/Moscow. Поэтому он преобразует время в часовой пояс текущий сессии.
Домашнее задание: Вызовите now() и now() :: timestamp . Смените часовой пояс сессии и повторите эксперимент. Объясните результат.
Для решения описанной проблемы есть альтернативный синтаксис:
=# select (‘2021-10-01 12:34:56 Europe/Moscow’ :: timestamptz) ⏎
at time zone ‘Europe/Moscow’;
timezone
———————
2021-10-01 12:34:56
Или, что полностью эквивалентно, можно воспользоваться функцией timezone:
=# select timezone(‘Europe/Moscow’, ⏎
‘2021-10-01 12:34:56 Europe/Moscow’ :: timestamptz);
timezone
———————
2021-10-01 12:34:56
Для преобразования в обратную сторону есть перегруженная версия timezone, принимающая timestamp и возвращающая timestamptz:
=# set time zone ‘Europe/Moscow’;
SET
=# select timezone(‘Europe/Moscow’, ‘2021-10-01 12:34:56’ :: timestamp);
timezone
————————
2021-10-01 12:34:56+03
Синтаксис at time zone также работает для timestamp:
=# select (‘2021-10-01 12:34:56’ :: timestamp) ⏎
at time zone ‘Europe/Moscow’;
timezone
————————
2021-10-01 12:34:56+03
Когда вы работаете в UTC или каком-нибудь UTC+3, все просто и понятно. Но при использовании часового пояса вроде MSK все интереснее. Вот пример:
=# set time zone ‘Europe/Moscow’;
SET
=# select (timestamptz ‘2014 Oct 26 01:00:00 MSK’);
timestamptz
————————
2014-10-26 01:00:00+03
=# select (timestamptz ‘2014 Oct 26 01:00:00 MSK’) — interval ‘1 hour’;
?column?
————————
2014-10-26 01:00:00+04
Был час ночи. Отняли один час, и получили час ночи. Удобно, не правда ли?
Дело в том, что московское время несколько раз менялось в прошлом, и наверняка еще изменится в будущем. 26 октября 2014-го года часовой пояс MSK как раз изменился с UTC+4 на UTC+3, о чем PostgreSQL прямым текстом и говорит — обратите внимание на +03 и +04 в выводе. Аналогичные приколы вас ждут в часовых поясах, где есть перевод часов на летнее и зимнее время (daylight saving time, DST). В Москве сейчас не переводят часы, но до марта 2011-го года переводили.
Fun fact! Если у времени в конкретном часовом поясе могут быть «дырки», то как timezone(zone, timestamp) обрабатывает невозможное время, попадающие в эти «дырки»? В данном сценарии функция завершается успешно, при этом невозможное время преобразуется в ближайшее возможное.
Для timestamptz из прошлого PostgreSQL применяет правила для часового пояса, которые были актуальны на тот момент времени. Приведенный выше пример наглядно это демонстрирует. Другими словами, если сейчас в вашем часовом поясе время T, оно всегда будет временем T в этом часовом поясе, даже если в будущем правила изменятся. Для времени из будущего применяются последние известные правила. Это означает, что при обновлении PostgreSQL и/или системы могут появится новые правила для часовых поясов, и функции, использующие timestamptz, начнут возвращать другой результат. Впрочем, это касается не только времени из будущего. Также были прецеденты исторических корректировок часовых поясов.
Несмотря на написанное выше, volatility функций, работающих с timestamptz, не обязательно является STABLE. В качестве STABLE-функции, работающей с timestamptz, можно привести в пример date_trunc() . Дело в том, что ее работа зависит от параметров сессии. Кастинг timestamptz в timestamp является STABLE по той же причине. Однако функция timezone() не смотрит на параметры сессии. Ее возвращаемое значение зависит только от переданных аргументов, и потому функция является IMMUTABLE. PostgreSQL не будет возражать, если вы решите использовать ее в функциональных индексах. И при обновлении базы часовых поясов индексы могут разъехаться.
Fun fact! PostgreSQL имеет собственную базу часовых поясов. В исходном коде эта база называется tzdata.zi. Но также PostgreSQL можно собрать с флагом —with-system-tzdata= . Он говорит использовать вместо собственной базы системные TZif-файлы (RFC 8536) . Эти файлы обычно живут в /usr/share/zoneinfo. Чтобы узнать, какую базу использует PostgreSQL, нужно проверить, с какими флагами был собран используемый вами пакет. Эти флаги отображает команда pg_config —configure .
Все перечисленное важно понимать для решения практических задач.
Допустим, вам нужно строить агрегаты по дням или месяцам в конкретном часовом поясе. Оказывается, что самое простое и надежное решение — это преобразовать время в timestamp при помощи at time zone , и затем обращаться с timestamp как с обычным временем. Если нужно несколько часовых поясов, строим несколько агрегатов. Здесь предполагается, что вы не работаете с временем из далекого будущего и своевременно обновляетесь. В противном случае агрегаты рано или поздно сломаются, и починить их сможет только перестройка из сырых данных. Хранить последние — всегда хорошая мысль, как минимум, потому что требования к системе меняются, и возникает необходимость в новых агрегатах. Ну и баги в коде никто не отменял.
Вы можете прислать свой комментарий мне на почту, или воспользоваться комментариями в Telegram-группе.
Europe/Moscow и Etc/GMT-3 — это одно и то же?
А вы ведь в PHP разбираетесь?
У меня в PHP.ini date.timezone = «Europe/Moscow», а в скрипте date_default_timezone_set(‘Etc/GMT-3’);
Что нужно сделать, чтобы они были одинаковыми?
Дед Мазай Гений (58121) PHP я не знаю, однако иногда отвечаю на вопросы по PHP. Думаю, надо убрать эту строку из скрипта. Попробуйте.
Остальные ответы
GMT-3 — это московское время. Это значит, что время по Гринвичу, плюс три часа.
Andry NevermindУченик (71) 5 лет назад
Europe/Moscow и Etc/GMT-3 — это одно и то же?
Кубин Мастер (2022) Ну если это касается времени, то это одно и то же.
Etc/UTC — это timezone в базе данных Olson-timezone-database ( TZ database), также известной как IANA-timezones-database, в которой все часовые пояса соответствуют единому соглашению об именовании: Area/Location .
Поскольку некоторые часовые пояса не могут быть отнесены к какой-либо области мира (например, континентам или океанам), была введена специальная область Etc ( и так далее). Это относится в основном к административным часовым поясам, таким как UTC .
Таким образом, в соответствии с Соглашением об именовании универсальное координированное время (зона) называется Etc/UTC в базе данных tz.
Для административных часовых поясов, отличных от UTC (например, GMT+4, GMT-8), база данных tz использует знаки в стиле POSIX в названиях зон. POSIX имеет положительные знаки для зон, которые находятся позади Гринвича (к западу от Гринвича), и отрицательные знаки для зон, которые находятся впереди Гринвича (к востоку от Гринвича).
POSIX-стилевые знаки в часовых поясах противоположны определению часовых поясов в ныне широко распространенном и в основном используемом ISO 8601 . В формате ISO 8601 timezone отрицательные знаки указывают на то, что зона находится позади UTC (к западу от Гринвича), а положительные знаки указывают на то, что зона находится впереди UTC (к востоку от Гринвича). Это то, что стало стандартным использованием в наши дни.
Возможными причинами противоположного определения в POSIX являются:
POSIX является частью UNIX, который был разработан в USA, который находится за UTC (к западу от Гринвича). Формат POSIX позволяет представлять часовые пояса US как EST5, PST8, то есть опуская знак ( + ).
Как правило, компьютерных программ и операционных систем, вынужденных делать все, что в UTC время. С помощью знаков в стиле POSIX вы можете добавить время и timezone, чтобы получить время UTC. Пример: «03:30 PST8 «или» 03:30 GMT+8 «означают, что это»11:30 UTC».
И у Вас получится что Московское время GMT+3 = Etc/GMT-3.
Не спорьте! Всем добра! 😉
Пользовательские часовые пояса

Как работать в одном приложении со множеством часовых поясов, которые выбирают сами пользователи, но при этом в базе данных все временные метки будут храниться в едином UTC-формате, либо в часовом поясе заданном по умолчанию в приложении, что позволит нам отображать одну и ту же информацию по-разному без нарушения ее целостности.
Часовые пояса в PHP и MySQL
Заданный часовой пояс будет использоваться для PHP-функций date и datetime , и это важно, поскольку он влияет не только на вычисления даты и времени, например, на разницу между указанной датой и текущим моментом, но и на то, как метки времени будут храниться в базе данных.
Часовой пояс в PHP задается в файле php.ini , например:
[Date] date.timezone = "Europe/Moscow"
Также его можно задать на системном уровне:
date_default_timezone_set ('Europe/Moscow');
Все часовые пояса, которые поддерживает PHP, можно увидеть здесь.
В MySQL дефолтный часовой пояс задается глобально в файле /etc/mysql/my.cnf , через указание GMT-разницы ((Greenwich Mean Time, Среднее время по Гринвичу)) желаемой зоны:
[mysqld] default-time-zone = "+03:00"
Также можно задать глобальную переменную:
sudo mysql -e "SET GLOBAL time_zone = '+03:00';"
Если мы используем PDO, то можем задать часовой пояс для каждого соединения, указая GMT-разницу:
$conn->exec("SET time_zone='+03:00';");
Часовые пояса в Laravel
В Laravel по умолчанию часовой пояс установлен на UTC (Coordinated Universal Time, Всемирное координированное время), хотя можно задать другой, изменив значение timezone в config/app.php . Получить доступ к этому значению можно через config(‘app.timezone’) .
Если ваше приложение будет использоваться пользователями с разными часовыми поясами, то рекомендуется использовать UTC в качестве общесистемного часового пояса, чтобы все даты и временные метки хранились в базе данных как UTC, независимо от часового пояса пользователя (тем более, что он может измениться, например, если пользователь путешествует и зашел из другой страны), а затем конвертировать временные метки в часовой пояс пользователя, чтобы он видел их правильно.
Существуют разные подходы к решению этой проблемы, в этой статье мы попытаемся охватить различные глобальные параметры, чтобы пользователь мог задать свой часовой пояс и увидеть дату и время в соответствии со своими предпочтениями.
Поле timezone в таблице пользователей
Поскольку каждый пользователь может задать свой часовой пояс, то нам нужно дополнительное поле в таблице пользователей для хранения этой информации.
php artisan make:migration add_timezone_field_to_users_table --table=users
use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; class AddTimezoneFieldToUsersTable extends Migration < /** * Run the migrations. * * @return void */ public function up() < Schema::table('users', function (Blueprint $table) < $table->string('timezone', 40)->nullable(); >); > /** * Reverse the migrations. * * @return void */ public function down() < Schema::table('users', function (Blueprint $table) < $table->dropColumn('timezone'); >); > >
При этом подходе мы задаем поле как nullable , так как нет нужды хранить ненужную информацию, если пользователь использует UTC или дефолтный часовой пояс. Хранить нужно только отличающиеся часовые пояса, если пользователь их задаст.
Добавление Аксессора и Мутатора для часового пояса
После добавления timezone в $fillable[] в модели User , мы зададим аксессор и мутатор для установки и получения этого атрибута.
Поскольку, при этом подходе, поле часового пояса может быть нулевым (например, если оно равно системному часовому поясу, или если мы добавляем эту функцию в уже существующую систему, где у пользователей пока нет часового пояса), то сделаем аксессор, который, если часовой пояс пользователя пуст, выдает дефолтный часовой пояс.
В пользовательской модели зададим:
public function getTimeZoneAttribute ($value): string
Так как мы не будем хранить в базе данных часовые пояса аналогичные дефолтному, то добавим Мутатор, который, если часовой пояс, выбранный пользователем, равен дефолтному, то он не будет сохранен в базе данных.
public function setTimeZoneAttribute($value) < $this->attributes['timezone'] = $value == config('app.timezone') || is_null($value) ? null : $value; >
Хелпер Timezone
Напишем несколько хелперов, которые можно будет использовать многократно.
Существует много способов подключения хелперов, например создать app/Helpers/Helpers.php со следующей структурой:
namespace App\Helpers; use Illuminate\Support\Facades\App; class Helpers < //. >
И добавить в файл composer.json ссылку на него:
"files": [ "app/Helpers/Helpers.php" ]
Также добавим в файл config/app.php в $aliases[] ссылки на этот класс:
'aliases' => [ //.. 'Helpers' => App\Helpers\Helpers::class, ],
В Helpers.php добавим два новых хелпера для часовых поясов:
static public function getTimeZoneList() < return \Cache::rememberForever('timezones_list_collection', function () < $timestamp = time(); foreach (timezone_identifiers_list(\DateTimeZone::ALL) as $key =>$value) < date_default_timezone_set($value); $timezone[$value] = $value . ' (UTC ' . date('P', $timestamp) . ')'; >return collect($timezone)->sortKeys(); >); >
Этот метод создаст упорядоченную коллекцию с доступными часовыми поясами (используемыми в качестве ключа) вместе со смещением по Гринвичу, как показано на изображении ниже. Поскольку эта информация не будет меняться, то мы бессрочно закэшируем её.

Мы можем заменить этот метод упрощенной версией и использовать встроенную PHP-функцию timezone_identifiers_list() , которая вернет массив со всеми поддерживаемыми часовыми поясами.
Также добавим метод getUserTimeZone() , который попытается получить часовой пояс, заданный пользователем, если таковой существует, а в противном случае он вернет дефолтный часовой пояс.
Здесь мы будем использовать Laravel-хелпер optional() , чтобы избежать ошибок, если пользователь не залогинен.
static public function getUserTimeZone() < return optional(auth()->user())->timezone ?? config('app.timezone'); >
Выбор часового пояса пользователем
Используя ранее созданный хелпер, добавим форму, в которой пользователь может задавать свой часовой пояс. Например:
В Laravel есть правило проверки timezone , поэтому мы можем легко валидировать введенное значение:
'timezone' => ['required', 'timezone']
Получение часового пояса пользователя
Если мы хотим предварительно задать часовой пояс пользователя в форме или добавить его в скрытом поле в процессе регистрации, то можно получить текущий часовой пояс пользователя из js-библиотеки moment .
Для этого нам нужны библиотеки moment и moment-timezone . Будем использовать метод moment.tz.guess() :
Либо можно получить эту информацию и без использования внешних библиотек, но такой код может не заработать на старых браузерах:
Intl.DateTimeFormat().resolvedOptions().timeZone;
Использование часового пояса пользователя в контроллерах и blade-шаблонах
Существуют различные варианты преобразования дат и временных меток с использованием часового пояса пользователя.
Если раздел требует, чтобы пользователь был авторизован, то мы можем использовать, например:
timezone ) >>
Или хелпер, чтобы учесть неавторизованные случаи и возвращать дефолтный часовой пояс:
Чтобы отпарсить временную метку с использованием часового пояса пользователя:
Carbon::parse( $model->timestamp_field )->setTimezone( Helpers::getUserTimeZone() );
Сохранение временных меток в базе данных в формате UTC
Обычно пользователь добавляет временные метки, выбирая дату и время из javascript-библиотеки, используя свой собственный часовой пояс, поэтому мы должны преобразовать эту дату в UTC, чтобы сохранить ее в нашей базе данных.
Предположим, что пользователь может добавлять элементы и задавать их дату создания, а часовой пояс отличается от дефолтного часового пояса. Один из способов преобразования такой даты перед сохранением:
$request->merge([ 'created_at' => Carbon::parse( $request->input('created_at'), Helpers::getUserTimeZone() ) ->setTimeZone( config('app.timezone') ) ->format( 'Y-m-d H:i:s' ), ]);
Таким образом, мы указываем парсеру, что часовой пояс данной временной метки имеет тип, и что мы преобразуем его в UTC или дефолтный часовой пояс нашего приложения.
Важно, чтобы это преобразование выполнялось после валидации, поскольку в противном случае, если проверка не удалась, то при вызове old() в шаблоне, пользователь увидит дату в формате UTC, а не в своем часовом поясе.
Пользователь имеет возможность редактировать этот элемент, поэтому сделаем, чтобы он отображался в его часовом поясе — преобразуем его перед отправкой в шаблон:
$model->created_at = Carbon::parse( $model->created_at )->setTimezone( Helpers::getUserTimeZone() );
Аксессоры часового пояса пользовательской модели
Если мы хотим, чтобы временные метки времени по умолчанию возвращались в часовом поясе пользователя (или дефолтном, если пользователь не залогинен или не установил свой часовой пояс), то можна задать аксессоры в модели.
public function getCreatedAtAttribute($value): Carbon < return Carbon::parse($value)->timezone(Helpers::getUserTimeZone()); > public function getUpdatedAtAttribute($value): Carbon < return Carbon::parse($value)->timezone(Helpers::getUserTimeZone()); >
Blade-компонент для дат с часовым поясом пользователя
Чтобы упростить преобразование временных меток с часовым поясом пользователя в Blade, мы можем создать отдельный компонент.
php artisan make:component DateTimeZone --inline
Используем —inline , чтобы избежать создания шаблона, поскольку для упрощения этого компонента в нём нет необходимости. Преобразованная дата будет возвращена непосредственно из метода render() класса app/View/Components/DateTimeZone.php .
Код этого файла:
namespace App\View\Components; use App\Helpers\Helpers; use Carbon\Carbon; use Illuminate\View\Component; class DateTimeZone extends Component < public Carbon $date; public mixed $format; /** * Create a new component instance. * * @return void */ public function __construct(Carbon $date, $format = null) < $this->date = $date->setTimezone(Helpers::getUserTimeZone()); $this->format = $format; > protected function format() < return $this->format ?? 'Y-m-d H:i:s'; > /** * Get the view / contents that represent the component. * * @return \Illuminate\Contracts\View\View|\Closure|string */ public function render() < return $this->date->format($this->format()); > >
$format задан как mixed , поскольку может иметь тип как string так и null , если отсутствует. Снова будем использовать хелпер getUserTimeZone() , чтобы получить, либо часовой пояс, заданный пользователем, либо дефолтный.
В Blade можем использовать этот компонент, передавая ему временную метку для преобразования и, необязательно, указывая выходной формат, который по умолчанию будет, если этот параметр не указан, Y md H: i: s .
created_at" /> created_at" format="d-m-Y H:i:s" /> created_at" format="d/m/YH:i" />
Нерекомендуемый подход
Некоторые, при работе с несколькими часовыми поясами в Laravel, создают мидлвар, в котором дефолтный часовой пояс задается на основе часового пояса пользователя с помощью команды date_default_timezone_set() .
Этого делать не рекомендуется, поскольку таким образом часовой пояс всей системы изменяется для каждого пользователя и в базе данных будут сохраняться временные метки с разными часовыми поясами.
Наш Телеграм-канал — следите за новостями о Laravel.
Задать вопросы по урокам можно на нашем форуме.
Yii Framework
совершенно случайно наткнулся на следующее — в таблицах выводится время с неправильным таймзон, несмотря на то что в системе, в бд, в пхп все прописано, и все работает, а в yii2 нет.
система centos7, php 5.6, postgresql9.4, yii2.0.3
везде установлена Europe/Moscow, сервисы перегружены для применения настроек
$date
Mon Mar 30 23:47:54 MSK 2015
$php -r ‘echo date(«Y-m-d H:i:s T»);’
2015-03-30 23:49:24 MSK
в код вставляю — тоже самое
2015-03-30 23:52:44 MSK
в конфиге Yii пробывал и внутренний и php
'formatter' => [ 'timeZone' => 'Europe/Moscow', 'dateFormat' => 'd.MM.Y', 'timeFormat' => 'H:mm:ss', //'datetimeFormat' => 'd.MM.Y HH:mm', 'datetimeFormat' => 'php:Y-m-d H:i:s', ],
в бд, в ячейке:
2015-03-30 23:42:49.420899+03
а в таблице выводит
2015-03-31 00:42:49
откуда такое может быть ?
yii имеет свои файлы таймзон ?
upd
указал в конфиге
'datetimeFormat' => 'php:Y-m-d H:i:s T O',
2015-03-31 00:42:49 GMT+04:00
а если убрать форматирование, то выдает в таблице как и должно быть
2015-03-30 23:42:49.420899+03
yiijeka Сообщения: 3103 Зарегистрирован: 2012.01.28, 09:14 Откуда: Беларусь Контактная информация:
Re: Time zone — Europe/Moscow
Сообщение yiijeka » 2015.03.31, 08:09
в GridView по умолчанию используется Intl форматирование, для него нужно тоже указать:
'formatter' => [ 'defaultTimeZone' => 'Europe/Minsk' ],
‘dateFormat’ и прочие форматы лучше оставить без изменений, обычно в нужном, принятом формате всё показывается само.
В таблице можно явно указать формат:
[ 'attribute'=>'value1', 'format'=>['date', 'short'] // или вместо short в формате php:d.m.Y ,что равнозначно ]
sm-vasya Сообщения: 191 Зарегистрирован: 2015.03.04, 01:12
Re: Time zone — Europe/Moscow
Сообщение sm-vasya » 2015.03.31, 13:03
yiijeka писал(а): в GridView по умолчанию используется Intl форматирование, для него нужно тоже указать:
'formatter' => [ 'defaultTimeZone' => 'Europe/Minsk' ],
‘dateFormat’ и прочие форматы лучше оставить без изменений, обычно в нужном, принятом формате всё показывается само.
а не подскажете более точнее куда вписать это ? если в конфиг, в ‘components’ => ‘formatter’ то уже вписал
yiijeka писал(а): В таблице можно явно указать формат:
[ 'attribute'=>'value1', 'format'=>['date', 'short'] // или вместо short в формате php:d.m.Y ,что равнозначно ]
да дело собственно не в формате, а в том что часы в другом часовом поясе
mickgeek Сообщения: 957 Зарегистрирован: 2014.05.31, 20:50 Откуда: Санкт-Петербург Контактная информация: