views:

379

answers:

2

I have a calendar application which utilizes the newer PHP DateTime classes. I have a way that I handle recurring events, but it seems hack-ish and I wanted to see if you guys have better ideas:

  1. I have a recurring event that starts 11/16/2009 (Nov 16, 2009)
  2. It will occur every 3 days
  3. The event will recur indefinitely

Let's say the user looks at the calendar for Dec, 3100 - this event should show there repeating every 3 days like normal. The question is - how do I calculate those days in that month?

=========================================

This is how I basically do it, but I know I'm missing something easier:

  1. I calculate the difference in days between the start of the month being looked at (Dec 1, 3100) and the event start date (Nov 16, 2009) stored as $daysDiff
  2. I subtract the modulus, so that I get a factor of 3 days from the start like this: $daysDiff - ($daysDiff % 3)
  3. For the sake of argument lets say that gives me Nov 29, 3100 as a date.
  4. I then add 3 days to that date repeatedly until I have all the dates within Dec 3100

My main problem comes with step 1. The PHP DateInterval::date_diff function does not calculate differences in days. It will give me years, months, and days. I then have to fudge the numbers to get an estimate date around Dec, 3100. 11/16/2009 + (1090 years * 365.25 days) + (9 months * 30.5 days) + 15 days

When you go REAL far into the future like the year 9999, this estimation can be off by a month, then I have to subtract a lot of 3 day intervals to get where I need.

A: 

You could format your date as a unix timestamp then use modular division to find the first instance in the selected month, then step in increments of 3 days from there. So for your example:

$startDate = new DateTime(20091116);
$startTimestamp = $startDate->format('u');
$recursEvery = 259200; // 60*60*24*3 = seconds in 3 days

// find the first occurrence in the selected month (September)
$calendarDate = new DateTime(31000901); // init to Sept 1, 3100
while (0 != (($calendarDate->format('u') - $startTimestamp) % $recursEvery)) {
    $calendarDate->modify('+1 day');
}

$effectiveDates = array();
while ($calendarDate->format('m') == 9) {
    $effectiveDates[] = clone $calendarDate;
    $calendarDate->modify('+3 day');
}

//$effectiveDates is an array of every date the event occurs in September, 3100.

Obviously, you've got a few variables to swap out so the user can select any month, but that should be the basic algorithm. Also, ensure your DateTimes are the correct date, but with the time set as 00:00:00, or else the first while loop will never resolve. This also assumes you've ensured the selected date is later than the beginning date of the event.

keithjgrant
...of course, the unix timestamp thing won't work for the year 3100 unless you're running in 64-bits.
keithjgrant
Hi Keith - I like the way you think with this, but thats unfortunately a limitation for me as well (on 32 bit). It seems like straightout "math" with these dates cant be done, check out my "fudging" way of doing it above
John
A: 

This can be done nicely using a DatePeriod as an Iterator and then filterd to the start date you want to display:

<?php
class DateFilterIterator extends FilterIterator {
    private $starttime;
    public function __construct(Traversable $inner, DateTime $start) {
        parent::__construct(new IteratorIterator($inner));
        $this->starttime = $start->getTimestamp();
    }
    public function accept() {
        return ($this->starttime < $this->current()->getTimestamp());
    }
}

$db = new DateTime( '2009-11-16' );
$de = new DateTime( '2020-12-31 23:59:59' );
$di = DateInterval::createFromDateString( '+3 days' );
$df = new DateFilterIterator(
    new DatePeriod( $db, $di, $de ),
    new DateTime( '2009-12-01') );

foreach ( $df as $dt )
{
    echo $dt->format( "l Y-m-d H:i:s\n" );
}
?>
johannes