web-dev-qa-db-ja.com

PHP DateTime :: modifyは月の加算と減算を行います

私は DateTime class そして最近、月を追加するときにバグだと思っていたものに出くわしました。少し調査した結果、バグではなく、意図したとおりに動作しているようです。見つかったドキュメントによると here

例#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か月が意図したものではなく期待どおりに機能するようにするエレガントなソリューションはありますか?

90
tplaner

バグではない理由:

現在の動作は正しいです。内部的には次のことが起こります。

  1. +1 monthは、月数(元は1)を1つ増やします。これにより、日付が2010-02-31になります。

  2. 2番目の月(2月)は2010年に28日しかないので、PHP 2月1日からの日数を数え続けるだけでこれを自動修正します。その後、3月3日に終わります。

欲しいものを入手する方法:

欲しいものを手に入れるには、次の月を手動でチェックします。次に、来月の日数を追加します。

これを自分でコーディングできるといいのですが。私はただやるべきことを与えています。

PHP 5.3の方法:

正しい動作を得るために、相対時間スタンザfirst day ofを導入するPHP 5.3の新しい機能のいずれかを使用できます。このスタンザは、next monthfifth 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は日番号を1に設定し、結果は2010-02-01の日付になります。

98
shamittomar

これは役に立つかもしれません:

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

これは、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
6
Rudiger W.

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

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
A-R

これは直感に反し、イライラするというOPの感情に同意しますが、これが発生するシナリオで+1 monthが何を意味するかを決定することにも同意します。これらの例を考慮してください:

2015-01-31から始めて、1か月を6回追加して、メールニュースレターを送信するためのスケジュールサイクルを取得します。 OPの最初の期待を念頭に置いて、これは以下を返します。

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

すぐに、+1 monthlast day of monthを意味すること、または反復ごとに1か月を追加するが常に開始点を参照することを期待していることに注意してください。これを「月の最後の日」と解釈する代わりに、「来月の31日目またはその月の最後に利用可能」と読むことができます。これは、5月30日ではなく、4月30日から5月31日までジャンプすることを意味します。これは「月の最後の日」であるためではなく、「開始月の日付に最も近い日」を使用するためです。

そのため、あるユーザーが別のニュースレターを購読して、2015-01-30に開始するとします。 +1 monthの直感的な日付は何ですか? 1つの解釈は、「翌月の30日または利用可能な最も近い日」となり、次の結果が返されます。

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

ユーザーが同じ日に両方のニュースレターを受け取る場合を除き、これは問題ありません。これは需要側ではなく供給側の問題だとしましょう多くのニュースレター。これを念頭に置いて、「+ 1か月」の別の解釈に戻り、「各月の最後から2番目の日に送信する」と返します。

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

これで、最初のセットとの重複が回避されましたが、4月と6月29日になります。これは、+1 monthm/$d/Yまたは魅力的でシンプルなm/30/Yをすべての可能な月で返すという元の直感と確かに一致します。それでは、両方の日付を使用した+1 monthの3番目の解釈について考えてみましょう。

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倍になる場合)と需要側(ユーザーが2月からだまされて余分な3月を感じる場合)の両方の問題になる可能性があります間違いを修正する試みとして)。一方、2つの日付が設定されていることに注意してください。

  • 重複しない
  • その月に日付があるときは常に同じ日付になります(1月30日のセットはかなりきれいに見えます)
  • 「正しい」日付とみなされる可能性のあるものから3日以内(ほとんどの場合1日)にあります。
  • すべて後継者と前身から少なくとも28日間(太陰月)であるため、非常に均等に分散されています。

最後の2つのセットを考えると、実際の翌月の外にある場合(最初のセットでは2月28日と4月30日にロールバックする)、いずれかの日付を単純にロールバックすることは難しくありません。 「月の最後の日」と「月の最後から2番目の日」パターンとの時折の重なりと相違。しかし、ライブラリが「最もきれい/自然」、「02/31および他の月のオーバーフローの数学的解釈」、「月の最初または先月に関連する」のいずれかを選択すると、常に誰かの期待に応えられず、 「間違った」解釈がもたらす現実の問題を回避するために、「間違った」日付を調整する必要のあるスケジュール。

繰り返しになりますが、+1 monthが実際に翌月の日付を返すことも期待しますが、直感ほど簡単ではなく、選択肢があるため、Web開発者の期待を数学で処理することはおそらく安全な選択です。

これは他のソリューションと同じくらい不格好ですが、素晴らしい結果が得られると思います。

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週間を追加します。 2つのテスト日付の結果は次のとおりです。

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週間。)

3
Anthony

私は次のコードを使用してそれを回避するより短い方法を見つけました:

                   $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');
2
Rommel Paras

関連する質問の Juhana's answer の改良版の実装を次に示します。

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

これは、関連する質問の 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'
1
Hải Phong

「今年の今月」の日付を取得する必要がありましたが、今月がうるう年の2月になると、すぐに不快になります。しかし、私はこれがうまくいくと信じています...://トリックは月の最初の日にあなたの変更の基礎になるようです。

$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
Just Plain High
_$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'));
_

重要:

DateTimeクラスのadd()メソッドは、オブジェクト値を変更するため、DateTimeオブジェクトでadd()を呼び出した後、新しい日付オブジェクトを返し、それ自体のオブジェクトを変更します。

0
MiharbKH

strtotime()を使用する場合は、単に$date = strtotime('first day of +1 month');を使用します

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

2(2月)を出力します。他の月でも機能します。

0
galki

月の加算または減算の問題を解決するDateTimeクラスの拡張機能

https://Gist.github.com/66Ton99/60571ee49bf1906aaa1c

0
66Ton99

月のスキップを避けたい場合は、次のようにして日付を取得し、次の月にループを実行して日付を1つ減らし、$ starting_calculatedがstrtotimeの有効な文字列である有効な日付(つまりmysql datetimeまたは "now")。これにより、月をスキップする代わりに、1分間から深夜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
user1590391

実際には、date()とstrtotime()だけでもできます。たとえば、今日の日付に1か月を追加するには:

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

datetimeクラスを使用したい場合でも、それは同じくらい簡単です。 詳細はこちら

0
PHP Addict