Zgadzam się z opinią OP, że jest to sprzeczne z intuicją i frustrujące, ale tak samo jest z określeniem, co +1 month
oznacza w scenariuszach, w których tak się dzieje. Rozważ te przykłady:
Rozpoczynasz od 31.01.2015 i chcesz dodać miesiąc 6 razy, aby uzyskać cykl planowania wysyłania biuletynu e-mail. Biorąc pod uwagę początkowe oczekiwania PO, powróci to:
- 2015-01-31
- 2015-02-28
- 2015-03-31
- 2015-04-30
- 2015-05-31
- 2015-06-30
Od razu zauważ, że spodziewamy się, że będziemy +1 month
oznaczać last day of month
lub, alternatywnie, dodawać 1 miesiąc na iterację, ale zawsze w odniesieniu do punktu początkowego. Zamiast interpretować to jako „ostatni dzień miesiąca”, możemy go odczytać jako „31 dzień następnego miesiąca lub ostatni dostępny w tym miesiącu”. Oznacza to, że skaczemy od 30 kwietnia do 31 maja zamiast do 30 maja. Zauważ, że nie dzieje się tak dlatego, że jest to „ostatni dzień miesiąca”, ale dlatego, że chcemy mieć „najbliższą dostępną datę miesiąca początkowego”.
Załóżmy więc, że jeden z naszych użytkowników subskrybuje inny biuletyn, który rozpocznie się 30.01.2015. Po co jest intuicyjna data +1 month
? Jedna interpretacja to „30 dzień następnego miesiąca lub najbliższy dostępny”, co zwróci:
- 2015-01-30
- 2015-02-28
- 2015-03-30
- 2015-04-30
- 2015-05-30
- 2015-06-30
Byłoby to w porządku, z wyjątkiem sytuacji, gdy nasz użytkownik otrzyma oba biuletyny tego samego dnia. Załóżmy, że jest to problem po stronie podaży, a nie po stronie popytu Nie martwimy się, że użytkownik będzie zirytowany otrzymaniem 2 biuletynów tego samego dnia, ale zamiast tego nasze serwery pocztowe nie mogą pozwolić sobie na przepustowość do wysyłania dwa razy więcej wiele biuletynów. Mając to na uwadze, wrócimy do innej interpretacji „+1 miesiąc” jako „wyślij przedostatniego dnia każdego miesiąca”, która zwróci:
- 2015-01-30
- 2015-02-27
- 2015-03-30
- 2015-04-29
- 2015-05-30
- 2015-06-29
Teraz uniknęliśmy jakiegokolwiek nakładania się pierwszego zestawu, ale kończymy również na 29 kwietnia i czerwca, co z pewnością pasuje do naszych pierwotnych intuicji, które +1 month
po prostu powinny powrócić m/$d/Y
lub są atrakcyjne i proste m/30/Y
na wszystkie możliwe miesiące. Rozważmy teraz trzecią interpretację +1 month
użycia obu dat:
31 stycznia
- 2015-01-31
- 2015-03-03
- 2015-03-31
- 2015-05-01
- 2015-05-31
- 2015-07-01
30 stycznia
- 2015-01-30
- 2015-03-02
- 2015-03-30
- 2015-04-30
- 2015-05-30
- 2015-06-30
Powyższe ma pewne problemy. Luty jest pomijany, co może stanowić problem zarówno pod koniec podaży (powiedzmy, że miesięczny przydział przepustowości, a luty się marnuje, a marzec się podwoi) i popyt (użytkownicy czują się oszukani po lutym i dostrzegają dodatkowy marzec jako próba naprawienia błędu). Z drugiej strony zwróć uwagę, że te dwa zestawy dat:
- nigdy się nie nakładają
- są zawsze w tym samym dniu, w którym data ma ten miesiąc (więc zestaw z 30 stycznia wygląda całkiem czysto)
- są w ciągu 3 dni (w większości przypadków 1) od daty, którą można uznać za „prawidłową”.
- wszystkie są co najmniej 28 dni (miesiąc księżycowy) od swojego następcy i poprzednika, więc są bardzo równomiernie rozłożone.
Biorąc pod uwagę ostatnie dwa zestawy, nie byłoby trudno po prostu cofnąć jedną z dat, jeśli przypada ona poza faktyczny następny miesiąc (więc cofnij się do 28 lutego i 30 kwietnia w pierwszym zestawie) i nie stracić snu w ciągu sporadyczne nakładanie się i rozbieżność między wzorcem „ostatni dzień miesiąca” a „od drugiego do ostatniego dnia miesiąca”. Ale oczekiwanie, że biblioteka wybierze między „najpiękniejszą / naturalną”, „matematyczną interpretacją danych z 31 lutego i innych przepełnień miesiąca” oraz „w stosunku do pierwszego lub ostatniego miesiąca” zawsze kończy się niespełnieniem czyichś oczekiwań i jakiś harmonogram wymaga dostosowania „złej” daty, aby uniknąć rzeczywistego problemu, który wprowadza „zła” interpretacja.
Więc znowu, chociaż spodziewałbym +1 month
się zwrócić datę, która faktycznie przypada w następnym miesiącu, nie jest to tak proste, jak intuicja, a biorąc pod uwagę możliwości, przejście z matematyką ponad oczekiwania twórców stron internetowych jest prawdopodobnie bezpiecznym wyborem.
Oto alternatywne rozwiązanie, które nadal jest tak samo niezgrabne, jak inne, ale myślę, że ma dobre wyniki:
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);
}
Nie jest to optymalne, ale podstawowa logika jest taka: jeśli dodanie 1 miesiąca skutkuje datą inną niż oczekiwany następny miesiąc, wyrzuć tę datę i zamiast tego dodaj 4 tygodnie. Oto wyniki z dwoma datami testu:
31 stycznia
- 2015-01-31
- 2015-02-28
- 2015-03-31
- 2015-04-28
- 2015-05-31
- 2015-06-28
30 stycznia
- 2015-01-30
- 2015-02-27
- 2015-03-30
- 2015-04-30
- 2015-05-30
- 2015-06-30
(Mój kod jest bałaganem i nie działałby w scenariuszu wieloletnim. Zapraszam każdego do przepisania rozwiązania z bardziej eleganckim kodem, o ile podstawowa przesłanka pozostanie nienaruszona, tj. Jeśli +1 miesiąc zwraca funkową datę, użyj +4 tygodnie zamiast tego.)