PHP DateTime :: 달 더하기 및 빼기 수정


102

나는에서 많은 일을 해왔고 DateTime class최근 몇 달을 추가 할 때 내가 버그라고 생각했던 것을 발견했습니다. 약간의 조사 끝에 버그가 아니라 의도 한대로 작동하는 것으로 나타났습니다. 여기 에있는 문서에 따르면 :

Example # 2 월을 더하거나 뺄 때주의하십시오

<?php
$date = new DateTime('2000-12-31');

$date->modify('+1 month');
echo $date->format('Y-m-d') . "\n";

$date->modify('+1 month');
echo $date->format('Y-m-d') . "\n";
?>
The above example will output:
2001-01-31
2001-03-03

이것이 버그로 간주되지 않는 이유를 누구든지 정당화 할 수 있습니까?

또한 누구든지 문제를 해결하고 의도 한대로 +1 개월이 예상대로 작동하도록 할 수있는 우아한 해결책이 있습니까?


"2001-01-31"에 1 개월을 더하면 어떻게 될까요? ... "2001-02-28"? "2001-03-01"?
Artefacto

57
개인적으로 2001-02-28이 될 것으로 예상합니다.
tplaner 2010 년


2
네, 꽤 성가신 특징입니다. P1M이 31 일이라는 것을 알기 위해 작은 글씨를 읽었습니다. 사람들이 "올바른"행동으로 계속 옹호하는 이유를 이해하지 못합니다.
Indivision Dev

PHP는 반올림 (3/1)하지만 로직은 반올림 (2/28)해야한다는 일반적인 의견처럼 보입니다 ...하지만 PHP 방식을 선호하지만 Microsoft의 Excel은 반올림하여 웹 개발자를 스프레드 시트 사용자에 맞 춥니 다. ...
Dave

답변:


107

버그가 아닌 이유 :

현재 동작이 정확합니다. 다음은 내부적으로 발생합니다.

  1. +1 month월 번호 (원래 1)를 1 씩 증가시킵니다. 이것은 날짜를 만듭니다 2010-02-31.

  2. 두 번째 달 (2 월)은 2010 년에 28 일 밖에 없기 때문에 PHP는 2 월 1 일부터 계속해서 일을 계산하여이를 자동 수정합니다. 그런 다음 3 월 3 일에 끝납니다.

원하는 것을 얻는 방법 :

원하는 것을 얻으려면 다음 달을 수동으로 확인하십시오. 그런 다음 다음 달의 일 수를 추가하십시오.

이 코드를 직접 작성할 수 있기를 바랍니다. 나는 단지 할 일을주고있다.

PHP 5.3 방식 :

올바른 동작을 얻으려면 상대 시간 스탠자를 도입하는 PHP 5.3의 새로운 기능 중 하나를 사용할 수 있습니다 first day of. 이 스 D와 함께 사용 할 수 있습니다 next month, fifth month또는 +8 months지정된 달의 첫날로 이동합니다. +1 month당신이하는 일 대신에 , 다음과 같이 다음 달의 첫날을 얻기 위해이 코드를 사용할 수 있습니다 :

<?php
$d = new DateTime( '2010-01-31' );
$d->modify( 'first day of next month' );
echo $d->format( 'F' ), "\n";
?>

이 스크립트는 February. PHP가이 first day of next month스탠자를 처리 할 때 다음과 같은 일이 발생합니다 .

  1. next month월 번호 (원래 1)를 1 씩 증가시킵니다. 이렇게하면 날짜가 2010-02-31이됩니다.

  2. first day of날짜 번호를로 설정하여 12010-02-01 날짜가됩니다.


1
그래서 당신이 말하는 것은 문자 그대로 1 개월을 추가하고 일을 완전히 무시한다는 것입니까? 윤년에 추가하면 +1 년과 비슷한 문제가 발생할 수 있다고 가정합니다.
tplaner 2010 년

@evolve, 예, 문학은 1 개월을 추가합니다.
shamittomar 2010-08-30

13
그리고 그것을 더한 후 1 개월을 빼면 완전히 다른 날짜로 끝납니다. 그것은 매우 직관적이지 않은 것 같습니다.
Dan Breen

2
PHP 5.3의 새로운 스탠자를 사용하는 멋진 예입니다. 첫날, 마지막 날, 이번 달, 다음 달 및 이전 달을 사용할 수 있습니다.
Kim Stacks

6
imho 이것은 버그입니다. 심각한 버그. 31 일을 더하고 싶다면 31 일을 더합니다. 31 일이 아닌 한 달을 추가해야합니다.
low_rents

