私は 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か月が意図したものではなく期待どおりに機能するようにするエレガントなソリューションはありますか?
現在の動作は正しいです。内部的には次のことが起こります。
+1 month
は、月数(元は1)を1つ増やします。これにより、日付が2010-02-31
になります。
2番目の月(2月)は2010年に28日しかないので、PHP 2月1日からの日数を数え続けるだけでこれを自動修正します。その後、3月3日に終わります。
欲しいものを手に入れるには、次の月を手動でチェックします。次に、来月の日数を追加します。
これを自分でコーディングできるといいのですが。私はただやるべきことを与えています。
正しい動作を得るために、相対時間スタンザfirst day of
を導入するPHP 5.3の新しい機能のいずれかを使用できます。このスタンザは、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
スタンザを処理すると、次のことが起こります。
next month
は、月数(元は1)を1つ増やします。これにより、日付が2010-02-31になります。
first day of
は日番号を1
に設定し、結果は2010-02-01の日付になります。
これは役に立つかもしれません:
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' );
}
}
これは、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
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;
}
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');
}
}
これは直感に反し、イライラするというOPの感情に同意しますが、これが発生するシナリオで+1 month
が何を意味するかを決定することにも同意します。これらの例を考慮してください:
2015-01-31から始めて、1か月を6回追加して、メールニュースレターを送信するためのスケジュールサイクルを取得します。 OPの最初の期待を念頭に置いて、これは以下を返します。
すぐに、+1 month
がlast day of month
を意味すること、または反復ごとに1か月を追加するが常に開始点を参照することを期待していることに注意してください。これを「月の最後の日」と解釈する代わりに、「来月の31日目またはその月の最後に利用可能」と読むことができます。これは、5月30日ではなく、4月30日から5月31日までジャンプすることを意味します。これは「月の最後の日」であるためではなく、「開始月の日付に最も近い日」を使用するためです。
そのため、あるユーザーが別のニュースレターを購読して、2015-01-30に開始するとします。 +1 month
の直感的な日付は何ですか? 1つの解釈は、「翌月の30日または利用可能な最も近い日」となり、次の結果が返されます。
ユーザーが同じ日に両方のニュースレターを受け取る場合を除き、これは問題ありません。これは需要側ではなく供給側の問題だとしましょう多くのニュースレター。これを念頭に置いて、「+ 1か月」の別の解釈に戻り、「各月の最後から2番目の日に送信する」と返します。
これで、最初のセットとの重複が回避されましたが、4月と6月29日になります。これは、+1 month
がm/$d/Y
または魅力的でシンプルなm/30/Y
をすべての可能な月で返すという元の直感と確かに一致します。それでは、両方の日付を使用した+1 month
の3番目の解釈について考えてみましょう。
上記にはいくつかの問題があります。 2月はスキップされます。これは、供給側(たとえば、毎月の帯域幅割り当てがあり、2月が無駄になり、3月が2倍になる場合)と需要側(ユーザーが2月からだまされて余分な3月を感じる場合)の両方の問題になる可能性があります間違いを修正する試みとして)。一方、2つの日付が設定されていることに注意してください。
最後の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か月でファンキーな日付が返される場合は、よりエレガントなコードでソリューションを書き直してください。代わりに+4週間。)
私は次のコードを使用してそれを回避するより短い方法を見つけました:
$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');
関連する質問の 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
これは、関連する質問の 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'
「今年の今月」の日付を取得する必要がありましたが、今月がうるう年の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);
_$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()
を呼び出した後、新しい日付オブジェクトを返し、それ自体のオブジェクトを変更します。
strtotime()
を使用する場合は、単に$date = strtotime('first day of +1 month');
を使用します
$month = 1; $year = 2017;
echo date('n', mktime(0, 0, 0, $month + 2, -1, $year));
2
(2月)を出力します。他の月でも機能します。
月の加算または減算の問題を解決するDateTimeクラスの拡張機能
月のスキップを避けたい場合は、次のようにして日付を取得し、次の月にループを実行して日付を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"));
実際には、date()とstrtotime()だけでもできます。たとえば、今日の日付に1か月を追加するには:
date( "Y-m-d"、strtotime( "+ 1 month"、time()));
datetimeクラスを使用したい場合でも、それは同じくらい簡単です。 詳細はこちら