Я погоджуюся з думкою ОП про те, що це контрінтуїтивно та розчаровує, але так визначається, що +1 monthозначає у сценаріях, де це відбувається. Розглянемо наступні приклади:
Ви починаєте з 31.01.2015 і хочете додати місяць 6 разів, щоб отримати цикл планування для надсилання електронних листів. Зважаючи на початкові очікування ОП, це повернеться:
- 2015-01-31
- 28.02.2015
- 2015-03-31
- 2015-04-30
- 2015-05-31
- 2015-06-30
Відразу зауважте, що ми очікуємо +1 monthозначати last day of monthабо, альтернативно, додавати 1 місяць за ітерацію, але завжди посилаючись на початкову точку. Замість того, щоб інтерпретувати це як "останній день місяця", ми могли б читати це як "31 день наступного місяця або останній доступний протягом цього місяця". Це означає, що ми стрибаємо з 30 квітня по 31 травня замість 30 травня. Зауважте, що це не тому, що це "останній день місяця", а тому, що ми хочемо "найближчий доступний до дати початку місяця".
Тож припустимо, що один з наших користувачів підписується на інший інформаційний бюлетень, який розпочнеться 30.01.2015. Для чого інтуїтивно зрозуміла дата +1 month? Одне тлумачення було б "30-го числа наступного місяця або найближчого доступного", яке поверне:
- 30.01.2015
- 28.02.2015
- 2015-03-30
- 2015-04-30
- 2015-05-30
- 2015-06-30
Це було б добре, за винятком випадків, коли наш користувач отримує обидва інформаційні бюлетені в один і той же день. Припустимо, що це питання щодо пропозиції, а не з боку попиту. Ми не переживаємо, що користувач буде роздратований тим, що отримає 2 бюлетені в той же день, але натомість наші поштові сервери не можуть дозволити собі пропускну здатність для надсилання вдвічі багато інформаційних бюлетенів. Зважаючи на це, ми повертаємось до іншої інтерпретації "+1 місяць" як "надіслати другий до останнього дня кожного місяця", яка б повернула:
- 30.01.2015
- 27.02.2015
- 2015-03-30
- 2015-04-29
- 2015-05-30
- 29.06.2015
Зараз ми уникнули будь-якого перекриття з першим набором, але також закінчуємо квітнем та 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
- 01.07.2015
30 січня
- 30.01.2015
- 02.03.2015
- 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
- 28.02.2015
- 2015-03-31
- 2015-04-28
- 2015-05-31
- 28.06.2015
30 січня
- 30.01.2015
- 27.02.2015
- 2015-03-30
- 2015-04-30
- 2015-05-30
- 2015-06-30
(Мій код безладний і не працює в багаторічному сценарії. Я вітаю когось, щоб переписати рішення на більш елегантний код до тих пір, поки базове приміщення залишається недоторканим, тобто, якщо +1 місяць повертає фанк-дату, використовуйте +4 тижні замість цього.)