skip to Main Content

I hope someone can help with this, i think perhaps the issue is i am overwriting the DateTime value in my second for loop, because its not outputting correct values, but not entirely sure.

<?php

$begin_from = new DateTime( "2023-01-01" );
$end_from   = new DateTime( "2023-12-31" );

$begin_to = new DateTime( "2023-01-31" );
$end_to   = new DateTime( "2023-12-31" );

for($i = $begin_from; $i <= $end_from; $i->modify('+1 month')){
    for($k = $begin_to; $k <= $end_to; $k->modify('first day of')->modify('+1 month')->modify('last day of')){
    echo $i->format("Y-m-d"),'..',$k->format("Y-m-d");
    echo "n";
    }
}

From the code above its outputting this:

2023-01-01..2023-01-31
2023-01-01..2023-02-28
2023-01-01..2023-03-31
2023-01-01..2023-04-30
2023-01-01..2023-05-31
2023-01-01..2023-06-30
2023-01-01..2023-07-31
2023-01-01..2023-08-31
2023-01-01..2023-09-30
2023-01-01..2023-10-31
2023-01-01..2023-11-30
2023-01-01..2023-12-31

But, if you run these for loops separately you will get the correct values like below.

2023-01-01..2023-01-31
2023-02-01..2023-02-28
2023-03-01..2023-03-31
2023-04-01..2023-04-30
2023-05-01..2023-05-31
2023-06-01..2023-06-30
2023-07-01..2023-07-31
2023-08-01..2023-08-31
2023-09-01..2023-09-30
2023-10-01..2023-10-31
2023-11-01..2023-11-30
2023-12-01..2023-12-31

Can anyone tell what i am doing wrong here?

4

