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
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)The above will produce the following in PHP v7+:
PHP v5.6.40 requires you set the timezone or the default timezone like
date_default_timezone_set('UTC');
before creating theDateTime
s, but then you get the same output as above.Run it live here.
To use datetime objects in a loop like @ArleighHix’s answer, I’d only modify the start date and use a calls of
min()
andmax()
in conjunction with01
andt
to ensure that date boundaries are not exceeded.max()
andmin()
are safe in this case becauseY-m-d
is a "big-endian" date format — this means that the string can be evaluated as a simple string.Code: (Demo)
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.
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:
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 themax()
andmin()
functions: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!):
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$k <= $end_to
, with the expected result$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$k <= $end_to
, it will already be false: we won’t go into the loop at allTo 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. WithDateTimeImmutable
, 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(...)
: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: