PHPのDateTime :: modifyで月の加算と減算を行う


101

私は多くの作業をしてきましたがDateTime class、最近、数か月追加するとバグだと思ったことがありました。少し調べたところ、それはバグではなく、意図したとおりに機能しているようです。ここにあるドキュメントによると:

例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 2010

57
個人的には2001-02-28になると思います。
tplaner 2010


2
うん、それはかなり迷惑な癖です。P1Mが31日であることを理解するために、細字を読みました。人々がなぜそれを「正しい」行動として擁護し続けるのか、本当に理解してはいけません。
Indivision Dev

人気のある意見のようですが、PHPは切り上げ(3/1に)しますが、ロジックは切り捨て(2/28に)する必要があります...私はPHPの方法を好みますが、MicrosoftのExcelは切り捨てて、Web開発者をスプレッドシートユーザーと対戦させます...
Dave Heq

回答:


106

バグではない理由:

現在の動作は正しいです。以下が内部で発生します。

  1. +1 month月数(最初は1)を1増やします。これは日付になります2010-02-31

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

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

必要なものを取得するには、次の月を手動で確認します。次に、来月の日数を追加します。

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

PHP 5.3の方法:

正しい動作を得るには、相対時間スタンザを導入するPHP 5.3の新機能の1つを使用できますfirst day of。このスタンザはと組み合わせて使用することができ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にします。


1
だから、あなたが言っているのは、完全に日を無視して、文字通り1か月加算されるということですか?うるう年に追加すると、+ 1年で同様の問題が発生する可能性があると思いますか?
tplaner 2010

@evolve、はい、それは文学的に1か月加算されます。
shamittomar 2010

13
そして、それを追加してから1か月差し引くと、完全に別の日付になります。それは非常に直感的ではないようです。
Dan Breen

2
PHP 5.3での新しいスタンザの使用に関する素晴らしい例。初日、最終日、今月、来月、前月を使用できます。
キムスタック2014

6
これはバグです。深刻なバグ。31日を追加する場合は、31日を追加します。IIは月を追加したいのですが、31日ではなく、月を追加する必要があります。
low_rents 2016

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 2018年

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 month意味することを期待していることに注意してくださいlast 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

これは、ユーザーが同じ日に両方のニュースレターを受け取る場合を除き、問題ありません。これが需要側ではなく供給側の問題であると仮定しましょう。ユーザーが同じ日に2通のニュースレターを受信することに煩わされることはありませんが、メールサーバーは2回送信するための帯域幅を利用できません。多くのニュースレター。このことを念頭に置いて、「各月の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両方の日付を使用する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つのセットを考えると、日付の1つが実際の翌月から外れている場合(最初のセットで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週間。)


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

どうもありがとうございます!!
ヤモリ

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値が小さい月に繰り越されるため、遅い "recurs on"日付は常に低い値にシフトしないので(たとえば、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

これは、関連する質問におけるカシハシの回答の改良版です。これにより、日付に任意の月数が正しく加算または減算されます。

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月になるとすぐに不快になります。しかし、私はこれがうまくいくと信じています...:-/トリックはあなたの変更を月の最初の日に基づいているようです。

$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

ありがとうございました!これは最も近い答えです。最後の2行を切り替えると、常に正しい月が表示されます。賞賛!
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()));

日時クラスを使用したい場合も問題ありませんが、これも同じくらい簡単です。詳細はこちら


0

受け入れられた答えはこれがなぜではないのかをすでに説明しています、そして他のいくつかの答えは次のようなphp式できちんとした解決策をもたらします first day of the +2 months。これらの式の問題は、それらがオートコンプリートされないことです。

解決策は非常に簡単です。まず、問題の空間を反映する有用な抽象化を見つける必要があります。この場合、それはISO8601DateTimeです。第二に、望ましいテキスト表現をもたらすことができる複数の実装があるはずです。たとえば、TodayTomorrowThe first day of this monthFuture-全ての具体的な実装を表す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.