
Perl solution to Event 4 in the 2008 Winter Scripting Games.
Solutions are also available for VBScript and Windows PowerShell.
This was the one event where our near-total lack of Perl knowledge almost betrayed us; as it turns out, working with dates in Perl isn’t quite as easy as working with dates in either VBScript of Windows PowerShell. (Or, at any rate, it isn’t nearly as obvious how to work with dates in either VBScript or Windows PowerShell.) Fortunately, though, we were able to come up with a solution. It might not be pretty but, then again, there are no style points awarded in the Scripting Games.
Thank goodness.
At any rate, here’s the solution we were able to come up with. Best of all, we were able to stick to our guns and solve the problem without having to download and install any modules; somehow or another we managed to write a script that gets by quite nicely using just the modules (in particular, the Time::Local module) that install alongside ActiveState Perl.
Here’s our calendar-creating code:
use Time::Local;
print "Please enter the month: ";
$intMonth = <>;
print "Please enter the year: ";
$intYear = <>;
$intDay = 1;
@days = qw( Sunday Monday Tuesday Wednesday Thursday Friday Saturday );
@months = qw( January February March April May June July August September October November December );
$strMonthName = $months[$intMonth - 1];
$strCalendar = "$strMonthName $intYear \n";
$startDay = timelocal(0,0,12, $intDay, $intMonth-1, $intYear-1900);
$intDayOfWeek = (localtime $startDay)[6];
$strFirstDay = $days[$intDayOfWeek];
$strCalendar = $strCalendar . "Sun Mon Tue Wed Thu Fri Sat\n";
$strSunday = "\n ";
$strMonday = " ";
$strTuesday = " ";
$strWednesday = " ";
$strThursday = " ";
$strFriday = " ";
$strSaturday = " ";
if ($strFirstDay eq "Sunday") {$strSpacer = $strSunday}
if ($strFirstDay eq "Monday") {$strSpacer = $strMonday}
if ($strFirstDay eq "Tuesday") {$strSpacer = $strTuesday}
if ($strFirstDay eq "Wednesday") {$strSpacer = $strWednesday}
if ($strFirstDay eq "Thursday") {$strSpacer = $strThursday}
if ($strFirstDay eq "Friday") {$strSpacer = $strFriday}
if ($strFirstDay eq "Saturday") {$strSpacer = $strSaturday}
$strCalendar = $strCalendar . $strSpacer . $intDay;
if ($strFirstDay eq "Saturday")
{
$strCalendar = $strCalendar . "\n";
}
do
{
$intDay++;
eval
{
timelocal(0,0,0,$intDay, $intMonth-1, $intYear);
} or print "";
if ($@) {print $strCalendar;exit}
else
{
$nextDay = timelocal(0,0,12, $intDay, $intMonth-1, $intYear-1900);
$nextDayOfWeek = (localtime $nextDay)[6];
$strNextDay = $days[$nextDayOfWeek];
if ($strNextDay eq "Sunday")
{$strSpacer = " "}
else
{$strSpacer = " "}
if ($intDay < 10)
{$strCalendar = $strCalendar . " " . $strSpacer . $intDay}
else
{$strCalendar = $strCalendar . $strSpacer . $intDay}
if ($strNextDay eq "Saturday")
{$strCalendar = $strCalendar . "\n"}
}
}
until $x == 1000;
As you can see, this script starts off by loading the module Time::Local, a module that gets installed along with ActiveState Perl:
use Time::Local;
After prompting the user to enter the month and the year (storing those values in the variable $intMonth and $intYear, respectively) we then assign the value 1 to the variable $intDay; that’s going to give us all the information we need to construct a date string (for example, 2, 2008, and 1 would let us construct the date 2/1/2008).
Our next step is to assign the days of the week to an array; that’s something we can do in a single line of code using the qw function:
@days = qw( Sunday Monday Tuesday Wednesday Thursday Friday Saturday );
Once that’s done we then use this chunk of code to determine the name of the month, then kick off our calendar string ($strCalendar) by adding the month name, the year, and a carriage return-lineefd (\n):
@months = qw( January February March April May June July August September October November December ); $strMonthName = $months[$intMonth - 1]; $strCalendar = "$strMonthName $intYear \n";
That brings us to this block of code:
$startDay = timelocal(0,0,12, $intDay, $intMonth-1, $intYear-1900); $intDayOfWeek = (localtime $startDay)[6]; $strFirstDay = $days[$intDayOfWeek];
In line 1, we’re constructing a date-time string using the timelocal function and the variables holding the day, month, and year. Note that we need to subtract 1 from the month value; that’s because Perl months are stored as an array, with January being month 0 and December month 11. We also have to subtract 1900 from the year value, making the year 2008 the “year” 108. That’s not particularly intutuive, but that’s how dates and times work in Perl.
The timelocal function returns a date-time value that includes the day of the week for that particular date; we can retrieve the integer value for that day of the week by using the localtime function:
$intDayOfWeek = (localtime $startDay)[6];
Once we’ve done that we can get back the actual day of the week (e.g., Friday) by accessing the corresponding item in the array @days:
$strFirstDay = $days[$intDayOfWeek];
Again, not the most obvious or intuitive way of doing things. But, like we said, it works.
Now that we know the day of the week for the first day in our month we can begin putting together our calendar. To that end, the first thing we do is set up the initial data row in our calendar, the row that displays the days of the week:
$strCalendar = "Sun Mon Tue Wed Thu Fri Sat\n";
Next we need to determine where day 1 needs to go. To help us in that regard, we assign various “starting spots” to a series of variables:
$strSunday = "\n "; $strMonday = " "; $strTuesday = " "; $strWednesday = " "; $strThursday = " "; $strFriday = " "; $strSaturday = " ";
All we’re doing here is adding in enough blank spaces to ensure that our calendar starts off on the right foot. For example, suppose the first day of the month occurs on a Friday; in that case, our calendar needs to start off like this:
Sun Mon Tue Wed Thu Fri Sat
1
Next we use a set of if statements similar to this one to assign the appropriate “spacer” variable based on day 1’s day of the week:
if ($strFirstDay eq "Sunday") {$strSpacer = $strSunday}
And once we’ve done that we can then add day 1 to our calendar by using this line of code:
$strCalendar = $strCalendar . $strSpacer . $intDay;
Assuming our month is February, 2008 (which, in this case, it is) that’s going to give us a calendar that currently looks like this:
Sun Mon Tue Wed Thu Fri Sat
1
Next we need to quickly check to see if the first day of the month occurs on a Saturday:
if ($strFirstDay eq "Saturday")
{
$strCalendar = $strCalendar . "\n";
}
Why do we do that? That’s easy: if day 1 does occur on a Saturday that means that day 2 occurs on a Sunday; that also means that day 2 needs to appear on the next row in the calendar. In that case, we have to tack a carriage return-linefeed (\n) onto the end of our calendar string.
Now it’s time to start adding the remaining days of the month. To do that, we set up a do loop designed to run until $x is equal to 1000 ($x == 1000); that’s basically just a trick to keep the loop running until we tell it to stop. Inside that loop, we first increment the value of $intDay by 1. Why? That’s right: because now we’re working with day 2 in the month. And that brings us to the following block of code:
eval
{
timelocal(0,0,0,$intDay, $intMonth-1, $intYear);
} or print "";
What we’re doing here is using the eval function to determine whether or not our new date (February 2, 2008) is a valid date. That’s how we’ll know when we’ve exhausted all the days in the month. February 2, 2008 will not generate an error; that’s because it’s a valid date. When we try February 30, 2008, however, we will get an error; that’s because this is not a valid date. Once our eval function generates an error we’ll know that we’ve finished adding all the legitimate days of the month to our calendar.
Incidentally, if eval does generate an error then that error information will automatically be stored in the special variable $@. That means we can determine whether or not an error occurred simply by checking to see if $@ has a value:
if ($@) {print $strCalendar;exit}
If $@ does have a value we use the print function to display our calendar onscreen, then use the exit function to terminate the script.
But what happens if we do have a valid date? In that case, we use this block of code to determine the day of the week for that particular date:
$nextDay = timelocal(0,0,12, $intDay, $intMonth-1, $intYear-1900);
$nextDayOfWeek = (localtime $nextDay)[6];
$strNextDay = $days[$nextDayOfWeek];
Based on the day of week, we then assign a value to the variable $strSpacer:
if ($strNextDay eq "Sunday")
{$strSpacer = " "}
else
{$strSpacer = " "}
Why do we need to assign a different value if a date occurs on a Sunday? Well, take a look at the partial calendar for February, 2008:
Sun Mon Tue Wed Thu Fri Sat
1 2
3 4
If you look closely, you’ll see that the number of blank spaces that precede a date differ between Sunday and all the other days. We have two blank spaces preceding the 3rd of February (a Sunday); we have 7 blank spaces preceding the 4th (a Monday).
Of course, if you count the number of blank spaces we assigned to $strSpacer you’ll notice that there are only 6 blank spaces. What’s the deal there? Well, we need to make allowances for dates that have two digits; in the case of a two-digit date (like the 11th) we only have 6 blank spaces separate that date from the previous date:
Sun Mon Tue Wed Thu Fri Sat
1 2
3 4 5 6 7 8 9
10 11
To account for that, we check to see if the day of the month is less than 10. If it is, we add an extra space to our calendar string; if it isn’t then we simply add our spacer variable ($strSpacer) and the current date to the calendar string. That’s what this block of code is for:
if ($intDay < 10)
{$strCalendar = $strCalendar . " " . $strSpacer . $intDay}
else
{$strCalendar = $strCalendar . $strSpacer . $intDay}
After that we check to see if our current day of the month happens to be a Saturday. If it is, we add a carriage return-linefeed character onto the end of the string, ensuring that the next day (a Sunday) will appear on the correct row in our calendar:
if ($strNextDay eq "Saturday")
{$strCalendar = $strCalendar . "\n"}
And then it’s back to the top of the loop, where we repeat the process with the next day in the month.
When all is said and done we should end up with a calendar the looks like this:
Sun Mon Tue Wed Thu Fri Sat
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
Which is just exactly what we wanted it to look like.