PHP DateTime :: modifier l'ajout et la soustraction de mois

102

J'ai beaucoup travaillé avec le DateTime classet j'ai récemment rencontré ce que je pensais être un bug lors de l'ajout de mois. Après quelques recherches, il semble que ce n'était pas un bug, mais fonctionnant comme prévu. Selon la documentation trouvée ici :

Exemple # 2 Attention lors de l'ajout ou de la soustraction de mois

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

Quelqu'un peut-il justifier pourquoi cela n'est pas considéré comme un bogue?

De plus, quelqu'un a-t-il des solutions élégantes pour corriger le problème et faire en sorte que +1 mois fonctionnera comme prévu au lieu de comme prévu?

tplaner
la source
À quoi vous attendez-vous que "2001-01-31" plus 1 mois sera? ... "2001-02-28"? "2001-03-01"?
Artefacto
57
Personnellement, je m'attendrais à ce que ce soit le 28/02/2001.
tplaner
Même histoire avec strtotime() stackoverflow.com/questions/7119777/…
Valentin Despa
2
Ouais, c'est une bizarrerie assez ennuyeuse. Vous avez lu les petits caractères pour comprendre que P1M est de 31 jours. Je ne comprends pas vraiment pourquoi les gens continuent de le défendre comme un comportement «juste».
Indivision Dev
On dirait que l'opinion populaire est que la logique devrait arrondir à la baisse (à 2/28), bien que PHP arrondisse à la hausse (à 3/1) ... bien que je préfère la méthode de PHP, mais Excel de Microsoft arrondit à la baisse, opposant les développeurs Web aux utilisateurs de feuilles de calcul ...
Dave Heq

Réponses:

107

Pourquoi ce n'est pas un bug:

Le comportement actuel est correct. Ce qui suit se produit en interne:

  1. +1 monthaugmente de un le numéro du mois (à l'origine 1). Cela rend la date 2010-02-31.

  2. Le deuxième mois (février) ne compte que 28 jours en 2010, donc PHP corrige automatiquement cela en continuant simplement à compter les jours à partir du 1er février. Vous vous retrouvez ensuite le 3 mars.

Comment obtenir ce que vous voulez:

Pour obtenir ce que vous voulez, c'est: vérifier manuellement le mois suivant. Ajoutez ensuite le nombre de jours du mois prochain.

J'espère que vous pourrez vous-même le coder. Je ne fais que donner quoi faire.

Manière PHP 5.3:

Pour obtenir le comportement correct, vous pouvez utiliser l'une des nouvelles fonctionnalités de PHP 5.3 qui introduit la strophe de temps relatif first day of. Cette strophe peut être utilisé en combinaison avec next month, fifth monthou +8 monthspour aller au premier jour du mois spécifié. Au lieu de +1 monthce que vous faites, vous pouvez utiliser ce code pour obtenir le premier jour du mois prochain comme ceci:

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

Ce script sortira correctement February. Les choses suivantes se produisent lorsque PHP traite cette first day of next monthstrophe:

  1. next monthaugmente de un le numéro du mois (à l'origine 1). Cela rend la date 2010-02-31.

  2. first day ofdéfinit le numéro du jour sur 1, ce qui donne la date 01/02/2010.

shamittomar
la source
1
Donc ce que vous dites, c'est que cela ajoute littéralement 1 mois, en ignorant complètement les jours? Je suppose donc que vous pourriez rencontrer un problème similaire avec +1 an si vous l'ajoutez pendant une année bissextile?
tplaner
@evolve, Oui, cela ajoute 1 mois littéraire.
shamittomar
13
Et si vous soustrayez 1 mois après l'avoir ajouté, vous vous retrouvez avec une date entièrement différente, je suppose. Cela semble très peu intuitif.
Dan Breen
2
Exemple génial d'utilisation des nouvelles strophes de PHP 5.3 où vous pouvez utiliser le premier jour, le dernier jour, ce mois-ci, le mois suivant et le mois précédent.
Kim Stacks
6
à mon humble avis c'est un bug. un bug sérieux. si je veux ajouter 31 jours, j'ajoute 31 jours. Je veux ajouter un mois, un mois doit être ajouté, pas 31 jours.
low_rents
12

Voici une autre solution compacte utilisant entièrement les méthodes DateTime, modifiant l'objet sur place sans créer de clones.

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

Il sort:

2012-01-31
2012-02-29
Rudiger W.
la source
1
Merci. La meilleure solution fournie ici jusqu'à présent. Vous pouvez également raccourcir le code en $dt->modify()->modify(). Fonctionne aussi bien.
Alph.Dev
10

Cela peut être utile:

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
nicolaas thiemen francken
la source
2
Ce n'est pas une solution générale, car cela ne fonctionne que pour certaines entrées, comme le 1er du mois. Par exemple, faire cela pour le 30 janvier conduit à la souffrance.
Jens Roland
Ou vous pourriez faire$dateTime->modify('first day of next month')->modify('-1day')
Anthony
6

Ma solution au problème:

$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' );
    }
}
Bernland
la source
3
La commande "clone" était la solution à mes problèmes d'affectation de variables. Merci pour ça.
Steph Rose le
4

