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
Несколько похоже на ответ Джуханы, но более интуитивно и меньше ожидаемых осложнений. Идея такова:
- Сохранение исходной даты и даты + n месяцев в переменных
- Извлечь дневную часть обеих переменных
- Если дни не совпадают, вычтите количество дней из будущей даты
Плюс этого решения заключается в том, что он работает для любой даты (не только для дат границы), а также для вычитания месяцев (путем добавления - вместо +). Вот пример реализации:
$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;