12

다음은 DateTime 메서드를 전적으로 사용하여 복제본을 만들지 않고 개체를 제자리에서 수정하는 또 다른 압축 솔루션입니다.

$dt = new DateTime('2012-01-31');

echo $dt->format('Y-m-d'), PHP_EOL;

$day = $dt->format('j');
$dt->modify('first day of +1 month');
$dt->modify('+' . (min($day, $dt->format('t')) - 1) . ' days');

echo $dt->format('Y-m-d'), PHP_EOL;

다음을 출력합니다.

2012-01-31
2012-02-29

1
감사. 지금까지 여기에 제공된 최상의 솔루션입니다. 코드를 $dt->modify()->modify(). 잘 작동합니다.
Alph.Dev

10

이것은 유용 할 수 있습니다.

echo Date("Y-m-d", strtotime("2013-01-01 +1 Month -1 Day"));
  // 2013-01-31

echo Date("Y-m-d", strtotime("2013-02-01 +1 Month -1 Day"));
  // 2013-02-28

echo Date("Y-m-d", strtotime("2013-03-01 +1 Month -1 Day"));
  // 2013-03-31

echo Date("Y-m-d", strtotime("2013-04-01 +1 Month -1 Day"));
  // 2013-04-30

echo Date("Y-m-d", strtotime("2013-05-01 +1 Month -1 Day"));
  // 2013-05-31

echo Date("Y-m-d", strtotime("2013-06-01 +1 Month -1 Day"));
  // 2013-06-30

echo Date("Y-m-d", strtotime("2013-07-01 +1 Month -1 Day"));
  // 2013-07-31

echo Date("Y-m-d", strtotime("2013-08-01 +1 Month -1 Day"));
  // 2013-08-31

echo Date("Y-m-d", strtotime("2013-09-01 +1 Month -1 Day"));
  // 2013-09-30

echo Date("Y-m-d", strtotime("2013-10-01 +1 Month -1 Day"));
  // 2013-10-31

echo Date("Y-m-d", strtotime("2013-11-01 +1 Month -1 Day"));
  // 2013-11-30

echo Date("Y-m-d", strtotime("2013-12-01 +1 Month -1 Day"));
  // 2013-12-31

2
이것은 월 1 일과 같은 특정 입력에 대해서만 작동하므로 일반적인 솔루션은 아닙니다. 예를 들어 1 월 30 일에 이것을하는 것은 고통으로 이어집니다.
Jens Roland

또는 당신은 할 수$dateTime->modify('first day of next month')->modify('-1day')
안토니

6

문제에 대한 나의 해결책 :

$startDate = new \DateTime( '2015-08-30' );
$endDate = clone $startDate;

$billing_count = '6';
$billing_unit = 'm';

$endDate->add( new \DateInterval( 'P' . $billing_count . strtoupper( $billing_unit ) ) );

if ( intval( $endDate->format( 'n' ) ) > ( intval( $startDate->format( 'n' ) ) + intval( $billing_count ) ) % 12 )
{
    if ( intval( $startDate->format( 'n' ) ) + intval( $billing_count ) != 12 )
    {
        $endDate->modify( 'last day of -1 month' );
    }
}

3
"clone"명령은 내 변수 할당 문제에 대한 해결책이었습니다. 감사합니다.
Steph Rose

4

나는 이것이 반 직관적이고 실망 스럽다는 OP의 감정에 동의하지만 무엇을 +1 month , 이것이 발생하는 시나리오에서 의미 기도합니다. 다음 예를 고려하십시오.

2015-01-31로 시작하여 한 달을 6 번 추가하여 이메일 뉴스 레터를 보내기위한 일정주기를 얻으려고합니다. OP의 초기 기대치를 염두에두고 다음을 반환합니다.

  • 2015-01-31
  • 2015-02-28
  • 2015-03-31
  • 2015-04-30
  • 2015-05-31
  • 2015-06-30

바로 시작점을 기준으로 반복 당 1 개월을 추가하거나 +1 month의미 할 것으로 예상 됩니다 last day of month. 이것을 "월의 마지막 날"로 해석하는 대신 "다음 달의 31 일 또는 해당 달의 마지막 사용 가능 날짜"로 읽을 수 있습니다. 즉, 5 월 30 일이 아니라 4 월 30 일에서 5 월 31 일로 점프합니다. 이것은 "마지막 날"이기 때문이 아니라 "시작 월의 날짜에 가장 가까운 날짜"를 원하기 때문입니다.

