I think the best thing you could do (as in easiest, most reliable and least code) is to go back to the DatePeriod
class. Yes, it doesn't correct itself in terms of "month-overflow" (yet), but that's an easy fix:
Assume the data from your example:
$replaceVars = array(
'effectivedate' => '30-9-2014',
'vatpc' => 0.15,
'paymentamount' => 150.00,
'duration' => 12
);
Now let's see what we can do with this:
function getPaymentDates(array $data)
{
$begin = new DateTime($data['effectivedate']);
$period = new DatePeriod(
$begin,
new DateInterval('P1M'),
(int) $data['duration'] //added cast to be sure, in case this data comes from DB
);
$clean = array();//<-- return this
$last = (int) $begin->format('m');
foreach ($period as $date)
{
while($last != $date->format('m'))
$date->modify('-1 day');//subtract days until we get to the last day of the previous month...
$clean[] = $date;
if (++$last > 12)
$last = 1;//no 13th month, of course...
}
return $clean;
}
I've tried this with starting date 2014-01-31
, and it produced the exact outcome you'd expect, no problem:
2014-01-31 00:00:00
2014-02-28 00:00:00
2014-03-31 00:00:00
2014-04-30 00:00:00
2014-05-31 00:00:00
2014-06-30 00:00:00
2014-07-31 00:00:00
2014-08-31 00:00:00
2014-09-30 00:00:00
2014-10-31 00:00:00
2014-11-30 00:00:00
2014-12-31 00:00:00
2015-01-31 00:00:00
But there is a problem here, of course: The period is 12 months in total, but this function returns 13 dates.
I did this for a reason, of course. There are 2 paths to go down for you, either pass a fourth argument to the DatePeriod
constructor (DatePeriod::EXCLUDE_START_DATE
), which will exclude the starting date from the returned dates, or return a slice of the array:
return array_slice(
$clean,
0,
$data['duration']//in this case 12
);
Which drops the last date from the return array.
Ideally, I'd have this function take a second argument, with 3 possible values. Say, for example 1, 2 and 3: If that second argument is 1, then the first date won't be returned (basically, you'll pass DatePeriod::EXCLUDE_START_DATE
to the constructor), if 2 is passed, then you'll slice the returned array (excluding the last date in the generated period). Passing 3 means that you return the resulting array ($clean
) as is, no dates are removed, and its length will be that of $data['duration']
+ 1.
If this function will be used as a class method, then I'd suggest using constants for this.
In that case, your code could end up like this:
class Foo
{
const EXCLUDE_EFFECTIVE_DATE = 1;
const EXCLUDE_FINAL_DATE = 2;
const EXCLUDE_NO_DATE = 3;
public function getPaymentDates(array $data, $exclude = self::EXCLUDE_NO_DATE)
{
$begin = new DateTime($data['effectivedate']);
$exArg = null;
if ($exclude === self::EXCLUDE_EFFECTIVE_DATE)
$exArg = DatePeriod::EXCLUDE_START_DATE;
$period = new DatePeriod(
$begin,
new DateInterval('P1M'),
(int) $data['duration'],
$exArg
);
$clean = array();
$last = (int) $begin->format('m');
foreach ($period as $date)
{
while($last != $date->format('m'))
$date->modify('-1 day');
$clean[] = $date;
if (++$last > 12)
$last = 1;
}
if ($exclude === self::EXCLUDE_FINAL_DATE)
return array_slice($clean, 0, $data['duration']);
return $clean;
}
To generate the same output as you are getting now, becomes a piece of cake, and doesn't require ugly code (computation mixed in with echo
's isn't nice...):
$foo = new Foo;//<-- class-like example
$replaceVars = array();//<-- imagine your data here
$dates = getPaymentDates($replaceVars, Foo::EXCLUDE_FINAL_DATE); //is what you're doing now
//assuming you have echo '<table><tbody>'; here somewhere
//Calculate the payment values up front please!!
$payment = round($replaceVars['paymentamount'] + $replaceVars['paymentamount'] * $replaceVars['vatpc'], 2);
foreach ($dates as $i => $date)
{
echo '<tr><td>Payment number: ', $i+1, '</td><td>',
$date->format('d/m/Y'), '</td><td>',
'£', $payment ,'</td></tr>';
}
echo '</tbody></table>';
So one thing: calculate the payment value once, and echo it in the loop. There is no point in calculating the same thing over and over, just assign it to a temp variable and use that instead.
As an asside: yes, those are comma's I'm using in the echo
statement, and not concatenating .
(dot)-operators. echo
is a language construct that works in a similar fashion to C++'s std::COUT
. You can push any number of values to it, it'll just push them to the stdin
sequentially. That way, there's no reason to allocate new memory, and copy string chars to create a new, big string constant that'll just be passed to echo anyway.
I've already explained this in great detail here, in the comments below my answer there, there's a link to a page where the differences in exec. time are mentioned, too. Bottom line: The difference isn't dramatic, but echo
without concatenation is faster.