it-swarm.com.de

PHP DateTime :: Ändern und Hinzufügen von Monaten

Ich habe viel mit dem DateTime class gearbeitet und bin kürzlich auf ein Problem gestoßen, das ich als Fehler empfand, als ich Monate hinzufügte. Nach ein wenig Recherche scheint es sich nicht um einen Fehler zu handeln, sondern funktioniert wie beabsichtigt. Laut der gefundenen Dokumentation hier :

Beispiel # 2 Vorsicht beim Hinzufügen von oder Monate abziehen

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

Kann jemand rechtfertigen, warum dies nicht als Fehler angesehen wird?

Hat jemand außerdem elegante Lösungen, um das Problem zu korrigieren und einen Monat so zu gestalten, dass es wie erwartet funktioniert und nicht wie beabsichtigt?

86
tplaner

Warum es kein Fehler ist:

Das aktuelle Verhalten ist korrekt. Folgendes geschieht intern:

  1. +1 month erhöht die Monatsnummer (ursprünglich 1) um eins. Dies macht das Datum 2010-02-31.

  2. Der zweite Monat (Februar) hat im Jahr 2010 nur 28 Tage. Daher korrigiert PHP dies automatisch, indem die Tage ab dem 1. Februar weiter gezählt werden. Sie landen dann am 3. März.

Wie Sie bekommen, was Sie wollen:

Um zu bekommen, was Sie möchten, können Sie den nächsten Monat manuell überprüfen. Fügen Sie dann die Anzahl der Tage hinzu, die der nächste Monat hat.

Ich hoffe du kannst das selbst programmieren. Ich gebe nur was zu tun ist.

PHP 5.3 Weg:

Um das korrekte Verhalten zu erhalten, können Sie eine der neuen Funktionen von PHP 5.3 verwenden, die die relative Zeitreihengruppe first day of einführt. Diese Zeilengruppe kann in Kombination mit next month, fifth month oder +8 months verwendet werden, um zum ersten Tag des angegebenen Monats zu gelangen. Anstelle von +1 month können Sie diesen Code verwenden, um den ersten Tag des nächsten Monats wie folgt abzurufen:

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

Dieses Skript gibt February korrekt aus. Die folgenden Dinge treten auf, wenn PHP diese first day of next month-Zeilengruppe verarbeitet:

  1. next month erhöht die Monatsnummer (ursprünglich 1) um eins. Dies macht das Datum 2010-02-31.

  2. first day of setzt die Tagesnummer auf 1, was das Datum 2010-02-01 ergibt.

94
shamittomar

Das kann nützlich sein:

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

Meine Lösung für das Problem:

$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

Hier ist eine weitere kompakte Lösung, die ausschließlich DateTime-Methoden verwendet und das Objekt direkt an Ort und Stelle ändert, ohne Klone zu erstellen.

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

Es gibt aus:

2012-01-31
2012-02-29
4
Rudiger W.

In Verbindung mit der Antwort von Shamittomar könnte es dann so sein, um Monate "sicher" hinzuzufügen:

