calendar-functions.ps1


# http://www.leeholmes.com/blog/2008/12/03/showing-calendars-in-your-oof-messages/

Function Get-Calendar {

    [cmdletbinding(DefaultParameterSetName = "month")]
    [OutputType([System.String])]
    [Alias("cal")]

    Param(
        [Parameter(Position = 1, ParameterSetName = "month")]
        [ValidateNotNullorEmpty()]
        [ValidateScript( {
                $names = Get-MonthsByCulture
                if ($names -contains $_) {
                    $True
                }
                else {
                    Throw "You entered an invalid month. Valid choices are $($names -join ',')"
                    $False
                }
            })]
        [string]$Month = (Get-Date -format MMMM),

        [Parameter(Position = 2, ParameterSetName = "month")]
        [ValidatePattern('^\d{4}$')]
        [int]$Year = (Get-Date).Year,

        [Parameter(Mandatory, HelpMessage = "Enter the start of a month like 1/1/2019", ParameterSetName = "span")]
        [ValidateNotNullOrEmpty()]
        [DateTime]$Start,

        [Parameter(Mandatory, HelpMessage = "Enter an ending date for the month like 2/1/2019", ParameterSetName = "span")]
        [ValidateNotNullOrEmpty()]
        [ValidateScript( {
                if ($_ -ge $Start) {
                    $True
                }
                else {
                    Throw "The end date ($_) must be later than the start date ($start)"
                    $False
                }
            })]
        [DateTime]$End,

        [ValidateNotNullorEmpty()]
        [string[]]$HighlightDate = (Get-Date).date.toString()
    )

    Begin {
        Write-Verbose "Starting $($myinvocation.MyCommand)"
    }
    Process {
        Write-Verbose "Using parameter set: $($pscmdlet.ParameterSetName)"
        Write-Verbose "Using culture $((Get-Culture).name)"

        if ($pscmdlet.ParameterSetName -eq "month") {
            [datetime]$start = Get-Date "1 $month,$year"
            [datetime]$end = $start.date
        }
        else {
            #Figure out the first day of the start and end months
            $start = New-Object DateTime $start.Year, $start.Month, 1
            $end = New-Object DateTime $end.Year, $end.Month, 1
        }

        Write-Verbose "Starting at $start"
        Write-Verbose "Ending at $End"
        #Convert the highlight dates into real dates
        #using the .NET Class to parse because this works better for culture-specific datetime strings
        [DateTime[]]$highlightDates = foreach ($item in $highlightDate) {
            [datetime]::parse($item)
        }
        #Retrieve the DateTimeFormat information so that we can manipulate the calendar
        $dateTimeFormat = (Get-Culture).DateTimeFormat
        $firstDayOfWeek = $dateTimeFormat.FirstDayOfWeek

        Write-Verbose "First day of the week is $firstDayofWeek"

        $currentDay = $start

        Write-Verbose "Go through the requested months"
        while ($start -le $end) {
            #We may need to backpedal a bit if the first day of the month
            #falls in the middle of the week
            while ($currentDay.DayOfWeek -ne $dateTimeFormat.FirstDayOfWeek) {
                $currentDay = $currentDay.AddDays(-1)
            }

            #Prepare to store information about this date range
            $currentWeek = New-Object PsObject
            $dayNames = @()
            $weeks = @()

            Write-Verbose "Go until we've hit the end of the month."
            #Even once we've done that, continue until we fill up the week
            #with days from the next month.
            while (($currentDay -lt $start.AddMonths(1)) -or
                ($currentDay.DayOfWeek -ne $dateTimeFormat.FirstDayOfWeek)) {
                #Figure out the day names we'll be using to label the columns
                $dayName = "{0:ddd}" -f $currentDay
                if ($dayNames -notcontains $dayName) {
                    $dayNames += $dayName
                }

                #Pad the day number for display, highlighting if necessary
                $displayDay = " {0,2} " -f $currentDay.Day

                #highlight a specific date
                if ($highlightDates -ne (Get-Date).date.toString() ) {
                    $highlightDates += (Get-Date).date.toString()
                }

                $compareDate = New-Object DateTime $currentDay.Year,
                $currentDay.Month, $currentDay.Day
                if ($highlightDates -contains $compareDate) {
                    $displayDay = "*" + ("{0,2}" -f $currentDay.Day) + "*"
                }


                #Add in the day of week and day number as note properties.
                $currentWeek | Add-Member NoteProperty $dayName $displayDay

                Write-Verbose "Move to the next day in the month"
                $currentDay = $currentDay.AddDays(1)

                #If we've reached the next week, store the current week
                #in the week list and continue on.
                if ($currentDay.DayOfWeek -eq $dateTimeFormat.FirstDayOfWeek) {
                    $weeks += $currentWeek
                    $currentWeek = New-Object PsObject
                }
            }

            Write-Verbose "Format our weeks into a table"
            $calendar = $weeks | Format-Table $dayNames -auto | Out-String

            Write-Verbose "Add a centered header"
            $width = ($calendar.Split("`n") | Measure-Object -Max Length).Maximum
            $header = "{0:MMMM yyyy}" -f $start
            $padding = " " * (($width - $header.Length) / 2)
            #use this line to insert a blank line before the calendar
            $displayCalendar = " `n" + $padding + $header + "`n " + $calendar

            #use this line to not insert a blank line before the calendar
            #$displayCalendar = $padding + $header + "`n " + $calendar
            $displayCalendar.TrimEnd()

            #And now move onto the next month
            $start = $start.AddMonths(1)
        }
    } #process

    End {
        Write-verbose "Ending $($myinvocation.MyCommand)"
    }
} #end Get-Calendar


