Writing an Archive Calendar, part 1

It's time to reveal all about that Archive Calendar I've been developing that now adorns my site. It's not quite done yet, but by the time I'll have it finished, this series of articles on how to write it will be too.

The first thing I did, before I got all embroiled in how to create a query and execute it to get a list of posts, and so on, was to make sure I knew how to create the calendar display. It's a basic HTML table, with a particular id, and the actual look and feel is provided by CSS.

We'll start off with a class I'm calling, for now, HtmlCalendar. It will have a Render() method that will return a string comprising the HTML markup for the month passed to the constructor. (Note I won't be showing absolutely all the code in this series, just the important stuff. I'll make the code available later on.)

  public class HtmlCalendar {

    public HtmlCalendar(int year, int month) {
      this.year = year;
      this.month = month;
    }

    public string Render() {
      StringBuilder sb = new StringBuilder();
      sb.AppendLine("<table id='jmbCalendar'>");
      AddCalendarCaption(sb);
      AddCalendarHeader(sb);
      AddCalendarFooter(sb);
      AddCalendarBody(sb);
      sb.AppendLine("</table>");
      return sb.ToString();
    }

  }

As you can see the Render() method merely creates a string builder, adds the opening tag for the table element, calls other methods to add the caption, the header row, the footer row, the body of the table, and finally closes off the table with the ending tag. The string builder's string is returned. The caption is the month being displayed as a calendar, the header is the days of the week (starting on Sunday) as initial letters, the footer is the two links to the previous and next month, and the body is the actual calendar.

Note something interesting: the footer is rendered before the body of the table, and yet will appear after it. This is actually a requirement for tables since the browser can more efficiently render the table before all the possibly many body rows are parsed. For print environments, for example, the footer may be rendered at the bottom of each page, so it would be very inefficient to read the entire body of rows before knowing what the footer looked like. The W3C validation suite will fail an HTML page on this point.

In order to render the links of the table, the calendar class would have to know about posts and links on the actual website. I didn't want this, so I created a couple of delegates that could be called whenever a link was required to insert into the HTML. If no delegate had been set, the calling code had some smarts to create some standard text.

  public delegate string DayLinkGenerator(object sender, DayLinkEventArgs e);
  public delegate string MonthLinkGenerator(object sender, MonthLinkEventArgs e);
    protected virtual string OnGenerateDayLink(int day) {
      DayLinkGenerator handler = GenerateDayLink;
      if (handler == null)
        return string.Empty;
      DayLinkEventArgs args = new DayLinkEventArgs(year, month, day);
      return handler(this, args);
    }

    protected virtual string OnGenerateMonthLink(int year, int month, bool fullName, bool forPrevMonth) {
      MonthLinkGenerator handler = GenerateMonthLink;
      if (handler == null) 
        return String.Empty;
      MonthLinkEventArgs args = new MonthLinkEventArgs(year, month, fullName, forPrevMonth);
      return handler(this, args);
    }

    private string GetMonthLink() {
      string monthLink = OnGenerateMonthLink(year, month, true, false);
      if (string.IsNullOrEmpty(monthLink))
        monthLink = string.Format("{0} {1}", GetMonthName(month), year);
      return monthLink;
    }

    private string GetAbbrMonthLink(int year, int month, bool forPrevMonth) {
      string monthLink = OnGenerateMonthLink(year, month, false, forPrevMonth);
      if (string.IsNullOrEmpty(monthLink))
        if (forPrevMonth)
          monthLink = string.Format(PrevMonthFormat, GetAbbrMonthName(month));
        else
          monthLink = string.Format(NextMonthFormat, GetAbbrMonthName(month));
      return monthLink;
    }