Je partage le sentiment du PO selon lequel cela est contre-intuitif et frustrant, mais il en va de même pour déterminer ce que +1 monthsignifie les scénarios où cela se produit. Considérez ces exemples:

Vous commencez avec le 31/01/2015 et souhaitez ajouter un mois 6 fois pour obtenir un cycle de planification pour l'envoi d'une newsletter par e-mail. Compte tenu des attentes initiales du PO, cela reviendrait:

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

Tout de suite, notez que nous nous attendons +1 monthà vouloir dire last day of monthou, alternativement, à ajouter 1 mois par itération mais toujours en référence au point de départ. Au lieu d'interpréter cela comme "le dernier jour du mois", nous pourrions le lire comme "le 31e jour du mois prochain ou le dernier jour disponible au cours de ce mois". Cela signifie que nous sautons du 30 avril au 31 mai au lieu du 30 mai. Notez que ce n'est pas parce que c'est "le dernier jour du mois", mais parce que nous voulons "le plus proche disponible à la date du mois de début".

Supposons donc qu'un de nos utilisateurs s'abonne à une autre newsletter pour commencer le 30/01/2015. À quoi sert la date intuitive +1 month? Une interprétation serait "30e jour du mois suivant ou le plus proche disponible" qui renverrait:

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

Ce serait bien sauf lorsque notre utilisateur reçoit les deux newsletters le même jour. Supposons qu'il s'agit d'un problème du côté de l'offre plutôt que du côté de la demande. de nombreuses newsletters. Dans cet esprit, nous revenons à l'autre interprétation de "+1 mois" comme "envoyer l'avant-dernier jour de chaque mois" qui renverrait:

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

Nous avons maintenant évité tout chevauchement avec le premier set, mais nous nous retrouvons également avec avril et 29 juin, ce qui correspond certainement à nos intuitions originales qui +1 monthdevraient simplement revenir m/$d/You à l'attrait et au simple m/30/Ypour tous les mois possibles. Considérons maintenant une troisième interprétation de l' +1 monthutilisation des deux dates:

31 janvier

  • 31/01/2015
  • 03/03/2015
  • 31/03/2015
  • 01/05/2015
  • 31/05/2015
  • 01/07/2015

30 janvier

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

