сегодня работаю над своим проектом - нашел одну странную ошибку.
Используя компонент 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
Я бы сказал, что это не совсем неправильный, просто другой интерпретация или другой выбор решения сложной проблемы: как считать в обратном направлении от начала используемого вами календаря? Окончательная ошибка, которую вы видите, является результатом смешивания библиотек, которые выбирают решения другой и поэтому неправильно интерпретируют вывод друг друга.
Первое, что нужно знать, это то, что функции, которые вы использовали, построены на двух разных библиотеках:
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 года, но расходятся во мнениях относительно более ранних дат:
Документация по отделению интенсивной терапии объясняет это поведение переключения и как на него повлиять. Хотя 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?
@yAnTar Да, в частности, не следует смешивать два набора функций даты — он должен либо последовательно использовать IntlDateFormatter, либо последовательно использовать DateTime/DateTimeImmutable; обе библиотеки будут вести себя согласованно внутри себя. Если ему действительно нужно смешать эти два, он может использовать трюк с календарем, описанный выше, чтобы использовать IntlDateFormatter с той же «пролептической григорианской» интерпретацией прошлых дат, которую использует timelib.
Спасибо за такой развернутый ответ, так что в данном случае это ошибка symfony/form (потому что код взят оттуда), создаст проблему на Github.