This continues my series of solution posts for the 2008 Scripting Games with my solution for the Advanced Event 4 which was about drawing a “graphical” calendar in the console. Basically, this is a simple task, and the only complication was the requirement to allow passing in the month.
There are solutions from the Scripting Guys and Ed Wilson already available, and both of them are pretty good solutions, so I would have left this post in the draft bin if it wasn’t for the fact that I already showed off screenshots of my final script (which is a bit over the top). Because I wrote such a complicated script in the end, I wanted to show you how it started before I show you how it ended up:
param([DateTime]$now = (Read-Host "Please enter a date"))
# make sure that $now is the first day of the month:
$now = $now.AddDays(1-$now.Day)
# Write the month string at the top
$now.ToString(" MMMM, yyyy ")
# Write the day names below it
" Su Mo Tu We Th Fr Sa "
# Figure out the day of the week for the first day of the month
# Note: if you don't cast to [int], this returns the name, like "Monday"
$dow = [int]$now.DayOfWeek
# Total number of days ...
$days = [DateTime]::DaysInMonth($now.Year, $now.Month)
# write the calendar as a single string by joining an array of strings ...
# Starting on Sunday before the first day of the month (aka: 1-$dow)
# Ending after the number of days in the month ...
Write-Host "" @(switch((1-$dow)..$days){
{$_ -lt 1} {" "} # -lt 1, just insert padding
{$_ -gt 0} {"{0,2}" -f $_} # -gt 0, the number, padded to 2 chars
{0 -eq (($_+$dow) % 7)}{"`n"}}) # 7 Mod 7, insert a new-line
See that? Not really very complicated, right? It’s just about ten lines of code, and three of them are unnecessary variable assignments which just help make the script more readable (actually, there’s really only 2 necessary lines). There’s a couple of tricks here though:
- PowerShell has range notation that allows you to create an array of numbers from
$x to $y by saying $x..$y, and it works with negative numbers too, so I can iterate from the Sunday (Day of the Week = 0) before the start of the month by doing: (1-$dow)..$days
- The PowerShell
switch statement has built in foreach support, so if you write switch($array){...}, it’s basically equivalent to: foreach($_ in $array){ switch($_) {... } } — pretty neat, right?
- The PowerShell switch statement allows script blocks as the conditions, so rather than having to switch on integers or characters or even strings, like in C# ... I can switch on script blocks.
- The PowerShell switch statement supports fall-through with testing — unlike the C# switch where you have to explicitly
goto case, and unlike the old C++ switch where you would fall through to the next case regardless of the what the case was ... In PowerShell, if you don’t use break to break out of the switch, each item will be tested against each case, and every case which matches will be executed. This allows me to have a case for less than 1 and greater than 0 and know that they exclude each other, but that the last case (testing whether this day is the last day of the week and needs to line-wrap) will always be tested.
The actual script I submitted does some fancy formatting. It shows the last few days of the month before and the first few of the month after (just enough to fill out the week) in a different color, and also allows you to specify the day of the month. If you do specify the day of the month, it highlights it, and optionally colors past days in another color (to sort-of highlight the remaining days of the month). All of the colors are parameters to the script, so you can feel free to take this and have fun integrating it into your startup profile or whatever 
At it’s essence, this script is basically the same as the script above, but split into five parts to allow for custom colors:
param([string]$date = (Read-Host "Please enter a date")
# and optionally, a bunch of color settings ...
,[ConsoleColor]$background = $Host.UI.RawUI.BackgroundColor # "DarkBlue"
,[ConsoleColor]$foreground = $Host.UI.RawUI.ForegroundColor # "White"
,[ConsoleColor]$othermonths = "DarkGray"
,[ConsoleColor]$monthname = "Yellow"
,[ConsoleColor]$today = "Green"
,[ConsoleColor]$pastdays = "Gray"
,[ConsoleColor]$dayNames = "Cyan"
)
# Convert the string to a DateTime ...
# We accept it as a string only so that we can ...
[DateTime]$now = $date; if(!$?) { exit }
# Use this silly trick to see if you typed a DAY as well as month (more than one separator)
[bool]$highlightDay = ($date.Length - ($date -replace "[- ,./]","").Length) -gt 1
# Mess with the colors...
$bg = $Host.UI.RawUI.BackgroundColor
$fg = $Host.UI.RawUI.ForegroundColor
$Host.UI.RawUI.BackgroundColor = $background
$Host.UI.RawUI.ForegroundColor = $foreground
$dow = [int]$now.AddDays(1-$now.Day).DayOfWeek
$days = [DateTime]::DaysInMonth($now.Year, $now.Month)
## Some fancy string-centering and coloring for the headers
$monthString = $now.ToString("MMMM, yyyy")
$monthPad = " "*((22-$monthString.Length)/2);
Write-Host -back $background
Write-Host "`n$monthPad$monthString$monthPad " -fore $monthname
Write-Host "`n Su Mo Tu We Th Fr Sa " -fore $dayNames
## Write out the last few days of last month
if($dow -gt 0 ) {
$lastMonth = $now.AddMonths(-1)
$daysLM = [DateTime]::DaysInMonth($lastMonth.Year, $lastMonth.Month)
Write-Host "" @(($daysLM-$dow+1)..$daysLM | % {"{0,2}" -f $_}) -fore $othermonths -nonew
}
## Then, the days BEFORE today (if there are any) get their own color...
if($now.Day -gt 1) {
Write-Host "" @(switch(1..($now.Day-1)){
{$true} {"{0,2}" -f $_} # by default, just the number
{0 -eq (($_+$dow) % 7)}{"`n"}} # 0 Mod 7, insert a new-line
) -fore $pastdays -nonew
}
## And of course, "Today" (if specified) gets its own color
## and it's padded to 3 characters because it's not part of an array
if($highlightDay) {
Write-Host ("{0,3}" -f $($now.Day)) -fore $today -nonew:$($now.DayOfWeek -ne 6)
} else {
Write-Host ("{0,3}" -f $($now.Day)) -fore $foreground -nonew:$($now.DayOfWeek -ne 6)
}
## And then, the days AFTER today (if there are any)
if($now.Day -lt $days) {
Write-Host "" @(switch(($now.Day+1)..$days){
{$true} {"{0,2}" -f $_} # by default, just the number
{0 -eq (($_+$dow) % 7)}{"`n"}} # 0 Mod 7, insert a new-line
) -fore $foreground -nonew
}
## And finally, the first few days of next month (if they fit)
$nextMonth = $now.AddDays($days - $now.Day + 1)
if(0 -lt $nextMonth.DayOfWeek) {
Write-Host "" @(1..(7-[int]$nextMonth.DayOfWeek) |% {"{0,2}" -f $_}) -fore $othermonths
}
Write-Host # a trailing line of $background colored space
# set the colors back ...
$Host.UI.RawUI.BackgroundColor = $bg
$Host.UI.RawUI.ForegroundColor = $fg
Write-Host # another trailing line of whitespace
Write-Host "" @(switch((1-[DateTime]::Now.AddDays(1-[DateTime]::Now.Day).DayOfWeek)..([DateTime]::DaysInMonth([DateTime]::now.Year, [DateTime]::now.Month))){{$_ -eq }{[DateTime]::now.ToString(" MMMM, yyyy `n \S\u \M\o \T\u \W\e \T\h \F\r \S\a `n")}{$_ -lt 1}{" "}{$_ -gt 0}{"{0,2}" -f $_}{0 -eq (($_+([DateTime]::Now.AddDays(1-[DateTime]::Now.Day).DayOfWeek)) % 7)}{"`n"}})
Clearly this is not good code:
- I’m recalculating the
$dow #Day Of Week variable for each loop iteration, just to avoid a line of code declaring it
- The
$x..$y range statement is just hideous and incomprehensible
- I’ve added a test to the switch statement which is ONLY ever true the first time through the loop just so I could write out the header without having a separate line outside the switch.
Technically, it’s only one line, and has no semicolons … I think I’m going to have to amend my definition of Lines Of Code to count each case test and corresponding script block of a switch statement as at least one line of code, just to discourage the writing of code like that. 
Add New Comment
Thanks. Your comment is awaiting approval by a moderator.
Do you already have an account? Log in and claim this comment.
Add New Comment
Trackbacks