Calculating Future Dates with PHP

I recently received a request to set up a form that displays a recurring schedule of dates and times, allowing visitors to request one of those blocks of time as an appointment. The schedule would be the same every week, with numerous available blocks of time on specific days of each week.

I had two choices; I could either manually figure out each of the dates and then insert them into the database (either by-hand or with a script of some sort), which would require me to update the database to add new future dates rather frequently, or I could figure out a way to let PHP figure out the dates for me. I chose the latter, and I’ll show you how I did it after the jump.

For example, let’s say that I was given the following list of weekdays and times:

  • Tuesdays, 4-4:30 p.m.
  • Tuesdays, 4:30-5 p.m.
  • Tuesdays, 5-5:30 p.m.
  • Tuesdays, 5:30-6 p.m.
  • Wednesdays, 10-10:30 a.m.
  • Wednesdays, 10:30-11 a.m.
  • Wednesdays, 11-11:30 a.m.
  • Wednesdays, 11:30 a.m.-noon
  • Wednesdays, Noon-12:30 p.m.
  • Wednesdays, 12:30-1 p.m.
  • Wednesdays, 2-2:30 p.m.
  • Wednesdays, 2:30-3 p.m.
  • Wednesdays, 3-3:30 p.m.
  • Wednesdays, 3:30-4 p.m.
  • Wednesdays, 4-4:30 p.m.
  • Wednesdays, 4:30-5 p.m.
  • Wednesdays, 5-5:30 p.m.
  • Wednesdays, 5:30-6 p.m.
  • Thursdays, 5-5:30 p.m.
  • Thursdays, 5:30-6 p.m.

Getting the Days

The first thing I did was to build a simple array of the weekdays and time blocks. I noticed that each time block was 30 minutes long (no variations there), so I could cut out some of the work I needed to do. I set up a nested, associative array.

For the keys of the top-level of the array, I used the weekday names (Tuesday, Wednesday and Thursday). I then used an associative array as the value for each of those keys. I figured out which blocks of time were contiguous and used the starting time of the first meeting in each contiguous block as the key and the starting time of the last meeting in each contiguous block as the value.

For instance, on Wednesdays, meeting availability occurs consistently from 10 a.m. until 1 p.m. (so, the start time for the first meeting in that block would be 10 a.m. and the start time for the last meeting in that block is 12:30 p.m.) and then from 2 to 6 p.m. (so the start time for the first meeting in that block is 2 p.m. and the start time for the last meeting is 5:30 p.m.).

My array for the list of meeting times shown above ended up looking like:

$days_times = array(
  'Tuesday' => array(
    '16'=>'17.5',
  ),
  'Wednesday' => array(
    '10'=>'12.5',
    '14'=>'17.5',
  ),
  'Thursday' => array(
    '17'=>'17.5',
  ),
);

Notice that I converted the times to floats based on 24-hour time, rather than leaving them in time format (so, 2:30 p.m., for example, became 14.5). I did so in order to make it easier to add and subtract the half-hour (which is how long each meeting lasts).

Times and Floats

The next thing I did was to create a function that converts that integer into an array of the hour, the minutes and the seconds. That function is extremely simple, and looks like:

function int_to_time( $int=0 ) {
  return array(
    'hours'=>floor( $int ),
    'minutes'=>substr('00' . ( ( $int - floor( $int ) ) * 60), -2 ),
    'seconds'=>'00'
  );
}

Setting Up for Adding Time

Next, because my server is not running PHP 5.3.0, yet (which introduces the date_add() function), I had to calculate the amount of seconds in one week (since these appointment times recur once each week, and I need a way to calculate the dates). That calculation is a simple mathematical product that looks like:

$oneweek = (/*s=*/60 * /*m=*/60 * /*h=*/24 * /*d=*/7);

I then instantiate an empty array to hold my meeting blocks.

$mtgs = array();

Doing the Calculations

In my case, I decided to show availability 6 weeks in advance. Therefore, I started by running a loop through a range from 0 to 6 (technically, looking back at it, that’s actually showing 7 weeks at a time). Inside of that loop, I loop through the days of the week that I defined as my top-level keys above. I then use the strtotime() function to figure out the date for “next $day”, where “$day” is the day of the week. From there, I use the getdate() function to turn that timestamp into an array.