따라서 사용자 중 한 명이 2015-01-30에 시작하기 위해 다른 뉴스 레터를 구독한다고 가정합니다. 직관적 인 날짜는 무엇입니까 +1 month? 한 가지 해석은 "다음 달의 30 일 또는 가장 가까운 사용 가능 날짜"이며 다음을 반환합니다.

  • 2015-01-30
  • 2015-02-28
  • 2015-03-30
  • 2015-04-30
  • 2015-05-30
  • 2015-06-30

사용자가 같은 날 두 뉴스 레터를받는 경우를 제외하고는 괜찮습니다. 이것이 수요 측이 아닌 공급측 문제라고 가정 해 보겠습니다. 사용자가 같은 날 뉴스 레터 2 개를받는 데 짜증이 날 것이라 걱정하지 않지만 대신 메일 서버가 두 번 전송하는 대역폭을 감당할 수 없습니다. 많은 뉴스 레터. 이를 염두에두고 "+1 개월"을 "매월 두 번째에서 마지막 날까지 보내기"라는 다른 해석으로 돌아가서 다음을 반환합니다.

  • 2015-01-30
  • 2015-02-27
  • 2015-03-30
  • 2015-04-29
  • 2015-05-30
  • 2015-06-29

이제 우리는 첫 번째 세트와의 겹침을 피했지만 4 월과 6 월 29 일로 끝납니다. 이것은 +1 month단순히 돌아와야 하는 원래의 직관 m/$d/Y이나 m/30/Y가능한 모든 달 동안 매력적이고 단순한 것과 일치 합니다. 이제 +1 month두 날짜 를 사용 하는 세 번째 해석을 고려해 보겠습니다 .

1 월 31 일

  • 2015-01-31
  • 2015-03-03
  • 2015-03-31
  • 2015-05-01
  • 2015-05-31
  • 2015-07-01

1 월 30 일

  • 2015-01-30
  • 2015-03-02
  • 2015-03-30
  • 2015-04-30
  • 2015-05-30
  • 2015-06-30

위는 몇 가지 문제가 있습니다. 2 월은 건너 뛰기 때문에 공급 단 (예 : 월별 대역폭 할당이 있고 2 월이 낭비되고 3 월이 두 배가되는 경우)과 수요 단 (사용자가 2 월에 속임을 느끼고 추가 3 월을 인식하는 경우) 모두 문제가 될 수 있습니다. 실수를 수정하려는 시도). 반면에 두 개의 날짜 세트는 다음과 같습니다.

  • 겹치지 않음
  • 그 달에 날짜가있는 날짜는 항상 같은 날짜입니다 (1 월 30 일 세트는 꽤 깔끔해 보입니다)
  • 모두 "올바른"날짜로 간주 될 수있는 날짜로부터 3 일 (대부분의 경우 1 일) 이내입니다.
  • 후임자 및 전임자로부터 모두 최소 28 일 (음력)이므로 매우 고르게 분포되어 있습니다.

마지막 두 세트를 감안할 때 실제 다음 달이 아닌 경우 날짜 중 하나를 롤백하고 (첫 번째 세트에서 2 월 28 일 및 4 월 30 일로 롤백) "월 말일"과 "월의 두 번째 날에서 말일"패턴이 가끔씩 겹치고 발산됩니다. 그러나 도서관이 "가장 예쁘고 자연스러운", "02/31 및 다른 달의 수학적 해석 넘침", "월 1 일 또는 지난달에 상대적"중에서 선택하기를 기대하는 것은 항상 누군가의 기대를 충족하지 못하고 끝날 것입니다. "잘못된"해석으로 인해 발생하는 실제 문제를 피하기 위해 "잘못된"날짜를 조정해야하는 일정.

다시 말하지만 +1 month실제로 다음 달의 날짜를 반환 할 것으로 예상 하지만 직관만큼 간단하지 않고 선택 사항이 주어지면 웹 개발자의 기대를 뛰어 넘는 수학을 사용하는 것이 안전한 선택 일 것입니다.

다음은 여전히 ​​어색하지만 좋은 결과가 있다고 생각하는 대체 솔루션입니다.

foreach(range(0,5) as $count) {
    $new_date = clone $date;
    $new_date->modify("+$count month");
    $expected_month = $count + 1;
    $actual_month = $new_date->format("m");
    if($expected_month != $actual_month) {
        $new_date = clone $date;
        $new_date->modify("+". ($count - 1) . " month");
        $new_date->modify("+4 weeks");
    }
    
    echo "* " . nl2br($new_date->format("Y-m-d") . PHP_EOL);
}