Once that plumbing was out of the way, the code to add the caption, header and footer was pretty simple. In essence, it was a case of remembering to start and end the various elements (row groups, rows, cells, etc) in the table.

    private void AddCalendarCaption(StringBuilder sb) {
      sb.Append("<caption>");
      sb.Append(GetMonthLink());
      sb.AppendLine("</caption>");
    }

    private void AddCalendarHeader(StringBuilder sb) {
      sb.Append("<thead>");
      sb.Append("<tr>");
      foreach (char d in DayLetters)
        sb.AppendFormat("<th>{0}</th>", d);
      sb.Append("</tr>");
      sb.AppendLine("</thead>");
    }

    private void AddCalendarFooter(StringBuilder sb) {
      int prevMonth = month - 1;
      int prevYear = year;
      if (prevMonth == 0) {
        prevMonth = 12;
        prevYear = year - 1;
      }

      int nextMonth = month + 1;
      int nextYear = year;
      if (nextMonth == 13) {
        nextMonth = 1;
        nextYear = year + 1;
      }
      DateTime firstOfNextMonth = new DateTime(nextYear, nextMonth, 1);
      bool displayNextLink = (firstOfNextMonth <= DateTime.Now);

      sb.Append("<tfoot>");
      sb.Append("<tr>");
      sb.AppendFormat("<td colspan='3'>{0}</td>", GetAbbrMonthLink(prevYear, prevMonth, true));
      sb.Append("<td colspan='1'>&nbsp;</td>");
      if (displayNextLink) 
        sb.AppendFormat("<td colspan='3'>{0}</td>", GetAbbrMonthLink(nextYear, nextMonth, false));
      else
        sb.Append("<td colspan='3'>&nbsp;</td>");
      sb.Append("</tr>");
      sb.AppendLine("</tfoot>");
    }

Note that the next month link is only rendered if it's not in the future.

The AddCalendarBody() method is the most fun. Look at a printed calendar. You'll see that every month in the calendar has at least four rows — it will only have exactly four rows for a February in a non-leap year where the 1st is on a Sunday. Conversely, there are some months that will have six rows, the last one being November 2008. So our table body, at most, will have 6 rows of 7 cells in each row.

The way I approached this is to create an array of 42 elements (6 × 7). The logical way to look at this array is six weeks, where each row represents a week, Sunday, Monday, through to Saturday. So if the first day of the month is on a Sunday, I need to fill in element [0] first, and then the next however many elements as there are days in the month. If the first day of the month is a Monday, I fill in element [1] first and then continue, if Tuesday, element [2} and then onwards, and so on. Generally the fifth row will be rendered, and rarely the sixth.

    private static int AddCalendarRow(StringBuilder sb, string[] days, int cdn) {
      sb.Append("<tr>");
      for (int i = 0; i < 7; i++) {
        sb.AppendFormat("<td>{0}</td>", days[cdn++]);
      }
      sb.AppendLine("</tr>");
      return cdn;
    }

    private void AddCalendarBody(StringBuilder sb) {
      sb.AppendLine("<tbody>");

      // a month calendar has a maximum of 6 rows, 42 cells
      string[] days = new string[42];

      int firstEntry = (int)new DateTime(year, month, 1).DayOfWeek;
      int lastDay = DateTime.DaysInMonth(year, month);
      int day = 1;
      for (int i = firstEntry; i < firstEntry + lastDay; i++) {
        string dayLink = OnGenerateDayLink(day);
        if (string.IsNullOrEmpty(dayLink))
          dayLink = DayNumbers[day];
        days[i] = dayLink;
        day++;
      }

      // there will always be at least 4 rows
      int cdn = 0;
      for (int i = 0; i < 4; i++) {
        cdn = AddCalendarRow(sb, days, cdn);
      }
      // rows 5 and 6 may or may not be there
      if (!string.IsNullOrEmpty(days[cdn])) {
        cdn = AddCalendarRow(sb, days, cdn);
        if (!string.IsNullOrEmpty(days[cdn])) {
          cdn = AddCalendarRow(sb, days, cdn);
        }
      }

      sb.AppendLine("</tbody>");
    }

