Eu concordo com o sentimento do OP de que isso é contra-intuitivo e frustrante, mas também determina o que +1 month
significa nos cenários em que isso ocorre. Considere estes exemplos:
Você começa com 31/01/2015 e deseja adicionar um mês 6 vezes para obter um ciclo de agendamento para envio de um boletim informativo por e-mail. Com as expectativas iniciais do OP em mente, isso retornaria:
- 31/01/2015
- 28/02/2015
- 31/03/2015
- 30/04/2015
- 31/05/2015
- 30/06/2015
De imediato, observe que esperamos +1 month
significarlast day of month
ou, alternativamente, adicionar 1 mês por iteração, mas sempre em referência ao ponto inicial. Em vez de interpretar isso como "último dia do mês", poderíamos lê-lo como "31º dia do próximo mês ou o último disponível dentro desse mês". Isso significa que saltamos de 30 de abril para 31 de maio em vez de 30 de maio. Observe que isso não ocorre porque é o "último dia do mês", mas porque queremos "o mais próximo disponível para a data do mês de início".
Então, suponha que um de nossos usuários assine outro boletim informativo para começar em 30/01/2015. Qual é a data intuitiva para +1 month
? Uma interpretação seria "30º dia do próximo mês ou o mais próximo disponível", que retornaria:
- 30/01/2015
- 28/02/2015
- 30/03/2015
- 30/04/2015
- 30/05/2015
- 30/06/2015
Isso seria bom, exceto quando nosso usuário recebesse os dois boletins no mesmo dia. Vamos supor que este é um problema do lado da oferta em vez do lado da demanda. Não estamos preocupados que o usuário fique incomodado em receber 2 newsletters no mesmo dia, mas em vez disso, nossos servidores de e-mail não podem pagar pela largura de banda para enviar duas vezes mais muitos boletins informativos. Com isso em mente, voltamos à outra interpretação de "+1 mês" como "enviar do penúltimo dia de cada mês" que retornaria:
- 30/01/2015
- 27/02/2015
- 30/03/2015
- 29/04/2015
- 30/05/2015
- 29/06/2015
Agora evitamos qualquer sobreposição com o primeiro conjunto, mas também terminamos com 29 de abril e 29 de junho, o que certamente corresponde às nossas intuições originais que +1 month
simplesmente deveriam retornar m/$d/Y
ou o atraente e simples m/30/Y
para todos os meses possíveis. Portanto, agora vamos considerar uma terceira interpretação do +1 month
uso de ambas as datas:
31 de janeiro
- 31/01/2015
- 03/03/2015
- 31/03/2015
- 01-05-2015
- 31/05/2015
- 01-07-2015
30 de janeiro
- 30/01/2015
- 02/03/2015
- 30/03/2015
- 30/04/2015
- 30/05/2015
- 30/06/2015
A descrição acima tem alguns problemas. Fevereiro é ignorado, o que pode ser um problema tanto no fim do fornecimento (digamos, se houver uma alocação mensal de largura de banda e fevereiro for desperdiçado e março for dobrado) quanto no final da demanda (os usuários se sentem enganados em fevereiro e percebem o março extra como tentativa de corrigir o erro). Por outro lado, observe que os dois conjuntos de datas:
- nunca se sobrepõe
- estão sempre na mesma data quando esse mês tem a data (então o conjunto de 30 de janeiro parece bem limpo)
- são todos dentro de 3 dias (1 dia na maioria dos casos) do que pode ser considerado a data "correta".
- estão todos a pelo menos 28 dias (um mês lunar) de seu sucessor e predecessor, de modo muito bem distribuído.
Dados os dois últimos conjuntos, não seria difícil simplesmente reverter uma das datas se ela caísse fora do mês seguinte real (então volte para 28 de fevereiro e 30 de abril no primeiro conjunto) e não perder o sono durante o sobreposição ocasional e divergência do padrão "último dia do mês" vs "penúltimo dia do mês". Mas esperar que a biblioteca escolha entre "mais bonito / natural", "interpretação matemática de 31/02 e outros estouros do mês" e "em relação ao primeiro dia do mês ou mês passado" sempre vai acabar com as expectativas de alguém não sendo atendidas e alguma programação precisa ajustar a data "errada" para evitar o problema do mundo real que a interpretação "errada" apresenta.
Então, novamente, embora eu também espere +1 month
retornar uma data que na verdade é no mês seguinte, não é tão simples quanto a intuição e, dadas as escolhas, ir com a matemática acima das expectativas dos desenvolvedores da web é provavelmente a escolha segura.
Aqui está uma solução alternativa que ainda é tão desajeitada quanto qualquer outra, mas acho que tem bons resultados:
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);
}
Não é o ideal, mas a lógica subjacente é: Se adicionar 1 mês resultar em uma data diferente do mês seguinte esperado, descarte essa data e adicione 4 semanas. Aqui estão os resultados com as duas datas de teste:
31 de janeiro
- 31/01/2015
- 28/02/2015
- 31/03/2015
- 28/04/2015
- 31/05/2015
- 28/06/2015
30 de janeiro
- 30/01/2015
- 27/02/2015
- 30/03/2015
- 30/04/2015
- 30/05/2015
- 30/06/2015
(Meu código é uma bagunça e não funcionaria em um cenário de vários anos. Agradeço a qualquer pessoa que reescreva a solução com um código mais elegante, desde que a premissa subjacente seja mantida intacta, ou seja, se +1 mês retorna uma data funky, use +4 semanas em vez disso.)