최적은 아니지만 기본 논리는 다음과 같습니다. 1 개월을 추가 한 결과 다음 달 예상 날짜가 아닌 경우 해당 날짜를 스크랩하고 대신 4 주를 추가합니다. 두 테스트 날짜의 결과는 다음과 같습니다.

1 월 31 일

  • 2015-01-31
  • 2015-02-28
  • 2015-03-31
  • 2015-04-28
  • 2015-05-31
  • 2015-06-28

1 월 30 일

  • 2015-01-30
  • 2015-02-27
  • 2015-03-30
  • 2015-04-30
  • 2015-05-30
  • 2015-06-30

(내 코드는 엉망이고 다년간의 시나리오에서는 작동하지 않습니다. 기본 전제가 그대로 유지되는 한, 즉 +1 개월이 펑키 한 날짜를 반환하는 한, 더 우아한 코드로 솔루션을 다시 작성하는 사람을 환영합니다. 대신 +4 주.)


4

DateInterval을 반환하는 함수를 만들어 월을 추가하면 다음 달이 표시되고 그 이후의 날짜는 제거됩니다.

$time = new DateTime('2014-01-31');
echo $time->format('d-m-Y H:i') . '<br/>';

$time->add( add_months(1, $time));

echo $time->format('d-m-Y H:i') . '<br/>';



function add_months( $months, \DateTime $object ) {
    $next = new DateTime($object->format('d-m-Y H:i:s'));
    $next->modify('last day of +'.$months.' month');

    if( $object->format('d') > $next->format('d') ) {
        return $object->diff($next);
    } else {
        return new DateInterval('P'.$months.'M');
    }
}

4

shamittomar의 대답과 함께 다음과 같이 "안전하게"개월을 추가 할 수 있습니다.

/**
 * Adds months without jumping over last days of months
 *
 * @param \DateTime $date
 * @param int $monthsToAdd
 * @return \DateTime
 */

public function addMonths($date, $monthsToAdd) {
    $tmpDate = clone $date;
    $tmpDate->modify('first day of +'.(int) $monthsToAdd.' month');

    if($date->format('j') > $tmpDate->format('t')) {
        $daysToAdd = $tmpDate->format('t') - 1;
    }else{
        $daysToAdd = $date->format('j') - 1;
    }

    $tmpDate->modify('+ '. $daysToAdd .' days');


    return $tmpDate;
}

정말 고맙습니다!!
geckos

2

다음 코드를 사용하여 더 짧은 방법을 찾았습니다.

                   $datetime = new DateTime("2014-01-31");
                    $month = $datetime->format('n'); //without zeroes
                    $day = $datetime->format('j'); //without zeroes

                    if($day == 31){
                        $datetime->modify('last day of next month');
                    }else if($day == 29 || $day == 30){
                        if($month == 1){
                            $datetime->modify('last day of next month');                                
                        }else{
                            $datetime->modify('+1 month');                                
                        }
                    }else{
                        $datetime->modify('+1 month');
                    }
echo $datetime->format('Y-m-d H:i:s');

1

다음은 관련 질문에 대한 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 일에 고정되지 않습니다. 윤년이 아닌 2 월까지).

다음은 알고리즘을 테스트하기위한 드라이버 코드입니다.

$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

1

이것은 관련 질문에서 Kasihasi의 답변 을 개선 한 버전입니다 . 이것은 날짜에 임의의 개월 수를 올바르게 더하거나 뺍니다.

public static function addMonths($monthToAdd, $date) {
    $d1 = new DateTime($date);

    $year = $d1->format('Y');
    $month = $d1->format('n');
    $day = $d1->format('d');

    if ($monthToAdd > 0) {
        $year += floor($monthToAdd/12);
    } else {
        $year += ceil($monthToAdd/12);
    }
    $monthToAdd = $monthToAdd%12;
    $month += $monthToAdd;
    if($month > 12) {
        $year ++;
        $month -= 12;
    } elseif ($month < 1 ) {
        $year --;
        $month += 12;
    }

    if(!checkdate($month, $day, $year)) {
        $d2 = DateTime::createFromFormat('Y-n-j', $year.'-'.$month.'-1');
        $d2->modify('last day of');
    }else {
        $d2 = DateTime::createFromFormat('Y-n-d', $year.'-'.$month.'-'.$day);
    }
    return $d2->format('Y-m-d');
}

예를 들면 :

addMonths(-25, '2017-03-31')

다음을 출력합니다.

'2015-02-28'

0