#display a colorized calendar in the console

Function Show-Calendar {

    [cmdletbinding()]
    [Alias("scal")]
    Param(

        [Parameter(Position = 1, ParameterSetName = "month")]
        [ValidateNotNullorEmpty()]
        [ValidateScript( {
                $names = Get-MonthsByCulture
                if ($names -contains $_) {
                    $True
                }
                else {
                    Throw "You entered an invalid month. Valid choices are $($names -join ',')"
                    $False
                }
            })]
        [string]$Month = (Get-Date -format MMMM),

        [Parameter(Position = 2, ParameterSetName = "month")]
        [ValidatePattern('^\d{4}$')]
        [int]$Year = (Get-Date).Year,

        [string[]]$HighlightDate = (Get-Date).date.toString(),

        [Parameter(HelpMessage = "Specify a color for the highlighted days.")]
        [ValidateNotNullOrEmpty()]
        [consolecolor]$HighlightColor = "Green",

        [Parameter(HelpMessage = "Specify a color for the days of the month heading.")]
        [ValidateNotNullOrEmpty()]
        [consolecolor]$TitleColor = "Yellow",

        [Parameter(HelpMessage = "Specify a color for the days of the week heading.")]
        [ValidateNotNullOrEmpty()]
        [consolecolor]$DayColor = "Cyan",
        [System.Management.Automation.Host.Coordinates]$Position
    )

    Write-Verbose "Starting $($myinvocation.mycommand)"

    if ($position) {
        #save current cursor location
        $here = $host.ui.RawUI.CursorPosition
        $PSBoundParameters.remove("Position") | out-Null
    }

    #add default values if not bound
    $params = "Month", "Year", "HighlightDate"
    foreach ($param in $params) {
        if (-not $PSBoundParameters.ContainsKey($param)) {
            $PSBoundParameters.Add($param, $((get-variable -Name $param).value))
        }
    }

    #remove color parameters if specified
    "HighlightColor", "TitleColor", "DayColor" | foreach-object {
        if ($PSBoundParameters.Containskey($_)) {
            $PSBoundParameters.Remove($_) | Out-Null
        }
    } #foreach color parameter

    $cal = Get-Calendar @PSBoundParameters

    #turn the calendar into an array of strings
    $calarray = $cal.split("`n")

    # a regular expression pattern to match on highlighted days
    [regex]$m = "(\*)?[\s|\*]\d{1,2}(\*)?"

    #go through each line and write it back to the console using Write-Host
    foreach ($line in $calarray) {
        if ($line -match "\d{4}") {
            #write the line with the month and year
            if ($position) {
                $host.ui.RawUI.CursorPosition = $Position
            }
            write-Host $line -ForegroundColor $TitleColor
        }
        elseif ($line -match "\w{3}|-{3}") {
            #write the day names and underlines
            if ($Position) {
                $Position.y++
                $host.ui.RawUI.CursorPosition = $Position
            }
            Write-Host $line -ForegroundColor $DayColor
        }
        elseif ($line -match "\*") {
            #break apart lines with asterisks
            $week = $line
            if ($position) {
                $Position.y++
                $host.ui.RawUI.CursorPosition = $Position
            }
            $m.Matches($week).Value | foreach-object {

                $day = "$_"
                if ($day -match "\*") {
                    write-host "$($day.replace('*','').padleft(3," ")) " -NoNewline -ForegroundColor $HighlightColor
                }
                else {
                    write-host "$($day.PadLeft(3," ")) " -nonewline
                }
            }

            write-host ""
        }
        else {
            if ($Position) {
                $Position.y++
                $host.ui.RawUI.CursorPosition = $Position
            }
            Write-host $line
        }
    } #foreach line in calarray

    if ($Position) {
        #set cursor position back
        $here.y++
        $host.ui.RawUI.CursorPosition = $here
    }

    Write-Verbose "Ending $($myinvocation.mycommand)"

} #end Show-Calendar