Answers


  1. I’m not sure why your code produces that output, but it can be simplified using a while loop. You just need one date to manually modify within each loop, and one target date (DateTimeImmutable is recommended for datetimes that are meant to remain unchanged)

    $i_date = new DateTime( "2023-01-01" );
    $end_date = new DateTimeImmutable( "2023-12-31" );
    
    
    while($i_date <= $end_date){
        // echo first of month..
        echo $i_date->format("Y-m-d..");
        // goto last of month
        $i_date->modify('last day of');
        // echo last of month n
        echo $i_date->format("Y-m-d") . "n";
    
        // goto first of next month for the next iteration
        $i_date->modify('first day of')->modify('+1 month');
    }
    

    The above will produce the following in PHP v7+:

    2023-01-01..2023-01-31
    2023-02-01..2023-02-28
    2023-03-01..2023-03-31
    2023-04-01..2023-04-30
    2023-05-01..2023-05-31
    2023-06-01..2023-06-30
    2023-07-01..2023-07-31
    2023-08-01..2023-08-31
    2023-09-01..2023-09-30
    2023-10-01..2023-10-31
    2023-11-01..2023-11-30
    2023-12-01..2023-12-31
    

    PHP v5.6.40 requires you set the timezone or the default timezone like date_default_timezone_set('UTC'); before creating the DateTimes, but then you get the same output as above.

    Run it live here.

    Login or Signup to reply.
  2. To use datetime objects in a loop like @ArleighHix’s answer, I’d only modify the start date and use a calls of min() and max() in conjunction with 01 and t to ensure that date boundaries are not exceeded.

    max() and min() are safe in this case because Y-m-d is a "big-endian" date format — this means that the string can be evaluated as a simple string.

    Code: (Demo)

    $start_date = new DateTime("2023-01-03");
    $end_date = new DateTime("2023-12-15");
    
    while ($start_date <= $end_date) {
        printf(
            "%s..%sn",
            max($start_date->format("Y-m-01"), $start_date->format("Y-m-d")),
            min($end_date->format("Y-m-d"), $start_date->format("Y-m-t"))
        );
        $start_date->modify('+1 month first day of');
    }
    

    If your actual project requirement is to have non-full date ranges each month, then a refactored approach would be necessary. Please provide a more challenging example by editing your question if this is a real concern/possibility.

    Login or Signup to reply.
  3. If your main goal is to output the start and end of each month within a given date range, inclusive of the end month, you can simplify this down to:

    $from = new DateTime("2023-01-01");
    $to   = new DateTime("2023-12-31");
    
    while ($from <= $to) {
        echo $from->format("Y-m-01") . ".." . $from->format("Y-m-t") . "n";
        $from->add(new DateInterval("P1M"));
    }
    

    If you need to respect the $from and $to date, that is; if you need first date range to start at $from and the last date range to end at $to, you can adjust the above code slightly using the max() and min() functions:

    $from = new DateTime("2023-01-05");
    $to   = new DateTime("2023-12-25");
    
    $current = new DateTime($from->format("Y-m-01"));
    while ($current <= $to) {
        echo max($from->format("Y-m-d"), $current->format("Y-m-01")) . ".." . min($to->format("Y-m-d"), $current->format("Y-m-t")) . "n";
        $current->add(new DateInterval("P1M"));
    }
    
    Login or Signup to reply.
  4. Mutable objects are really hard to reason about. In particular, you need to be aware that $bar = $foo makes $bar point at the same object as $foo, not at a new object with the same value.

    With that in mind, let’s "unroll" your loops (never write code like this!):

    $begin_from = new DateTime( "2023-01-01" );
    $end_from   = new DateTime( "2023-12-31" );
    
    $begin_to = new DateTime( "2023-01-31" );
    $end_to   = new DateTime( "2023-12-31" );
    
    $i = $begin_from;
    outerforloop:
    if ( $i <= $end_from ) {
    
        $k = $begin_to;
        innerforloop:
        if ( $k <= $end_to ) {
            echo $i->format("Y-m-d"),'..',$k->format("Y-m-d");
            echo "n";
        
            $k->modify('first day of')->modify('+1 month')->modify('last day of');
            goto innerforloop;
        }
    
        $i->modify('+1 month');
        goto outerforloop;
    }
    

    In particular, look at the start and end of the inner loop:

    • $k = $begin_to; will make $k point at the same object as $begin_to
    • $k->modify(...) will then modify that object, meaning both $k and $begin_to have moved forward by a month
    • Next time around the loop, we check $k <= $end_to, with the expected result
    • However, when we come back around the outer loop, we run $k = $begin_to; again, expecting this to reset the value; but $k and $begin_to already point at the same object, which has been modified; the assignment doesn’t do anything
    • So now when we check $k <= $end_to, it will already be false: we won’t go into the loop at all

    To actually copy the value of object, you can use the clone keyword, e.g. $k = clone $begin_to;

    However, this particular case is why the DateTimeImmutable class was created. With DateTimeImmutable, you never change the value of an existing object, and instead always assign the result somewhere. In short, replace $i->modify(...) with $i = $i->modify(...) and $k->modify(...) with $k = $k->modify(...):

    $begin_from = new DateTimeImmutable( "2023-01-01" );
    $end_from   = new DateTimeImmutable( "2023-12-31" );
    
    $begin_to = new DateTimeImmutable( "2023-01-31" );
    $end_to   = new DateTimeImmutable( "2023-12-31" );
    
    for($i = $begin_from; $i <= $end_from; $i = $i->modify('+1 month')){
        for($k = $begin_to; $k <= $end_to; $k = $k->modify('first day of')->modify('+1 month')->modify('last day of')){
        echo $i->format("Y-m-d"),'..',$k->format("Y-m-d");
        echo "n";
        }
    }
    

    That fixes your for loops … but it doesn’t give the results you wanted. That’s because if you have two nested loops with 12 iterations each, the result is going to be 144 iterations – think of filling out a grid with 12 columns and 12 rows.

    What you actually wanted was a single loop, which controls both the start and the end date. There are a few ways to write that, but the most similar to your existing code is probably to keep the loop for $i, then define $k based on it:

    $begin_from = new DateTimeImmutable( "2023-01-01" );
    $end_from   = new DateTimeImmutable( "2023-12-31" );
    
    for($i = $begin_from; $i <= $end_from; $i = $i->modify('+1 month')){
        $k = $i->modify('last day of');
        echo $i->format("Y-m-d"),'..',$k->format("Y-m-d");
        echo "n";
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search