/**
 * 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;
}
3
patrickzzz

Ich habe eine Funktion erstellt, die ein DateInterval zurückgibt, um sicherzustellen, dass durch das Hinzufügen eines Monats der nächste Monat angezeigt wird, und die Tage in den folgenden Monat entfernt werden.

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

Ich stimme mit dem Gefühl des OP überein, dass dies nicht intuitiv und frustrierend ist, sondern auch, was +1 month in den Szenarien bedeutet, in denen dies auftritt. Betrachten Sie diese Beispiele:

Sie beginnen mit 2015-01-31 und möchten 6-mal einen Monat hinzufügen, um einen Zeitplan für den Versand eines E-Mail-Newsletters zu erhalten. In Anbetracht der ursprünglichen Erwartungen des OP käme dies zu folgenden Ergebnissen:

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

Beachten Sie sofort, dass wir erwarten, dass +1 monthlast day of month bedeutet oder alternativ 1 Monat pro Iteration hinzufügt, jedoch immer in Bezug auf den Startpunkt. Anstatt dies als "letzten Tag des Monats" zu interpretieren, könnten wir es als "31. Tag des nächsten Monats oder als letzter verfügbarer Monat innerhalb dieses Monats" lesen. Das bedeutet, dass wir vom 30. April bis 31. Mai statt vom 30. Mai springen. Beachten Sie, dass dies nicht auf den "letzten Tag des Monats" zurückzuführen ist, sondern auf "am nächsten verfügbaren Datum des Startmonats".

Nehmen wir an, einer unserer Benutzer abonniert einen anderen Newsletter, der am 30.01.2015 startet. Was ist das intuitive Datum für +1 month? Eine Interpretation wäre "30. Tag des nächsten Monats oder nächst verfügbarer", die zurückgegeben würde:

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

Dies wäre in Ordnung, es sei denn, unser Benutzer erhält am selben Tag beide Newsletter. Nehmen wir an, es handelt sich hierbei um ein Angebotsproblem anstelle von Nachfrageseite. Wir machen uns keine Sorgen, dass der Benutzer sich darüber ärgert, dass er am selben Tag zwei Newsletter erhält, sondern dass sich unsere Mail-Server die Bandbreite für das zweimalige Senden nicht leisten können viele Newsletter. In diesem Sinne kehren wir zur anderen Interpretation von "+1 Monat" als "Senden am vorletzten Tag jedes Monats" zurück, die zurückkehren würde:

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

Jetzt haben wir jede Überlappung mit dem ersten Satz vermieden, aber wir enden auch mit April und 29. Juni, was sicherlich unseren ursprünglichen Intuitionen entspricht, dass +1 month einfach m/$d/Y oder den attraktiven und einfachen m/30/Y für alle möglichen Monate zurückgeben sollte. Betrachten wir nun eine dritte Interpretation von +1 month mit beiden Daten:

31. Januar

  • 2015-01-31
  • 2015-03-03
  • 2015-03-31
  • 2015-05-01
  • 2015-05-31
  • 01.07.2015

30. Januar

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

Das oben genannte hat einige Probleme. Der Februar wird übersprungen, was sowohl für das Angebot als auch für das Angebot (zB wenn eine monatliche Bandbreitenzuweisung vorliegt und der Februar verschwendet wird und der März verdoppelt wird) ein Problem darstellte als Versuch, Fehler zu korrigieren). Beachten Sie jedoch, dass die beiden Datumsangaben festgelegt sind:

  • niemals überlappen
  • sind immer am selben Datum, an dem der Monat das Datum hat (also das Set vom 30. Januar sieht ziemlich sauber aus)
  • sind alle innerhalb von 3 Tagen (in den meisten Fällen 1 Tag) des "richtigen" Datums.
  • sind alle mindestens 28 Tage (ein Mondmonat) von ihrem Nachfolger und Vorgänger, also sehr gleichmäßig verteilt.

Angesichts der letzten beiden Sätze wäre es nicht schwierig, eines der Datumsangaben zurückzusetzen, wenn es außerhalb des tatsächlichen Folgemonats liegt (also auf den 28. Februar und den 30. April im ersten Satz zurückrollen) und keinen Schlaf zu verlieren gelegentliche Überschneidungen und Abweichungen vom "letzten Tag des Monats" im Vergleich zum "vorletzten Tag des Monats". Wenn man jedoch erwartet, dass die Bibliothek zwischen "am schönsten/natürlichsten", "mathematischen Interpretationen des Über- schusses des 2. und 31. Monats" und "relativ zum ersten Monat oder letzten Monat" wählen kann, endet dies immer damit, dass die Erwartungen einer Person nicht erfüllt werden Einige Zeitpläne müssen das "falsche" Datum anpassen, um das reale Problem zu vermeiden, das durch die "falsche" Interpretation entsteht.

Auch wenn ich erwarten würde, dass +1 month ein Datum zurückgibt, das tatsächlich im folgenden Monat liegt, ist es nicht so einfach wie Intuition und angesichts der Wahlmöglichkeiten, mit Mathe über die Erwartungen der Webentwickler zu gehen, ist wahrscheinlich die sichere Wahl.

Hier ist eine alternative Lösung, die immer noch so klobig ist wie alle anderen, aber ich denke, hat Nizza Ergebnisse:

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

Dies ist nicht optimal, aber die zugrunde liegende Logik lautet: Wenn das Hinzufügen von 1 Monat zu einem anderen Datum als dem erwarteten nächsten Monat führt, entfernen Sie dieses Datum und fügen Sie stattdessen 4 Wochen hinzu. Hier sind die Ergebnisse mit den zwei Testdaten:

31. Januar

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

30. Januar

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

(Mein Code ist ein Durcheinander und würde in einem mehrjährigen Szenario nicht funktionieren. Ich begrüße jeden, der die Lösung mit eleganterem Code umschreibt, solange die zugrunde liegende Prämisse intakt bleibt, dh, wenn +1 Monat ein funky Datum zurückgibt, verwenden Sie Stattdessen +4 Wochen.)

2
Anthony

Ich habe mit folgendem Code einen kürzeren Weg gefunden:

                   $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

Hier ist eine Implementierung einer verbesserten Version von Juhanas Antwort in einer verwandten Frage:

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

Diese Version setzt $createdDate unter der Annahme, dass Sie einen wiederkehrenden monatlichen Zeitraum (z. B. ein Abonnement) verwenden, der an einem bestimmten Datum (z. B. dem 31.) begonnen hat. Es dauert immer $createdDate, so dass sich verspätete "wiederkehrende" Daten nicht in niedrigere Werte verschieben, wenn sie durch weniger wertvolle Monate nach vorne verschoben werden (z. B. werden alle 29., 30. oder 31. Wiederholungsdaten am 28. nachher nicht festgehalten (bis Februar).

Hier ist ein Treibercode zum Testen des Algorithmus:

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

Welche Ausgänge:

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

Dies ist eine verbesserte Version von der Antwort von Kasihasi in einer verwandten Frage. Dadurch wird eine beliebige Anzahl von Monaten zu einem Datum hinzugefügt oder subtrahiert.

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

Zum Beispiel:

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

wird ausgeben:

'2015-02-28'
1
Hải Phong

Ich musste einen Termin für "diesen Monat letztes Jahr" bekommen und es wird ziemlich unangenehm, wenn dieser Monat im Februar ein Schaltjahr ist. Ich glaube jedoch, dass dies funktioniert.

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

Für Tage: 

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

Wichtig:

Die Methode add() der DateTime-Klasse ändert den Objektwert so, dass nach dem Aufruf von add() für ein DateTime-Objekt das neue Datumsobjekt zurückgegeben wird und das Objekt selbst geändert wird.

0
MiharbKH

Wenn Sie strtotime() verwenden, verwenden Sie einfach $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));

wird 2 (Februar) ausgeben. wird auch für andere Monate arbeiten.

0
galki

Erweiterung für die DateTime-Klasse, die das Problem des Hinzufügens oder Abziehens von Monaten löst

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

0
66Ton99

Wenn Sie nur vermeiden möchten, dass ein Monat übersprungen wird, können Sie so etwas ausführen, um das Datum herauszuholen und eine Schleife für den nächsten Monat auszuführen, das Datum um einen zu reduzieren und bis zu einem gültigen Datum zu überprüfen, wobei $ starting_calculated eine gültige Zeichenfolge für strtotime ist (dh mysql datetime oder "now"). Dies findet das Ende des Monats bei 1 Minute bis Mitternacht statt, anstatt den Monat zu überspringen.

    $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

sie können es eigentlich nur mit date () und strtotime () machen. Zum Beispiel, um 1 Monat zum heutigen Datum hinzuzufügen:

datum ("Y-m-d", Strtotime ("+ 1 Monat", Zeit ()));

wenn Sie die datetime-Klasse verwenden möchten, ist das auch in Ordnung, aber das ist genauso einfach. Weitere Details hier

0
PHP Addict