한 달을 건너 뛰지 않으려면 다음과 같은 작업을 수행하여 날짜를 꺼내 다음 달에 루프를 실행하고 날짜를 1만큼 줄이고 유효한 날짜까지 다시 확인합니다. 여기서 $ starting_calculated가 strtotime에 유효한 문자열 (즉, mysql datetime 또는 "now"). 이렇게하면 월을 건너 뛰지 않고 1 분에서 자정까지 월말을 찾습니다.

    $start_dt = $starting_calculated;

    $next_month = date("m",strtotime("+1 month",strtotime($start_dt)));
    $next_month_year = date("Y",strtotime("+1 month",strtotime($start_dt)));

    $date_of_month = date("d",$starting_calculated);

    if($date_of_month>28){
        $check_date = false;
        while(!$check_date){
            $check_date = checkdate($next_month,$date_of_month,$next_month_year);
            $date_of_month--;
        }
        $date_of_month++;
        $next_d = $date_of_month;
    }else{
        $next_d = "d";
    }
    $end_dt = date("Y-m-$next_d 23:59:59",strtotime("+1 month"));



0

'작년의 이번 달'데이트가 필요했는데 윤년의 이달이 2 월이면 금방 불쾌 해집니다. 그러나 나는 이것이 효과가 있다고 믿는다 ... :-/ 트릭은 매월 1 일에 당신의 변화를 기반으로하는 것 같다.

$this_month_last_year_end = new \DateTime();
$this_month_last_year_end->modify('first day of this month');
$this_month_last_year_end->modify('-1 year');
$this_month_last_year_end->modify('last day of this month');
$this_month_last_year_end->setTime(23, 59, 59);

0
$ds = new DateTime();
$ds->modify('+1 month');
$ds->modify('first day of this month');

1
답변을 설명해야합니다. 코드 전용 답변은 품질이 낮은 것으로 간주됩니다.
Machavity

감사합니다! 이것은 아직 가장 정확한 답변입니다. 마지막 두 줄을 전환하면 항상 올바른 월을 제공합니다. 명성!
Danny Schoemann

0
$month = 1; $year = 2017;
echo date('n', mktime(0, 0, 0, $month + 2, -1, $year));

출력됩니다 2(2 월). 다른 달에도 작동합니다.


0
$current_date = new DateTime('now');
$after_3_months = $current_date->add(\DateInterval::createFromDateString('+3 months'));

몇일 동안:

$after_3_days = $current_date->add(\DateInterval::createFromDateString('+3 days'));

중대한:

add()DateTime 클래스 의 메서드 는 개체 값을 수정하므로 add()DateTime 개체를 호출 한 후 새 날짜 개체를 반환하고 자체적으로 개체를 수정합니다.


0

실제로 date ()와 strtotime ()으로도 할 수 있습니다. 예를 들어 오늘 날짜에 1 개월을 추가하려면 :

date("Y-m-d",strtotime("+1 month",time()));

datetime 클래스를 사용하고 싶다면 괜찮지 만 이것은 간단합니다. 여기에 자세한 내용


0

허용 된 답변은 이미 이것이 왜 아닌지 설명하고 있으며 다른 답변은 다음과 같은 PHP 표현식으로 깔끔한 솔루션을 제공합니다. first day of the +2 months . 이러한 표현식의 문제점은 자동 완성되지 않는다는 것입니다.

하지만 해결책은 아주 간단합니다. 먼저, 문제 공간을 반영하는 유용한 추상화를 찾아야합니다. 이 경우 ISO8601DateTime. 둘째, 원하는 텍스트 표현을 가져올 수있는 여러 구현이 있어야합니다. 예를 들어, Today, Tomorrow, The first day of this month, Future- 모두의 특정 구현 표현 ISO8601DateTime개념.

따라서 귀하의 경우 필요한 구현은 TheFirstDayOfNMonthsLater. IDE에서 하위 클래스 llist를 보는 것만으로 쉽게 찾을 수 있습니다. 코드는 다음과 같습니다.

$start = new DateTimeParsedFromISO8601String('2000-12-31');
$firstDayOfOneMonthLater = new TheFirstDayOfNMonthsLater($start, 1);
$firstDayOfTwoMonthsLater = new TheFirstDayOfNMonthsLater($start, 2);
var_dump($start->value()); // 2000-12-31T00:00:00+00:00
var_dump($firstDayOfOneMonthLater->value()); // 2001-01-01T00:00:00+00:00
var_dump($firstDayOfTwoMonthsLater->value()); // 2001-02-01T00:00:00+00:00

한 달의 마지막 날도 마찬가지입니다. 이 접근 방식의 더 많은 예를 보려면이를 읽으십시오 .


당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.