Sono d'accordo con il parere dell'OP che questo è controintuitivo e frustrante, ma lo è anche determinare cosa +1 month
significa negli scenari in cui ciò si verifica. Considera questi esempi:
Inizi con 2015-01-31 e desideri aggiungere un mese 6 volte per ottenere un ciclo di pianificazione per l'invio di una newsletter via email. Tenendo a mente le aspettative iniziali del PO, questo ritornerebbe:
- 2015/01/31
- 2015/02/28
- 2015/03/31
- 2015/04/30
- 2015/05/31
- 2015/06/30
Notare subito che ci aspettiamo +1 month
di significare last day of month
o, in alternativa, di aggiungere 1 mese per iterazione ma sempre in riferimento al punto di partenza. Invece di interpretarlo come "ultimo giorno del mese", potremmo leggerlo come "31esimo giorno del mese successivo o ultimo disponibile entro quel mese". Ciò significa che passiamo dal 30 aprile al 31 maggio invece che al 30 maggio. Tieni presente che questo non è perché è "l'ultimo giorno del mese" ma perché vogliamo "la disponibilità più vicina alla data del mese di inizio".
Supponiamo quindi che uno dei nostri utenti si iscriva a un'altra newsletter per iniziare il 30/01/2015. A cosa serve la data intuitiva +1 month
? Un'interpretazione potrebbe essere "30 ° giorno del mese successivo o più vicino disponibile" che restituirebbe:
- 2015/01/30
- 2015/02/28
- 2015/03/30
- 2015/04/30
- 2015/05/30
- 2015/06/30
Questo andrebbe bene tranne quando il nostro utente riceve entrambe le newsletter lo stesso giorno. Supponiamo che questo sia un problema dal lato dell'offerta anziché dal lato della domanda Non siamo preoccupati che l'utente sarà infastidito dal ricevere 2 newsletter nello stesso giorno, ma invece che i nostri server di posta non possono permettersi la larghezza di banda per inviare molte newsletter. Con questo in mente, torniamo all'altra interpretazione di "+1 mese" come "invio dal penultimo giorno di ogni mese" che restituirebbe:
- 2015/01/30
- 2015/02/27
- 2015/03/30
- 2015/04/29
- 2015/05/30
- 2015/06/29
Ora abbiamo evitato qualsiasi sovrapposizione con il primo set, ma ci ritroviamo anche con aprile e 29 giugno, che sicuramente corrispondono alle nostre intuizioni originali che +1 month
semplicemente dovrebbero tornare m/$d/Y
o attraenti e semplici m/30/Y
per tutti i mesi possibili. Quindi ora consideriamo una terza interpretazione +1 month
dell'uso di entrambe le date:
31 gennaio
- 2015/01/31
- 2015/03/03
- 2015/03/31
- 2015/05/01
- 2015/05/31
- 2015/07/01
30 gennaio
- 2015/01/30
- 2015/03/02
- 2015/03/30
- 2015/04/30
- 2015/05/30
- 2015/06/30
Quanto sopra ha alcuni problemi. Febbraio viene saltato, il che potrebbe essere un problema sia alla fine dell'offerta (diciamo se c'è un'allocazione mensile della larghezza di banda e febbraio va sprecato e marzo viene raddoppiato) che alla fine della domanda (gli utenti si sentono ingannati da febbraio e percepiscono il marzo in più come tentativo di correggere l'errore). D'altra parte, nota che i due set di date:
- non si sovrappongono mai
- sono sempre nella stessa data in cui quel mese ha la data (quindi il set del 30 gennaio sembra abbastanza pulito)
- sono tutti entro 3 giorni (1 giorno nella maggior parte dei casi) da quella che potrebbe essere considerata la data "corretta".
- sono tutti ad almeno 28 giorni (un mese lunare) dal loro successore e predecessore, quindi distribuiti in modo molto uniforme.
Date le ultime due serie, non sarebbe difficile ripristinare semplicemente una delle date se cade al di fuori del mese successivo effettivo (quindi tornare al 28 febbraio e al 30 aprile nella prima serie) e non perdere il sonno durante il sovrapposizione occasionale e divergenza dal modello "ultimo giorno del mese" rispetto al modello "penultimo giorno del mese". Ma aspettarsi che la biblioteca scelga tra "molto carino / naturale", "interpretazione matematica del 31/02 e altri mesi in eccesso" e "relativo al primo o all'ultimo mese" finirà sempre con le aspettative di qualcuno che non vengono soddisfatte e un programma che necessita di aggiustare la data "sbagliata" per evitare il problema del mondo reale che l'interpretazione "sbagliata" introduce.
Quindi di nuovo, anche se mi aspetterei +1 month
di restituire una data che in realtà è nel mese successivo, non è così semplice come intuizione e date le scelte, andare con la matematica oltre le aspettative degli sviluppatori web è probabilmente la scelta sicura.
Ecco una soluzione alternativa che è ancora goffa come qualsiasi altra, ma penso che abbia buoni risultati:
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);
}
Non è ottimale, ma la logica sottostante è: se l'aggiunta di 1 mese risulta in una data diversa dal mese successivo previsto, elimina quella data e aggiungi invece 4 settimane. Ecco i risultati con le due date dei test:
31 gennaio
- 2015/01/31
- 2015/02/28
- 2015/03/31
- 2015/04/28
- 2015/05/31
- 2015/06/28
30 gennaio
- 2015/01/30
- 2015/02/27
- 2015/03/30
- 2015/04/30
- 2015/05/30
- 2015/06/30
(Il mio codice è un disastro e non funzionerebbe in uno scenario pluriennale. Accolgo con favore chiunque riscriva la soluzione con un codice più elegante purché la premessa sottostante sia mantenuta intatta, cioè se +1 mese restituisce una data funky, usa +4 settimane invece.)