Я согласен с мнением OP о том, что это противоречит интуиции и разочаровывает, но также определяет, что +1 month
означает в сценариях, где это происходит. Рассмотрим эти примеры:
Вы начинаете с 2015-01-31 и хотите добавить месяц 6 раз, чтобы получить цикл планирования для отправки информационного бюллетеня по электронной почте. Учитывая первоначальные ожидания OP, это вернется:
- 2015-01-31
- 2015-02-28
- 2015-03-31
- 2015-04-30
- 2015-05-31
- 2015-06-30
Сразу же обратите внимание, что мы ожидаем +1 month
иметь в виду last day of month
или, альтернативно, добавить 1 месяц за итерацию, но всегда со ссылкой на начальную точку. Вместо того, чтобы интерпретировать это как «последний день месяца», мы могли бы прочитать его как «31-й день следующего месяца или последний доступный в этом месяце». Это означает, что мы переходим с 30 апреля на 31 мая, а не на 30 мая. Обратите внимание, что это не потому, что это «последний день месяца», а потому, что мы хотим «ближайший доступный к дате начала месяца».
Итак, предположим, что один из наших пользователей подписывается на другой информационный бюллетень, который начнется 30 января 2015 года. Для чего нужна интуитивная дата +1 month
? Одна интерпретация будет «30-е число следующего месяца или ближайший доступный», который вернет:
- 2015-01-30
- 2015-02-28
- 2015-03-30
- 2015-04-30
- 2015-05-30
- 2015-06-30
Это было бы хорошо, кроме случаев, когда наш пользователь получает обе рассылки в один и тот же день. Предположим, что это проблема со стороны предложения, а не со стороны спроса. Мы не беспокоимся о том, что пользователя раздражает получение 2 информационных бюллетеней в один и тот же день, а вместо этого наши почтовые серверы не могут позволить себе пропускную способность для отправки в два раза больше. много информационных бюллетеней. Имея это в виду, мы возвращаемся к другой интерпретации «+1 месяц» как «отправлять со второго по последний день каждого месяца», которая вернется:
- 2015-01-30
- 2015-02-27
- 2015-03-30
- 2015-04-29
- 2015-05-30
- 2015-06-29
Теперь мы избежали какого-либо совпадения с первым набором, но мы также закончили апрель и 29 июня, что, безусловно, соответствует нашей первоначальной интуиции, которая +1 month
просто должна вернуться m/$d/Y
или привлекательной и простой m/30/Y
для всех возможных месяцев. Итак, теперь давайте рассмотрим третью интерпретацию +1 month
использования обеих дат:
31 января
- 2015-01-31
- 2015-03-03
- 2015-03-31
- 2015-05-01
- 2015-05-31
- 2015-07-01
30 января
- 2015-01-30
- 2015-03-02
- 2015-03-30
- 2015-04-30
- 2015-05-30
- 2015-06-30
Вышесказанное имеет некоторые проблемы. Февраль пропускается, что может быть проблемой как при окончании предложения (скажем, если есть ежемесячное распределение пропускной способности и февраль тратится впустую, а март удваивается), так и при окончании спроса (пользователи чувствуют себя обманутыми после февраля и воспринимают дополнительный мартовский период). как попытка исправить ошибку). С другой стороны, обратите внимание, что установлены две даты:
- никогда не перекрывать
- всегда в тот же день, когда в этом месяце есть дата (так что набор 30 января выглядит довольно чистым)
- все находятся в пределах 3 дней (в большинстве случаев 1 день) от того, что можно считать "правильной" датой.
- все они находятся на расстоянии не менее 28 дней (лунный месяц) от своего преемника и предшественника, поэтому распределены очень равномерно.
Учитывая последние два набора, было бы несложно просто откатить одну из дат, если она выпадет за пределы фактического следующего месяца (поэтому откатитесь к 28 февраля и 30 апреля в первом наборе) и не потерять сон в течение случайное перекрытие и отклонение от модели «последний день месяца» и «второй и последний день месяца». Но ожидание, что библиотека будет выбирать между «самым красивым / естественным», «математической интерпретацией переполнения 02/31 и других месяцев» и «относительно первого или последнего месяца», всегда заканчивается тем, что чьи-то ожидания не оправдываются и какой-то график, требующий корректировки «неправильной» даты, чтобы избежать реальной проблемы, связанной с «неправильной» интерпретацией.
Итак, опять же, хотя я также ожидал, +1 month
что верну дату, которая на самом деле будет в следующем месяце, это не так просто, как интуиция, и, учитывая выбор, использование математики вместо ожиданий веб-разработчиков, вероятно, является безопасным выбором.
Вот альтернативное решение, которое по-прежнему неуклюже, но, на мой взгляд, дает хорошие результаты:
foreach(range(0,5) as $count) {
$new_date = clone $date;
$new_date->modify("+$count month");
$expected_month = $count + 1;
$actual_month = $new_date->format("m");
if($expected_month != $actual_month) {
$new_date = clone $date;
$new_date->modify("+". ($count - 1) . " month");
$new_date->modify("+4 weeks");
}
echo "* " . nl2br($new_date->format("Y-m-d") . PHP_EOL);
}
Это не оптимально, но основная логика такова: если добавление 1 месяца приводит к дате, отличной от ожидаемой в следующем месяце, отбросьте эту дату и вместо этого добавьте 4 недели. Вот результаты с двумя датами тестирования:
31 января
- 2015-01-31
- 2015-02-28
- 2015-03-31
- 2015-04-28
- 2015-05-31
- 2015-06-28
30 января
- 2015-01-30
- 2015-02-27
- 2015-03-30
- 2015-04-30
- 2015-05-30
- 2015-06-30
(Мой код беспорядочный и не будет работать в многолетнем сценарии. Я приветствую всех, кто переписывает решение с более элегантным кодом, пока основная предпосылка остается неизменной, т.е. если +1 месяц возвращает фанковую дату, используйте +4 недели вместо этого.)