Generating a PDF Calendar

by Dan Duda

Requirements

  • HTML and CSS Experience
  • C# Experience
  • Visual Studio 2017 Community

Introduction

In the age of smart phones and online calendars I find I still like to also have a paper calendar at my desk. Having a single page per month gives a lot of space to jot down notes and quickly glance at upcoming appointments, etc. Over the years I have come across downloadable year calendars in PDF format that you can print out and staple together. But I wanted more control to add my own recurring “special” dates and holidays. I also wanted to be able to include colorful icons with my events.

My first concern was how to render the calendar. I wanted to keep it simple so I decided to use just plain HTML and tables with CSS to style them. This will also enable the use of the web browser for printing and PDF conversion without having to write anything myself. There were some things I learned along the way that I hope may help someone else. One in particular was the use of media queries to override the browsers default print themes. Another was discovering the FontAwesome library for icons.

Project Setup

For this project I decided to create a console application in C# and the .Net framework. I’ll be using Visual Studio 2017 Comminity Edition. The first step is to create a “C# console application”. I’m using .net framework version 4.7.


By default Visual Studio gives us a single class file for console applications called Program.cs with a single method called Main. I didn’t feel I really needed a GUI for this and wanted to keep it simple so chose the console application approach which means it will run on the command line. The program should take a single command line paramter for the year to generate. We’ll check that the input is a valid number and then also check that it’s a pratical year. Yes, I’m pretty optimistic putting in 2400 as an end year. :)

Our main method will be fairly short and will look like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
using System;

namespace CalendarCreator
{
class Program
{
static void Main(string[] args)
{
// Get year from args
int year = DateTime.Now.Year;

if (args.Length == 1)
{
int testYear = year;
if (Int32.TryParse(args[0], out testYear))
{
if (testYear >= 1950 && testYear < 2400)
{
year = testYear;
}
}
}

var path = new CalendarGenerator().GenerateCalendar(year);
System.Diagnostics.Process.Start(path);
}
}
}

In line 10 we default the year to the current year. This way if we call our program without passing in a year it will just generate the current year. We check for a passed in year on line 12 and then check that it’s a valid year. Line 24 instantiates a class called CalendarGenerator (which we will build in the next steps) and calls a single method on it called GenerateCalendar passing in the year to generate. This method should return a path to the html file that it created. Line 25 just tells Windows to open the file at that path with the default browser.

Adding our CalendarGenerator class

From the project menu we’ll chose “Add class” and add a class named “CalendarGenerator”.


This will give us an empty class like this:

1
2
3
4
5
6
7
8
9
10
11
12
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace CalendarCreator
{
class CalendarGenerator
{
}
}

I also want to add a folder to our project called “data”. This will be where we create the html file containing our calendar. Later on it will also hold extra files we’ll use to enhance the calendar like CSS files.


Let’s add our GenerateCalendar method that we call from our Main method. This method should accept a year parameter and create an html file containing the 12 months for that year. It should then return the path to the new html file.

To start simple let’s just get it to generate the month headers (month names) for each month and create the file. We’ll need to get the path to where we’re executing this program so we can get the path to our data folder. To do this we’ll add a class variable and constructor. We’ll create some basic boiler plate html 5 and then plug in the generated months. A helper method named “GenerateMonth” will be used to generate the individual months.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
using System;
using System.IO;
using System.Reflection;
using System.Text;

namespace CalendarCreator
{
class CalendarGenerator
{
private string _baseDir;

public CalendarGenerator()
{
_baseDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
}

public string GenerateCalendar(int year)
{
var html = new StringBuilder();

html.Append("<!DOCTYPE html>");
html.Append("<html lang=\"en\">");
html.Append("<head>");
html.Append("<meta charset=\"UTF-8\">");
html.Append("<title>My Calendar</title>");
html.Append("</head>");
html.Append("<body>");

for (var m = 1; m <= 12; m++)
{
html.Append(GenerateMonth(new DateTime(year, m, 1)));
}

html.Append("</body>");
html.Append("</html>");

var dataPath = Path.Combine(_baseDir, "data");
if (!Directory.Exists(dataPath))
{
Directory.CreateDirectory(dataPath);
}

var outputPath = Path.Combine(dataPath, "calendar.html");
using (StreamWriter sw = new StreamWriter(outputPath))
{
sw.WriteLine(html.ToString());
}

return outputPath;
}

private string GenerateMonth(DateTime month)
{
var sb = new StringBuilder();

// Month Header
sb.AppendFormat("<h1>{0:y}</h1>", month);

return sb.ToString();
}
}
}

