Helmut: strtotime()

problematische Seite

Hallo,

bei meinen SVG-Basteleien habe ich für die Auswahl des Zeitbereiches auch strtotime() benutzt.

Leider habe ich dann nach dem Umschalten vom 30. auf den 31. Tag des Monats eine Überraschung erlebt:

# fehlerhaft:
#$date = date('Ymd');

# Würg-Around funktioniert bisher:
$date = date('Ym') . '01' ;

$date_0 = date('Ym');
$date_1 = date('Ym', strtotime("$date -1 month"));
$date_2 = date('Ym', strtotime("$date -2 month"));

Ich brauche die Zeitstrings in der Form 202307, 202306, 202305 (immer aktueller Monat, letzter Monat, vorletzter Monat) für mein glob()-Filter, um die passenden Datendateien für die Kurven auszuwählen.

Leider liefert die fehlerhafte Version am 31.07.2023 statt 202306 auch 202307. Der Mai wir allerdings wieder richtig ermittelt.

Ist das jetzt ein Bug in strtotime(), oder mache ich nur einen Denkfehler?

LG
Helmut

  1. problematische Seite

    Hm. Bei mir funktioniert Dein Beispiel...

    <?php
    # fehlerhaft:
    #$date = date('Ymd');
    
    # Würg-Around funktioniert bisher:
    $date = date('Ym') . '01' ;
    
    $date_0 = date('Ym');
    $date_1 = date('Ym', strtotime("$date -1 month"));
    $date_2 = date('Ym', strtotime("$date -2 month"));
    
    # Ausgaben:
    echo "Aktueller Monat:  " .  $date_0 . PHP_EOL;
    echo "Letzter Monat:    " .  $date_1 . PHP_EOL;
    echo "Vorletzter Monat: " .  $date_2 . PHP_EOL;
    
    Aktueller Monat:  202307
    Letzter Monat:    202306
    Vorletzter Monat: 202305
    
    

    Da ich im ersten Versuch der Ausgabe die letzten beiden Zeilen durch Copy & Paste erzeugte und dabei großzügig das „_0“, „_1“, „_2“ hinter „$date“ übersehen habe:

    Hast Du in den nicht gezeigten Ausgaben bzw. bei der Verwendung einen Typo drin?

  2. problematische Seite

    Moin Helmut,

    Leider liefert die fehlerhafte Version am 31.07.2023 statt 202306 auch 202307. Der Mai wir allerdings wieder richtig ermittelt.

    Ist das jetzt ein Bug in strtotime(), oder mache ich nur einen Denkfehler?

    die erste Anmerkung in der verlinkten Dokumentation lautet:

    Hinweis:

    "Relatives" Datum bedeutet in diesem Fall auch, dass nicht angegebene Komponenten des Datums-/Zeitstempels unverändert aus dem baseTimestamp übernommen werden. Das heißt, dass strtotime('February') am 31. Mai 2022 als 31 February 2022 interpretiert wird, was zum Zeitstempel 3 March führt (in einem Schaltjahr wäre es 2 March). Stattdessen strtotime('1 February') oder strtotime('first day of February') zu verwenden, würde dieses Problem vermeiden.

    Das dürfte deine Frage beantworten 😉

    Viele Grüße
    Robert

    1. problematische Seite

      Moin Rolf,

      Leider liefert die fehlerhafte Version am 31.07.2023 statt 202306 auch 202307. Der Mai wir allerdings wieder richtig ermittelt.

      Ist das jetzt ein Bug in strtotime(), oder mache ich nur einen Denkfehler?

      die erste Anmerkung in der verlinkten Dokumentation lautet:

      Hinweis:

      "Relatives" Datum bedeutet in diesem Fall auch, dass nicht angegebene Komponenten des Datums-/Zeitstempels unverändert aus dem baseTimestamp übernommen werden. Das heißt, dass strtotime('February') am 31. Mai 2022 als 31 February 2022 interpretiert wird, was zum Zeitstempel 3 March führt (in einem Schaltjahr wäre es 2 March). Stattdessen strtotime('1 February') oder strtotime('first day of February') zu verwenden, würde dieses Problem vermeiden.

      Das dürfte deine Frage beantworten 😉

      Dann sollte ich noch testen, was mit Januar als aktuellem Monat passiert, ob dann für die Vormonate wenigstens das Jahr richtig um eins zurückgesetzt wird.

      Viele Grüße
      Helmut

      1. problematische Seite

        Hallo Helmut,

        Robert ≠ Rolf 😉

        Du kannst schlichtweg nicht mit der Additionsfunktion für PHP Datümer arbeiten. Die ist an dieser Stelle so spezifiziert, dass sie Dir auf die Füße fällt.

        Die Frage, welches Datum "einen Monat vor dem 31.07.2023" liegt, ist aber auch nicht brauchbar zu beantworten. Den 31.06. gibt's nicht. Also nehmen wir den 30.06.? Tjaaa - damit wird die Operation "einen Monat zurück" aber nicht mehr korrekt invertierbar: Einen Monat zurück, einen Monat vor, einen Tag daneben?

        Deshalb ist ein Bankmonat immer 30 Tage lang… Und Du musst das Problem selbst lösen.

        Meine Lösung würde die YM-Arithmetik von der Druckdarstellung trennen.

        // $currentdate = getDate();
        $currentdate = [ "year" => 2023, "mon" => 7 ];
        echo formatYM(addMonths($currentdate, -1)) . "\n";
        echo formatYM(addMonths($currentdate, -2)) . "\n";
        
        function addMonths($dateStruct, $offset) {
        	$newMonth = $dateStruct["mon"] - 1 + $offset;   // Nullbasierend!
        	$yearOffset = floor($newMonth / 12);
          return [
            'year' => $dateStruct['year'] + $yearOffset,
            'mon'  => $newMonth + 1 - 12*$yearOffset
          ];
        }
        
        function formatYM($dateStruct) {
        	return sprintf("%04d%02d", $dateStruct['year'], $dateStruct['mon']);
        }
        

        Die addMonths-Funktion erwartet ein assozatives Array mit den Schlüsseln year und mon (und ggf weiteren) und gibt ein Array mit NUR diesen beidem Schlüsseln zurück. Aussagen über den Tag werden damit beseitigt, weil sie sowieso potenziell inkorrekt sind.

        Sie zieht vom Monat erstmal 1 ab, bevor sie rechnet, weil Reste-Arithmetik besser 0-basierend funktioniert. $yearOffset ist der Überschuss an Jahren. floor() liefert für die Monatswerte 0-11 den Wert 0, bleibt also im Jahr. Für -12 bis -1 liefert es -1, für 12 bis 23 liefert es +1.

        In der Rückgabe wird das Jahr um den $yearOffset korrigiert und von den Monaten das zwölffache des $yearOffset abgezogen. Damit kommen sie wieder in den Bereich 0-11. Ein "+1" macht das anfängliche -1 wieder rückgängig, so dass Du die Monatsnummer 1-basiert hast.

        Die formatYM Funktion hat NUR die Aufgabe, ein assoziatives Array mit den Schlüsseln year und mon im Format YYYYMM zu formatieren.

        Trennung der Zuständigkeiten nennt man das. Und seit PHP 7 hast Du für Funktionsaufrufe auch keine gravierenden Strafzeiten mehr.

        Rolf

        --
        sumpsi - posui - obstruxi
  3. problematische Seite

    Dein „Würg-Around“ ist nicht so falsch. Was mir eher nicht gefällt ist der Ansatz via strtotime();

    Wenn Du aber etwas anderes willst kannst Du gerne mktime() nehmen.

    <?php
    
    echo "Dieser Monat:     " . date( 'Ym', mktime( 0, 0, 0, date("m" )-0, 1,   date("Y") ) ) . PHP_EOL;
    echo "Letzter Monat:    " . date( 'Ym', mktime( 0, 0, 0, date("m" )-1, 1,   date("Y") ) ) . PHP_EOL;
    echo "Vorletzter Monat: " . date( 'Ym', mktime( 0, 0, 0, date("m" )-2, 1,   date("Y") ) ) . PHP_EOL;
    
    echo  PHP_EOL . "Januar:" . PHP_EOL;
    
    echo "Dieser Monat:     " . date( 'Ym', mktime( 0, 0, 0, 1-0, 1,   date("Y") ) ) . PHP_EOL;
    echo "Letzter Monat:    " . date( 'Ym', mktime( 0, 0, 0, 1-1, 1,   date("Y") ) ) . PHP_EOL;
    echo "Vorletzter Monat: " . date( 'Ym', mktime( 0, 0, 0, 1-2, 1,   date("Y") ) ) . PHP_EOL;
    
    

    Ausgaben:

    Dieser Monat:     202307
    Letzter Monat:    202306
    Vorletzter Monat: 202305
    
    Januar:
    Dieser Monat:     202301
    Letzter Monat:    202212
    Vorletzter Monat: 202211
    
    1. problematische Seite

      Hallo Raketenwilli,

      ooookay - mktime enthält die Logik aus meiner addMonths-Funktion bereits…

      Rolf

      --
      sumpsi - posui - obstruxi
      1. Und jetzt noch die Variante, die Verwirrungen genau am Monatsende (durch den mehrfachen Aufruf von date() ) strikt vermeidet …

        <?php
        
        list( $y, $m ) = explode( '-', date( 'Y-m' ) );
        
        echo "Dieser Monat:     " . date( 'Ym', mktime( 0, 0, 0, $m-0, 1,   $y ) ) . PHP_EOL;
        echo "Letzter Monat:    " . date( 'Ym', mktime( 0, 0, 0, $m-1, 1,   $y ) ) . PHP_EOL;
        echo "Vorletzter Monat: " . date( 'Ym', mktime( 0, 0, 0, $m-2, 1,   $y ) ) . PHP_EOL;
        
        echo  PHP_EOL . "Willkürlich: Januar 2000" . PHP_EOL;
        
        $y = 2000;
        $m = 1;
        
        echo "Dieser Monat:     " . date( 'Ym', mktime( 0, 0, 0, $m-0, 1,   $y ) ) . PHP_EOL;
        echo "Letzter Monat:    " . date( 'Ym', mktime( 0, 0, 0, $m-1, 1,   $y ) ) . PHP_EOL;
        echo "Vorletzter Monat: " . date( 'Ym', mktime( 0, 0, 0, $m-2, 1,   $y ) ) . PHP_EOL;
        
        1. Großen Dank an Euch.. So wurde aus der Mücke doch noch ein Elefant.

          Ich werde den Link auf diesen Thread im Quelltext-Kommentar hinterlassen, für den nächsten Trottel, der seine Mitarbeit leichtfertig anbietet ;-)

          Liebe Grüße Helmut

  4. problematische Seite

    Lieber Helmut,

    # fehlerhaft:
    #$date = date('Ymd');
    

    das wage ich zu bezweifeln. Das Datum nimmt immer den Timestamp von „jetzt“.

    Suchst Du das hier?

    $months_backwards = [];
    $number_of_months_including_current = 3;
    
    for ($i = 0; $i < $number_of_months_including_current; $i++) {
    	$months_backwards[$i] = date(
    		"Ym",
    		strtotime(
    			date(
    				'Ym01',
    				strtotime('this month')
    			). " -$i months"
    		)
    	);
    }
    
    print_r($months_backwards);
    
    Array
    (
        [0] => 202307
        [1] => 202306
        [2] => 202305
    )
    

    Liebe Grüße

    Felix Riesterer

    1. problematische Seite

      Hallo Felix,

      jetzt hätt ich fast ein Minus rausgekeilt und rumgepoltert, dass Helmut damit doch angefangen hat.

      Aber ich hab das unschuldige "01" im strtotime übersehen. Damit löst man das Problem, gute Idee.

      Rolf

      --
      sumpsi - posui - obstruxi
    2. problematische Seite

      Lieber Felix,

      # fehlerhaft:
      #$date = date('Ymd');
      

      das wage ich zu bezweifeln. Das Datum nimmt immer den Timestamp von „jetzt“.

      Suchst Du das hier?

      $months_backwards = [];
      $number_of_months_including_current = 3;
      
      for ($i = 0; $i < $number_of_months_including_current; $i++) {
      	$months_backwards[$i] = date(
      		"Ym",
      		strtotime(
      			date(
      				'Ym01',
      				strtotime('this month')
      			). " -$i months"
      		)
      	);
      }
      
      print_r($months_backwards);
      
      Array
      (
          [0] => 202307
          [1] => 202306
          [2] => 202305
      )
      

      Mit der '01' hat es bei mir mit strtotime() doch auch schon funktioniert. Nur mit der '31' ging es schief!

      Aber das war ja nur rein äußerlich. Welche Fehler da sonst noch und vor allem wann und warum auftreten könnten, konnte ich nicht überschauen.

      Liebe Grüße
      Helmut

      1. problematische Seite

        Hallo Helmut,

        du übersiehst den Trick. Felix lässt da 4 Schritte ablaufen (ob das performant ist, sei dahingestellt).

        (1) Unix Timestamp von heute holen mit strtotime("this month"). Da könnte man auch this day oder this year oder today angeben. (2) Den mit date() als YYYYMM01 formatieren und "-$i months" anhängen (3) Daraus mit strtotime wieder einen Unix Timestamp machen (4) Den wiederum als YYYYMM formatieren.

        Geht auch etwas kompakter. date() verwendet den aktuellen Timestamp, wenn man den 2. Parameter weglässt. Damit könnte man dann auch

        $months_backwards[$i] = date(
        		"Ym",
        		strtotime(
        			date('Ym01') . " -$i months"
        		)
        	);
        

        schreiben. Entscheidend ist jedenfalls, durch das Format Ym01 erstmal auf den Monatsersten zu wechseln und damit das Problem loszuwerden.

        Rolf

        --
        sumpsi - posui - obstruxi