Functions/Show-Calendar.ps1

function Show-Calendar {
    <#
        .SYNOPSIS
            Show calendar.
        .DESCRIPTION
            This function is a PowerShell version of the *NIX cal command and will show a
            calendar of the chosen month(s). The current day will be marked with a '*'.
            For best results, use together with the FormatPx module by Kirk Munro.
        .EXAMPLE
            Show-Calendar
            Will show a calendar view of the current month.
        .EXAMPLE
            Show-Calendar -InvariantCulture
            Will show a calendar view of the current month, using the Invariant Culture
        .EXAMPLE
            Show-Calendar -Culture 'de-DE'
            Will show a calendar view of the current month, using the de-DE (German) culture.
        .EXAMPLE
            Show-Calendar 1 2015 -m 3
            Will show a calendar view of the first three months in 2015.
        .EXAMPLE
            Show-Calendar 12 -MarkDay 25 -Abbreviated
            Will show a calendar view of december and mark December 25, with abbreviated day names.
        .EXAMPLE
            Show-Calendar 1 2015 -m 12 -MarkDate (Get-Date -Year 2015 -Month 2 -Day 14)
            Will show a calendar view of 2015 and mark 14th of February.
        .LINK
            https://github.com/KirkMunro/FormatPx
        .NOTES
            Author: Øyvind Kallstad
            Date: 21.12.2014
            Version: 1.1
                22.12.2014
                    Added Alignment parameter
                    Made the entire function culture aware
                    Added support for abbreviated day names
    #>

    [CmdletBinding(DefaultParameterSetName = 'Culture')]
    param (
        # The starting month number. Default is current month.
        [Parameter(Position = 0)]
        [Alias('Month')]
        [ValidateRange(1,12)]
        [int] $StartMonth = [DateTime]::Now.Month,

        # The starting year. Default is current year.
        [Parameter(Position = 1)]
        [Alias('Year')]
        [ValidateRange(1,9999)]
        [int32] $StartYear = [DateTime]::Now.Year,

        # How many months to show. Default is 1.
        [Parameter()]
        [Alias('m')]
        [ValidateRange(1,[int]::MaxValue)]
        [int32] $Months = 1,

        # Day to mark on the calendar.
        [Parameter()]
        [ValidateRange(1,31)]
        [int32] $MarkDay,

        # Date to mark on the calendar.
        [Parameter()]
        [datetime] $MarkDate,

        # Set alignment of the dates in the output. Default value is 'Right'.
        [Parameter()]
        [ValidateSet('Left','Right')]
        [string] $Alignment = 'Right',

        # Use the Invariant Culture (English).
        [Parameter(ParameterSetName = 'InvariantCulture')]
        [switch] $InvariantCulture,

        # Use this parameter to choose what culture to use. If this parameter is not used and
        # the InvariantCulture parameter is false, the current culture will be used instead.
        [Parameter(ParameterSetName = 'Culture')]
        [string] $Culture,

        # Use this parameter to use abbreviated day names. Default is to use the full name of days.
        [Parameter()]
        [switch] $AbbreviatedDayNames
    )

    function New-DayOfWeekString {
        <#
            .SYNPSIS
                Output a culture aware string of a specified week day.
            .NOTES
                Author: Øyvind Kallstad
        #>

        [CmdletBinding()]
        [OutputType([String])]
        param (
            # The day of week in Invariant Culture (English).
            [Parameter(Position = 0, Mandatory)]
            [System.DayOfWeek] $DayOfWeek,

            [Parameter(Position = 1, ValueFromPipeline)]
            [CultureInfo] $CultureInfo = [System.Globalization.CultureInfo]::CurrentCulture,

            # Show the week day in an abbreviated form.
            [Parameter()]
            [switch] $Abbreviated
        )

        if ($Abbreviated) {
            Write-Output ($cultureInfo.TextInfo.ToTitleCase($cultureInfo.DateTimeFormat.GetAbbreviatedDayName([System.DayOfWeek]::$DayOfWeek)))
        }

        else {
            Write-Output ($cultureInfo.TextInfo.ToTitleCase($cultureInfo.DateTimeFormat.GetDayName([System.DayOfWeek]::$DayOfWeek)))
        }
    }

    function New-Week {
        <#
            .SYNOPSIS
                Returns an ordered dictionary representing a week.
            .NOTES
                Author: Øyvind Kallstad
        #>

        [CmdletBinding()]
        [OutputType([System.Collections.Specialized.OrderedDictionary])]
        param (
            [Parameter(Position = 0, ValueFromPipeline)]
            [CultureInfo] $CultureInfo = [System.Globalization.CultureInfo]::CurrentCulture,

            # Show the week day in an abbreviated form.
            [Parameter()]
            [switch] $Abbreviated
        )

        $propHash = @{
            CultureInfo = $CultureInfo
        }

        if ($Abbreviated) {
            $propHash.Abbreviated = $true
        }

        # define week day names
        $monday = New-DayOfWeekString 'Monday' @propHash
        $tuesday = New-DayOfWeekString 'Tuesday' @propHash
        $wednesday = New-DayOfWeekString 'Wednesday' @propHash
        $thursday = New-DayOfWeekString 'Thursday' @propHash
        $friday = New-DayOfWeekString 'Friday' @propHash
        $saturday = New-DayOfWeekString 'Saturday' @propHash
        $sunday = New-DayOfWeekString 'Sunday' @propHash
 
        $week = [Ordered] @{
            $tuesday = $null
            $wednesday = $null
            $thursday = $null
            $friday = $null
            $saturday = $null
        }
 
        if (($CultureInfo.DateTimeFormat.FirstDayOfWeek) -eq 'Monday') {
            $week.Insert(0, $monday, $null)
            $week.Add($sunday, $null)
        }
 
        else {
            $week.Insert(0, $sunday, $null)
            $week.Insert(1, $monday, $null)
        }

        Write-Output $week
    }

    # get a CultureInfo object based on user input
    # either with an Invariant Culture...
    if ($InvariantCulture) {
        $cultureInfo = [System.Globalization.CultureInfo]::InvariantCulture
    }
    # ... user defined culture...
    elseif ($Culture) {
        $cultureInfo = [System.Globalization.CultureInfo]::CreateSpecificCulture($Culture)
    }
    # ... or the current culture
    else {
        $cultureInfo = [System.Globalization.CultureInfo]::CurrentCulture
    }

    # supporting internationalization
    # add additional languages as needed
    switch ($cultureInfo.Name) {
        'nb-NO' {$translate = @{Year = 'År';       Month = 'Måned';    Week = 'Uke'}}        # Norwegian (Bokmål)
        'da-DK' {$translate = @{Year = 'År';       Month = 'Måned';    Week = 'Uge'}}        # Danish
        'sv-SE' {$translate = @{Year = 'År';       Month = 'Månad';    Week = 'Vecka'}}      # Swedish
        'fi-FI' {$translate = @{Year = 'Vuosi';    Month = 'Kuukausi'; Week = 'Viikko'}}     # Finnish
        'de-DE' {$translate = @{Year = 'Jahr';     Month = 'Monat';    Week = 'Woche'}}      # German
        'de-AT' {$translate = @{Year = 'Jahr';     Month = 'Monat';    Week = 'Woche'}}      # German (Austria)
        'de-CH' {$translate = @{Year = 'Jahr';     Month = 'Monat';    Week = 'Woche'}}      # German (Switzerland)
        'nl-NL' {$translate = @{Year = 'Jaar';     Month = 'Maand';    Week = 'Week'}}       # Dutch
        'fr-FR' {$translate = @{Year = 'Année';    Month = 'Mois';     Week = 'Semaine'}}    # French
        'fr-CH' {$translate = @{Year = 'Année';    Month = 'Mois';     Week = 'Semaine'}}    # French (Switzerland)
        'fr-LU' {$translate = @{Year = 'Année';    Month = 'Mois';     Week = 'Semaine'}}    # French (Luxembourg)
        'it-IT' {$translate = @{Year = 'Anno';     Month = 'Mese';     Week = 'Settimana'}}  # Italian
        'es-ES' {$translate = @{Year = 'Año';      Month = 'Mes';      Week = 'Semana'}}     # Spanish
        'ru-RU' {$translate = @{Year = 'Год';      Month = 'Месяц';    Week = 'Неделя'}}     # Russian
        'cs-CZ' {$translate = @{Year = 'Rok';      Month = 'Měsíc';    Week = 'Týden'}}      # Czech
        'ms-MY' {$translate = @{Year = 'Tahun';    Month = 'Bulan';    Week = 'Minggu'}}     # Malay
        'ja-JP' {$translate = @{Year = '年';       Month = '月';       Week = '1週間'}}      # Japanese
        'is-IS' {$translate = @{Year = 'Ári';      Month = 'Mánuði';   Week = 'Viku'}}       # Icelandic
        'pl-PL' {$translate = @{Year = 'Rok';      Month = 'Miesiąc';  Week = 'Tydzień'}}    # Polish
        'cy-GB' {$translate = @{Year = 'Blwyddyn'; Month = 'Mis';      Week = 'Wythnos'}}    # Welsh (United Kingdom)
        DEFAULT {$translate = @{Year = 'Year';     Month = 'Month';    Week = 'Week'}}       # Invariant Culture (English)
    }

    $thisMonth = $StartMonth - 1
    $thisYear = $StartYear
    $output = @()
    $weekProperties = @{}
    if ($AbbreviatedDayNames) { $weekProperties.Abbreviated = $true }

    # loop through the months
    for ($i = 1; $i -le $Months; $i++) {

        # increment month
        $thisMonth++

        # when month is greater than 12, a new year is triggered, so reset month to 1 and increment year
        if ($thisMonth -gt 12) {
            $thisMonth = 1
            $thisYear++
        }

        # get the number of days in the month
        $daysInMonth = $cultureInfo.Calendar.GetDaysInMonth($thisYear,$thisMonth)

        # define new week
        $thisWeek = $cultureInfo | New-Week @weekProperties
        $thisWeek.Insert(0, $translate.Month, $null)
        $thisWeek.Insert(1, $translate.Year, $null)
        $thisWeek.Insert(2, $translate.Week, $null)

        # loop through each day in the month
        for ($y = 1; $y -lt ($daysInMonth + 1); $y++) {

            # get a datetime object of the current date
            $thisDate = New-Object -TypeName 'System.DateTime' -ArgumentList ($thisYear,$thisMonth,$y,$cultureInfo.Calendar)

            # if current date is the first day of a week (but not if it's the very first day of the month at the same time)
            if (($thisDate.DayOfWeek -eq ($cultureInfo.DateTimeFormat.FirstDayOfWeek)) -and ($y -gt 1)) {
 
                # add the week object to the output array
                $weekObject = New-Object -TypeName 'PSCustomObject' -Property $thisWeek
                $output += $weekObject
 
                # reset the week
                $thisWeek = $cultureInfo | New-Week @weekProperties
                $thisWeek.Insert(0, $translate.Month, $null)
                $thisWeek.Insert(1, $translate.Year, $null)
                $thisWeek.Insert(2, $translate.Week, $null)
            }

            # get string representation of the month and the current week number (if week number is 53, change to 1)
            $monthString = $cultureInfo.TextInfo.ToTitleCase($thisDate.ToString('MMMM',$cultureInfo))
            $thisWeekNumber = $cultureInfo.calendar.GetWeekOfYear($thisDate,$cultureInfo.DateTimeFormat.CalendarWeekRule,$cultureInfo.DateTimeFormat.FirstDayOfWeek)
            if ($thisWeekNumber -eq 53) { $thisWeekNumber = 1 }

            # overload the ToString method of the datetime object
            $thisDate | Add-Member -MemberType ScriptMethod -Name 'ToString' -Value {
                # mark today with '*'
                if (($this.Date) -eq ([DateTime]::Now.Date)) {
                    if($Alignment -eq 'Left') {$this.Day.ToString() + '*'}
                    else {'*' + $this.Day.ToString()}
                }
                elseif ($MarkDay -eq $this.Day) {
                    if($Alignment -eq 'Left') {$this.Day.ToString() + '!'}
                    else {'!' + $this.Day.ToString()}
                }
                elseif ($MarkDate.Date -eq $this.Date) {
                    if($Alignment -eq 'Left') {$this.Day.ToString() + '!'}
                    else {'!' + $this.Day.ToString()}
                }
                else {
                    $this.Day.ToString()
                }
            } -Force

            # update the week hashtable with the current day, week, month and year
            $thisWeek[($cultureInfo | New-DayOfWeekString ($thisDate.DayOfWeek) -Abbreviated:$AbbreviatedDayNames)] = $thisDate
            $thisWeek[$translate.Week] = $thisWeekNumber
            $thisWeek[$translate.Month] = $monthString
            $thisWeek[$translate.Year] = $thisYear
        }

        # add the final week to the output array
        $weekObject = New-Object -TypeName 'PSCustomObject' -Property $thisWeek
        $output += $weekObject
    }

    # translate day names
    $monday    = $cultureInfo | New-DayOfWeekString 'Monday' -Abbreviated:$AbbreviatedDayNames
    $tuesday   = $cultureInfo | New-DayOfWeekString 'Tuesday' -Abbreviated:$AbbreviatedDayNames
    $wednesday = $cultureInfo | New-DayOfWeekString 'Wednesday' -Abbreviated:$AbbreviatedDayNames
    $thursday  = $cultureInfo | New-DayOfWeekString 'Thursday' -Abbreviated:$AbbreviatedDayNames
    $friday    = $cultureInfo | New-DayOfWeekString 'Friday' -Abbreviated:$AbbreviatedDayNames
    $saturday  = $cultureInfo | New-DayOfWeekString 'Saturday' -Abbreviated:$AbbreviatedDayNames
    $sunday    = $cultureInfo | New-DayOfWeekString 'Sunday' -Abbreviated:$AbbreviatedDayNames

    # define a hashtable to hold format properties for the table output
    if (($cultureInfo.DateTimeFormat.FirstDayOfWeek) -eq 'Monday') {
        $formatProperties = @{
            Property =
                "$($translate.Week)",
                @{Name = "$monday"    ;Expression = {$_.$monday}    ;Alignment = $Alignment}, 
                @{Name = "$tuesday"   ;Expression = {$_.$tuesday}   ;Alignment = $Alignment},
                @{Name = "$wednesday" ;Expression = {$_.$wednesday} ;Alignment = $Alignment},
                @{Name = "$thursday"  ;Expression = {$_.$thursday}  ;Alignment = $Alignment},
                @{Name = "$friday"    ;Expression = {$_.$friday}    ;Alignment = $Alignment},
                @{Name = "$saturday"  ;Expression = {$_.$saturday}  ;Alignment = $Alignment},
                @{Name = "$sunday"    ;Expression = {$_.$sunday}    ;Alignment = $Alignment}
        }
    }

    else {
        $formatProperties = @{
            Property =
                "$($translate.Week)",
                @{Name = "$sunday"    ;Expression = {$_.$sunday}    ;Alignment = $Alignment},
                @{Name = "$monday"    ;Expression = {$_.$monday}    ;Alignment = $Alignment}, 
                @{Name = "$tuesday"   ;Expression = {$_.$tuesday}   ;Alignment = $Alignment},
                @{Name = "$wednesday" ;Expression = {$_.$wednesday} ;Alignment = $Alignment},
                @{Name = "$thursday"  ;Expression = {$_.$thursday}  ;Alignment = $Alignment},
                @{Name = "$friday"    ;Expression = {$_.$friday}    ;Alignment = $Alignment},
                @{Name = "$saturday"  ;Expression = {$_.$saturday}  ;Alignment = $Alignment}
        }
    }

    # if FormatPx is loaded, use it to format the output
    if (Get-Module -Name 'FormatPx') {
        Write-Output ($output | Format-Table @formatProperties -AutoSize -GroupBy @{Name = "$($translate.Month)";Expression = {"$($_.($translate.Month)) $($_.($translate.Year))"}} -PersistWhenOutput)
    }

    # else use default PowerShell formatting
    else {
        Write-Output ($output | Format-Table @formatProperties -AutoSize -GroupBy @{Name = "$($translate.Month)";Expression = {"$($_.($translate.Month)) $($_.($translate.Year))"}})
    }
}

New-Alias -Name 'cal' -Value 'Show-Calendar' -Force