You should be able to run this now right from Visual Studio. It should open up a browser automatically and display something like this:


Not especially pretty but we have it creating and opening an html file like we want.

Adding styling

Let’s add some styling by utilizing a popular CSS framework called Bootstrap. For this project I’m using Bootstrap v3.3.7. You can download it from getbootstrap.com. Click on the “Download Boostrap” button. This will download a zip file containing 3 folders. For now we’re interested in the css folder. Unzip the file to a temporary location and look for the file “bootstrap.min.css” under the css folder. We want to add this to our project. In Visual Studio right click on our “data” folder in the “Solution Explorer” and choose “Add” and then “Existing Item”. Browse to where you unzipped the bootstrap file and select the bootstrap.min.css file. You’ll need to change the file type dropdown to “All files” so that you can see the css file.


You should now see the boostrap file underneath “data”.


While we’re add it let’s make things a little cleaner by creating a template html file under our data file that we can just load instead of hard coding it in our method. Let’s also create our own CSS file for anything custom we do beyond what bootstrap gives us.

Right-click on our data folder and choose “Add” and then “New Item”. Select “Style Sheet” as the type and name it “calendar.css”.


Right-click the data folder again and choose “Add” and then “New Item” and this time select “HTML page” and name it “template.html”.


Our data folder should now look like this:


Before we move on one important thing we must do is set these files to be included when our project builds. Select all 3 files under the data folder and then click on the “Properties” tab at the bottom of the “Solution Explorer”. Change the “Copy to Output Directory” to “Copy Always”.


Just including the bootstrap file will not change anything. We need to include it in our html template. Instead of hard coding it let’s create a template. Open the “template.html” we just created and add the following html.

1
2
3
4
5
6
7
8
9
10
11
12
13
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>[TITLE]</title>

<link href="bootstrap.min.css" rel="stylesheet">
<link href="calendar.css" rel="stylesheet">
</head>
<body>
[BODY]
</body>
</html>

On lines 7 and 8 we include the 2 css files we added. The [TITLE] and [BODY] tokens will be replaced by our generator code. Now let’s remove the hard coded html from our generator method and instead read in our template file.

Our GenerateCalendar method should now looke like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public string GenerateCalendar(int year)
{
var body = new StringBuilder();
for (var m = 1; m <= 12; m++)
{
body.Append(GenerateMonth(new DateTime(year, m, 1)));
}

var dataPath = Path.Combine(_baseDir, "data");
if (!Directory.Exists(dataPath))
{
Directory.CreateDirectory(dataPath);
}

var templatePath = Path.Combine(dataPath, "template.html");

var html = new StringBuilder();
using (StreamReader r = new StreamReader(templatePath))
{
html.Append(r.ReadToEnd());
}

html.Replace("[TITLE]", "My Calendar");
html.Replace("[BODY]", body.ToString());

var outputPath = Path.Combine(dataPath, "calendar.html");
using (StreamWriter sw = new StreamWriter(outputPath))
{
sw.WriteLine(html.ToString());
}

return outputPath;
}

Also, let’s use a bootstrap style on our month headers.

1
2
3
4
5
6
7
8
9
private string GenerateMonth(DateTime month)
{
var sb = new StringBuilder();

// Month Header
sb.AppendFormat("<h1 class=\"text-center\">{0:y}</h1>", month);

return sb.ToString();
}

If we run it again we should now see that the month headers are centered and in a different font.


Weekday header

