Поведение PHP strtotime +1 месяц

Я знаю о нежелательном поведении функции PHP

strtotime

например, при добавлении месяца (+1 месяц) к датам типа: 31.01.2011 -> 03.03.2011

Я знаю, что это не официально ошибка PHP, и что у этого решения есть некоторые аргументы, но, по крайней мере, для меня это поведение вызвало много траты времени (в прошлом и настоящем), и я лично ненавижу.


то, что я нашел еще более странным, это, например, в:

MySQL: DATE_ADD ('2011-01-31', интервал 1 месяц) возвратит 2011-02-28 или

C# где новый DateTime (2011, 01, 31).AddMonths (1); вернет 28.02.2011

wolframalpha.com давать 31.01.2013 + 1 month в качестве ввода; вернется в четверг, 28 февраля 2013

он видит, что другие нашли больше достойное решение глупого вопроса что я видел много в отчетах об ошибках PHP "какой день будет, если я скажу, что мы встретимся через месяц" или что-то в этом роде. Ответ: если 31 не существует в следующем месяце, дай мне последний день этого месяца, но пожалуйста, придерживайтесь следующего месяца.


Итак, мой вопрос: есть ли функция PHP (написанная кем-то), которая разрешает эту официально не признанную ошибку? Я не думаю, что я единственный, кто требуется другое поведение при добавлении / вычитании месяцев.

меня особенно интересуют решения, которые также работают не только в конце месяца, но и полная замена strtotime. Также Дело strotime +n months также должны быть рассмотрены.

удачи в кодировании!

8 ответов


вот алгоритм, который вы можете использовать. Это должно быть достаточно просто, чтобы реализовать себя.

  • иметь исходную дату и дату +1 месяц в переменных
  • извлеките часть месяца обеих переменных
  • если разница больше, чем 1 месяц (или если оригинал декабрь, а другой не январь) измените последнюю переменную на последний день следующего месяца. Вы можете использовать, например t на date() чтобы получить последний день: date( 't.m.Y' )

что вам нужно, это сказать PHP, чтобы быть умнее

$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));

предполагая, что вы интересны только в последний день следующего месяца

ссылка -http://www.php.net/manual/en/datetime.formats.relative.php


PHP devs конечно не рассматривайте это как ошибку. Но в strtotime'с документами есть несколько комментариев с решениями для вашей проблемы (посмотрите примеры 28 февраля;)), т. е. этот расширения класс DateTime:

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

Класс PHPDateTime:

<?php
/**
 * IA FrameWork
 * @package: Classes & Object Oriented Programming
 * @subpackage: Date & Time Manipulation
 * @author: ItsAsh <ash at itsash dot co dot uk>
 */

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));
    }

}
?>

недавно была такая же проблема и закончилась написанием класса, который обрабатывает добавление / вычитание различных временных интервалов для объектов DateTime.
Вот код:
https://gist.github.com/pavlepredic/6220041#file-gistfile1-php
Я использую этот класс некоторое время, и он, кажется, работает нормально, но меня действительно интересует какая-то экспертная оценка. Что вы делаете, это создать объект TimeInterval (в вашем случае вы бы указали 1 месяц в качестве интервала), а затем вызвать метод addToDate (), убедившись, что вы установили аргумент $preventMonthOverflow в true. Код гарантирует, что полученная дата не будет переполнена в следующий месяц.

пример использования:

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

конечной дате: 2013-02-28


вот реализация улучшенной версии Джухона это выше:

<?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;
    }
}

эта версия $createdDate при условии, что вы имеете дело с повторяющимся ежемесячным периодом, таким как подписка, которая началась в определенную дату, например 31-го. Это всегда занимает $createdDate так поздно" повторяется " даты не будут смещаться на более низкие значения, поскольку они продвигаются вперед через менее значимые месяцы (например, все 29, 30 или 31-й даты повторения не будут в конце концов застрять на 28th после прохождения через не високосный февраль).

вот код драйвера для проверки алгоритма:

$createdDate = new DateTime("2015-03-31");
echo "created date = " . $createdDate->format("Y-m-d") . PHP_EOL;

$next = sameDateNextMonth($createdDate, $createdDate);
echo "   next date = " . $next->format("Y-m-d") . PHP_EOL;

foreach(range(1, 12) as $i) {
    $next = sameDateNextMonth($createdDate, $next);
    echo "   next date = " . $next->format("Y-m-d") . PHP_EOL;
}

выходы:

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

Я решил это таким образом:

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

несколько похоже на ответ Джуханы, но более интуитивный и меньше ожидаемых осложнений. Идея такая:

  1. сохранить исходную дату и + N месяц(ы) дата в переменных
  2. извлеките дневную часть обеих переменных
  3. если дни не совпадают, вычесть количество дней из будущего дата

положительная сторона этого решения-это то, что работает для любой даты (а не только даты границы), а также работает для вычитания месяцев (по ставлю-вместо+). Вот пример реализации:

$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('d-m-Y', $end) . '|';
}

и вывод следующий:

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;       

}