Question

I'm working on a payment scheduler which plans out n payments based on an arbitrary start date and one of a set of frequencies (daily, weekly, monthly, etc...) and am seeking a general purpose algorithm for doing so.

I have attempted a brute force means of doing this, by casing the frequency and adding a certain number of days, weeks, months as needed. This works for most purposes.

Where it fails is when the arbitrary start date is after the 28th of a month and the frequency is somewhere between monthly and annually, especially for frequencies like 'first of each month' and 'last of each month'. Because days 29, 30, and 31 do not appear on all months, adding a month like date('2013-10-31')->addMonth(1) has unpredictable results. As does adding months like date('2014-01-31')->addDays(30), again, due to February being unnecessarily short.

Is there a general solution to this problem without the hideously complex cases I need for moving any given frequency through any given month?

Bonus points for PHP, but I can translate if needed.

Was it helpful?

Solution

The "add a month", etc., annoyance due to different month lengths is, indeed, irritating.

The solution, if you have PHP >= 5.2, is the DateTime class.

Though it is simple to use this class to obtain total control, it is not entirely trivial.

Here is one version of correct code to add a month.

// Variables defining the start date
// Example only - this could be any valid date
$year = '2013';
$month = '01';
$day = '31';

// set to the desired starting date and time
$the_date = new DateTime($year . '-' . $month . '-' . $day);

// Jump to the first day of this month
$the_date->modify("first day of this month");

// add 14 days, so we'll land on the 15th
$the_date->add(new DateInterval("P14D"));

// add 1 month - guaranteed to work!
$the_date->add(new DateInterval("P1M"));

// calculate how many days to add to 15 to get back to the **day** we started with...
// (as an integer, regardless of whether it is a valid day of the current month)
$number_days_to_add_back = intval($day) - 15;

// determine the last day of the month stored in $the_date
$test_last_date = clone $the_date;
$test_last_date->modify("last day of this month");
$day_last = $test_last_date->format('j'); // This provides the day, 01-31

// Test if adding $number_days_to_add_back runs past
// the end of the month; if so, adjust it so it won't run past
// the last day of the month
if (15 + $number_days_to_add_back > intval($day_last)) {
    $number_days_to_add_back = intval($day_last) - 15;
}

// Now make the final adjustment
$the_date->modify("" . $number_days_to_add_back . " day");

// Test it - a month has been added
$test = date_format($the_date, 'Y-m-d');

OTHER TIPS

First and foremost you need to define how you want it to work. This is a business logic problem, much less a technical problem. How long is "one month"? Do you mean "one month" as a time span of roughly 30 days (how long exactly then?) or does "+ one month" mean "same day next month"? Once you have defined how "31st + 1 month" should work, it's simply a matter of implementing it correctly.

My suggestion would be that "+1 month" means "increase the month number by one, keeping the day number the same, unless the day does not exist in the month, in which case use the last day of the month". Which can be implemented using something like this:

$date      = mktime(0, 0, 0, 1, 31);  // midnight Jan 31st
$nextMonth = mktime(0, 0, 0, date('n', $date) + 1, 1, date('Y', $date));  // 1st of next month
$newDate   = mktime(0, 0, 0,
                    date('n', $nextMonth),
                    min(date('t', $nextMonth), date('j', $date)),
                    date('Y', $nextMonth));

It ain't pretty, but date/time calculations rarely are, especially if the definition of the operation is vague.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top