Now let’s add a weekday header in our “GenerateMonth” method. I’m using a width of “14.29%” on the table cell since I want to divide the table by 7 week days. I’m also using some more bootstrap styles to add some coloring and borders.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private string GenerateMonth(DateTime month)
{
var sb = new StringBuilder();

// Month Header
sb.AppendFormat("<h1 class=\"text-center\">{0:y}</h1>", month);

// Weekday Header
sb.Append("<table class=\"table table-bordered\">");

sb.Append("<tr class=\"success\">");
for (var dow = DayOfWeek.Sunday; dow <= DayOfWeek.Saturday; dow++)
{
sb.AppendFormat("<td class=\"text-center\" width=\"14.29%\">{0}</td>", dow);
}
sb.Append("</tr>");

sb.Append("</table>");

return sb.ToString();
}

With this in place, running it should output the following (showing just first 2 months here).


Adding the day cells

To add the days of the month we’ll use a while loop. We’re passing in a date object to the GenerateMonth method that always has the day part of 1 (first day of month). So we set a “day” variable to the month variable so it’s the first of the month passed in. We then loop while the month is still the month that was passed in. Unless the first daay of the month is a Sunday we need to skip days which we do on lines 27 - 31. We also skip any ending days on line 46. We increment the day variable by one day at the end of each cycle.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
private string GenerateMonth(DateTime month)
{
var sb = new StringBuilder();

// Month Header
sb.AppendFormat("<h1 class=\"text-center\">{0:y}</h1>", month);

// Weekday Header
sb.Append("<table class=\"table table-bordered\">");

sb.Append("<tr class=\"success\">");
for (var dow = DayOfWeek.Sunday; dow <= DayOfWeek.Saturday; dow++)
{
sb.AppendFormat("<td class=\"text-center\" width=\"14.29%\">{0}</td>", dow);
}
sb.Append("</tr>");

// Days
var day = month;
while (day.Month == month.Month)
{
sb.Append("<tr>");

if (day.Day == 1)
{
// Skip beginning days
var startDay = (int)day.DayOfWeek;
for (var i = 0; i < startDay; i++)
{
sb.Append("<td></td>");
}
}

var cont = true;
while (cont)
{
if (day.Month == month.Month)
{
sb.Append("<td>");
sb.AppendFormat("<span class=\"pull-right\">{0}</span>", day.Day);
sb.Append("</td>");
}
else
{
// Skip ending days
sb.Append("<td></td>");
}

cont = (day.DayOfWeek != DayOfWeek.Saturday);
day = day.AddDays(1);
}

sb.Append("</tr>");
}

sb.Append("</table>");

return sb.ToString();
}

Running it should now output day cells (showing just first 2 months here).


Getting there. Now let’s fill out the day cells so that they’re more square and so a single month will fit on a single page. Later on we’ll add support for adding holidays, etc. Inside the loop where it displays the day number add lines 9 - 13. This will add 3 breaks to each cell.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
var cont = true;
while (cont)
{
if (day.Month == month.Month)
{
sb.Append("<td>");
sb.AppendFormat("<span class=\"pull-right\">{0}</span><br />", day.Day);

var breakCount = 3;
for (var i = 0; i < breakCount; i++)
{
sb.Append("<br />");
}

sb.Append("</td>");
}
else
{
// Skip ending days
sb.Append("<td></td>");
}

cont = (day.DayOfWeek != DayOfWeek.Saturday);
day = day.AddDays(1);
}

And now running it we see for a single month we get:


Let’s also add some margins so it isn’t flush with the edges of the browser. Open the “calendar.css” file we added under “data” and add the following css style.

1
2
3
body {
margin: 10px;
}

Adding Holidays and Events

My requirements for my calendar’s holidays, etc. are to be able to have an icon associated with them and a color. Searching the web for good web icons I came across a great library called “FontAwesome”. You can download it at FontAwesome. I highly recommend the “Pro” version if you have a need for high quality icons but for this project you can just download the free version.

