How to create a Year-to-a-Page calendar

Intended Audience
Prerequisites
Sample output
Analysing the Problem
Building the Solution
- Form elements
- Date calculations
- Outputting the results
- HTML tables
- Stylesheets

Intended Audience

This tutorial is intended to show developers how to create a self-executing script that will ask for a year number, then display all the dates for that year in a typical calendar format. It will use date functions, arrays, loops, HTML tables and stylesheets in order to construct its output.

This is a program which I have actually written three times as a training exercise - once in COBOL, then in a 4th generation language called UNIFACE, and now in PHP.

Prerequisites

No special PHP modules are required for this code to work. It is assumed the novice has a grasp of basic PHP, HTML and stylesheets.

Sample output

Here is what the output will look like:

You can run the program from here and view the complete source code from here.

Analysing the Problem

This diagram may look deceptively simple, but notice the following:

• The 12 months are displayed in 3 rows of 4 going left-to-right, top-to-bottom.
• The days for each month are shown in a grid of 6 rows and 7 columns, with the month name at the top.
• Each of the 7 rows contains dates that fall on the same day of the week (Monday to Sunday).
• Each of the 6 columns for each month contains dates that fall within the same week. Note that some cells are blank because there is no date in that month or that day.

Those may not appear to have any great significance, but when you consider that the HTML code to produce this display has to be created in a sequential manner starting at the top of the page then going down line by line, from left to right, you should see the following difficulties:

• The 12 months have to be broken down into 3 groups of 4.
• When printing each group the 1st line contains just the month names.
• The next line contains only those dates which fall on the same day for each of the months within the group. This starts with Monday, with successive lines for Tuesday all the way down to Sunday. This means that I have to build a line containing all the Mondays for the 4 months before I can move on to the Tuesdays.
• The grid for each month has 6 columns for each of the possible weeks which can be covered by that month. Note that if the 1st Monday in the month is not dated the 1st then it will appear in the column for the 2nd week, not the 1st.

Building the Solution

Some much for the problems, now let us look at how they can be solved.

Form elements

We first need to specify the HTML 'form' elements that will cause the same PHP script to be called when the user presses the GO button. The PHP keyword is `\$_SERVER['PHP_SELF']` which instructs PHP to use the name of the current script. This produces what is known as a 'self-executing script' as the script which generates the blank form for the user to fill in is the same script which processes the user's input. Notice that whatever value in passed in the `\$_GET` array is echoed back to the user.

```<form action="<?php echo \$_SERVER['PHP_SELF'] ?>" method="GET">
<p>Enter Year: <input type="text" name="year" size="4" value="<?= \$_GET['year'] ?>" />
<input type="submit" value="GO" /></p>
...
</form>
```

Date calculations

The 1st step is to valid the user's input. It must be a whole (only digits), and must be between 1970 and 2037. This is because the `strtotime` function parses dates into a UNIX timestamp which is a long integer containing the number of seconds which has elapsed since the start of the Unix Epoch (January 1 1970). As this is a 32 bit integer it can only go up to some time in 2037, but hopefully by then we will all be using 64+ bit machines. The following code performs this validation:

```// validate year
if (isset(\$_GET['year'])) {
\$valid = TRUE;
if (!ereg('^[[:digit:]]+\$', \$year)) {
echo "Year must be an integer";
\$valid = FALSE;
} // if
if (\$year < 1970) {
echo "Year cannot be less than 1970";
\$valid = FALSE;
} // if
if (\$year > 2037) {
echo "Year cannot be greater than 2037";
\$valid = FALSE;
} // if
} // if
```

The following code sets the start date to the 1st day of the 1st month for the selected year, then converts it into a UNIX timestamp so that it can be manipulated further.

```if (isset(\$_GET['year']) and (\$valid)) {
// start at 1st day of 1st month in this year
\$dd = 1;
\$mm = 1;
\$ccyy = \$year;
// convert components into date format
\$date = strtotime("\$ccyy-\$mm-\$dd");
```

This next statement will create an associative array so that each individual element of a date, such as year, month and day, can be extracted.

```   \$today = getdate(\$date);
```

Next we call a function repeatedly and only stop when we have processed all the days in the selected year:

```   \$date_array = array();
do {
build_array(\$ccyy, \$mm, \$dd);
} while(\$ccyy == \$year);
```

The `build_array` function accepts three variables which are passed by reference. It also uses other variables which have already been defined outside of itself. This ensures that any changes made to these variables are also made available outside of this function.

```function build_array(&\$ccyy, &\$mm, &\$dd)
{
global \$date;
global \$today;
global \$date_array;
static \$month_no;
static \$week_no;
```

We start off at week 1, and when the month changes (after week 6) we reset the week number back to 1.

```if (\$mm > \$month_no) {
\$month_no = \$mm;
\$week_no  = 1;
} // if
```

This code obtains the day-of-week for the current date. Note that we convert Sunday from day 0 to day 7.

```\$dow = \$today['wday'];    // get day of week (0 = Sunday, 6 = Saturday)
if (\$dow == 0) {
\$dow = 7;             // convert Sunday to day 7
} // if
```

We then need to add this date to an array in such a way that we can easily extract it in the desired order when building the HTML output. Note that this is a multi-dimensional array - each date is stored in day-of-week (1=Monday, 7=Sunday) within week number (1-6) within month (1-12).

```\$date_array[\$month_no][\$week_no][\$dow] = \$dd;
```

After processing the last day of the current week we must increment to the next week:

