1

I have a customisable DateInterval object, along with a customisable start and end date. I want to find the dates between the start and end using the interval. I am using Carbon to try and help with this.

Here's the problem: I the interval is months but the start date is > 28 I cannot control the overflow using CarbonPeriod.

Here is the code I am testing with:

$di = CarbonInterval::create('P1M');
$start = Carbon::parse('31 january 2020')->startOfDay();
$end = Carbon::parse('01 april 2020')->startOfDay();

$period = CarbonPeriod::create($start, $di, $end);

$items = [];
foreach ($period as $item) {
    $items[] = $item;
}

I want the above to result in

2020-01-31
2020-02-29
2020-03-30

But I get

2020-01-31
2020-03-02
2020-04-02

Remember, the DateInterval is customisable (or I would just use Carbon::addMonthNoOverflow()).

Can anyone please help with how I achieve what I need to, above?

Wildcard27
  • 1,437
  • 18
  • 48

2 Answers2

1

tl;dr: It doesn't work with Carbon.

Date arithmetics are hard to convey and harder to get right. So for example, you couldn't even express, that you want "the last day of a month" in an interval, when the start date is for example in february:

start: 2020-02-29
P1M -> 2020-03-29 (not end of month, obviously)

An interval alone can't express the semantics that you want the end of month.

And this problem will carry over even if you would find a way to get the overflow working such that you don't end up on the start of the next month. (I have tried some approaches that all failed)

All you can offer (to the user) is to apply extra functions to the array of dates to achieve your goal perhaps, like $item->endOfMonth(). But you would still have to pay attention that the start's day of month is <= 28.

Ironically, you can call ->settings(['monthOverflow'=>false, 'yearOverflow'=>false]) on all Carbon, CarbonInterval and CarbonPeriod, and it has no effect (except when you call addMonth() on it, which is rather disappointing, it won't be applied on $start->add($di)). This comes down to the fact that Carbon is ultimately just a wrapper around the standard DateTime objects, which don't support overflow either.

Long story short, there is no elegant and/or easy solution with Carbon (current version). ;o/

Jakumi
  • 8,043
  • 2
  • 15
  • 32
  • A thorough answer, thank you. Shame it isn't what I was hoping to read. Looks like I'll be writing some ugly custom code – Wildcard27 Jul 19 '20 at 06:58
  • @Wildcard27 I was disappointed too ;o/ maybe opening a ticket at carbon may change this in the future. – Jakumi Jul 19 '20 at 08:08
  • Created an issue https://github.com/briannesbitt/Carbon/issues/2133 but I'm not going to hold my breath – Wildcard27 Jul 19 '20 at 12:04
1

After much digging, and realising that @Jakumi is correct, I have just come up with a solution using Carbon after asking the same question here.

The reason why DatePeriod() does not work is that it is always reliant on the previous date in the loop. That is why you get stuck at 29/28 for february and then repeating throughout the rest of the loop.

Here is my solution:

$endDate = CarbonImmutable::parse('10 april 2020')->startOfDay();
$startDate = CarbonImmutable::parse('31 january 2020')->startOfDay();
$interval = CarbonInterval::create('P1M'); // only works for number of months, not composite intervals


$workingDate = $startDate->copy();

for ($i = 1; $workingDate <= $endDate; $i = $i + $interval->m) {
    echo = $workingDate->format('Y-m-d') . "\n";
    $workingDate = $startDate->addMonthsNoOverflow($i);
}

I am involved in a bit of a code-off with a contributor of the Carbon codebase. If he finds a better solution then I will update my answer. For now, this works.

Wildcard27
  • 1,437
  • 18
  • 48