Once downloaded, unzip the file. We want the file, “fontawesome-all.min.js” that resides in the “svg-with-js\js” folder. Like we did for bootstrap, right-click on the data folder in the “Solution Explorer” and choose “Add” then “Existing item”. Select the “fontawesome-all.min.js” and click “Add”.

Important: Like we did with the other 3 files under “data”, select the “fontawesome-all.min.js” file and click the properties tab at the bottom of the “Solution Explorer” and change the “Copy to Output Directory” to “Copy Always”.

Our data folder should now resemble the following:


We need to add a reference to this in our html template so open the “template.html” file under the data folder and add line 10.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>[TITLE]</title>

<link href="bootstrap.min.css" rel="stylesheet">
<link href="calendar.css" rel="stylesheet">

<script defer src="fontawesome-all.min.js"></script>
</head>
<body>
[BODY]
</body>
</html>

We will also add some color styles to our “calendar.css”. Open it and add the following color styles after the “body” style so that it looks like the following.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
body {
margin: 10px;
}

.red {
color: red;
}

.green {
color: green;
}

.blue {
color: blue;
}

.purple {
color: purple;
}

.yellow {
color: yellow;
}

.orange {
color: orange;
}

.brown {
color: brown;
}

.black {
color: black;
}

Back in our “CalendarGenerator” class we need to add support for holidays. We’ll need a type to hold information about holidays. To do this let’s add a couple property classes to help us out. Add the following to the “CalendarGenerator.cs” file underneath the “CalendarGenerator” class.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class SpecialDay
{
public DateTime Date { get; set; }
public string Title { get; set; }
public string Icon { get; set; }
public string Color { get; set; }
}

class DayOptions
{
public string Title { get; set; }
public string DayColor { get; set; }
public string Icon { get; set; }

public string Class
{
get { return $"{DayColor} {Icon}"; }
}
}

We’ll need a class variable in “CalendarGenerator” to hold all the holidays. At the top of the class add a private variable “_days” and initialize it in the constructor (lines 12 and 17). We’ll also need to include the “System.Collections.Generic” namespace on line 2 since we’re using a Dictionary collection object to store our holidays.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Text;

