PHP strtotime +1 mês de comportamento

Eu sei sobre o comportamento indesejado da function do PHP

strtotime

Por exemplo, ao adicionar um mês (+1 meses) a datas como: 31.01.2011 -> 03.03.2011

Eu sei que não é oficialmente um bug de PHP , e que esta solução tem alguns argumentos por trás disso, mas pelo menos para mim, esse comportamento causou muito desperdício de tempo (no passado e no presente) e eu pessoalmente o odeio.


O que achei ainda mais estranho é que, por exemplo, em:

MySQL: DATE_ADD (‘2011-01-31’, INTERVALO 1 MÊS) retorna 2011-02-28 ou

C # onde novo DateTime (2011, 01, 31) .AddMonths (1); retornará 28.02.2011

wolframalpha.com dando 31.01.2013 + 1 month como input; voltará quinta-feira, 28 de fevereiro de 2013

Isso me diz que outros encontraram uma solução mais decente para a pergunta estúpida que eu vi muito em relatórios de bug do PHP “que dia será, se eu disser que nos encontramos dentro de um mês a partir de agora” ou algo assim. A resposta é: se 31 não existir no mês que vem, pegue-me o último dia desse mês, mas fique com o próximo mês.


Então, minha pergunta é: existe uma function PHP (escrita por alguém) que resolve esse erro não oficialmente reconhecido? Como eu não acho que sou o único que quer outro comportamento ao adicionar / subtrair meses.

Estou particularmente interessado em soluções que também funcionam não apenas para o final do mês, mas uma substituição completa do strtotime . Também o caso strotime +n months também deve ser tratado.

Codificação feliz!

Aqui está o algoritmo que você pode usar. Deve ser simples o suficiente para se implementar.

  • Tem a data original e a data de +1 meses nas variables
  • Extraia a parte do mês de ambas as variables
  • Se a diferença for superior a 1 mês (ou se o original for dezembro e o outro não for janeiro), altere a última variável para o último dia do próximo mês. Você pode usar, por exemplo, t in date() para obter o último dia: date( 'tmY' )

o que você precisa é dizer ao PHP para ser mais esperto

 $the_date = strtotime('31.01.2011'); echo date('r', strtotime('last day of next month', $the_date)); $the_date = strtotime('31.03.2011'); echo date('r', strtotime('last day of next month', $the_date)); 

supondo que você só seja interessante no último dia do próximo mês

referência – http://www.php.net/manual/en/datetime.formats.relative.php

Os desenvolvedores do PHP certamente não consideram isso como bug . Mas, nos documentos do strtotime , há alguns comentários com soluções para o seu problema (procure os exemplos de 28 de fevereiro;)), ou seja, este está estendendo a class DateTime :

 < ?php // this will give us 2010-02-28 () echo PHPDateTime::DateNextMonth(strftime('%F', strtotime("2010-01-31 00:00:00")), 31); ?> 

