[php] PHP DateTime :: 달 더하기 및 빼기 수정

나는에서 많은 일을 해왔고 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 개월이 예상대로 작동하도록 할 수있는 우아한 해결책이 있습니까?



답변

버그가 아닌 이유 :

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

  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 날짜가됩니다.


답변

다음은 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


답변

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

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


답변

문제에 대한 나의 해결책 :

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


답변

나는 이것이 반 직관적이고 실망 스럽다는 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 주.)


답변

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


답변

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