Ce qui précède a quelques problèmes. Février est ignoré, ce qui pourrait être un problème à la fois en termes d'approvisionnement (par exemple, s'il y a une allocation de bande passante mensuelle et que février est gaspillé et mars est doublé) et de demande (les utilisateurs se sentent trompés par rapport à février et perçoivent le mois de mars supplémentaire. comme tentative de corriger l'erreur). En revanche, notez que les deux dates définissent:

  • ne jamais se chevaucher
  • sont toujours à la même date lorsque ce mois a la date (donc l'ensemble du 30 janvier semble assez propre)
  • sont tous dans les 3 jours (1 jour dans la plupart des cas) de ce qui pourrait être considéré comme la date «correcte».
  • sont tous au moins 28 jours (un mois lunaire) de leur successeur et prédécesseur, donc très uniformément répartis.

Compte tenu des deux derniers sets, il ne serait pas difficile de simplement revenir en arrière sur l'une des dates si elle tombe en dehors du mois suivant (donc revenez au 28 février et au 30 avril dans le premier set) et ne perdez pas de sommeil sur le chevauchement occasionnel et divergence entre le modèle «dernier jour du mois» et «avant-dernier jour du mois». Mais s'attendre à ce que la bibliothèque choisisse entre "la plus jolie / naturelle", "l'interprétation mathématique du 31/02 et des autres débordements du mois" et "par rapport au premier du mois ou au mois dernier" se terminera toujours par le non-respect des attentes de quelqu'un et certains horaires doivent ajuster la «mauvaise» date pour éviter le problème du monde réel que la «mauvaise» interprétation introduit.

Donc, encore une fois, même si je m'attendrais également +1 monthà renvoyer une date qui est en fait le mois suivant, ce n'est pas aussi simple que l'intuition et étant donné les choix, aller avec les mathématiques sur les attentes des développeurs Web est probablement le choix sûr.

Voici une solution alternative qui est toujours aussi maladroite que toute autre mais qui, à mon avis, donne de bons résultats:

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

Ce n'est pas optimal, mais la logique sous-jacente est la suivante: si l'ajout d'un mois entraîne une date autre que le mois prochain prévu, supprimez cette date et ajoutez 4 semaines à la place. Voici les résultats avec les deux dates de test:

31 janvier

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

30 janvier

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

(Mon code est un gâchis et ne fonctionnerait pas dans un scénario pluriannuel. Je souhaite à quiconque de réécrire la solution avec un code plus élégant tant que le principe sous-jacent est conservé intact, c'est-à-dire si +1 mois renvoie une date géniale, utilisez +4 semaines à la place.)

Anthony
la source
4

J'ai créé une fonction qui renvoie un DateInterval pour m'assurer que l'ajout d'un mois affiche le mois suivant et supprime les jours suivants.

$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');
    }
}
AR
la source
4

En conjonction avec la réponse de shamittomar, cela pourrait alors être ceci pour ajouter des mois "en toute sécurité":

/**
 * 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;
}
patrickzzz
la source
Merci beaucoup!!
geckos
2

J'ai trouvé un moyen plus court de le contourner en utilisant le code suivant:

                   $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');
Rommel Paras
la source
1

Voici une implémentation d'une version améliorée de la réponse de Juhana à une question connexe:

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

Cette version $createdDatesuppose que vous avez affaire à une période mensuelle récurrente, comme un abonnement, qui a commencé à une date précise, comme le 31. Il faut toujours$createdDate tellement tard que les dates "récurrentes" ne passeront pas à des valeurs inférieures car elles sont repoussées à travers des mois de moindre valeur (par exemple, les 29e, 30e ou 31e dates récurrentes ne seront finalement pas bloquées le 28 après le passage à travers une année non bissextile février).

Voici un code de pilote pour tester l'algorithme:

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

Quelles sorties:

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
derekm
la source
1

Ceci est une version améliorée de la réponse de Kasihasi à une question connexe. Cela ajoutera ou soustrayera correctement un nombre arbitraire de mois à une date.

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

Par exemple:

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

affichera:

'2015-02-28'
Hải Phong
la source
0

Si vous voulez simplement éviter de sauter un mois, vous pouvez effectuer quelque chose comme ceci pour obtenir la date et exécuter une boucle le mois suivant en réduisant la date de un et en revérifiant jusqu'à une date valide où $ starting_calculated est une chaîne valide pour strtotime (ie mysql datetime ou "now"). Cela trouve la toute fin du mois à 1 minute à minuit au lieu de sauter le mois.

    $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"));
user1590391
la source
0

Si vous utilisez strtotime()juste utiliser$date = strtotime('first day of +1 month');

Primoz Rome
la source
0

J'avais besoin d'un rendez-vous pour «ce mois-ci l'année dernière» et cela devient assez vite désagréable quand ce mois est février dans une année bissextile. Cependant, je crois que cela fonctionne ...: - / L'astuce semble être de baser votre changement sur le 1er jour du mois.

$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);
Tout simplement haut
la source
0
$ds = new DateTime();
$ds->modify('+1 month');
$ds->modify('first day of this month');
Tim Graham
la source
1
Vous devez expliquer votre réponse. Les réponses au code uniquement sont considérées comme de mauvaise qualité
Machavity
Je vous remercie! C'est la meilleure réponse à ce jour. Si vous changez les 2 dernières lignes, le mois est toujours correct. Gloire!
Danny Schoemann
0
$month = 1; $year = 2017;
echo date('n', mktime(0, 0, 0, $month + 2, -1, $year));

sortira 2(février). fonctionnera aussi pendant d'autres mois.

galki
la source
0
$current_date = new DateTime('now');
$after_3_months = $current_date->add(\DateInterval::createFromDateString('+3 months'));

Pendant des jours:

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

Important:

La méthode add()de la classe DateTime modifie la valeur de l'objet afin qu'après avoir appelé add()un objet DateTime, elle retourne le nouvel objet date et modifie également l'objet lui-même.

MiharbKH
la source
0

vous pouvez également le faire avec uniquement date () et strtotime (). Par exemple, pour ajouter 1 mois à la date d'aujourd'hui:

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

si vous souhaitez utiliser la classe datetime c'est bien aussi mais c'est tout aussi simple. plus de détails ici

Addict PHP
la source
0

La réponse acceptée explique déjà pourquoi ce n'est pas un mais, et certaines autres réponses posent une solution soignée avec des expressions php comme first day of the +2 months. Le problème avec ces expressions est qu'elles ne sont pas complétées automatiquement.

La solution est cependant assez simple. Tout d'abord, vous devriez trouver des abstractions utiles qui reflètent votre espace de problème. Dans ce cas, c'est un fichier ISO8601DateTime. Deuxièmement, il devrait y avoir plusieurs implémentations qui peuvent apporter une représentation textuelle souhaitée. Par exemple, Today, Tomorrow, The first day of this month, Future- représentent tous une implémentation spécifique du ISO8601DateTimeconcept.

Donc, dans votre cas, une implémentation dont vous avez besoin est TheFirstDayOfNMonthsLater. C'est facile à trouver simplement en regardant la liste des sous-classes dans n'importe quel IDE. Voici le code:

$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

La même chose avec les derniers jours d'un mois. Pour plus d'exemples de cette approche, lisez ceci .

Vadim Samokhin
la source
-2
     $date = date('Y-m-d', strtotime("+1 month"));
     echo $date;
Mohammed F. Ghazo
la source