```if (\$dow == 7) {
\$week_no = \$week_no + 1;
} // if
```

After storing the current date we need to increment it to the next date. For this we use the versatile `strtotime` function which can manipulates a UNIX timestamp in a variety of ways. This function knows when one month finishes and the next month begins, and it can deal with leap years.

```\$date = strtotime("+1 day", \$date);
```

We then create an array of information about this new date and extract the portions we need:

```\$today = getdate(\$date);
\$ccyy = \$today['year'];
\$mm   = \$today['mon'];
\$dd   = \$today['mday'];
```

Outputting the results

Once we have constructed the array containing all the dates for the selected year we must then output the results in HTML format. This is done through the `print_array` function. The first thing this does is to create arrays containing the day names and the month names. Note that these arrays are forced to start at the number 1 instead of the default 0.

```function print_array(\$date_array) {
\$day_names = array(1 => 'MON','TUE','WED','THU','FRI','SAT','SUN');
\$month_names = array(1 => 'J A N U A R Y', '...', 'D E C E M B E R');
```

We must then start our HTML table:

```echo "<table border='0'>\n";
```

We need to loop through the date array 3 times, once for each row of 4 months:

```for (\$row = 1; \$row <= 3; \$row++) {
```

Next we need to identify the number of the 1st month in the current row:

```   if (\$row == 1) {
\$first = 1;
} elseif (\$row == 2) {
\$first = 5;
} else {
\$first = 9;
} // if
```

For the current set of months we start by outputting a table row containing the month names:

```   echo "<tr class='month'>\n";
for (\$m = \$first; \$m <= \$first+3; \$m++) {
echo "<td>&nbsp;</td><td colspan='6'>" .\$month_names[\$m] ."</td>\n";
} // for
echo "</tr>\n";
```

Then we need a loop to create a new table row for each day-of-week. This starts at day 1 (Monday) and stops at day 7 (Sunday).

```   for (\$dow = 1; \$dow <= 7; \$dow++) {
echo "<tr class='\$day_names[\$dow]'>\n";
```

Next we have to loop through each of the 4 months in the current row:.

```      for (\$m = \$first; \$m <= \$first+3; \$m++) {
```

If this is the 1st month in the row the 1st table cell contains the day of week, otherwise it is left empty.

```         if (\$m == \$first) {
echo "<td class='dayname'>" .\$day_names[\$dow] ."</td>";
} else {
echo "<td>&nbsp;</td>";
} // if
```

Now we loop through each of the six weeks in the current month, and if there is an entry for the current day-of-week we put it into the table cell. If there is no date for this month/week/day combination then we create an empty table cell.

```         for (\$week = 1; \$week <= 6; \$week++) {
if (isset(\$date_array[\$m][\$week][\$dow])) {
echo "<td>" .\$date_array[\$m][\$week][\$dow] ."</td>";
} else {
echo "<td>&nbsp;</td>";
} // if
} // for
```

After processing all 4 months in the current row we close the table row.

```      } // for
echo "\n</tr>\n";
```

To separate one row of months from the next we output a table row containing nothing but empty cells.

```   } // for
echo "<tr><td colspan='28'>&nbsp;</td></tr>\n";
```

After the 6th week of the 3rd row of 4 months has been processed the last step is to close the HTML table:

```} // for
echo "</table>\n";
```

HTML tables

If you turn on the table borders you will see that each block of 4 months is output using the following structure of rows and cells:

You may notice that some rows contain cells which span more columns than others. This is done in two places - the first ensures that the cell containing the month name is 6 columns wide.

```      echo "<td>&nbsp;</td><td colspan='6'>" .\$month_names[\$m] ."</td>\n";
```

At the end of each block of 4 months there is a separator constructed as one blank cell which is 28 columns wide. This is easier than creating 28 blank cells.

```   echo "<tr><td colspan='28'>&nbsp;</td></tr>\n";
```

Lastly, I use the `<colgroup span="?" width="?">` tag to specify column widths in a single place. This saves me having to specify these widths on each individual cell in the table.

```echo '<colgroup width="50">';
echo '<colgroup span="6" width="20">';
echo '<colgroup width="20">';
echo '<colgroup span="6" width="20">';
echo '<colgroup width="20">';
echo '<colgroup span="6" width="20">';
echo '<colgroup width="20">';
echo "<colgroup span='6' width='20'>\n";
```

Stylesheets

For those of you who are unfamiliar with stylesheets, they are a method of defining a style with a particular name so that your code can contain as many references as you like to that name. Thus, if you then decide to change a particular style you do it once where the style is defined and not in every place that references that style. This document contains the following stylesheet definitions:

```   <style type="text/css">
<!--
.month { text-align: center; font-weight: bold; }
.dayname { font-weight: bold; }
.sat { color: red; }
.sun { color: red; }
-->
</style>
```

Because each of these names is preceded by a period it denotes a class name, so any HTML tag which contains a `class="classname"` attribute will inherit the style defined for that classname. Thus anything with `class="month"` will have the text centered and in bold, and anything with `class="sat"` will have its text in red, and so on. In fact if you look at my PHP code where I define a new table row for a day-of-week you will see the following:

```      echo "<tr class='\$day_names[\$dow]'>\n";
```

This means that if I so wished I could output each row of dates with one colour for Monday, another colour for Sunday, and so on. In this example I have only defined styles for Saturday and Sunday, so any reference to the other week days will not find a matching style and will simply use the default values instead.