So far, the code for this section looks like:

foreach( range(0,6) as $w ) {
  foreach( $days_times as $day=>$times ) {
    $datestr = "next $day";
    $datetime = strtotime( $datestr );
    $datetime = getdate( ( $datetime + ( $w * $oneweek ) ) );

Next, I start looping through the time blocks I used in my array above, and set up a range (with a step of .5) from the first start time (the key in that associative array) to the last start time (the value in that associative array). To make sure that my times are processed properly, I cast them as floats. The code for this portion looks like:

    foreach( $times as $first=>$last ) {
      $first = (float) $first;
      $last = (float) $last;

      foreach( range( $first, $last, .5 ) as $t ) {

Next, I use the int_to_time() function I defined above to convert the start time for each block in the range to an array of hours, minutes and seconds. Then, I add .5 to the start time in order to get my end time, and use the int_to_time() function to convert it to an array of hours, minutes and seconds, as well. That portion of the code looks like:

        $tmptime = int_to_time( $t );
        $tmpend = int_to_time( ( .5 + $t ) );

I then create an array of the meeting date, the meeting start time and the meeting end time (because those are the three fields I store for each appointment block in my database) and append that to the end of the empty $mtgs array I instantiated above. That looks like:

        $mtgs[] = array(
          'mtgdate' => $datetime['year'] . '-' .
            substr('00' . $datetime['mon'], -2) . '-' .
            substr('00' . $datetime['mday'], -2),
          'mtgstart' => $tmptime['hours'] . ':' .
            $tmptime['minutes'] . ':' .
            $tmptime['seconds'],
          'mtgend' => $tmpend['hours'] . ':' .
            $tmpend['minutes'] . ':' .
            $tmpend['seconds'],
        );

Once it’s all put together, I now have an array of each available block of time for the next 6 (or 7) weeks. In my case, I then wrote a little code to insert those blocks into the MySQL database (hint: if you’re going to do this on a regular basis, you should get to know the “INSERT IGNORE” command and set up a cron job to do this).

Wrapping It Up

All together, the full code ends up looking like:

<?php
$days_times = array(
  'Tuesday' => array(
    '16'=>'17.5',
  ),
  'Wednesday' => array(
    '10'=>'12.5',
    '14'=>'17.5',
  ),
  'Thursday' => array(
    '17'=>'17.5',
  ),
);

function int_to_time( $int=0 ) {
  return array(
    'hours'=>floor( $int ),
    'minutes'=>substr('00' . ( ( $int - floor( $int ) ) * 60), -2 ),
    'seconds'=>'00'
  );
}

$oneweek = (/*s=*/60 * /*m=*/60 * /*h=*/24 * /*d=*/7);

$mtgs = array();
foreach( range(0,6) as $w ) {
  foreach( $days_times as $day=>$times ) {
    $datestr = "next $day";
    $datetime = strtotime( $datestr );
    $datetime = getdate( ( $datetime + ( $w * $oneweek ) ) );
    foreach( $times as $first=>$last ) {
      $first = (float) $first;
      $last = (float) $last;

      foreach( range( $first, $last, .5 ) as $t ) {
        $tmptime = int_to_time( $t );
        $tmpend = int_to_time( ( .5 + $t ) );
        $mtgs[] = array(
          'mtgdate' => $datetime['year'] . '-' .
            substr('00' . $datetime['mon'], -2) . '-' .
            substr('00' . $datetime['mday'], -2),
          'mtgstart' => $tmptime['hours'] . ':' .
            $tmptime['minutes'] . ':' .
            $tmptime['seconds'],
          'mtgend' => $tmpend['hours'] . ':' .
            $tmpend['minutes'] . ':' .
            $tmpend['seconds'],
        );
      }
    }
  }
}
$sql = "INSERT IGNORE INTO `appointments` (mtgdate, mtgstart, mtgend) VALUES";
foreach( $mtgs as $k=>$mtg ) {
  $mtgs[$k] = "'" . implode( "','", $mtg ) . "'";
}
$sql .= "\n(" . implode("),\n(",$mtgs) . ");";

if( !mysql_query( $sql ) ) {
  print( mysql_error() );
}
?>