#create a WPF-based calendar
Function Show-GuiCalendar {
    [cmdletbinding()]
    [OutputType("None")]
    [Alias("gcal")]

    Param(
        [Parameter(Position = 1, HelpMessage = "Enter the first month to display by date, like 1/1/2019.")]
        [ValidateNotNullOrEmpty()]
        [datetime]$Start = [datetime]::new([datetime]::now.year, [datetime]::now.month, 1),

        [Parameter(Position = 2, HelpMessage = "Enter the last month to display by date, like 3/1/2019. You cannot display more than 3 months.")]
        [ValidateNotNullOrEmpty()]
        [ValidateScript( {
                if ($_ -ge $Start) {
                    $True
                }
                else {
                    Throw "The end date ($_) must be later than the start date ($start)"
                    $False
                }
            })]
        [datetime]$End = [datetime]::new([datetime]::now.year, [datetime]::now.month, 1),

        [Parameter(HelpMessage = "Enter an array of dates to highlight like 12/25/2019.")]
        [datetime[]]$HighlightDate,

        [Parameter(HelpMessage = "Select a font family for your calendar" )]
        [ValidateSet("Segoi UI", "QuickType", "Tahoma", "Lucida Console", "Century Gothic")]
        [string]$Font = "Segoi UI",


        [Parameter(HelpMessage = "Select a font style for your calendar." )]
        [ValidateSet("Normal", "Italic", "Oblique")]
        [string]$FontStyle = "Normal",

        [Parameter(HelpMessage = "Select a font weight for your calendar." )]
        [ValidateSet("Normal", "DemiBold", "Light", "Bold")]
        [string]$FontWeight = "Normal"
    )

    Write-Verbose "Starting $($myinvocation.mycommand)"

    if ($psedition -eq 'Core') {
        Write-Warning "This function requires Windows PowerShell."
        #bail out
        Return
    }

    $months = do {
        $start
        $start = $start.AddMonths(1)
    } while ($start -le $end)

    if ($months.count -gt 3) {
        Write-Warning "You can't display more than 3 months at a time with this command."
        #bail out
        Return
    }

    #add the necessary type library and bail out if there are errors

    Try {
        Add-Type -AssemblyName PresentationFramework -ErrorAction Stop
        Add-Type â€“assemblyName PresentationCore -ErrorAction Stop
        Add-Type â€“assemblyName WindowsBase -ErrorAction Stop
    }
    Catch {
        Write-Warning "Failed to load a required type library. $($_.exception.message)"
        #bail out
        Return
    }

    $myParams = @{
        Months        = $months
        Height        = (200 * $months.count)
        Title         = "My Calendar"
        HighlightDate = $HighlightDate
        Font          = $Font
        FontStyle     = $FontStyle
        FontWeight    = $FontWeight
    }

    Write-Verbose "Using these parameters"
    $myparams | Out-String | Write-Verbose

    $newRunspace = [RunspaceFactory]::CreateRunspace()
    $newRunspace.ApartmentState = "STA"
    $newRunspace.ThreadOptions = "ReuseThread"
    $newRunspace.Open()

    Write-Verbose "Defining runspace script"
    $psCmd = [PowerShell]::Create().AddScript( {

            Param (
                [datetime[]]$HighlightDate,
                [string]$Font,
                [string]$FontStyle,
                [string]$FontWeight,
                [int]$Height,
                [string]$Title,
                [datetime[]]$Months
            )

            #create a window form.
            $form = New-Object System.Windows.Window

            $form.AllowsTransparency = $True
            $form.WindowStyle = "none"
            #the title won't be shown when window style is set to none
            $form.Title = $Title
            $form.Height = $height
            $form.Width = 200

            $bg = new-object System.Windows.Media.SolidColorBrush

            $form.Background = $bg
            #color is set for development purposes. It won't be seen normally.
            $form.Background.Color = "green"
            $form.background.Opacity = 0
            $form.ShowInTaskbar = $False
            $form.Add_Loaded( {
                    $form.Topmost = $True
                    $form.Activate()
                })

            $form.Add_MouseLeftButtonDown( {$form.DragMove()})

            #add event handlers to adjust opacity by using the +/- keys
            $form.add_KeyDown( {

                    switch ($_.key) {
                        {'Add', 'OemPlus' -contains $_} {
                            foreach ($cal in $myCals) {
                                If ($cal.Opacity -lt 1) {
                                    $cal.Opacity = $cal.opacity + .1
                                    $cal.UpdateLayout()
                                }
                            }
                        }
                        {'Subtract', 'OemMinus' -contains $_} {
                            foreach ($cal in $myCals) {
                                If ($cal.Opacity -gt .2) {
                                    $cal.Opacity = $cal.Opacity - .1
                                    $cal.UpdateLayout()
                                }
                            }
                        }
                    }
                })

            $stack = $stack = New-object System.Windows.Controls.StackPanel
            $stack.Width = $form.Width
            $stack.Height = $form.Height
            $stack.HorizontalAlignment = "center"
            $stack.VerticalAlignment = "top"

            #create an array to store calendars so that opacity can be
            #set for multiple calendars in unison
            $myCals = @()
            foreach ($month in $months) {
                $cal = New-Object System.Windows.Controls.Calendar
                $cal.DisplayMode = "Month"

                $cal.Opacity = 1
                $cal.FontFamily = $font
                $cal.FontSize = 24
                $cal.FontWeight = $FontWeight
                $cal.FontStyle = $fontStyle

                $cal.DisplayDateStart = $month

                $cal.HorizontalAlignment = "center"
                $cal.VerticalAlignment = "top"

                $cal.SelectionMode = "multipleRange"
                if ($highlightdate) {
                    foreach ($d in $HighlightDate) {
                        if ($d.month -eq $month.Month) {
                            $cal.SelectedDates.add($d)
                        }
                    }
                }

                $cal.add_DisplayDateChanged( {
                        # add the selected days for the currently displayed month
                        $cal | out-string | write-host
                        [datetime]$month = $cal.Displaydate
                        if ($highlightdate) {
                            foreach ($d in $HighlightDate) {
                                if ($d.month -eq $month.Month) {
                                    $cal.SelectedDates.add($d)
                                }
                            }
                        }
                        $cal.UpdateLayout()
                    })

                $stack.addchild($cal)
                $myCals += $cal
            }

            $btn = New-Object System.Windows.Controls.Button
            $btn.Content = "_Close"
            $btn.Width = 75
            $btn.VerticalAlignment = "Bottom"
            $btn.HorizontalAlignment = "Center"
            $btn.Opacity = 1
            $btn.Add_click( {$form.close()})

            $stack.AddChild($btn)

            $form.AddChild($stack)
            $form.ShowDialog() | out-null
        })


    $pscmd.AddParameters($myparams) | Out-Null
    $psCmd.Runspace = $newRunspace
    Write-Verbose "Invoking calendar runspace"
    $psCmd.BeginInvoke() | Out-Null

    Write-Verbose "Ending $($myinvocation.mycommand)"

} #close Show-GuiCalendar

#a helper function to retrieve names
function Get-MonthsbyCulture {
    [cmdletbinding()]
    Param([string]$Culture = ([system.threading.thread]::currentThread).CurrentCulture)
    Write-Verbose "Getting months for culture $Culture"
    [cultureinfo]::GetCultureInfo($culture).DateTimeFormat.Monthnames
}