namespace CalendarCreator
{
class CalendarGenerator
{
private string _baseDir;
private Dictionary<DateTime, List<DayOptions>> _days;

public CalendarGenerator()
{
_baseDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
_days = new Dictionary<DateTime, List<DayOptions>>();
}

In the “GenerateMonth” method we’ll now utilize the “_days” to display any holidays that are in the collection. In the while loop add lines 10 - 17. This code checks for any holidays in our colelction for the current day that the loop is generating.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
var cont = true;
while (cont)
{
if (day.Month == month.Month)
{
sb.Append("<td>");
sb.AppendFormat("<span class=\"pull-right\">{0}</span><br />", day.Day);

var breakCount = 3;
if (_days.ContainsKey(day))
{
foreach (var d in _days[day])
{
breakCount--;
sb.AppendFormat("<i class=\"{0} {1}\" aria-hidden=\"true\"></i> <span class=\"daytitle\">{2}</span><br />", d.DayColor, d.Icon, d.Title);
}
}

for (var i = 0; i < breakCount; i++)
{
sb.Append("<br />");
}

sb.Append("</td>");
}
else
{
// Skip ending days
sb.Append("<td></td>");
}

cont = (day.DayOfWeek != DayOfWeek.Saturday);
day = day.AddDays(1);
}

We’re also going to need a method to add the holidays to our collection. After the “GenerateMonth” method add the following 2 methods. Since I’m in the U.S.A. I’ll be adding some of the popular US holidays.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
private void AddDay(DateTime day, DayOptions options)
{
if (!_days.ContainsKey(day))
{
_days.Add(day, new List<DayOptions>());
}

// Restrict to at most 3 events per day
if (_days[day].Count < 3)
{
_days[day].Add(options);
}
}

private void AddHolidays(int year)
{
AddDay(new DateTime(year, 1, 1), new DayOptions() { Title = "New Years", DayColor = "blue", Icon = "fas fa-snowflake" });
AddDay(new DateTime(year, 1, 15), new DayOptions() { Title = "Martin Luther King, Jr. Day", DayColor = "black", Icon = "far fa-calendar-alt" });
AddDay(new DateTime(year, 2, 2), new DayOptions() { Title = "Groundhog Day", DayColor = "brown", Icon = "far fa-calendar-alt" });
AddDay(new DateTime(year, 2, 14), new DayOptions() { Title = "Valentine's Day", DayColor = "red", Icon = "fas fa-heart" });
AddDay(new DateTime(year, 3, 17), new DayOptions() { Title = "St. Patrick's Day", DayColor = "green", Icon = "far fa-calendar-alt" });
AddDay(new DateTime(year, 4, 1), new DayOptions() { Title = "April Fools Day", DayColor = "black", Icon = "far fa-calendar-alt" });
AddDay(new DateTime(year, 6, 14), new DayOptions() { Title = "Flag Day", DayColor = "blue", Icon = "fas fa-flag" });
AddDay(new DateTime(year, 7, 4), new DayOptions() { Title = "Independence Day", DayColor = "blue", Icon = "fas fa-flag" });
AddDay(new DateTime(year, 10, 31), new DayOptions() { Title = "Halloween", DayColor = "orange", Icon = "fas fa-jack-o-lantern" });
AddDay(new DateTime(year, 11, 11), new DayOptions() { Title = "Veterans Day", DayColor = "black", Icon = "far fa-calendar-alt" });
AddDay(new DateTime(year, 12, 25), new DayOptions() { Title = "Christmas", DayColor = "green", Icon = "fas fa-tree" });
}

And lastly we need to call the “AddHolidays” method from our “GenerateCalendar” method by adding line 3 below.

1
2
3
4
5
6
7
8
9
public string GenerateCalendar(int year)
{
AddHolidays(year);

var body = new StringBuilder();
for (var m = 1; m <= 12; m++)
{
body.Append(GenerateMonth(new DateTime(year, m, 1)));
}

Running now you should see holidays listed in the appropriate day cells and also colored icons next to them. Looking at February we should see:


Holidays that don’t fall on the same date each year

Some holidays occur on the same date each year, like the ones we’ve already added, while others follow certain rules. For those that require special rules we’ll add a helper class to handle the calculations.

In “Solution Explorer” right-click on the “CalendarCreator” project and select “Add” then “class” and name it “HolidayHelper.cs”.

Searching the web I was able to find some code to calculate the days for them. You can add any others that you require in here as well.

In the new “HolidayHelper” class I have:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
using System;

namespace CalendarCreator
{
public static class HolidayHelper
{
public static DateTime GetFathersDayDate(int Year)
{
if (Year < 1583 || Year > 4099)
{
throw new Exception("Invalid Year");
}

DateTime firstDayInJune = new DateTime(Year, 6, 1);

DateTime tempDate = firstDayInJune;

int count = 0;
while (count < 3)
{
if (tempDate.DayOfWeek == DayOfWeek.Sunday)
{
count++;
}

tempDate = tempDate.AddDays(1);
}

return tempDate.AddDays(-1);
}

public static DateTime GetMothersDayDate(int Year)
{
if (Year < 1583 || Year > 4099)
{
throw new Exception("Invalid Year");
}

DateTime firstDayInMay = new DateTime(Year, 5, 1);

DateTime tempDate = firstDayInMay;

int count = 0;
while (count < 2)
{
if (tempDate.DayOfWeek == DayOfWeek.Sunday)
{
count++;
}

tempDate = tempDate.AddDays(1);
}

return tempDate.AddDays(-1);
}

public static DateTime GetThanksgivingDate(int Year)
{
if (Year < 1583 || Year > 4099)
{
throw new Exception("Invalid Year");
}

DateTime firstDayInNovember = new DateTime(Year, 11, 1);

DateTime tempDate = firstDayInNovember;

int count = 0;
while (count < 4)
{
if (tempDate.DayOfWeek == DayOfWeek.Thursday)
{
count++;
}

tempDate = tempDate.AddDays(1);
}

return tempDate.AddDays(-1);
}

public static DateTime GetMemorialDayDate(int Year)
{
if (Year < 1583 || Year > 4099)
{
throw new Exception("Invalid Year");
}

DateTime lastDayInMay = new DateTime(Year, 6, 1).AddDays(-1);

DateTime tempDate = lastDayInMay;
while (tempDate.DayOfWeek != DayOfWeek.Monday)
{
tempDate = tempDate.AddDays(-1);
}

return tempDate;
}

public static DateTime GetEasterDate(int Year)
{
if (Year < 1583 || Year > 4099)
{
throw new Exception("Invalid Easter Year");
}

int firstDig = Year / 100; // first 2 digits of year
int remain19 = Year % 19; // remainder of year / 19

// Calculate PFM date
int temp = (firstDig - 15) / 2 + 202 - 11 * remain19;
switch (temp)
{
case 21:
case 24:
case 25:
case 27:
case 28:
case 29:
case 30:
case 31:
case 32:
case 34:
case 35:
case 38:
temp--;
break;

case 33:
case 36:
case 37:
case 39:
case 40:
temp -= 2;
break;
}

temp %= 30;

int tA = temp + 21;
if (temp == 29)
{
tA--;
}

if (temp == 28 && remain19 > 10)
{
tA--;
}

// Find the next Sunday
int tB = (tA - 19) % 7;
int tC = (40 - firstDig) % 4;

if (tC == 3)
{
tC++;
}

if (tC > 1)
{
tC++;
}

temp = Year % 100;
int tD = (temp + temp / 4) % 7;
int tE = ((20 - tB - tC - tD) % 7) + 1;
int d = tA + tE;

// Return the date
int m = 4;
if (d > 31)
{
d -= 31;
}
else
{
m = 3;
}

return new DateTime(Year, m, d);
}

public static DateTime GetPresidentsDay(int Year)
{
if (Year < 1583 || Year > 4099)
{
throw new Exception("Invalid Year");
}

var day = new DateTime(Year, 2, 1);
DateTime thirdMonday = day;

int count = 0;
for (var d = 1; d <= DateTime.DaysInMonth(Year, 2); d++)
{
if (day.DayOfWeek == DayOfWeek.Monday)
{
count++;
}

if (count == 3)
{
thirdMonday = day;
break;
}

day = day.AddDays(1);
}

return thirdMonday;
}

public static DateTime GetLaborDay(int Year)
{
if (Year < 1583 || Year > 4099)
{
throw new Exception("Invalid Year");
}

var day = new DateTime(Year, 9, 1);
DateTime firstMonday = day;

int count = 0;
for (var d = 1; d <= DateTime.DaysInMonth(Year, 9); d++)
{
if (day.DayOfWeek == DayOfWeek.Monday)
{
count++;
}

if (count == 1)
{
firstMonday = day;
break;
}

day = day.AddDays(1);
}

return firstMonday;
}
}
}

Go back to the “AddHolidays” method in the “CalendarGenerator” class and add lines 15 - 21 below.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private void AddHolidays(int year)
{
AddDay(new DateTime(year, 1, 1), new DayOptions() { Title = "New Years", DayColor = "blue", Icon = "fas fa-snowflake" });
AddDay(new DateTime(year, 1, 15), new DayOptions() { Title = "Martin Luther King, Jr. Day", DayColor = "black", Icon = "far fa-calendar-alt" });
AddDay(new DateTime(year, 2, 2), new DayOptions() { Title = "Groundhog Day", DayColor = "brown", Icon = "far fa-calendar-alt" });
AddDay(new DateTime(year, 2, 14), new DayOptions() { Title = "Valentine's Day", DayColor = "red", Icon = "fas fa-heart" });
AddDay(new DateTime(year, 3, 17), new DayOptions() { Title = "St. Patrick's Day", DayColor = "green", Icon = "far fa-calendar-alt" });
AddDay(new DateTime(year, 4, 1), new DayOptions() { Title = "April Fools Day", DayColor = "black", Icon = "far fa-calendar-alt" });
AddDay(new DateTime(year, 6, 14), new DayOptions() { Title = "Flag Day", DayColor = "blue", Icon = "fas fa-flag" });
AddDay(new DateTime(year, 7, 4), new DayOptions() { Title = "Independence Day", DayColor = "blue", Icon = "fas fa-flag" });
AddDay(new DateTime(year, 10, 31), new DayOptions() { Title = "Halloween", DayColor = "orange", Icon = "far fa-calendar-alt" });
AddDay(new DateTime(year, 11, 11), new DayOptions() { Title = "Veterans Day", DayColor = "black", Icon = "far fa-calendar-alt" });
AddDay(new DateTime(year, 12, 25), new DayOptions() { Title = "Christmas", DayColor = "green", Icon = "fas fa-tree" });

AddDay(HolidayHelper.GetEasterDate(year), new DayOptions() { Title = "Easter", DayColor = "purple", Icon = "far fa-calendar-alt" });
AddDay(HolidayHelper.GetFathersDayDate(year), new DayOptions() { Title = "Father's Day", DayColor = "black", Icon = "far fa-calendar-alt" });
AddDay(HolidayHelper.GetMemorialDayDate(year), new DayOptions() { Title = "Memorial Day", DayColor = "black", Icon = "far fa-calendar-alt" });
AddDay(HolidayHelper.GetMothersDayDate(year), new DayOptions() { Title = "Mother's Day", DayColor = "black", Icon = "far fa-calendar-alt" });
AddDay(HolidayHelper.GetThanksgivingDate(year), new DayOptions() { Title = "Thanksgiving", DayColor = "brown", Icon = "far fa-calendar-alt" });
AddDay(HolidayHelper.GetPresidentsDay(year), new DayOptions() { Title = "Presidents' Day", DayColor = "black", Icon = "far fa-calendar-alt" });
AddDay(HolidayHelper.GetLaborDay(year), new DayOptions() { Title = "Labor Day", DayColor = "black", Icon = "far fa-calendar-alt" });
}

Adding birthdays and other non-holiday events

We have holidays working. Since they’re for the most part static it’s fine having then hard-coded. For personal events we want it to be more customizable so we can manage them without having to change the code and recompile. I decided to utilize the JSON format (Javascript Object Notation) which is a popular open-standard file format similar to XML.

Right-click on the “data” folder and choose “Add” and then “New item”. Type “json” in the search box and then select “JSON File” as the type. Name the file “specialdates.json”.

Select “specialdates.json” in the “Solution Explorer” and then click the “Properties” tab at the bottom. Change the “Copy to Output Directory” to “Copy If Newer”. Each time we compile / run from Visual Studio we don’t want the file being overwritten. We’ll be able to open this file outside of Visual Studio and add special dates to it and don’t want to lose any items.

Open the file and add these 2 test entries.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[
{
"Date": "1-7-1986",
"Title": "John Doe",
"Icon": "fas fa-birthday-cake",
"Color": "red"
},
{
"Date": "1-18-2008",
"Title": "Anniversary",
"Icon": "fas fa-heart",
"Color": "red"
}
]

We will also need to include a module for working with JSON. In the “Tools” menu of Visual Studio, select “NuGet Package Manager” and then “Manage NuGet Packages for Solution”. Click on the “Browse” tab at the top and type “json” into the search box. Select “Newtonsoft.Json” in the results. On the right hand side check the box next to the project “CalendarCreator” and then click “Install”. Click “OK” on the dialog that pops up.


In the “GenerateCalendar” method of the “CalendarGenerator” class let’s add support for special days. We include “Newtonsoft.Json” on line 1. Note I moved the code that creates the “dataPath to the beginning of the method. Lines 31 - 51 handle the speacial dates. We create a collection called specialDays. We then read in the json file and deserialize it into our list. On lines 43 - 51 we calculate the age of the event so that it can display the person’s age next to their name for a birthday or length of marriage for an anniversary. It then calls the same “AddDay” method we used for holidays.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Text;

namespace CalendarCreator
{
class CalendarGenerator
{
private string _baseDir;
private Dictionary<DateTime, List<DayOptions>> _days;

public CalendarGenerator()
{
_baseDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
_days = new Dictionary<DateTime, List<DayOptions>>();
}

public string GenerateCalendar(int year)
{
var dataPath = Path.Combine(_baseDir, "data");
if (!Directory.Exists(dataPath))
{
Directory.CreateDirectory(dataPath);
}

AddHolidays(year);

// Check for specialdates.json
var specialDays = new List<SpecialDay>();
var specialDatesPath = Path.Combine(dataPath, "specialdates.json");
if (File.Exists(specialDatesPath))
{
using (StreamReader r = new StreamReader(specialDatesPath))
{
var json = r.ReadToEnd();
specialDays = JsonConvert.DeserializeObject<List<SpecialDay>>(json);
}
}

foreach (var sd in specialDays)
{
var date = new DateTime(year, sd.Date.Month, sd.Date.Day);

int age = year - sd.Date.Year;
var title = $"{sd.Title} ({age})";

AddDay(date, new DayOptions() { Title = title, DayColor = sd.Color, Icon = sd.Icon });
}

var body = new StringBuilder();
for (var m = 1; m <= 12; m++)
{
body.Append(GenerateMonth(new DateTime(year, m, 1)));
}

var templatePath = Path.Combine(dataPath, "template.html");

var html = new StringBuilder();
using (StreamReader r = new StreamReader(templatePath))
{
html.Append(r.ReadToEnd());
}

html.Replace("[TITLE]", "My Calendar");
html.Replace("[BODY]", body.ToString());

var outputPath = Path.Combine(dataPath, "calendar.html");
using (StreamWriter sw = new StreamWriter(outputPath))
{
sw.WriteLine(html.ToString());
}

return outputPath;
}

Now if we run it we should see the two special days we added in January.


Printing and Generating PDFs

There are a couple of items we still need to address.

Run the program and viewing the calendar in the browser, right click on it and select “Print”. In the “Print Preview” make sure to change the “Layout” to “Landscape” if it isn’t already. You should notice that the months are not one to a page. I can see part of February on January’s page.


We can fix this easily with some HTML and CSS.

In the “GenerateMonth” method at the very bottom add the lines 3 - 4.

1
2
3
4
5
6
7
    sb.Append("</table>");

// For printing one month per page
sb.Append("<p style=\"page-break-after: always;\">&nbsp;</p>");

return sb.ToString();
}

Also notice that we’ve lost all our color. By default some browsers will change to black and white for printing. The event titles are also a little too big could cause a lot of word wrapping.

We can fix this by overriding the browser’s default print CSS using CSS media queries.

Open up the “calendar.css” file and add the following CSS to the bottom below where we defined our color styles.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* For printing */
@media print {
.success {
background-color: #DFF0D7 !important;
}

.table td {
background-color: transparent !important;
}

.daytitle {
font-size: 9px !important;
}
}

Running it again we see the font is smaller and the months are one to a page but the colors are all still black. There’s one more thing we need to do. Bootstrap is great because it offers so much styling that we don’t need to create ourselves, however in this instance it actually causes a promblem for us. Just like the browsers have there own default print styles, so does Bootstrap. Normally we override styles by adding them to our own css file but in this case I have not found a way to override Bootstrap’s media print styles. What I did to fix this is make a minor change in the bootstrap css. Since we’re using a minified version of the bootstrap file it’s not as easy to read.

Open the “boostrap.min.css” under the “data” folder. You’ll notice it’s all bascially on one line. This makes it more efficient to send to the browser from the server.

Search for “media print” by pressing CTRL+F and type “media print” in the search box. You should see something similar to the screenshot below.


1
@media print{*,:after,:before{color:#000!important;text-shadow:none!important;

Change the color from “#000” to “#inherit”.

1
@media print{*,:after,:before{color:#inherit!important;text-shadow:none!important;

Make sure to save everything and run again.


Now we can save as a PDF and/or print our calendars. Enjoy!