Я согласен с мнением ОП, что это нелогично и неприятно, но так же определяет, что означает +1 month
в сценариях, где это происходит. Рассмотрим эти примеры:
Вы начинаете с 2015-01-31 и хотите добавить месяц 6 раз, чтобы получить цикл планирования для отправки новостной рассылки по электронной почте. С учетом первоначальных ожиданий ОП это вернулось бы:
- 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 мая. Обратите внимание, что это не потому, что это «последний день месяца», а потому, что мы хотим «ближайший доступный к дате начала месяца».
Итак, предположим, что один из наших пользователей подписался на другую рассылку, которая начнется 2015-01-30. Какая интуитивная дата для +1 month
? Одна интерпретация будет «30-й день следующего месяца или ближайший доступный», который будет возвращать:
- 2015-01-30
- 2015-02-28
- 2015-03-30
- 2015-04-30
- 2015-05-30
- 2015-06-30
Это было бы хорошо, за исключением случаев, когда наш пользователь получает обе рассылки в один и тот же день. Давайте предположим, что это проблема стороны предложения, а не стороны спроса. Мы не беспокоимся о том, что пользователь будет раздражен получением двух информационных бюллетеней в один и тот же день, но вместо этого наши почтовые серверы не могут позволить пропускную способность для отправки вдвое много новостных рассылок. Имея это в виду, мы возвращаемся к другой интерпретации «+1 месяц» как «отправка со второго по последний день каждого месяца», которая будет возвращать:
- 2015-01-30
- 2015-02-27
- 2015-03-30
- 2015-05-30
- 2015-06-29
Теперь мы избежали какого-либо совпадения с первым сетом, но мы также заканчиваем с апрелем и 29 июня, что, безусловно, соответствует нашим первоначальным представлениям о том, что +1 month
просто должно вернуть m/$d/Y
или привлекательный и простой m/30/Y
на все возможные месяцы. Итак, теперь давайте рассмотрим третью интерпретацию +1 month
с использованием обеих дат:
Январь * 1063 тридцать первого *
2015-01-31
2015-03-03
2015-03-31
2015-05-01
2015-05-31
2015-07-01
Январь Тридцатые
- 2015-01-30
- 2015-03-02
- 2015-03-30
- 2015-04-30
- 2015-05-30
- 2015-06-30
Выше есть некоторые проблемы. Февраль пропущен, что может быть проблемой как в конце предложения (скажем, если есть ежемесячное распределение пропускной способности, а февраль тратится впустую, а март удваивается), так и в конце спроса (пользователи чувствуют себя обманутыми в феврале и чувствуют дополнительный март как попытка исправить ошибку). С другой стороны, обратите внимание, что два набора дат:
- никогда не перекрываются
- всегда в одну и ту же дату, когда в этом месяце есть дата (поэтому набор 30 января выглядит довольно чистым)
- все в течение 3 дней (в большинстве случаев - 1 день) от того, что можно считать «правильной» датой.
- все по крайней мере 28 дней (лунный месяц) от их преемника и предшественника, поэтому очень равномерно распределены.
Учитывая последние два сета, не составит труда просто откатить одну из дат, если она выходит за пределы фактического следующего месяца (поэтому откат к 28 февраля и 30 апреля в первом сете) и не потерять ни одного сон из-за случайного совпадения и расхождения от паттерна «последний день месяца» против «второго до последнего дня месяца». Но ожидание выбора библиотек между «наиболее красивым / естественным», «математическим толкованием 31/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 недели. Вот результаты с двумя датами испытаний:
Январь * 1117 тридцать первого *
2015-01-31
2015-02-28
2015-03-31
2015-04-28
2015-05-31
2015-06-28
Январь Тридцатые
- 2015-02-27
- 2015-03-30
- 2015-04-30
- 2015-05-30
- 2015-06-30
(Мой код - беспорядок и не будет работать в многолетнем сценарии. Я приветствую всех, кто переписывает решение более элегантным кодом, если основная предпосылка сохраняется, то есть, если +1 месяц вернет фанк вместо даты используйте +4 недели.)