Classe PHPDateTime:

 < ?php /** * IA FrameWork * @package: Classes & Object Oriented Programming * @subpackage: Date & Time Manipulation * @author: ItsAsh  */ final class PHPDateTime extends DateTime { // Public Methods // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /** * Calculate time difference between two dates * ... */ public static function TimeDifference($date1, $date2) $date1 = is_int($date1) ? $date1 : strtotime($date1); $date2 = is_int($date2) ? $date2 : strtotime($date2); if (($date1 !== false) && ($date2 !== false)) { if ($date2 >= $date1) { $diff = ($date2 - $date1); if ($days = intval((floor($diff / 86400)))) $diff %= 86400; if ($hours = intval((floor($diff / 3600)))) $diff %= 3600; if ($minutes = intval((floor($diff / 60)))) $diff %= 60; return array($days, $hours, $minutes, intval($diff)); } } return false; } /** * Formatted time difference between two dates * * ... */ public static function StringTimeDifference($date1, $date2) { $i = array(); list($d, $h, $m, $s) = (array) self::TimeDifference($date1, $date2); if ($d > 0) $i[] = sprintf('%d Days', $d); if ($h > 0) $i[] = sprintf('%d Hours', $h); if (($d == 0) && ($m > 0)) $i[] = sprintf('%d Minutes', $m); if (($h == 0) && ($s > 0)) $i[] = sprintf('%d Seconds', $s); return count($i) ? implode(' ', $i) : 'Just Now'; } /** * Calculate the date next month * * ... */ public static function DateNextMonth($now, $date = 0) { $mdate = array(0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31); list($y, $m, $d) = explode('-', (is_int($now) ? strftime('%F', $now) : $now)); if ($date) $d = $date; if (++$m == 2) $d = (($y % 4) === 0) ? (($d < = 29) ? $d : 29) : (($d <= 28) ? $d : 28); else $d = ($d <= $mdate[$m]) ? $d : $mdate[$m]; return strftime('%F', mktime(0, 0, 0, $m, $d, $y)); } } ?> 

Tinha o mesmo problema recentemente e acabou escrevendo uma class que lida com a adição / subtração de vários intervalos de tempo aos objects DateTime.
Aqui está o código:
https://gist.github.com/pavlepredic/6220041#file-gistfile1-php
Eu tenho usado esta class por um tempo e parece funcionar bem, mas estou realmente interessado em alguma revisão pelos pares. O que você faz é criar um object TimeInterval (no seu caso, você especificaria 1 mês como o intervalo) e, em seguida, chamar o método addToDate (), garantindo que o argumento $ preventMonthOverflow seja verdadeiro. O código assegurará que a data resultante não se transborde no próximo mês.

Uso da amostra:

 $int = new TimeInterval(1, TimeInterval::MONTH); $date = date_create('2013-01-31'); $future = $int->addToDate($date, true); echo $future->format('Ym-d'); 

A data resultante é: 2013-02-28

Aqui está uma implementação de uma versão aprimorada da resposta de Juhana acima:

 < ?php function sameDateNextMonth(DateTime $createdDate, DateTime $currentDate) { $addMon = clone $currentDate; $addMon->add(new DateInterval("P1M")); $nextMon = clone $currentDate; $nextMon->modify("last day of next month"); if ($addMon->format("n") == $nextMon->format("n")) { $recurDay = $createdDate->format("j"); $daysInMon = $addMon->format("t"); $currentDay = $currentDate->format("j"); if ($recurDay > $currentDay && $recurDay < = $daysInMon) { $addMon->setDate($addMon->format("Y"), $addMon->format("n"), $recurDay); } return $addMon; } else { return $nextMon; } } 

Esta versão leva $createdDate sob a presunção de que você está lidando com um período mensal recorrente, como uma assinatura, que começou em uma data específica, como a 31. Isso demora sempre $createdDate tão tarde ” $createdDate on” as datas não $createdDate para valores mais baixos, pois são empurrados para a frente por meses de menor valor (por exemplo, todas as 29, 30 ou 31 datas recíprocas não ficarão presas no dia 28 depois de passar por um ano sem pulo em fevereiro).

Aqui está um código de driver para testar o algoritmo:

 $createdDate = new DateTime("2015-03-31"); echo "created date = " . $createdDate->format("Ymd") . PHP_EOL; $next = sameDateNextMonth($createdDate, $createdDate); echo " next date = " . $next->format("Ymd") . PHP_EOL; foreach(range(1, 12) as $i) { $next = sameDateNextMonth($createdDate, $next); echo " next date = " . $next->format("Ymd") . PHP_EOL; } 

Que saídas:

 created date = 2015-03-31 next date = 2015-04-30 next date = 2015-05-31 next date = 2015-06-30 next date = 2015-07-31 next date = 2015-08-31 next date = 2015-09-30 next date = 2015-10-31 next date = 2015-11-30 next date = 2015-12-31 next date = 2016-01-31 next date = 2016-02-29 next date = 2016-03-31 next date = 2016-04-30 

Resolvi dessa maneira:

 $startDate = date("Ymd"); $month = date("m",strtotime($startDate)); $nextmonth = date("m",strtotime("$startDate +1 month")); if((($nextmonth-$month) > 1) || ($month == 12 && $nextmonth != 1)) { $nextDate = date( 'tmY',strtotime("$initialDate +1 week")); }else { $nextDate = date("Ymd",strtotime("$initialDate +1 month")); } echo $nextDate; 

Um pouco semelhante à resposta do Juhana, mas mais intuitiva e menos complicações esperadas. A ideia é assim:

  1. Armazene a data original e a data de + n mês (s) nas variables
  2. Extraia a parte do dia de ambas as variables
  3. Se os dias não coincidirem, subtrair o número de dias da data futura

O lado positivo desta solução é que funciona para qualquer data (e não apenas as datas da borda) e também funciona para subtrair meses (colocando – em vez de +). Aqui está um exemplo de implementação:

 $start = mktime(0,0,0,1,31,2015); for ($contract = 0; $contract < 12; $contract++) { $end = strtotime('+ ' . $contract . ' months', $start); if (date('d', $start) != date('d', $end)) { $end = strtotime('- ' . date('d', $end) . ' days', $end); } echo date('dm-Y', $end) . '|'; } 

E a saída está a seguir:

 31-01-2015|28-02-2015|31-03-2015|30-04-2015|31-05-2015|30-06-2015|31-07-2015|31-08-2015|30-09-2015|31-10-2015|30-11-2015|31-12-2015| 
 function ldom($m,$y){ //return tha last date of a given month based on the month and the year //(factors in leap years) $first_day= strtotime (date($m.'/1/'.$y)); $next_month = date('m',strtotime ( '+32 day' , $first_day)) ; $last_day= strtotime ( '-1 day' , strtotime (date($next_month.'/1/'.$y)) ) ; return $last_day; }