PHP strtotime +1 месяц

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

StrToTime

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

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


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

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

С#, где новый 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.

Счастливое кодирование!

Ответ 1

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

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

Ответ 2

то, что вам нужно, это сказать, что 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

Ответ 3

PHP devs уверенно не считают это ошибкой. Но в strtotime docs есть несколько комментариев с решениями для вашей проблемы (смотрите примеры 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));
    }

}
?>

Ответ 4

В то же время возникла одна и та же проблема, и в итоге был создан класс, который обрабатывает добавление/вычитание различных временных интервалов для объектов 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

Ответ 5

Вот реализация улучшенной версии ответа Juhana выше:

<?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 даты повторения не будут в конечном итоге зависеть от 28-й после прохождения через февраль без високосного года).

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

$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

Ответ 6

Я решил это следующим образом:

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

Ответ 7

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

  • Сохранять исходную дату и дату + месяца (месяца) в переменных
  • Извлечь часть дня обеих переменных
  • Если дни не совпадают, вычтите количество дней с будущей даты.

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

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

Ответ 8

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;       

}