So, as this code shows, the creation of the body of the table is a two step process: first, work out what goes in each cell, and then render the rows and cells using standard markup. As I mentioned some time ago, this is exactly how I did it back in 1991 for a Turbo Vision calendar control.

As it stands, the class can be tested on its own in a simple test harness. I wrote some test code that didn't set the two delegates (the one for the month links and the second for the day links) and just created a whole set of calendars to check. The calendars therefore didn't have any links, but at least they were visible and had valid data.

Here's an example of the HTML generated for January 2009:

<table id='jmbCalendar'>
<caption>January 2009</caption>
<thead><tr><th>S</th><th>M</th><th>T</th><th>W</th><th>T</th><th>F</th><th>S</th></tr></thead>
<tfoot><tr><td colspan='3'>&laquo; Dec</td><td colspan='1'>&nbsp;</td><td colspan='3'>&nbsp;</td></tr></tfoot>
<tbody>
<tr><td></td><td></td><td></td><td></td><td>1</td><td>2</td><td>3</td></tr>
<tr><td>4</td><td>5</td><td>6</td><td>7</td><td>8</td><td>9</td><td>10</td></tr>
<tr><td>11</td><td>12</td><td>13</td><td>14</td><td>15</td><td>16</td><td>17</td></tr>
<tr><td>18</td><td>19</td><td>20</td><td>21</td><td>22</td><td>23</td><td>24</td></tr>
<tr><td>25</td><td>26</td><td>27</td><td>28</td><td>29</td><td>30</td><td>31</td></tr>
</tbody>
</table>

The CSS to style this is simple: just hang everything off the table's id. Here's what I have for my style sheet:

#jmbCalendar {border-spacing:0px;border-collapse:collapse;margin:0 auto;}
#jmbCalendar caption {margin:5px auto 5px auto;}
#jmbCalendar thead th {border:1px solid #5F5F5F;margin:0;}
#jmbCalendar tfoot td {border:1px solid #5F5F5F;margin-top:5px;text-align:center;}
#jmbCalendar tbody td {padding:2px 4px;text-align:right;}

Next time we'll look at the code that creates an instance of this class to see what it does.

(Part 1 is here, part 2 here, part 3 here, part 4 here, part 4a here, part 4b here.)

Album cover for Seven Ways Now playing:
Van Dyk, Paul - People
(from Seven Ways)


Loading similar posts...   Loading links to posts on similar topics...

3 Responses

#1 Writing an Archive Calendar, Part 1 said...
18-Jan-09 1:56 PM

Thank you for submitting this cool story - Trackback from DotNetShoutout

 avatar
#2 Mike B said...
02-Feb-09 9:34 AM

Wanted to ask how hard it would be to add a parameter to be able to filter the calendar posts to a single category, perhaps either the default category of the page the widget is on?

Love the work w/ Graffiti, by the way...

julian m bucknall avatar
#3 julian m bucknall said...
02-Feb-09 6:04 PM

Mike: an interesting suggestion. My first thought was, sure, no problem. Then the analyst part of me kicked in and I started to look into it.

First off: the archive calendar can work out if a category page is being displayed, so that's good. It could therefore calculate a different list of posts to analyze.

Second: displaying an archived category conflicts in the base page being used - is it / or /archive? I'd prefer the latter, but it might mean that you would have to replicate a page layout.

Let me think about it further.

Cheers, Julian

Leave a response

Note: some MarkDown is allowed, but HTML is not. Expand to show what's available.

  •  Emphasize with italics: surround word with underscores _emphasis_
  •  Emphasize strongly: surround word with double-asterisks **strong**
  •  Link: surround text with square brackets, url with parentheses [text](url)
  •  Inline code: surround text with backticks `IEnumerable`
  •  Unordered list: start each line with an asterisk, space * an item
  •  Ordered list: start each line with a digit, period, space 1. an item
  •  Insert code block: start each line with four spaces
  •  Insert blockquote: start each line with right-angle-bracket, space > Now is the time...
Preview of response