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 не считают это ошибкой. Но в 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));
    }

}
?>

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

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

Недавно возникла та же проблема, и в итоге мы написали класс, который обрабатывает сложение / вычитание различных временных интервалов для объектов DateTime.
Вот код:
https://gist.github.com/pavlepredic/6220041
Я использовал этот класс некоторое время, и он, кажется, работает нормально, но я действительно заинтересован в некоторой рецензии. Что вы делаете, это создаете объект TimeInterval (в вашем случае вы бы указали 1 месяц в качестве интервала), а затем вызываете метод addToDate(), убедившись, что вы установили для аргумента $protectMonthOverflow значение 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

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

  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;       

}

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

$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;
Другие вопросы по тегам