Ошибка с преобразованием «юлианских дат» в unixtimestamp и обратно в DateTime

сегодня работаю над своим проектом - нашел одну странную ошибку. Используя компонент symfony/form (5.4.5), обнаружил, что дата, переданная как 0001-01-01 после преобразования (на стороне Symfony), возвращается как 0000-12-30.

Итак, я провел небольшое расследование (возьмите код из symfony/form, который делает это преобразование с датами, и обнаружил одну очень интересную вещь - даты до 1582 года (переход от юлианского календаря к григорианскому) преобразуются неправильно.

Ниже я привожу пример кода и его вывод.

    $dateFormat = 2;
    $timeFormat = -1;
    $timezone = new \DateTimeZone('UTC');
    $calendar = 1;
    $pattern = 'yyyy-MM-dd';

    $dateFormatter = new \IntlDateFormatter(\Locale::getDefault(), $dateFormat, $timeFormat, $timezone, $calendar, $pattern);

    $dates = ['0001-01-01', '0002-01-01', '1000-01-01', '1582-01-01', '1583-01-01', '1600-01-01', '1800-01-01'];
    foreach ($dates as $dateInput) {
        $timestamp = $dateFormatter->parse($dateInput);

        $dateTime = new \DateTime(date('Y-m-d', $timestamp), new \DateTimeZone('Europe/Berlin'));
        $dateOutput = $dateTime->format('Y-m-d');
        echo $dateInput . " " .$dateOutput . PHP_EOL;
    }
0001-01-01 0000-12-30 - BAD
0002-01-01 0001-12-30 - BAD
1000-01-01 1000-01-06 - BAD
1582-01-01 1582-01-11 - BAD
1583-01-01 1583-01-01 - GOOD
1600-01-01 1600-01-01 - GOOD
1800-01-01 1800-01-01 - GOOD
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Symfony Station Communiqué - 7 июля 2023 г
Symfony Station Communiqué - 7 июля 2023 г
Это коммюнике первоначально появилось на Symfony Station .
Оживление вашего приложения Laravel: Понимание режима обслуживания
Оживление вашего приложения Laravel: Понимание режима обслуживания
Здравствуйте, разработчики! В сегодняшней статье мы рассмотрим важный аспект управления приложениями, который часто упускается из виду в суете...
Установка и настройка Nginx и PHP на Ubuntu-сервере
Установка и настройка Nginx и PHP на Ubuntu-сервере
В этот раз я сделаю руководство по установке и настройке nginx и php на Ubuntu OS.
Коллекции в Laravel более простым способом
Коллекции в Laravel более простым способом
Привет, читатели, сегодня мы узнаем о коллекциях. В Laravel коллекции - это способ манипулировать массивами и играть с массивами данных. Благодаря...
Как установить PHP на Mac
Как установить PHP на Mac
PHP - это популярный язык программирования, который используется для разработки веб-приложений. Если вы используете Mac и хотите разрабатывать...
0
0
30
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Я бы сказал, что это не совсем неправильный, просто другой интерпретация или другой выбор решения сложной проблемы: как считать в обратном направлении от начала используемого вами календаря? Окончательная ошибка, которую вы видите, является результатом смешивания библиотек, которые выбирают решения другой и поэтому неправильно интерпретируют вывод друг друга.

Первое, что нужно знать, это то, что функции, которые вы использовали, построены на двух разных библиотеках:

  • IntlDateFormatter, как часть расширения PHP «intl», построен на отделение интенсивной терапии (международные компоненты для Unicode), поддерживаемом Консорциумом Unicode.
  • Функция date и класс DateTime построены на timelib, поддерживаемом Дериком Ретансом.

Следующее, на что следует обратить внимание, как вы уже указали, это то, что они начинают расходиться во мнениях относительно даты, когда впервые был введен григорианский календарь. Мы можем уточнить год: он был введен в октябре 1582 года, а за 4 октября по юлианскому календарю последовало 15 октября по григорианскому календарю. Важно, это означает, что есть 10 дат, которые не существует в тех частях света, которые переключаются сразу.

Наконец, вместо того, чтобы форматировать туда и обратно, давайте посмотрим на фактические метки времени, созданные двумя библиотеками. Обратите внимание, что отметка времени Unix должна представлять количество секунд после (или, в данном случае, до) момента времени, который в григорианском календаре и часовом поясе UTC представлен как 1970-01-01 00:00:00.

Чтобы упростить чтение временных меток, давайте разделим их, чтобы показать количество дни, которое, по их словам, находится между указанной датой и 1 января 1970 года.

$dateFormatter = new \IntlDateFormatter(\Locale::getDefault(), 2, -1, new \DateTimeZone('UTC'), 1, 'yyyy-MM-dd');

$dates = [
    '1582-10-03', '1582-10-04', '1582-10-05', '1582-10-06', '1582-10-07', '1582-10-08', '1582-10-09',
    '1582-10-10', '1582-10-11', '1582-10-12', '1582-10-13', '1582-10-14', '1582-10-15', '1582-10-16'
];
foreach ($dates as $dateInput) {
    $icuTimestamp = $dateFormatter->parse($dateInput);
    $timelibTimestamp = DateTimeImmutable::createFromFormat('Y-m-d|', $dateInput)->getTimestamp();
    
    $icuDays = abs(intval( $icuTimestamp / 60 / 60 / 24 ));
    $timeLibDays = abs(intval( $timelibTimestamp / 60 / 60 / 24 ));

    echo "Is {$dateInput} {$icuDays} days or {$timeLibDays} days before 1970?\n";
}

Результат выглядит следующим образом:

Is 1582-10-03 141429 days or 141439 days before 1970?
Is 1582-10-04 141428 days or 141438 days before 1970?
Is 1582-10-05 141427 days or 141437 days before 1970?
Is 1582-10-06 141426 days or 141436 days before 1970?
Is 1582-10-07 141425 days or 141435 days before 1970?
Is 1582-10-08 141424 days or 141434 days before 1970?
Is 1582-10-09 141423 days or 141433 days before 1970?
Is 1582-10-10 141422 days or 141432 days before 1970?
Is 1582-10-11 141421 days or 141431 days before 1970?
Is 1582-10-12 141420 days or 141430 days before 1970?
Is 1582-10-13 141419 days or 141429 days before 1970?
Is 1582-10-14 141418 days or 141428 days before 1970?
Is 1582-10-15 141427 days or 141427 days before 1970?
Is 1582-10-16 141426 days or 141426 days before 1970?

Как и ожидалось, две библиотеки согласны с тем, что 15.10.82 было за 141 427 дней до 1970 года, но расходятся во мнениях относительно более ранних дат:

  • timelib использует «пролептический григорианский календарь», предполагая, что 14.10.1582 на один день раньше, чем 15.10.1582, и так далее; это исторически неточно, но с ним проще работать
  • ICU пытается интерпретировать даты так, как это сделали бы люди в Европе в то время, так что 04.10.1582 всего на 1 день раньше, чем 15.10.1582; это оставляет некоторые неоднозначные даты в середине, которые, согласно этой интерпретации, вообще никогда не должны были существовать, которые ICU интерпретирует как юлианские, со странным эффектом, что 1582-10-05 дает то же значение, что и 1582-10-15

Документация по отделению интенсивной терапии объясняет это поведение переключения и как на него повлиять. Хотя PHP не полностью задокументирован, PHP предоставляет соответствующие методы, поэтому можно установить отключение произвольно далеко в прошлое, чтобы соответствовать поведению timelib:

$cal = IntlGregorianCalendar::createInstance();
$cal->setGregorianChange(PHP_INT_MIN);
$dateFormatter->setCalendar($cal);

Добавьте это к предыдущему коду, и мы получим соответствующие выходные данные:

Is 1582-10-03 141439 days or 141439 days before 1970?
Is 1582-10-04 141438 days or 141438 days before 1970?
Is 1582-10-05 141437 days or 141437 days before 1970?
Is 1582-10-06 141436 days or 141436 days before 1970?
Is 1582-10-07 141435 days or 141435 days before 1970?
Is 1582-10-08 141434 days or 141434 days before 1970?
Is 1582-10-09 141433 days or 141433 days before 1970?
Is 1582-10-10 141432 days or 141432 days before 1970?
Is 1582-10-11 141431 days or 141431 days before 1970?
Is 1582-10-12 141430 days or 141430 days before 1970?
Is 1582-10-13 141429 days or 141429 days before 1970?
Is 1582-10-14 141428 days or 141428 days before 1970?
Is 1582-10-15 141427 days or 141427 days before 1970?
Is 1582-10-16 141426 days or 141426 days before 1970?

Спасибо за такой развернутый ответ, так что в данном случае это ошибка symfony/form (потому что код взят оттуда), создаст проблему на Github.

yAnTar 07.04.2022 12:53

@yAnTar Да, в частности, не следует смешивать два набора функций даты — он должен либо последовательно использовать IntlDateFormatter, либо последовательно использовать DateTime/DateTimeImmutable; обе библиотеки будут вести себя согласованно внутри себя. Если ему действительно нужно смешать эти два, он может использовать трюк с календарем, описанный выше, чтобы использовать IntlDateFormatter с той же «пролептической григорианской» интерпретацией прошлых дат, которую использует timelib.

IMSoP 07.04.2022 12:57

Другие вопросы по теме