ncal.psm1

#Requires -Version 7.2

function Get-NcalRow {
    <#
        .NOTES
        Helper function for Get-NCalendar. Prints one row of calendar output, given an Index corresponding to the
        first day of the month.
    #>

    param (
        [Parameter(Mandatory, Position = 0)]
        [System.Globalization.Calendar]$Calendar,

        [Parameter(Mandatory, Position = 1)]
        [Int]$Index,
        
        [Parameter(Position = 2)]
        [ValidateRange(28, 31)]
        [Int]$DayPerMonth,

        [Parameter(Position = 3)]
        [ValidateRange(1, 13)]
        [Int]$Month,

        [Parameter(Position = 4)]
        [ValidateRange(1, 9999)]
        [Int]$Year,

        [Parameter(Position = 5)]
        [Object]$Highlight,

        [Parameter(Position = 6)]
        [Bool]$JulianSpecified
    ) 

    begin {
        $WeekDay = $Index
        $Row = ''
        $JulianDay = 0

        if ($JulianSpecified) {
            $PadRow = 24
            $PadDay = 3
        }
        else {
            $PadRow = 19
            $PadDay = 2
        }
    }

    process {
        do {
            if ($WeekDay -lt 1) {
                $Row += "{0,$PadDay} " -f $null
            }
            else {
                if ($JulianSpecified) {
                    try {
                        $UseDate = $Calendar.ToDateTime($Year, $Month, $WeekDay, 0, 0, 0, 0, $Calendar.Eras[0] )
                        $JulianDay = $Calendar.GetDayOfYear($UseDate)
                        $Row += "{0,$PadDay} " -f $JulianDay
                    }
                    catch {
                        Write-Error "day beyond day/month - $($_.Message)"
                    }
                }
                else {
                    $Row += "{0,$PadDay} " -f $WeekDay
                }
            }
            $WeekDay += 7
        }
        until ($WeekDay -gt $DayPerMonth)
        $OutString = "$($Row.TrimEnd())".PadRight($PadRow, ' ')
        <#
            The secret to not screwing up formatted strings with non-printable characters is to mess with the string
            after the string is padded to the right length.
        #>

        if ( ($Highlight.Today -gt 0) -and $JulianSpecified) {
            $UseDate = $Calendar.ToDateTime($Year, $Month, $Highlight.Today, 0, 0, 0, 0, $Calendar.Eras[0] )
            $JulianDay = $Calendar.GetDayOfYear($UseDate)
        
            if ($OutString -match "\b$JulianDay\b") {
                $OutString = $OutString -replace "$JulianDay\b", "$($Highlight.DayStyle)$JulianDay$($Highlight.DayReset)"
            }
        }
        elseif ( ($Highlight.Today -gt 0) -and ($OutString -match "\b$($Highlight.Today)\b")) {
            if ($Highlight.Today -lt 10) {
                $OutString = $OutString -replace "\s$($Highlight.Today)\b", "$($Highlight.DayStyle) $($Highlight.Today)$($Highlight.DayReset)"
            }
            else {
                $OutString = $OutString -replace "$($Highlight.Today)\b", "$($Highlight.DayStyle)$($Highlight.Today)$($Highlight.DayReset)"
            }
        }
        Write-Output $OutString
    }
}

function Get-CalRow {
    <#
    .NOTES
    Helper function for Get-Calendar - Prints one row of calendar output, given an Index corresponding to the
    first day of the month.
#>

    param (
        [Parameter(Position = 0)]
        [System.Globalization.Calendar]$Calendar,

        [Parameter(Position = 1)]
        [Int]$Index,
        
        [Parameter(Position = 2)]
        [ValidateRange(28, 31)]
        [Int]$DayPerMonth,

        [Parameter(Position = 3)]
        [ValidateRange(1, 13)]
        [Int]$Month,

        [Parameter(Position = 4)]
        [ValidateRange(1, 9999)]
        [Int]$Year,

        [Parameter(Position = 5)]
        [Object]$Highlight,

        [Parameter(Position = 6)]
        [Bool]$JulianSpecified
    ) 

    begin {
        $WeekDay = $Index
        $Row = ''
        $JulianDay = 0

        if ($JulianSpecified) {
            $PadRow = 30
            $PadDay = 3
        }
        else {
            $PadRow = 23
            $PadDay = 2
        }
    }

    process {
        # Repeat 7 times for each week day in the row
        0..6 | ForEach-Object {
            if ($WeekDay -lt 1) {
                $Row += "{0,$PadDay} " -f $null
            }
            elseif ($WeekDay -gt $DayPerMonth) {
                #do nothing
            }
            else {
                if ($JulianSpecified) {
                    try {
                        $UseDate = $Calendar.ToDateTime($Year, $Month, $WeekDay, 0, 0, 0, 0, $Calendar.Eras[0] )
                        $JulianDay = $Calendar.GetDayOfYear($UseDate)
                        $Row += "{0,$PadDay} " -f $JulianDay
                    }
                    catch {
                        Write-Error "day beyond day/month - $($_.Message)"
                    }
                }
                else {
                    $Row += "{0,$PadDay} " -f $WeekDay
                }
            }
            $WeekDay++
        }
        $OutString = "$($Row.TrimEnd())".PadRight($PadRow, ' ')
    
        <#
            The secret to not screwing up formatted strings with non-printable characters is to mess with the
            string after the string is padded to the right length.
        #>

        if ( ($Highlight.Today -gt 0) -and $JulianSpecified) {
            $UseDate = $Calendar.ToDateTime($Year, $Month, $Highlight.Today, 0, 0, 0, 0, $Calendar.Eras[0] )
            $JulianDay = $Calendar.GetDayOfYear($UseDate)
            if ($OutString -match "\b$JulianDay\b") {
                $OutString = $OutString -replace "$JulianDay\b", "$($Highlight.DayStyle)$JulianDay$($Highlight.DayReset)"
            }
        }
        elseif ( ($Highlight.Today -gt 0) -and ($OutString -match "\b$($Highlight.Today)\b")) {
            if ($Highlight.Today -lt 10) {
                $OutString = $OutString -replace "\s$($Highlight.Today)\b", "$($Highlight.DayStyle) $($Highlight.Today)$($Highlight.DayReset)"
            }
            else {
                $OutString = $OutString -replace "$($Highlight.Today)\b", "$($Highlight.DayStyle)$($Highlight.Today)$($Highlight.DayReset)"
            }
        }
        Write-Output $OutString
    }
}

function Get-Today {
    [CmdletBinding()]
    param (
        [System.Globalization.Calendar]$Calendar
    )
    process {
        $Now = Get-Date
        [PSCustomObject]@{
            'DateTime' = $Now #Always shown in local calendar
            'Year'     = $Calendar.GetYear($Now)
            'Month'    = $Calendar.GetMonth($Now)
            'Day'      = $Calendar.GetDayOfMonth($Now)
        }
    }
}

function Get-MonthHeading {
    <#
        .NOTES
        Helper function for Get-NCalendar and Get-Calendar. Returns a string.
    #>

    param (
        [Parameter(Position = 0)]
        [System.Globalization.CultureInfo]$Culture,

        [Parameter(Position = 1)]
        [String]$MonthName,

        [Parameter(Position = 2)]
        [Bool]$JulianSpecified
    )

    begin {
        $CallStack = Get-PSCallStack
        if ($CallStack.Count -gt 1) {
            $CallingFunction = $CallStack[1].Command
        }
    }
    process {
        if ($CallingFunction -eq 'Get-Calendar') {
            if ($true -eq $JulianSpecified) {
                $HeadingLength = 30
            }
            else {
                $HeadingLength = 23
            }
            # Special cases - resorted to hard coding for double width character sets like Kanji.
            # Japanese, traditional Chinese and Korean have 1 double width character in month name, simplified Chinese
            # and Yi have two. This is a rough hack, but it works.
            if ($Culture.Name -match '^(ja|zh-hant|ko$|ko\-)') { $HeadingLength -= 1 }
            if ($Culture.Name -match '^(zh$|zh-hans|ii)') { $HeadingLength -= 2 }
            # cal month headings are centred.
            $Pad = $MonthName.Length + (($HeadingLength - 2 - $MonthName.Length) / 2)
            $MonthHeading = ($MonthName.PadLeft($Pad, ' ')).PadRight($HeadingLength, ' ')

        }
        # Get-NCalendar is the (default) calling function
        else {
            if ($true -eq $JulianSpecified) {
                $HeadingLength = 24
            }
            else {
                $HeadingLength = 19
            }
            if ($Culture.Name -match '^(ja|zh-hant|ko$|ko\-)') { $HeadingLength -= 1 }
            if ($Culture.Name -match '^(zh$|zh-hans|ii)') { $HeadingLength -= 2 }
            $MonthHeading = "$MonthName".PadRight($HeadingLength, ' ')
        }
        Write-Output $MonthHeading
    }
}

function Get-FirstDayOfMonth {
    <#
        .NOTES
        Helper function for Get-NCalendar and Get-Calendar that returns the date and day position of the first day
        of each required month, using the specified calendar. This function performs the parameter validation for
        both ncal and cal.
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory, Position = 0)]
        [System.Globalization.Calendar]$Calendar,
        
        # Could be an integer between 1 and 13 or the same with an 'f' or 'p' suffix
        [Parameter(Position = 1)]
        [String]$Month,
        
        [Parameter(Position = 2)]
        [ValidateRange(1, 9999)]
        [Int]$Year,

        [Parameter(Position = 3)]
        [Int]$Before,
        
        [Parameter(Position = 4)]
        [Int]$After,

        [Parameter(Position = 5)]
        [Switch]$Three
    )

    process {
        $Now = Get-Today -Calendar $Calendar
        Write-Verbose "today year = $($Now.Year), today month = $($Now.Month), today day = $($Now.Day)"

        if ($PSBoundParameters.ContainsKey('Month')) {
            [Int]$AfterOffset = 0

            if ($PSBoundParameters.ContainsKey('Year')) { 
                $YearSpecified = $true
            }
            else {
                $Year = $Now.Year
                $YearSpecified = $false
            }

            # allowing 13 to be specified for Asian Lunar calendars. Obviously you get an error for all other
            # calendars.
            if ($Month -in 1..13) {
                [Int]$MonthNumber = $Month
            }
            # trailing 'f' means month specified, but next year required
            elseif ($Month -match '^(0?[1-9]|1[0-2])[Ff]$') {
                [Int]$MonthNumber = ($Month | Select-String -Pattern '^\d+').Matches.Value
                $Year += 1
            }
            # trailing 'p' means month specified, but last year required
            elseif ($Month -match '^(0?[1-9]|1[0-2])[Pp]$') {
                [Int]$MonthNumber = ($Month | Select-String -Pattern '^\d+').Matches.Value
                $Year -= 1
            }
            else {
                Write-Error "'$Month' is not a valid month number"
                return
            }
            <#
                add additional month before and after required month. This is better than Linux ncal. It allows
                a month in any year to be specified with -three. Linux ncal just ignores -3 and displays the
                specified year.
            #>

            if ($PSBoundParameters.ContainsKey('Three')) {
                [Int]$BeforeOffset = 1
                [Int]$AfterOffset = 1
            }
        }
        else {
            # No month
            if ($PSBoundParameters.ContainsKey('Year')) {
                # Year specified with no month; showing whole year.
                [Int]$MonthNumber = 1
                [Int]$BeforeOffset = 0
                [Int]$AfterOffset = ($Calendar.GetMonthsInYear($Year) - 1)
                $YearSpecified = $true
                if ($PSBoundParameters.ContainsKey('Three')) {
                    <#
                        If we allow -year and -three (with no -month) then we get the first month of specified
                        year with the month before and after. This feels like a bug, so ignore -three in this case.
                    #>

                    Write-Warning 'The Three parameter is ignored when Year is specified with no Month.'
                }
            }
            else {
                # Default is this month only
                $MonthNumber = $Now.Month
                $Year = $Now.Year
                $YearSpecified = $false
                # add additional month before and after this month.
                if ($PSBoundParameters.ContainsKey('Three')) {
                    [Int]$BeforeOffset = 1
                    [Int]$AfterOffset = 1
                }
            }
        }

        # add specified number of months before the month(s) or year already identified
        if ($PSBoundParameters.ContainsKey('Before')) {
            $BeforeOffset += $Before
        }
        # add specified number of months following the month(s) or year already identified
        if ($PSBoundParameters.ContainsKey('After')) {
            $AfterOffset += $After
        }

        <#
            Parameter validation complete. We have the required months to show, and a target month (typically this
            month or possibly first month of required year). Get the date object of the first day of the specified
            month and then use this to determine the date object of the first day of the first required month.
        #>

        $MonthCount = 1 + $BeforeOffset + $AfterOffset
        try {
            $TargetDate = $Calendar.ToDateTime($Year, $MonthNumber, 1, 0, 0, 0, 0, $Calendar.Eras[0])
            $FirstDay = $Calendar.AddMonths($TargetDate, - $BeforeOffset)
        }
        catch {
            Write-Error "Date conversion error - $($PSItem.Exception.Message)"
            return
        }
    
        for ($i = 1; $i -le $MonthCount; $i++) {
            $ThisYear = $Calendar.GetYear($FirstDay)
            $ThisMonth = $Calendar.GetMonth($FirstDay)
            $DayPerMonth = $Calendar.GetDaysInMonth($ThisYear, $ThisMonth)
            $MonthPerYear = $Calendar.GetMonthsInYear($ThisYear)
            [pscustomobject] @{
                'Date'          = $FirstDay # illustrates funky looking date when shown in local culture
                'Year'          = $ThisYear
                'Month'         = $ThisMonth
                'Day'           = $Calendar.GetDayOfMonth($FirstDay) # for clarity, not used
                'FirstDay'      = $Calendar.GetDayOfWeek($FirstDay) # for clarity, not used
                'FirstDayIndex' = $Calendar.GetDayOfWeek($FirstDay).value__ # Monday=1, Sunday=7
                'DayPerMonth'   = $DayPerMonth
                'MonthPerYear'  = $MonthPerYear
                'YearSpecified' = $YearSpecified # Used to determine year and month headings
            }
            $FirstDay = $Calendar.AddMonths($FirstDay, 1)
        }
    } #end process
}

function Get-StartWeekIndex {
    <#
        .NOTES
        The first day index is always 1 through 7, Monday to Sunday. This function returns an index, based on the
        real/desired first day of the week and the actual start day. This will be a number between -5 and 1. When
        we reach an index=1, we start printing, otherwise we print a space.
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory, Position = 1)]
        [ValidateSet('Friday', 'Saturday', 'Sunday', 'Monday')]
        [string]$StartWeekDay,

        [Int]$FirstDayIndex
    )

    process {
        # Monday = -2 thru Sunday = -8, which means Friday-Sunday start on next column so force 1, 0, -1 resp.
        if ('Friday' -eq $StartWeekDay) {
            $ThisIndex = -1 - $FirstDayIndex
            if ($ThisIndex -eq -6) {
                $ThisIndex = 1
            }
            elseif ($ThisIndex -eq -7) {
                $ThisIndex = 0
            }
            elseif ($ThisIndex -eq -8) {
                $ThisIndex = -1
            }
        }
        # Monday = -1 thru Sunday = -7 which mean both Saturday and Sunday start on next column, so force 1 and 0 resp.
        elseif ('Saturday' -eq $StartWeekDay) {
            $ThisIndex = 0 - $FirstDayIndex
            if ($ThisIndex -eq -6) {
                $ThisIndex = 1
            }
            elseif ($ThisIndex -eq -7) {
                $ThisIndex = 0
            }
        }
        # Monday = 0 thru Sunday = -6, which would mean start on next column, so force Sunday to be 1
        elseif ('Sunday' -eq $StartWeekDay) {
            $ThisIndex = 1 - $FirstDayIndex
            if ($ThisIndex -eq -6) {
                $ThisIndex = 1
            }
        }
        # Week starts Monday. Here we need Monday = 1 thru Sunday = -5
        else {
            $ThisIndex = 2 - $FirstDayIndex
            if ($ThisIndex -eq 2) {
                $ThisIndex = -5
            }
        }
        Write-Output $ThisIndex
    }
}

function Get-WeekDayName {
    <#
        .NOTES
        Determine the localized week day names. There are globalisation issues with attempting to truncate day
        names in some cultures, so only truncate cultures with standard character sets.
 
        For ncal only, MonthOffset is an attempt to fix column formats with many cultures that have mixed length
        short and long day names. It seems to work ok providing an appropriate font is installed supporting unicode
        characters.
 
        According .Net, just one language (Dhivehi - cultures dv and dv-MV), spoken in Maldives, has a week day
        starting on Friday. Most Islamic countries (some Arabic, Persian, Pashto and one or two others) use
        Saturday. All Western and Eastern European countries, Russia, Indian, most of Africa, Asian-Pacific
        countries, except China and Japan follow ISO 1806 standard with Monday as the first day of the week. All
        North and South American countries, some African, China and Japan use Sunday.
 
        With ncal, we want full day names or abbreviated day names (modified for some cultures).
        With cal, we want abbreviated names (shortened for some cultures) or the shortest day names.
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory, Position = 0)]
        [System.Globalization.CultureInfo]$Culture,

        [Parameter(Mandatory, Position = 1)]
        [ValidateSet('Friday', 'Saturday', 'Sunday', 'Monday')]
        [string]$FirstDayOfWeek,

        [Parameter(Position = 2)]
        [Switch]$LongDayName,

        [Parameter(Position = 3)]
        [Switch]$JulianSpecified
    )

    begin {
        [Int]$MonthOffset = 0
        $CallStack = Get-PSCallStack
        if ($CallStack.Count -gt 1) {
            $CallingFunction = $CallStack[1].Command
        }
    }
    process {
        if ($CallingFunction -eq 'Get-Calendar') {
            # Some cultures use double width character sets. Attempt to capture these and do not pad day names
            if ($Culture.Name -match '^(ja|zh|ko$|ko\-|ii)') {
                $WeekDay = $Culture.DateTimeFormat.ShortestDayNames
                if ($true -eq $JulianSpecified) {
                    # For day of the year, each day is 2 characters wide with double-width character sets.
                    $WeekDay = $WeekDay | ForEach-Object { "$_".PadLeft(2, ' ') }
                }
            }
            else {
                # Truncate some Abbreviated day names to two characters (e.g. Mo, Tu), rather than Shortest day
                # names (e.g. M, T) for some Western and other languages.
                if ($Culture.Name -match '^(da|de|es|eo|en|fr|it|pt|wo|fil)') {
                    $WeekDay = $Culture.DateTimeFormat.AbbreviatedDayNames | ForEach-Object { "$_".Substring(0, 2) }
                }
                else {
                    # Most cultures use a single character, so ensure the names are 2 characters.
                    $WeekDay = $Culture.DateTimeFormat.ShortestDayNames | ForEach-Object { "$_".PadLeft(2, ' ') }
                }
                if ($true -eq $JulianSpecified) {
                    # For day of the year, each day is 3 characters wide.
                    $WeekDay = $WeekDay | ForEach-Object { "$_".PadLeft(3, ' ') }
                }
            }
            Write-Verbose "Short week day - $WeekDay"
        }
        else {
            # Get-NCalendar is the (default) calling function
            if ($true -eq $LongDayName) {
                $WeekDayLong = $Culture.DateTimeFormat.DayNames
                Write-Verbose "Long week day - $WeekDayLong"
                if ($Culture.Name -match '^(ja|zh|ko$|ko\-|ii)') {
                    # Full day name for cultures that use double width character sets, double the size of the month
                    # offset but not the weekday
                    $MonthOffset = 2 + ((($WeekDayLong | ForEach-Object { "$_".Length } | Measure-Object -Maximum).Maximum) * 2)
                    $WeekDayLength = ($WeekDayLong | ForEach-Object { "$_".Length } | Measure-Object -Maximum).Maximum
                    $WeekDay = $WeekDayLong | ForEach-Object { "$_".PadRight($WeekDayLength + 1, ' ') }
                }
                else {
                    # Full day name for cultures using latin and Persian character sets.
                    $MonthOffset = 2 + (($WeekDayLong | ForEach-Object { "$_".Length } | Measure-Object -Maximum).Maximum)
                    $WeekDay = $WeekDayLong | ForEach-Object { "$_".PadRight($MonthOffset - 1, ' ') }
                }
            }
            else {
                $WeekDayShort = $Culture.DateTimeFormat.AbbreviatedDayNames
                Write-Verbose "Abbreviated week day - $WeekDayShort"
                if ($Culture.Name -match '^(ja|zh|ko$|ko\-|ii)') {
                    # Short day names for cultures that use double width character sets
                    $MonthOffset = 2 + ((($WeekDayShort | ForEach-Object { "$_".Length } | Measure-Object -Maximum).Maximum) * 2)
                    $WeekDayLength = ($WeekDayShort | ForEach-Object { "$_".Length } | Measure-Object -Maximum).Maximum
                    $WeekDay = $WeekDayShort | ForEach-Object { "$_".PadRight($WeekDayLength + 1, ' ') }
                }
                elseif ($ThisCulture.Name -match '^(en|fr|de|it|es|pt|eo)') {
                    # Simulate the Linux ncal command with two character day names on some Western cultures.
                    $MonthOffset = 4
                    $WeekDay = $WeekDayShort | ForEach-Object { "$_".Substring(0, 2).PadRight(3, ' ') }
                }
                else {
                    # Short day name for all other cultures.
                    $MonthOffset = 2 + (($WeekDayShort | ForEach-Object { "$_".Length } | Measure-Object -Maximum).Maximum)
                    $WeekDay = $WeekDayShort | ForEach-Object { "$_".PadRight($MonthOffset - 1, ' ') }
                }
            }
        }
        Write-Verbose "Specified/assumed first day of week is $FirstDayOfWeek"
        Write-Verbose "Cultural first day of the week is $($Culture.DateTimeFormat.FirstDayOfWeek)"

        # DayNames and AbbreviatedDayNames properties in .Net are always Sunday based, regardless of culture.
        if ('Friday' -eq $FirstDayOfWeek) {
            $WeekDay = $WeekDay[5, 6, 0, 1, 2, 3, 4]
        }
        if ('Saturday' -eq $FirstDayOfWeek) {
            $WeekDay = $WeekDay[6, 0, 1, 2, 3, 4, 5]
        }
        elseif ('Monday' -eq $FirstDayOfWeek) {
            $WeekDay = $WeekDay[1, 2, 3, 4, 5, 6, 0]
        }
        [PSCustomObject]@{
            Name   = $WeekDay
            Offset = $MonthOffset
        }
    }
}

function Get-WeekRow {
    <#
        .NOTES
        Helper function for Get-NCalendar. Displays the week number beneath each column.
 
        Uses a .Net call to obtain the week number for the first week of the month for the required culture. We use
        the specified first day of the week to ensure the week numbers align (although this is not culturally
        correct). Otherwise, we are using the default first day of the week for the required culture.
 
        The other thing we're doing is ensuring the correct number of week numbers per month. Most of the time,
        this is five columns. However, if the first day of the month appears in the last day of the week position
        and there are 30 or 31 days in the month, then there are 6 columns. If it appears in the next to last
        position, and there 31 days, then are 6 columns. This equates to Index = -5 or -4 respectively.
 
        The only other combination is when there are 28 days in February, and the first day is position 1 (Index = 1).
        In this case, there are only 4 columns.
 
        All cultures are fine, but calendars (typically the Asian Lunar calendars) don't so correct week numbers.
        This is because non-default calendars are inheriting DateTimeFormat from some Culture (which typically
        uses Gregorian by default). DateTimeFormat is used to calculate the week number of the first week in the
        month. I these situations, don't allow week row being displayed.
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory, Position = 0)]
        [System.Globalization.CultureInfo]$Culture,

        [Parameter(Mandatory, Position = 1)]
        [System.Globalization.Calendar]$Calendar,

        [Parameter(Mandatory, Position = 2)]
        [ValidateRange(1, 13)]
        [Int]$Month,

        [Parameter(Mandatory, Position = 3)]
        [ValidateRange(1, 9999)]
        [Int]$Year,

        [Parameter(Mandatory, Position = 4)]
        [ValidateSet('Friday', 'Saturday', 'Sunday', 'Monday')]
        [String]$FirstDayOfWeek,

        [Parameter(Mandatory, Position = 5)]
        [ValidateRange(-5, 1)]
        [Int]$Index,

        [Parameter(Position = 6)]
        [Bool]$JulianSpecified
    )

    process {
        $FirstDate = $Calendar.ToDateTime($Year, $Month, 1, 0, 0, 0, 0, $Calendar.Eras[0] )
        $DayPerMonth = $Calendar.GetDaysInMonth($Year, $Month)
        $CultureWeekRule = $Culture.DateTimeFormat.CalendarWeekRule

        # Adjust the starting week number, based on the first day of the week.
        if ('Friday' -eq $FirstDayOfWeek) {
            $StartWeekDay = 5
        }
        if ('Saturday' -eq $FirstDayOfWeek) {
            $StartWeekDay = 6
        }
        elseif ('Sunday' -eq $FirstDayOfWeek) {
            $StartWeekDay = 0
        }
        elseif ('Monday' -eq $FirstDayOfWeek) {
            $StartWeekDay = 1
        }
        else {
            #can't be necessary, but use default for the requested culture, for illustration.
            $StartWeekDay = $Culture.DateTimeFormat.FirstDayOfWeek
        }
        # This is the starting week number of this month, taking into account the starting week day.
        $FirstWeek = $Culture.Calendar.GetWeekOfYear($FirstDate, $CultureWeekRule, $StartWeekDay)

        [String]$WeekRow = ''
        [Int]$WeekCount = 5

        # month starts last day of week and has at least 30 days is 6 columns wide
        if (-5 -eq $Index -and $DayPerMonth -ge 30) {
            $WeekCount = 6
        }
        # month starts next to last day of week and has over 30 day is 6 columns wide
        elseif (-4 -eq $Index -and $DayPerMonth -gt 30) {
            $WeekCount = 6
        }
        # February in some calendars that starts on the first day of the week is 4 columns wide
        elseif (1 -eq $Index -and $DayPerMonth -eq 28) {
            $WeekCount = 4
        }

        if ($true -eq $JulianSpecified) {
            $PadRow = 24
            $PadDay = 3
        }
        else {
            $PadRow = 19
            $PadDay = 2
        }

        $LastWeek = $FirstWeek + ($WeekCount - 1)
        $FirstWeek..$LastWeek | ForEach-Object {
            if ($_ -gt 52) {
                $WeekRow += "{0,$PadDay} " -f ($_ - 52)
            }
            else {
                $WeekRow += "{0,$PadDay} " -f $_
            }
        }
    
        $OutString = "$WeekRow".PadRight($PadRow, ' ')
        Write-Output $OutString
    } # end process
}

function Get-Highlight {
    <#
    .NOTES
        Helper function for Get-NCalendar and Get-Calendar. Returns a list of formatting strings and the day for
        today. This is used to highlighting year/month headings, week row and todays day.
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory, Position = 0)]
        [System.Globalization.Calendar]$Calendar,

        [Parameter(Mandatory, Position = 1)]
        [ValidateRange(1, 13)]
        [Int]$Month,
        
        [Parameter(Mandatory, Position = 2)]
        [ValidateRange(1, 9999)]
        [Int]$Year,

        [Parameter(Position = 3)]
        [ValidateSet('None', 'Red', 'Green', 'Blue', 'Yellow', 'Cyan', 'Magenta', 'White', 'Orange', 'Pink', $null)]
        [String]$Highlight
    )

    process {
        $Now = Get-Today -Calendar $Calendar
        if ( ($Month -eq $Now.Month) -and ($Year -eq $Now.Year) ) {
            $Today = $Now.Day
        }
        else {
            $Today = 0
        }
        if (-not $Highlight) {
            # This is the default; reverse highlight today but no highlighted month name
            Write-Output @{
                Today    = $Today
                MonStyle = $null
                MonReset = $null
                DayStyle = $PSStyle.Reverse
                DayReset = $PSStyle.ReverseOff
            }
        }
        elseif ('None' -eq $Highlight) {
            # Turn off highlighting today
            Write-Output @{
                Today = 0
            }
        }
        # Just for giggles, demonstrate some non-PSStyle supplied colours
        elseif ('Orange' -eq $Highlight) {
            Write-Output @{
                Today    = $Today
                MonStyle = "$($PSStyle.Foreground.FromRgb(255,131,0))$($PSStyle.Bold)"
                MonReset = $PSStyle.Reset
                DayStyle = "$($PSStyle.Background.FromRgb(255,131,0))$($PSStyle.Foreground.FromRgb(0,0,0))"
                DayReset = $PSStyle.Reset
            }
        }
        elseif ('Pink' -eq $Highlight) {
            Write-Output @{
                Today    = $Today
                MonStyle = "$($PSStyle.Foreground.FromRgb(255,0,255))$($PSStyle.Bold)"
                MonReset = $PSStyle.Reset
                DayStyle = "$($PSStyle.Background.FromRgb(255,0,255))$($PSStyle.Foreground.FromRgb(0,0,0))"
                DayReset = $PSStyle.Reset
            }
        }
        else {
            # Force the bright version of the specified colour
            $Colour = "Bright$Highlight"
            Write-Output @{
                Today    = $Today
                MonStyle = "$($PSStyle.Foreground.$Colour)$($PSStyle.Bold)"
                MonReset = $PSStyle.Reset
                DayStyle = "$($PSStyle.Background.$Colour)$($PSStyle.Foreground.FromRgb(0,0,0))"
                DayReset = $PSStyle.Reset
            }
        }
    }
}

function Get-NCalendar {
    <#
    .SYNOPSIS
        Get-NCalendar
    .DESCRIPTION
        This command displays calendar information similar to the Linux ncal command. It implements the same
        functionality, including the ability to display multiple months, years, week number per year, day of the
        year and month forward and month previous by one year.
 
        But in addition, the command can do a whole lot more:
        1. Display a calendar in any supported culture. Month and day names are displayed in the appropriate
        language for the specified culture and the appropriate calendar is used (e.g. Gregorian, Persian), along
        with appropriate DateTimeFormat information (e.g. default first day of the week).
        2. As well as display the primary calendar (used by each culture), also display optional calendars.
        These are Hijri, Hebrew, Japanese (Solar), Korean (Solar) and Taiwanese (Solar) calendars. In addition,
        the non-optional calendars (i.e. calendars not used by any culture, but still observed for religious,
        scientific or traditional purposes). These are the Julian and Chinese, Japanese, Korean and Taiwanese Lunar
        calendars. (Note: Only the current era is supported).
        3. Specify the first day of the week (Friday through Monday). The specified or current culture setting is
        used by default. Friday through Monday are supported because all cultures use one of these days.
        4. Display abbreviated (default) or full day names, specific to the culture.
        5. Display one to six months in a row, when multiple months are displayed (the default is 4).
        6. When displaying week numbers, they will align correctly with respect to the default or specified first
        day of the week.
        7. Highlight the year and month headings, todays date and week numbers using a specified colour.
 
        It is highly recommended that Windows Terminal is used with an appropriate font to ensure that ISO unicode
        character sets are both available and are displayed correctly. With other consoles, like Visual Studio Code,
        the ISE and the default PowerShell console, some fonts might not display correctly and with extended unicode
        character sets, calendars may appear misaligned.
 
        Note: From version 1.22.10352.0 (Feb 2025) of Windows Terminal, grapheme clusters are now supported and are
        turned on by default. A grapheme cluster is a single user-perceived character made up of multiple code
        points from the Unicode Standard, introduced in .NET 5. Whilst this is considered the correct method for
        handling and displaying Unicode character sets, PowerShell doesn't support grapheme clusters and thus,
        calandars in ncal appear misaligned. This can be remedied, in the short term, by disabling grapheme cluster
        support in Settings > Compatibility > Text measurement mode, selecting "Windows Console" and then
        restarting the Windows Terminal.
    .PARAMETER Month
        Specifies the required month. This must be specified as a number 0..13. An 'f' (forward by one year) or a
        'p' (previous by one year) suffix can also be appended to the month number.
    .PARAMETER Year
        Specifies the required year. If no month is specified, the whole year is shown.
    .PARAMETER Culture
        Specifies the required culture. The system default culture is used by default.
    .PARAMETER Calendar
        Instead of a culture, specify a calendar. This allows displaying optional and other calendars not used
        by any culture. They include Julian, Hijri, Hebrew and Chinese Lunar calendars.
    .PARAMETER FirstDayOfWeek
        Display the specified first day of the week. By default, the required culture is used to determine this.
    .PARAMETER MonthPerRow
        Display the specified number of months in each row. By default it is 4 months.
    .PARAMETER Highlight
        By default, today's date is highlighted. Specify a colour to highlight today, the year/month headings and
        week numbers or disable the default highlight with 'none'.
    .PARAMETER Before
        The specified number of months are added before the specified month(s). See -After for examples.
    .PARAMETER After
        The specified number of months are added after the specified month(s), i.e. in addition to any date range
        selected by the -Year or -Three options. Negative numbers are allowed, in which case the specified number
        of months is subtracted. For example ncal -after 11 simply shows the next 12 months in any culture.
    .PARAMETER Three
        Display the current month together with the previous and following month. This is ignored if -Year is also
        specified without a month.
    .PARAMETER DayOfYear
        Display the day of the year (days one-based, numbered from 1st January).
    .PARAMETER Week
        Display the number of the week below each week column.
    .PARAMETER LongDayName
        Display full day names for the required culture or calendar, instead of abbreviated day names (default).
        For some cultures, there is no difference.
    .PARAMETER Name
        Display the name of the specified culture and/or calendar name as a banner above the calendar.
    .EXAMPLE
        PS C:\> ncal
         
        Displays this month using the current culture
    .EXAMPLE
        PS C:\> ncal -month 1 -after 11 -culture fa
         
        Displays the first month and the following 11 months (this year) for any specified culture. For example,
        -Year 2025 with cultures that do not use the Gregorian calendar by default will not work or produce
        unintended results. Some cultures use the Persian (Iranian), ThaiBuddist and UmAlQura (Umm al-Qura, Saudi
        Arabian) calendars by default.
    .EXAMPLE
        PS C:\> ncal -m 1f
 
        Displays January next year. -m 4p shows April from the previous year
    .EXAMPLE
        PS C:\> ncal -m 4 -y 2025 -b 2 -a 1
 
        Displays April 2025 with the two months before and one month after it.
    .EXAMPLE
        PS C:\> ncal -y 2025 -a 24
         
        Shows 2025 through 2027
    .EXAMPLE
        PS C:\> ncal -DayOfYear -three
         
        Show the day number, starting from 1st January, for this month as well as last month and next month.
    .EXAMPLE
        PS C:\> ncal 2 2026 -three
         
        Show February 2026 with the month prior and month after.
    .EXAMPLE
        PS C:> ncal -Year 2025 -Week -H Cyan
 
        Shows the specified year with a highlighted colour. Supports red, blue,
        green, yellow, orange, pink, cyan, magenta and white. Disable all highlighting with - Highlight 'none'. Week
        numbers are shown below each week column and are also highlighted.
    .EXAMPLE
        PS C:> ncal -culture ja-JP -Year 2025 -Highlight Orange
 
        Display a calender using the Japanese (Japan) culture for the specified year.
    .EXAMPLE
        PS C:> 'Persian','Hijri','UmAlQura' | % { ncal -calendar $_ -name }
 
        Display three calendars (the current month) using the specified calendars with a banner to identify each
        culture/calendar.
    .EXAMPLE
        PC C:> 'en-au','en-us','dv','mzn' | % { ncal -c $_ -Name -Week -Highlight Yellow }
 
        Display calendars for the specified cultures. This example illustrates the different DateTimeFormat
        information for each culture (different start days for the week).
    .EXAMPLE
        PS C:> ncal -calendar Julian -m 1 -a 11
 
        Shows this year in the Julian calendar.
         
        Note: This actually works correctly, unlike the Linux ncal command (as at Feb 2025), which sometimes shows
        the wrong month (shows this Julian month but in terms of month number on the Gregorian calendar),
        depending on the day of the month.
    .EXAMPLE
        PS C:> ncal -cal Hijri -m 1 -a 11
 
        Shows this year in the Hijri (Muslim) calendar.
 
        Note: This is not supported with Linux ncal command.
    .LINK
        https://github.com/atkinsroy/ncal/docs
    .INPUTS
        [System.String]
        [System.Int]
    .OUTPUTS
        [System.String]
    .NOTES
        Author: Roy Atkins
    #>

    [Alias('ncal')]
    [CmdletBinding(DefaultParameterSetName = 'UseCulture')]
    param(
        # Could be integer between 1 and 13 or the same with an 'f' or 'p' suffix.
        [Parameter(Position = 0)]
        [Alias('m')]
        [String]$Month,

        [Parameter(Position = 1)]
        [ValidateRange(1, 9999)]
        [Int]$Year,

        [Parameter(Position = 2, ParameterSetName = 'UseCulture')]
        [Alias('c', 'cul')]
        [String]$Culture,

        [Parameter(Position = 2, ParameterSetName = 'UseCalendar')]
        [Alias('cal')]
        [ValidateSet(
            'Gregorian', 
            'Persian', 
            'Hijri', 
            'Hebrew', 
            'Japanese', 
            'Korean', 
            'Taiwan', 
            'UmAlQura', 
            'ThaiBuddhist',
            'Julian',
            'ChineseLunisolar',
            'JapaneseLunisolar',
            'KoreanLunisolar',
            'TaiwanLunisolar')]
        [String]$Calendar,

        [Parameter(Position = 3)]
        [ValidateSet('Friday', 'Saturday', 'Sunday', 'Monday')]
        [String]$FirstDayOfWeek,

        [Parameter(Position = 4)]
        [Alias('r', 'row')]
        [ValidateRange(1, 6)]
        [Int]$MonthPerRow = 4,

        [Parameter(Position = 5)]
        [ValidateSet('None', 'Red', 'Green', 'Blue', 'Yellow', 'Cyan', 'Magenta', 'White', 'Orange', 'Pink')]
        [String]$Highlight,

        [Int]$Before,

        [Int]$After,

        [Switch]$Three,

        [Switch]$DayOfYear,

        [Switch]$Week,

        [Switch]$LongDayName,

        [switch]$Name
    )

    begin {
        $Abort = $false
        if ($PSBoundParameters.ContainsKey('Culture')) {
            try {
                $ThisCulture = New-Object System.Globalization.CultureInfo($Culture) -ErrorAction Stop
                # The above doesn't alway capture a dodgy culture so test further
                $AllCulture = (Get-Culture -ListAvailable).Name
                if ($Culture -notin $AllCulture) {
                    Write-Warning "Invalid culture: '$Culture'. Using the system default culture ($((Get-Culture).Name)). Use 'Get-Culture -ListAvailable'."
                    $ThisCulture = [System.Globalization.CultureInfo]::CurrentCulture
                } 
            }
            catch {
                Write-Warning "Invalid culture specified:'$Culture'. Using the system default culture ($((Get-Culture).Name)). Use 'Get-Culture -ListAvailable'."
                $ThisCulture = [System.Globalization.CultureInfo]::CurrentCulture
            }
            $ThisCalendar = $ThisCulture.Calendar

        }
        elseif ($PSBoundParameters.ContainsKey('Calendar')) {
            $CultureLookup = @{
                'Gregorian'         = 'en-AU'
                'Persian'           = 'fa-IR'
                'Hijri'             = 'ar-SA'
                'Hebrew'            = 'he-IL'
                'Japanese'          = 'ja-JP'
                'Korean'            = 'ko-KR'
                'Taiwan'            = 'zh-Hant-TW'
                'UmAlQura'          = 'ar-SA'
                'ThaiBuddhist'      = 'th-th'
                'Julian'            = 'en-AU'
                'ChineseLunisolar'  = 'zh'
                'JapaneseLunisolar' = 'ja'
                'KoreanLunisolar'   = 'ko'
                'TaiwanLunisolar'   = 'zh-Hant-TW'
            }
            <#
                In order to support Julian and Asian Lunar calendars ('non-optional'), treat culture and calendar
                separately. With optional calenders you can set the culture to use them, but this doesn't work for
                the above.
            #>

            $ThisCulture = New-Object System.Globalization.CultureInfo($($CultureLookup[$Calendar]))
            $ThisCalendar = New-Object "System.Globalization.$($Calendar)Calendar"
        }
        else {
            $ThisCulture = [System.Globalization.CultureInfo]::CurrentCulture
            $ThisCalendar = $ThisCulture.Calendar
        }

        # Display the Culture/Calendar name as a heading
        if ($PSBoundParameters.ContainsKey('Name') -and $PSBoundParameters.ContainsKey('Calendar')) {
            $CalendarString = $($ThisCalendar.ToString().Replace('System.Globalization.', '').Replace('Calendar', ' Calendar'))   
            Write-Output "`n$($PSStyle.Reverse)--|$CalendarString|--$($PSStyle.ReverseOff)`n"
        }
        elseif ($PSBoundParameters.ContainsKey('Name')) {
            $CalendarString = $($ThisCulture.Calendar.ToString().Replace('System.Globalization.', '').Replace('Calendar', ' Calendar'))
            Write-Output "`n$($PSStyle.Reverse)--|$($ThisCulture.Name)|-|$($ThisCulture.DisplayName)|-|$CalendarString|--$($PSStyle.ReverseOff)`n"
        }

        # Some optional calendars don't display week numbers accurately. In these case don't display week numbers
        $IgnoreWeekRow = $false
        $IgnoreCalendar = @(
            'HijriCalendar',
            'HebrewCalendar',
            'JulianCalendar',
            'ChineseLunisolarCalendar',
            'JapaneseLunisolarCalendar',
            'KoreanLunisolarCalendar',
            'TaiwanLunisolarCalendar'
        )
        $CompareCalender = $($ThisCalendar.ToString().Replace('System.Globalization.', ''))
        if ($PSBoundParameters.ContainsKey('Week') -and $CompareCalender -in $IgnoreCalendar) {
            $IgnoreWeekRow = $true
            Write-Warning "Displaying week numbers is not supported with the $($CompareCalender.Replace('Calendar',' calendar'))"
        }

        # Full month names in current culture
        $MonthNameArray = $ThisCulture.DateTimeFormat.MonthGenitiveNames

        # Calling DayOfYear 'Julian' is incorrect. JulianDay is something else altogether. However keep 'Julian' as
        # its more eye-catching.
        if ($PSBoundParameters.ContainsKey('DayOfYear')) {
            [Bool]$JulianSpecified = $true
        }
        else {
            [Bool]$JulianSpecified = $false
        }

        # List of cultural specific day names in the required order.
        if ($PSBoundParameters.ContainsKey('FirstDayOfWeek')) {
            $FirstDay = $FirstDayOfWeek
        }
        else {
            $FirstDay = $ThisCulture.DateTimeFormat.FirstDayOfWeek
        }
        $Param = @{
            'Culture'         = $ThisCulture
            'FirstDayOfWeek'  = $FirstDay
            'JulianSpecified' = $JulianSpecified
            'LongDayName'     = $LongDayName
        }
        $WeekDay = Get-WeekDayName @Param

        # Get the date of the first day of each required month, based on the culture (common to ncal & cal)
        $DateParam = New-Object -TypeName System.Collections.Hashtable
        $DateParam.Add('Calendar', $ThisCalendar)
        if ($PSBoundParameters.ContainsKey('Month')) {
            $DateParam.Add('Month', $Month)
        }
        if ($PSBoundParameters.ContainsKey('Year')) {
            $DateParam.Add('Year', $Year)
        }
        if ($PSBoundParameters.ContainsKey('Before')) {
            $DateParam.Add('Before', $Before)
        }
        if ($PSBoundParameters.ContainsKey('After')) {
            $DateParam.Add('After', $After)
        }
        if ($PSBoundParameters.ContainsKey('Three')) {
            $DateParam.Add('Three', $Three)
        }
        # this is where most parameter validation occurs, and most of the date conversion stuff.
        try {
            $MonthList = Get-FirstDayOfMonth @DateParam -ErrorAction Stop
        }
        catch {
            Write-Error $PSItem.Exception.Message
            $Abort = $true
        }

        # To hold each row of 1 to 6 months, initialized with culture specific day abbreviation
        [System.Collections.Generic.List[String]]$MonthRow = $WeekDay.Name
        $MonthCount = 0
        $MonthHeading = ' ' * $WeekDay.Offset
        $WeekRow = ' ' * ($WeekDay.Offset - 1)
    }

    process {
        foreach ($RequiredMonth in $MonthList) {
            if ($true -eq $Abort) {
                return
            }
            $ThisYear = $RequiredMonth.Year
            $ThisMonth = $RequiredMonth.Month
            $DayPerMonth = $RequiredMonth.DayPerMonth
            $FirstDayIndex = $RequiredMonth.FirstDayIndex
            $YearSpecified = $RequiredMonth.YearSpecified
            $MonthName = $MonthNameArray[$ThisMonth - 1]  # MonthNameArray is zero based
            if (13 -eq $ThisMonth) {
                $MonthName = '13'
            }
            if ($PSBoundParameters.ContainsKey('Three') -or $PSBoundParameters.ContainsKey('Month') -or $false -eq $YearSpecified) {
                $MonthName = "$MonthName $ThisYear"
            }

            # for highlighting today
            $Pretty = Get-Highlight $ThisCalendar $ThisMonth $ThisYear $Highlight
            if ($PSBoundParameters.ContainsKey('Calendar')) {
                Write-Verbose "monthname = $MonthName, thismonth = $ThisMonth, thisyear = $ThisYear, dayspermonth = $DayPerMonth, monthcount = $MonthCount, calendar = $($ThisCalendar.ToString().Replace('System.Globalization.', '')), era = $($ThisCalendar.Eras[0])"
            }
            else {
                Write-Verbose "monthname = $MonthName, thismonth = $ThisMonth, thisyear = $ThisYear, dayspermonth = $DayPerMonth, monthcount = $MonthCount, culture = $($ThisCulture.Name)"
            }

            # User specified First day of the week, or use the default for the culture being used.
            if ($PSBoundParameters.ContainsKey('FirstDayOfWeek')) {
                if ('Friday' -eq $FirstDayOfWeek) {
                    $StartWeekDay = 'Friday'
                }
                elseif ('Saturday' -eq $FirstDayOfWeek) {
                    $StartWeekDay = 'Saturday'
                }
                elseif ('Sunday' -eq $FirstDayOfWeek) {
                    $StartWeekDay = 'Sunday'
                }
                else {
                    $StartWeekDay = 'Monday'
                }
            }
            else {
                $StartWeekDay = $ThisCulture.DateTimeFormat.FirstDayOfWeek
            }

            # Get the starting index for the month, to offset when to start printing dates in the row.
            $Param = @{
                'StartWeekDay'  = $StartWeekDay
                'FirstDayIndex' = $FirstDayIndex
            }
            $ThisIndex = Get-StartWeekIndex @Param

            # User can choose number of months to print per row, default is 4. In this case, just append the month.
            if ($MonthCount -lt $MonthPerRow) {
                $Param = @{
                    'Culture'         = $ThisCulture
                    'MonthName'       = $MonthName
                    'JulianSpecified' = $JulianSpecified
                }
                $MonthHeading += "$(Get-MonthHeading @Param)"
                if ($PSBoundParameters.ContainsKey('Week') -and -not $IgnoreWeekRow) {
                    $Param = @{
                        'Culture'         = $ThisCulture
                        'Calendar'        = $ThisCalendar
                        'Month'           = $ThisMonth
                        'Year'            = $ThisYear
                        'FirstDayOfWeek'  = $StartWeekDay
                        'Index'           = $ThisIndex
                        'JulianSpecified' = $JulianSpecified
                    }
                    $WeekRow += Get-WeekRow @Param
                }
            }
            else {
                # Print a year heading before January when year is specified when the year is not already in month name
                if ($MonthHeading -match "\b$($MonthNameArray[0])\b" -and $MonthName -notmatch $ThisYear) {
                    $YearPad = (((18 * $MonthPerRow) + 3 - 2 ) / 2) + 2 
                    $YearHeading = "$ThisYear".PadLeft($YearPad, ' ')
                    Write-Output "$($Pretty.MonStyle)$YearHeading$($Pretty.MonReset)"
                }
                Write-Output "$($Pretty.MonStyle)$MonthHeading$($Pretty.MonReset)"
                Write-Output $MonthRow
                if ($PSBoundParameters.ContainsKey('Week') -and -not $IgnoreWeekRow) {
                    Write-Output "$($Pretty.MonStyle)$WeekRow$($Pretty.MonReset)"
                }
                Write-Output ''

                # Reset for next row of months
                [System.Collections.Generic.List[String]]$MonthRow = $WeekDay.Name
                $MonthCount = 0
                $Param = @{
                    'Culture'         = $ThisCulture
                    'MonthName'       = $MonthName
                    'JulianSpecified' = $JulianSpecified
                }
                $MonthHeading = (' ' * $WeekDay.Offset) + "$(Get-MonthHeading @Param)"
                if ($PSBoundParameters.ContainsKey('Week') -and -not $IgnoreWeekRow) {
                    $Param = @{
                        'Culture'         = $ThisCulture
                        'Calendar'        = $ThisCalendar
                        'Month'           = $ThisMonth
                        'Year'            = $ThisYear
                        'FirstDayOfWeek'  = $StartWeekDay
                        'Index'           = $ThisIndex
                        'JulianSpecified' = $JulianSpecified
                    }
                    $WeekRow = Get-WeekRow @Param
                    
                    # starting padding for the week row is dependent on first week number
                    if (1 -eq "$WeekRow".Split(' ')[0].Length) {
                        $WeekRow = (' ' * $WeekDay.Offset) + $WeekRow
                    }
                    else {
                        $WeekRow = (' ' * ($WeekDay.Offset - 1)) + $WeekRow
                    }
                }
            }

            0..6 | ForEach-Object {
                $Param = @{
                    'Calendar'        = $ThisCalendar
                    'Index'           = $ThisIndex
                    'DayPerMonth'     = $DayPerMonth
                    'Month'           = $ThisMonth
                    'Year'            = $ThisYear
                    'Highlight'       = $Pretty
                    'JulianSpecified' = $JulianSpecified
                }
                $MonthRow[$_] += "$(Get-NcalRow @Param)"
                $ThisIndex++
            }
            $MonthCount++
        }
    }
    end {
        # Write the last month or row of months
        # Print a year heading before January when there is no year already in the month name.
        if (-Not $Abort) {
            if ($MonthHeading -match "\b$($MonthNameArray[0])\b" -and $MonthName -notmatch $ThisYear) {
                $YearPad = (((18 * $MonthPerRow) + 3 - 2 ) / 2) + 2 
                $YearHeading = "$ThisYear".PadLeft($YearPad, ' ')
                Write-Output "$($Pretty.MonStyle)$YearHeading$($Pretty.MonReset)"
            }
            Write-Output "$($Pretty.MonStyle)$MonthHeading$($Pretty.MonReset)"
            Write-Output $MonthRow
            if ($PSBoundParameters.ContainsKey('Week') -and -not $IgnoreWeekRow) {
                Write-Output "$($Pretty.MonStyle)$WeekRow$($Pretty.MonReset)"
            }
        }
    }
}

function Get-Calendar {
    <#
    .SYNOPSIS
        Get-Calendar
    .DESCRIPTION
        This command displays calendar information similar to the Linux cal command. It implements the same
        functionality, including the ability to display multiple months, years, day of the year and month forward
        and month previous by one year.
 
        But in addition, the command can do a whole lot more:
        1. Display a calendar in any supported culture. Month and day names are displayed in the appropriate
        language for the specified culture and the appropriate calendar is used (e.g. Gregorian, Persian), along
        with appropriate DateTimeFormat information (e.g. default first day of the week).
        2. As well as display the primary calendar (used by each culture), also display optional calendars.
        These are Hijri, Hebrew, Japanese (Solar), Korean (Solar) and Taiwanese (Solar) calendars. In addition,
        the non-optional calendars (i.e. calendars not used by any culture, but still observed for religious,
        scientific or traditional purposes). These are the Julian and Chinese, Japanese, Korean and Taiwanese Lunar
        calendars. (Note: Only the current era is supported).
        3. Specify the first day of the week (Friday through Monday). The specified or current culture setting is
        used by default. Friday through Monday are supported because all cultures use one of these days.
        4. Display one to six months in a row, when multiple months are displayed (the default is 3).
        5. Highlight the year and month headings and todays date using a specified colour.
 
        It is highly recommended that Windows Terminal is used with an appropriate font to ensure that ISO unicode
        character sets are both available and are displayed correctly. With other consoles, like Visual Studio Code,
        the ISE and the default PowerShell console, some fonts might not display correctly and with extended unicode
        character sets, calendars may appear misaligned.
 
        Note: From version 1.22.10352.0 (Feb 2025) of Windows Terminal, grapheme clusters are now supported and are
        turned on by default. A grapheme cluster is a single user-perceived character made up of multiple code
        points from the Unicode Standard, introduced in .NET 5. Whilst this is considered the correct method for
        handling and displaying Unicode character sets, PowerShell doesn't support grapheme clusters and thus,
        calandars in ncal appear misaligned. This can be remedied, in the short term, by disabling grapheme cluster
        support in Settings > Compatibility > Text measurement mode, selecting "Windows Console" and then
        restarting the Windows Terminal.
    .PARAMETER Month
        Specifies the required month. This must be specified as a number 0..13. An 'f' (forward by one year) or a
        'p' (previous year) suffix can also be appended to the month number.
    .PARAMETER Year
        Specifies the required year. If no month is specified, the whole year is shown.
    .PARAMETER Culture
        Specifies the required culture. The system default culture is used by default.
    .PARAMETER Calendar
        Instead of a culture, specify a calendar. This allows displaying optional and other calendars not used
        by any culture. They include Julian, Hijri, Hebrew and Chinese Lunar calendars.
    .PARAMETER FirstDayOfWeek
        Display the specified first day of the week. By default, the required culture is used to determine this.
    .PARAMETER MonthPerRow
        Display the specified number of months in each row. By default it is 3 months.
    .PARAMETER Highlight
        By default, today's date is highlighted. Specify a colour to highlight today, the year/month headings and
        week numbers or disable the default highlight with 'none'.
    .PARAMETER Before
        The specified number of months are added before the specified month(s). See -After for examples.
    .PARAMETER After
        The specified number of months are added after the specified month(s). This is in addition to any date range
        selected by the -Year or -Three options. Negative numbers are allowed, in which case the specified number
        of months is subtracted. For example ncal -after 11 simply shows the next 12 months in any culture.
    .PARAMETER Three
        Display the current month together with the previous and following month. This is ignored if -Year is also
        specified without a month.
    .PARAMETER DayOfYear
        Display the day of the year (days one-based, numbered from 1st January).
    .PARAMETER Name
        Display the name of the specified culture and/or calendar name as a banner above the calendar.
    .EXAMPLE
        PS C:\> cal
         
        Displays this month using the current culture
    .EXAMPLE
        PS C:\> cal -m 1 -a 11 -culture fa
         
        Displays the first month and the following 11 months (this year) for any specified culture. For example,
        -Year 2025 with cultures that do not use the Gregorian calendar by default will not work or produce
        unintended results. Some cultures use the Persian (Iranian), ThaiBuddist and UmAlQura (Umm al-Qura, Saudi
        Arabian) calendars by default.
    .EXAMPLE
        PS C:\> cal -m 1f
 
        Displays January next year. -m 4p shows April from the previous year
    .EXAMPLE
        PS C:\> cal -m 4 -y 2025 -b 2 -a 1
 
        Displays April 2025 with the two months before and one month after it.
    .EXAMPLE
        PS C:\> cal -y 2025 -a 24
         
        Shows 2025 through 2027
    .EXAMPLE
        PS C:\> cal -DayOfYear -Three
         
        Show the day number, starting from 1st January, for this month as well as last month and next month.
    .EXAMPLE
        PS C:\> cal 2 2026 -three
         
        Show February 2026 with the month prior and month after.
    .EXAMPLE
        PS C:> cal -Year 2025 -Highlight Cyan
 
        Shows the specified year with a highlighted colour. Supports red, blue,
        green, yellow, orange, pink, cyan, magenta and white. Disable all highlighting with - Highlight 'none'.
    .EXAMPLE
        PS C:> cal -culture ja-JP -Year 2025 -Highlight Orange
 
        Display a calender using the Japanese (Japan) culture for the specified year.
    .EXAMPLE
        PS C:> 'Persian','Hijri','UmAlQura' | % { cal -calendar $_ -name }
 
        Display three calendars (the current month) using the specified calendars with a banner to identify each
        culture/calendar.
    .EXAMPLE
        PC C:> 'en-au','en-us','dv','mzn' | % { ncal -c $_ -Name -Week -Highlight Yellow }
 
        Display calendars for the specified cultures. This example illustrates the different DateTimeFormat
        information for each culture (different start days for the week).
    .EXAMPLE
        PS C:> ncal -calendar Julian -m 1 -a 11
 
        Shows this year in the Julian calendar.
 
        Note: This actually works correctly, unlike the Linux ncal command (as at Feb 2025), which sometimes shows
        the wrong month (shows this Julian month but in terms of month number on the Gregorian calendar),
        depending on the day of the month.
    .EXAMPLE
        PS C:> ncal -cal Hijri -m 1 -a 11
 
        Shows this year in the Hijri (Muslim) calendar.
 
        Note: This is not supported with Linux ncal command.
    .LINK
        https://github.com/atkinsroy/ncal/docs
    .INPUTS
        [System.String]
        [System.Int]
    .OUTPUTS
        [System.String]
    .NOTES
        Author: Roy Atkins
    #>

    [Alias('cal')]
    [CmdletBinding(DefaultParameterSetName = 'UseCulture')]
    param(
        # Could be integer between 1 and 13 or the same with an 'f' or 'p' suffix.
        [Parameter(Position = 0)]
        [Alias('m')]
        [String]$Month,
        
        [Parameter(Position = 1)]
        [ValidateRange(1, 9999)]
        [Int]$Year,

        [parameter(Position = 2, ParameterSetName = 'UseCulture')]
        [Alias('c', 'cul')]
        [String]$Culture,

        [Parameter(Position = 2, ParameterSetName = 'UseCalendar')]
        [Alias('cal')]
        [ValidateSet(
            'Gregorian', 
            'Persian', 
            'Hijri', 
            'Hebrew', 
            'Japanese', 
            'Korean', 
            'Taiwan', 
            'UmAlQura', 
            'ThaiBuddhist',
            'Julian',
            'ChineseLunisolar',
            'JapaneseLunisolar',
            'KoreanLunisolar',
            'TaiwanLunisolar')]
        [String]$Calendar,

        [parameter(Position = 3)]
        [ValidateSet('Friday', 'Saturday', 'Sunday', 'Monday')]
        [String]$FirstDayOfWeek,

        [parameter(Position = 4)]
        [Alias('r', 'row')]
        [ValidateRange(1, 6)]
        [Int]$MonthPerRow = 3,

        [parameter(Position = 5)]
        [ValidateSet('None', 'Red', 'Green', 'Blue', 'Yellow', 'Cyan', 'Magenta', 'White', 'Orange', 'Pink')]
        [String]$Highlight,

        [Int]$Before,

        [Int]$After,

        [Switch]$Three,

        [Switch]$DayOfYear,

        [Switch]$Name
    )

    begin {
        $Abort = $false
        if ($PSBoundParameters.ContainsKey('Culture')) {
            try {
                $ThisCulture = New-Object System.Globalization.CultureInfo($Culture) -ErrorAction Stop
                # The above doesn't alway capture a dodgy culture so test further
                $AllCulture = (Get-Culture -ListAvailable).Name
                if ($Culture -notin $AllCulture) {
                    Write-Warning "Invalid culture: '$Culture'. Using the system default culture ($((Get-Culture).Name)). Use 'Get-Culture -ListAvailable'."
                    $ThisCulture = [System.Globalization.CultureInfo]::CurrentCulture
                } 
            }
            catch {
                Write-Warning "Invalid culture specified:'$Culture'. Using the system default culture ($((Get-Culture).Name)). Use 'Get-Culture -ListAvailable'."
                $ThisCulture = [System.Globalization.CultureInfo]::CurrentCulture
            }
            $ThisCalendar = $ThisCulture.Calendar
        }
        elseif ($PSBoundParameters.ContainsKey('Calendar')) {
            $CultureLookup = @{
                'Gregorian'         = 'en-AU'
                'Persian'           = 'fa-IR'
                'Hijri'             = 'ar-SA'
                'Hebrew'            = 'he-IL'
                'Japanese'          = 'ja-JP'
                'Korean'            = 'ko-KR'
                'Taiwan'            = 'zh-Hant-TW'
                'UmAlQura'          = 'ar-SA'
                'ThaiBuddhist'      = 'th-th'
                'Julian'            = 'en-AU'
                'ChineseLunisolar'  = 'zh'
                'JapaneseLunisolar' = 'ja'
                'KoreanLunisolar'   = 'ko'
                'TaiwanLunisolar'   = 'zh-Hant-TW'
            }
            <#
                In order to support Julian and Asian Lunar calendars ('non-optional'), treat culture and calendar
                separately. With optional calenders you can set the culture to use them, but this doesn't work for
                the above.
            #>

            $ThisCulture = New-Object System.Globalization.CultureInfo($($CultureLookup[$Calendar]))
            $ThisCalendar = New-Object "System.Globalization.$($Calendar)Calendar"
        }
        else {
            $ThisCulture = [System.Globalization.CultureInfo]::CurrentCulture
            $ThisCalendar = $ThisCulture.Calendar
        }

        # Display the Culture/Calendar name as a heading
        if ($PSBoundParameters.ContainsKey('Name') -and $PSBoundParameters.ContainsKey('Calendar')) {
            $CalendarString = $($ThisCalendar.ToString().Replace('System.Globalization.', '').Replace('Calendar', ' Calendar'))   
            Write-Output "`n$($PSStyle.Reverse)--|$CalendarString|--$($PSStyle.ReverseOff)`n"
        }
        elseif ($PSBoundParameters.ContainsKey('Name')) {
            $CalendarString = $($ThisCulture.Calendar.ToString().Replace('System.Globalization.', '').Replace('Calendar', ' Calendar'))
            Write-Output "`n$($PSStyle.Reverse)--|$($ThisCulture.Name)|-|$($ThisCulture.DisplayName)|-|$CalendarString|--$($PSStyle.ReverseOff)`n"
        }

        # Full month names in current culture
        $MonthNameArray = $ThisCulture.DateTimeFormat.MonthGenitiveNames

        # Calling DayOfYear 'Julian' is incorrect. JulianDay is something else altogether. However keep 'Julian' as
        # its more eye-catching.
        if ($PSBoundParameters.ContainsKey('DayOfYear')) {
            [Bool]$JulianSpecified = $true
        }
        else {
            [Bool]$JulianSpecified = $false
        }

        # List of cultural specific day names in the required order.
        if ($PSBoundParameters.ContainsKey('FirstDayOfWeek')) {
            $FirstDay = $FirstDayOfWeek
        }
        else {
            $FirstDay = $ThisCulture.DateTimeFormat.FirstDayOfWeek
        }
        $Param = @{
            'Culture'         = $ThisCulture
            'FirstDayOfWeek'  = $FirstDay
            'JulianSpecified' = $JulianSpecified
        }
        $WeekDay = Get-WeekDayName @Param

        # Get the date of the first day of each required month, based on the culture (common to ncal & cal)
        $DateParam = New-Object -TypeName System.Collections.Hashtable
        $DateParam.Add('Calendar', $ThisCalendar)
        if ($PSBoundParameters.ContainsKey('Month')) {
            $DateParam.Add('Month', $Month)
        }
        if ($PSBoundParameters.ContainsKey('Year')) {
            $DateParam.Add('Year', $Year)
        }
        if ($PSBoundParameters.ContainsKey('Three')) {
            $DateParam.Add('Three', $Three)
        }
        if ($PSBoundParameters.ContainsKey('Before')) {
            $DateParam.Add('Before', $Before)
        }
        if ($PSBoundParameters.ContainsKey('After')) {
            $DateParam.Add('After', $After)
        }
        # this is where most parameter validation occurs, and most of the date conversion stuff.
        try {
            $MonthList = Get-FirstDayOfMonth @DateParam -ErrorAction Stop
        }
        catch {
            Write-Error $PSItem.Exception.Message
            $Abort = $true
        }

        # initialize a strongly typed, fixed length array with no values.
        $MonthRow = New-Object -TypeName System.String[] -ArgumentList 7
        $MonthCount = 0
        $MonthHeading = ''
    }
    process {
        foreach ($RequiredMonth in $MonthList) {
            if ($true -eq $Abort) {
                return
            }
            $ThisYear = $RequiredMonth.Year
            $ThisMonth = $RequiredMonth.Month
            $DayPerMonth = $RequiredMonth.DayPerMonth
            $FirstDayIndex = $RequiredMonth.FirstDayIndex
            $YearSpecified = $RequiredMonth.YearSpecified
            $MonthName = $MonthNameArray[$ThisMonth - 1]  # MonthNameArray is zero based
            if (13 -eq $ThisMonth) {
                $MonthName = '13'
            }
            if ($PSBoundParameters.ContainsKey('Three') -or $PSBoundParameters.ContainsKey('Month') -or $false -eq $YearSpecified) {
                $MonthName = "$MonthName $ThisYear"
            }

            # for highlighting today
            $Pretty = Get-Highlight $ThisCalendar $ThisMonth $ThisYear $Highlight
            if ($PSBoundParameters.ContainsKey('Calendar')) {
                Write-Verbose "monthname = $MonthName, thismonth = $ThisMonth, thisyear = $ThisYear, dayspermonth = $DayPerMonth, monthcount = $MonthCount, calendar = $($ThisCalendar.ToString().Replace('System.Globalization.', '')), era = $($ThisCalendar.Eras[0])"
            }
            else {
                Write-Verbose "monthname = $MonthName, thismonth = $ThisMonth, thisyear = $ThisYear, dayspermonth = $DayPerMonth, monthcount = $MonthCount, culture = $($ThisCulture.Name)"
            }

            # User specified First day of the week, or use the default for the culture being used.
            if ($PSBoundParameters.ContainsKey('FirstDayOfWeek')) {
                if ('Friday' -eq $FirstDayOfWeek) {
                    $StartWeekDay = 'Friday'
                }
                elseif ('Saturday' -eq $FirstDayOfWeek) {
                    $StartWeekDay = 'Saturday'
                }
                elseif ('Sunday' -eq $FirstDayOfWeek) {
                    $StartWeekDay = 'Sunday'
                }
                else {
                    $StartWeekDay = 'Monday'
                }
            }
            else {
                $StartWeekDay = $ThisCulture.DateTimeFormat.FirstDayOfWeek
            }
            
            # Get the starting index for the month, to offset when to start printing dates in the row.
            $Param = @{
                'StartWeekDay'  = $StartWeekDay
                'FirstDayIndex' = $FirstDayIndex
            }
            $ThisIndex = Get-StartWeekIndex @Param

            # User can choose number of months to print per row, default is 3. In this case, just append the month.
            if ($MonthCount -lt $MonthPerRow) {
                $Param = @{
                    'Culture'         = $ThisCulture
                    'MonthName'       = $MonthName
                    'JulianSpecified' = $JulianSpecified
                }
                $MonthHeading += "$(Get-MonthHeading @Param)"
            }
            else {
                # Print a year heading before January when year is specified when the year is not already in month name
                if ($MonthHeading -match "\b$($MonthNameArray[0])\b" -and $MonthName -notmatch $ThisYear) {
                    $YearPad = (((22 * $MonthPerRow) - 2 ) / 2) + 2
                    $YearHeading = "$ThisYear".PadLeft($YearPad, ' ')
                    Write-Output "$($Pretty.MonStyle)$YearHeading$($Pretty.MonReset)"
                }
                Write-Output "$($Pretty.MonStyle)$MonthHeading$($Pretty.MonReset)"
                Write-Output $MonthRow
                Write-Output ''

                # Reset for next row of months
                $MonthRow = New-Object -TypeName System.String[] -ArgumentList 7
                $MonthCount = 0
                $Param = @{
                    'Culture'         = $ThisCulture
                    'MonthName'       = $MonthName
                    'JulianSpecified' = $JulianSpecified
                }
                $MonthHeading = "$(Get-MonthHeading @Param)"
            }

            $MonthRow[0] += "$($WeekDay.Name)" + ' ' # PadRight doesn't work here because of double width chars
            1..6 | ForEach-Object {
                $Param = @{
                    'Calendar'        = $ThisCalendar
                    'Index'           = $ThisIndex
                    'DayPerMonth'     = $DayPerMonth
                    'Month'           = $ThisMonth
                    'Year'            = $ThisYear
                    'Highlight'       = $Pretty
                    'JulianSpecified' = $JulianSpecified
                }
                $MonthRow[$_] += "$(Get-CalRow @Param)"
                $ThisIndex += 7
            }
            $MonthCount++
        }
    }

    end {
        # Write the last month or row of months
        # Print a year heading before Month 1 when year is specified
        if (-Not $Abort) {
            if ($MonthHeading -match "\b$($MonthNameArray[0])\b" -and $MonthName -notmatch $ThisYear) {
                $YearPad = (((22 * $MonthPerRow) - 2 ) / 2) + 2 
                $YearHeading = "$ThisYear".PadLeft($YearPad, ' ')
                Write-Output "$($Pretty.MonStyle)$YearHeading$($Pretty.MonReset)"
            }
            Write-Output "$($Pretty.MonStyle)$MonthHeading$($Pretty.MonReset)"
            Write-Output $MonthRow
        }
    }
}

function Get-Now {
    <#
    .SYNOPSIS
        Get today's date in any of the calendars supported by .NET
    .DESCRIPTION
        Displays today's date in any of the calendars supported by .NET Framework. By default, today's date for
        every supported calendar is shown. The Gregorian calendar is always shown, to compare with the specified
        calendar.
    .LINK
        https://github.com/atkinsroy/ncal/docs
    .EXAMPLE
        Get-Now
         
        Displays today's date in every supported calendar
    .EXAMPLE
        Get-Now -Calendar Persian,Hijri,UmAlQura
 
        Displays today's date for each specified calendar, along with the Gregorian calendar.
    #>


    [CmdletBinding()]
    param (
        [parameter(Position = 0)]
        [ValidateSet(
            'Julian',
            'Hijri',
            'Persian',
            'UmAlQura',
            'ChineseLunisolar',
            'Hebrew',
            'Japanese',
            'JapaneseLunisolar',
            'Korean',
            'KoreanLunisolar',
            'Taiwan',
            'TaiwanLunisolar',
            'ThaiBuddhist'
        )]
        [String[]]$Calendar = @(
            'Julian',
            'Hijri',
            'Persian',
            'UmAlQura',
            'ChineseLunisolar',
            'Hebrew',
            'Japanese',
            'JapaneseLunisolar',
            'Korean',
            'KoreanLunisolar',
            'Taiwan',
            'TaiwanLunisolar',
            'ThaiBuddhist')
    )

    begin {
        $Now = Get-Date
        $Cal = New-Object -TypeName 'System.Globalization.GregorianCalendar'
        $CalMonth = $Cal.GetMonth($Now)
        $CalYear = $Cal.GetYear($Now)
        [PSCustomObject] @{
            'PSTypeName'   = 'Ncal.Date'
            'Calendar'     = 'GregorianCalendar'
            'Day'          = $Cal.GetDayOfMonth($Now)
            'Month'        = $CalMonth
            'Year'         = $CalYear
            'DaysInMonth'  = $Cal.GetDaysInMonth($CalYear, $CalMonth)
            'MonthsInYear' = $Cal.GetMonthsInYear($CalYear)
            'Era'          = $Cal.Eras[0]
        } 
    }

    process {
        foreach ($ThisCalendar in $Calendar) {
            $ThisCalendarString = "$($ThisCalendar)Calendar"
            $Cal = New-Object -TypeName "System.Globalization.$ThisCalendarString"
            $CalMonth = $Cal.GetMonth($Now)
            $CalYear = $Cal.GetYear($Now)
            [PSCustomObject] @{
                'PSTypeName'   = 'Ncal.Date'
                'Calendar'     = $ThisCalendarString
                'Day'          = $Cal.GetDayOfMonth($Now)
                'Month'        = $CalMonth
                'Year'         = $CalYear
                'DaysInMonth'  = $Cal.GetDaysInMonth($CalYear, $CalMonth)
                'MonthsInYear' = $Cal.GetMonthsInYear($CalYear)
                'Era'          = $Cal.Eras[0]
            } 
        }
    }
}
# SIG # Begin signature block
# MIItkwYJKoZIhvcNAQcCoIIthDCCLYACAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCBple+6qZrveQBj
# 2ctij2oOU6K47yYGFXj71Def5ZmBpKCCEXUwggVvMIIEV6ADAgECAhBI/JO0YFWU
# jTanyYqJ1pQWMA0GCSqGSIb3DQEBDAUAMHsxCzAJBgNVBAYTAkdCMRswGQYDVQQI
# DBJHcmVhdGVyIE1hbmNoZXN0ZXIxEDAOBgNVBAcMB1NhbGZvcmQxGjAYBgNVBAoM
# EUNvbW9kbyBDQSBMaW1pdGVkMSEwHwYDVQQDDBhBQUEgQ2VydGlmaWNhdGUgU2Vy
# dmljZXMwHhcNMjEwNTI1MDAwMDAwWhcNMjgxMjMxMjM1OTU5WjBWMQswCQYDVQQG
# EwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMS0wKwYDVQQDEyRTZWN0aWdv
# IFB1YmxpYyBDb2RlIFNpZ25pbmcgUm9vdCBSNDYwggIiMA0GCSqGSIb3DQEBAQUA
# A4ICDwAwggIKAoICAQCN55QSIgQkdC7/FiMCkoq2rjaFrEfUI5ErPtx94jGgUW+s
# hJHjUoq14pbe0IdjJImK/+8Skzt9u7aKvb0Ffyeba2XTpQxpsbxJOZrxbW6q5KCD
# J9qaDStQ6Utbs7hkNqR+Sj2pcaths3OzPAsM79szV+W+NDfjlxtd/R8SPYIDdub7
# P2bSlDFp+m2zNKzBenjcklDyZMeqLQSrw2rq4C+np9xu1+j/2iGrQL+57g2extme
# me/G3h+pDHazJyCh1rr9gOcB0u/rgimVcI3/uxXP/tEPNqIuTzKQdEZrRzUTdwUz
# T2MuuC3hv2WnBGsY2HH6zAjybYmZELGt2z4s5KoYsMYHAXVn3m3pY2MeNn9pib6q
# RT5uWl+PoVvLnTCGMOgDs0DGDQ84zWeoU4j6uDBl+m/H5x2xg3RpPqzEaDux5mcz
# mrYI4IAFSEDu9oJkRqj1c7AGlfJsZZ+/VVscnFcax3hGfHCqlBuCF6yH6bbJDoEc
# QNYWFyn8XJwYK+pF9e+91WdPKF4F7pBMeufG9ND8+s0+MkYTIDaKBOq3qgdGnA2T
# OglmmVhcKaO5DKYwODzQRjY1fJy67sPV+Qp2+n4FG0DKkjXp1XrRtX8ArqmQqsV/
# AZwQsRb8zG4Y3G9i/qZQp7h7uJ0VP/4gDHXIIloTlRmQAOka1cKG8eOO7F/05QID
# AQABo4IBEjCCAQ4wHwYDVR0jBBgwFoAUoBEKIz6W8Qfs4q8p74Klf9AwpLQwHQYD
# VR0OBBYEFDLrkpr/NZZILyhAQnAgNpFcF4XmMA4GA1UdDwEB/wQEAwIBhjAPBgNV
# HRMBAf8EBTADAQH/MBMGA1UdJQQMMAoGCCsGAQUFBwMDMBsGA1UdIAQUMBIwBgYE
# VR0gADAIBgZngQwBBAEwQwYDVR0fBDwwOjA4oDagNIYyaHR0cDovL2NybC5jb21v
# ZG9jYS5jb20vQUFBQ2VydGlmaWNhdGVTZXJ2aWNlcy5jcmwwNAYIKwYBBQUHAQEE
# KDAmMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5jb21vZG9jYS5jb20wDQYJKoZI
# hvcNAQEMBQADggEBABK/oe+LdJqYRLhpRrWrJAoMpIpnuDqBv0WKfVIHqI0fTiGF
# OaNrXi0ghr8QuK55O1PNtPvYRL4G2VxjZ9RAFodEhnIq1jIV9RKDwvnhXRFAZ/ZC
# J3LFI+ICOBpMIOLbAffNRk8monxmwFE2tokCVMf8WPtsAO7+mKYulaEMUykfb9gZ
# pk+e96wJ6l2CxouvgKe9gUhShDHaMuwV5KZMPWw5c9QLhTkg4IUaaOGnSDip0TYl
# d8GNGRbFiExmfS9jzpjoad+sPKhdnckcW67Y8y90z7h+9teDnRGWYpquRRPaf9xH
# +9/DUp/mBlXpnYzyOmJRvOwkDynUWICE5EV7WtgwggXgMIIESKADAgECAhBe0Nem
# O8DFLKPUj+l9xr3tMA0GCSqGSIb3DQEBDAUAMFQxCzAJBgNVBAYTAkdCMRgwFgYD
# VQQKEw9TZWN0aWdvIExpbWl0ZWQxKzApBgNVBAMTIlNlY3RpZ28gUHVibGljIENv
# ZGUgU2lnbmluZyBDQSBSMzYwHhcNMjIwNjA3MDAwMDAwWhcNMjUwNjA2MjM1OTU5
# WjB3MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxKzApBgNVBAoMIkhld2xl
# dHQgUGFja2FyZCBFbnRlcnByaXNlIENvbXBhbnkxKzApBgNVBAMMIkhld2xldHQg
# UGFja2FyZCBFbnRlcnByaXNlIENvbXBhbnkwggGiMA0GCSqGSIb3DQEBAQUAA4IB
# jwAwggGKAoIBgQC6ng2ELduVFspn7zNxeY/AS4Tij830RQECD/dUgsnqHOpnTX60
# CjMefSLxVsqkcIlD4V+deOiSs5JQDnupfshA6pV+tBiN1yWa3gTEX05RO2rYe7bc
# xD1vaitATa1FpZ6n0s1KWP4M1f+UaKMcE6CE4dlCu1KATOXCMD0scA8qj4ScBMZR
# mvZ4S8sNbwrQC5KpzOIHhQmiLRWEMGsDkyDXu6cCwPnsnHvEiNdX9mIBfMnARY5C
# 0oD2ul2lKnjyMgZCWStI7zWXf8P8hU8Ji73+Pep1jeyn3jd/Gveza0fow+wd1fdx
# 1+TS01bf054mbaipE5KKwqUD9XukKhsAEC+WLvTXMCGRpJZasSX2bKnR6ew0aEoX
# c9zQ0BKa4ZFbWbEPceyBOlQVB+2f53Wplrvh+8z1B4yifG/91clQLsP+UZJqzvhX
# E0cTcdb4aHaW/iHZQRAFdID0CBIHMNDTcYuaB+K961mvXYp+CxVDJF5kAT6W5KaM
# QNuooNWjk/rO9FkCAwEAAaOCAYkwggGFMB8GA1UdIwQYMBaAFA8qyyCHKLjsb0iu
# K1SmKaoXpM0MMB0GA1UdDgQWBBRsoQDhdo3sYBJ+aurMJLkY4RrLUDAOBgNVHQ8B
# Af8EBAMCB4AwDAYDVR0TAQH/BAIwADATBgNVHSUEDDAKBggrBgEFBQcDAzBKBgNV
# HSAEQzBBMDUGDCsGAQQBsjEBAgEDAjAlMCMGCCsGAQUFBwIBFhdodHRwczovL3Nl
# Y3RpZ28uY29tL0NQUzAIBgZngQwBBAEwSQYDVR0fBEIwQDA+oDygOoY4aHR0cDov
# L2NybC5zZWN0aWdvLmNvbS9TZWN0aWdvUHVibGljQ29kZVNpZ25pbmdDQVIzNi5j
# cmwweQYIKwYBBQUHAQEEbTBrMEQGCCsGAQUFBzAChjhodHRwOi8vY3J0LnNlY3Rp
# Z28uY29tL1NlY3RpZ29QdWJsaWNDb2RlU2lnbmluZ0NBUjM2LmNydDAjBggrBgEF
# BQcwAYYXaHR0cDovL29jc3Auc2VjdGlnby5jb20wDQYJKoZIhvcNAQEMBQADggGB
# AGSOCQjjqdaLpLxf8u4P14ItH+lKoeDGsIgBPxOMwM4/BAElYOCW5XWzUTI+joRN
# zVi527GkhIrguRQrvS1q0xi9Sg/fTo/XyIMlW1/pssrwOuwUGw/LRSxpu50/Z0I8
# ZbHc55CbBSWC+LIskrxVvy+5nn1WMmyPaf1Hr/Fz2aUx/ILsGnR+vyPPRCaVV+0l
# dOpBuTkuN+fPwcf8lCIrADPyyujLf34MJHbBXhOjiTM9WSoROKGQUJBRufA+vMKr
# 45vPybW36vwMaMqPXLCfVQ/cXazuFZILmpmsn+6snKDkaLcBokZk7TPoKhSYrKLw
# vkPMChrd8vVxyaPAoEkQEOOGDkINNMmkjk2Y2I0XcrB4GiiLC2Vple0tOXH4MIku
# aWeIRxv+yELyNHLHj148LeN22FfucA2BPTbEH4YKOjw2Zbn1BP46QS0B3OlTGvwH
# 6Jsgh71kTGR695wFzcZLvtmInxzhnfZb5fmiSw1mG4rA86/r6x1EpXIN/s1c1q7D
# szCCBhowggQCoAMCAQICEGIdbQxSAZ47kHkVIIkhHAowDQYJKoZIhvcNAQEMBQAw
# VjELMAkGA1UEBhMCR0IxGDAWBgNVBAoTD1NlY3RpZ28gTGltaXRlZDEtMCsGA1UE
# AxMkU2VjdGlnbyBQdWJsaWMgQ29kZSBTaWduaW5nIFJvb3QgUjQ2MB4XDTIxMDMy
# MjAwMDAwMFoXDTM2MDMyMTIzNTk1OVowVDELMAkGA1UEBhMCR0IxGDAWBgNVBAoT
# D1NlY3RpZ28gTGltaXRlZDErMCkGA1UEAxMiU2VjdGlnbyBQdWJsaWMgQ29kZSBT
# aWduaW5nIENBIFIzNjCCAaIwDQYJKoZIhvcNAQEBBQADggGPADCCAYoCggGBAJsr
# nVP6NT+OYAZDasDP9X/2yFNTGMjO02x+/FgHlRd5ZTMLER4ARkZsQ3hAyAKwktlQ
# qFZOGP/I+rLSJJmFeRno+DYDY1UOAWKA4xjMHY4qF2p9YZWhhbeFpPb09JNqFiTC
# Yy/Rv/zedt4QJuIxeFI61tqb7/foXT1/LW2wHyN79FXSYiTxcv+18Irpw+5gcTbX
# nDOsrSHVJYdPE9s+5iRF2Q/TlnCZGZOcA7n9qudjzeN43OE/TpKF2dGq1mVXn37z
# K/4oiETkgsyqA5lgAQ0c1f1IkOb6rGnhWqkHcxX+HnfKXjVodTmmV52L2UIFsf0l
# 4iQ0UgKJUc2RGarhOnG3B++OxR53LPys3J9AnL9o6zlviz5pzsgfrQH4lrtNUz4Q
# q/Va5MbBwuahTcWk4UxuY+PynPjgw9nV/35gRAhC3L81B3/bIaBb659+Vxn9kT2j
# Uztrkmep/aLb+4xJbKZHyvahAEx2XKHafkeKtjiMqcUf/2BG935A591GsllvWwID
# AQABo4IBZDCCAWAwHwYDVR0jBBgwFoAUMuuSmv81lkgvKEBCcCA2kVwXheYwHQYD
# VR0OBBYEFA8qyyCHKLjsb0iuK1SmKaoXpM0MMA4GA1UdDwEB/wQEAwIBhjASBgNV
# HRMBAf8ECDAGAQH/AgEAMBMGA1UdJQQMMAoGCCsGAQUFBwMDMBsGA1UdIAQUMBIw
# BgYEVR0gADAIBgZngQwBBAEwSwYDVR0fBEQwQjBAoD6gPIY6aHR0cDovL2NybC5z
# ZWN0aWdvLmNvbS9TZWN0aWdvUHVibGljQ29kZVNpZ25pbmdSb290UjQ2LmNybDB7
# BggrBgEFBQcBAQRvMG0wRgYIKwYBBQUHMAKGOmh0dHA6Ly9jcnQuc2VjdGlnby5j
# b20vU2VjdGlnb1B1YmxpY0NvZGVTaWduaW5nUm9vdFI0Ni5wN2MwIwYIKwYBBQUH
# MAGGF2h0dHA6Ly9vY3NwLnNlY3RpZ28uY29tMA0GCSqGSIb3DQEBDAUAA4ICAQAG
# /4Lhd2M2bnuhFSCbE/8E/ph1RGHDVpVx0ZE/haHrQECxyNbgcv2FymQ5PPmNS6Da
# h66dtgCjBsULYAor5wxxcgEPRl05pZOzI3IEGwwsepp+8iGsLKaVpL3z5CmgELIq
# mk/Q5zFgR1TSGmxqoEEhk60FqONzDn7D8p4W89h8sX+V1imaUb693TGqWp3T32IK
# GfIgy9jkd7GM7YCa2xulWfQ6E1xZtYNEX/ewGnp9ZeHPsNwwviJMBZL4xVd40uPW
# UnOJUoSiugaz0yWLODRtQxs5qU6E58KKmfHwJotl5WZ7nIQuDT0mWjwEx7zSM7fs
# 9Tx6N+Q/3+49qTtUvAQsrEAxwmzOTJ6Jp6uWmHCgrHW4dHM3ITpvG5Ipy62KyqYo
# vk5O6cC+040Si15KJpuQ9VJnbPvqYqfMB9nEKX/d2rd1Q3DiuDexMKCCQdJGpOqU
# sxLuCOuFOoGbO7Uv3RjUpY39jkkp0a+yls6tN85fJe+Y8voTnbPU1knpy24wUFBk
# fenBa+pRFHwCBB1QtS+vGNRhsceP3kSPNrrfN2sRzFYsNfrFaWz8YOdU254qNZQf
# d9O/VjxZ2Gjr3xgANHtM3HxfzPYF6/pKK8EE4dj66qKKtm2DTL1KFCg/OYJyfrdL
# Jq1q2/HXntgr2GVw+ZWhrWgMTn8v1SjZsLlrgIfZHDGCG3QwghtwAgEBMGgwVDEL
# MAkGA1UEBhMCR0IxGDAWBgNVBAoTD1NlY3RpZ28gTGltaXRlZDErMCkGA1UEAxMi
# U2VjdGlnbyBQdWJsaWMgQ29kZSBTaWduaW5nIENBIFIzNgIQXtDXpjvAxSyj1I/p
# fca97TANBglghkgBZQMEAgEFAKB8MBAGCisGAQQBgjcCAQwxAjAAMBkGCSqGSIb3
# DQEJAzEMBgorBgEEAYI3AgEEMBwGCisGAQQBgjcCAQsxDjAMBgorBgEEAYI3AgEV
# MC8GCSqGSIb3DQEJBDEiBCAZRtoSN6B5mLfRQT89reJGC0z1NhJjcl8WQPZ1FsnA
# sTANBgkqhkiG9w0BAQEFAASCAYCXI+pEVle+JgCEqs98IASdtf1gOYTfVMM5zeFP
# SmfwXISFWlrXMMeOeGQGX6d9dcvsanFCq6Nbh4GcAscrv3h9yvHV6E+r7hksfrwb
# kuNgY8RhsBkv+uogZwoj29KmDTIQkZ3zViGo+8Q7PoxxTQm3K6Y9iqVB2vk39Qh8
# fiugEwAfVfwiKmYYB28BIKyKy7vIqY8nAyEQWC/PZpAGbafLMqTr2OZ4Dk1CMpRu
# f7svIkyTwk2l1a9SZftuPFCWtbcxMj6blQWwfsYO2OzrW+RjHjLrZ3+/+39gwga4
# GMIc5lzT82td3452oFUKmns3h9nUbbZu6vHU5l5iSSuJ+JubLvIrKc5HPNJwU1FP
# lpkCgaVzvB3oU11yawYxjHxRIovegieu0fbkjpmubXCElpEQgAxhoj7Xlg3XHwoE
# rsBLsTHmFg7GizeoUkSldmgJnLZr+NnU3hz7+oqqeqVBB6Gj7cWSfHzZUQCA9TXP
# 8nGFGKdMzLiPJEkZ2U0rFGIoPsWhghjfMIIY2wYKKwYBBAGCNwMDATGCGMswghjH
# BgkqhkiG9w0BBwKgghi4MIIYtAIBAzEPMA0GCWCGSAFlAwQCAgUAMIIBBAYLKoZI
# hvcNAQkQAQSggfQEgfEwge4CAQEGCisGAQQBsjECAQEwQTANBglghkgBZQMEAgIF
# AAQwVXaZ7crxv2kS+aSxOsY1WlTyVReit1kV58ODJJee7UjwnyJMzU1cojCBwU6r
# Kb2MAhUArdorHomUjy04bcAMEerFxE9LjIcYDzIwMjUwMjI1MDIwNjU2WqBypHAw
# bjELMAkGA1UEBhMCR0IxEzARBgNVBAgTCk1hbmNoZXN0ZXIxGDAWBgNVBAoTD1Nl
# Y3RpZ28gTGltaXRlZDEwMC4GA1UEAxMnU2VjdGlnbyBQdWJsaWMgVGltZSBTdGFt
# cGluZyBTaWduZXIgUjM1oIIS/zCCBl0wggTFoAMCAQICEDpSaiyEzlXmHWX8zBLY
# 6YkwDQYJKoZIhvcNAQEMBQAwVTELMAkGA1UEBhMCR0IxGDAWBgNVBAoTD1NlY3Rp
# Z28gTGltaXRlZDEsMCoGA1UEAxMjU2VjdGlnbyBQdWJsaWMgVGltZSBTdGFtcGlu
# ZyBDQSBSMzYwHhcNMjQwMTE1MDAwMDAwWhcNMzUwNDE0MjM1OTU5WjBuMQswCQYD
# VQQGEwJHQjETMBEGA1UECBMKTWFuY2hlc3RlcjEYMBYGA1UEChMPU2VjdGlnbyBM
# aW1pdGVkMTAwLgYDVQQDEydTZWN0aWdvIFB1YmxpYyBUaW1lIFN0YW1waW5nIFNp
# Z25lciBSMzUwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCN0Wf0wUib
# vf04STpNYYGbw9jcRaVhBDaNBp7jmJaA9dQZW5ighrXGNMYjK7Dey5RIHMqLIbT9
# z9if753mYbojJrKWO4ZP0N5dBT2TwZZaPb8E+hqaDZ8Vy2c+x1NiEwbEzTrPX4W3
# QFq/zJvDDbWKL99qLL42GJQzX3n5wWo60KklfFn+Wb22mOZWYSqkCVGl8aYuE12S
# qIS4MVO4PUaxXeO+4+48YpQlNqbc/ndTgszRQLF4MjxDPjRDD1M9qvpLTZcTGVzx
# fViyIToRNxPP6DUiZDU6oXARrGwyP9aglPXwYbkqI2dLuf9fiIzBugCDciOly8TP
# DgBkJmjAfILNiGcVEzg+40xUdhxNcaC+6r0juPiR7bzXHh7v/3RnlZuT3ZGstxLf
# mE7fRMAFwbHdDz5gtHLqjSTXDiNF58IxPtvmZPG2rlc+Yq+2B8+5pY+QZn+1vEif
# I0MDtiA6BxxQuOnj4PnqDaK7NEKwtD1pzoA3jJFuoJiwbatwhDkg1PIjYnMDbDW+
# wAc9FtRN6pUsO405jaBgigoFZCw9hWjLNqgFVTo7lMb5rVjJ9aSBVVL2dcqzyFW2
# LdWk5Xdp65oeeOALod7YIIMv1pbqC15R7QCYLxcK1bCl4/HpBbdE5mjy9JR70BHu
# Yx27n4XNOZbwrXcG3wZf9gEUk7stbPAoBQIDAQABo4IBjjCCAYowHwYDVR0jBBgw
# FoAUX1jtTDF6omFCjVKAurNhlxmiMpswHQYDVR0OBBYEFGjvpDJJabZSOB3qQzks
# 9BRqngyFMA4GA1UdDwEB/wQEAwIGwDAMBgNVHRMBAf8EAjAAMBYGA1UdJQEB/wQM
# MAoGCCsGAQUFBwMIMEoGA1UdIARDMEEwNQYMKwYBBAGyMQECAQMIMCUwIwYIKwYB
# BQUHAgEWF2h0dHBzOi8vc2VjdGlnby5jb20vQ1BTMAgGBmeBDAEEAjBKBgNVHR8E
# QzBBMD+gPaA7hjlodHRwOi8vY3JsLnNlY3RpZ28uY29tL1NlY3RpZ29QdWJsaWNU
# aW1lU3RhbXBpbmdDQVIzNi5jcmwwegYIKwYBBQUHAQEEbjBsMEUGCCsGAQUFBzAC
# hjlodHRwOi8vY3J0LnNlY3RpZ28uY29tL1NlY3RpZ29QdWJsaWNUaW1lU3RhbXBp
# bmdDQVIzNi5jcnQwIwYIKwYBBQUHMAGGF2h0dHA6Ly9vY3NwLnNlY3RpZ28uY29t
# MA0GCSqGSIb3DQEBDAUAA4IBgQCw3C7J+k82TIov9slP1e8YTx+fDsa//hJ62Y6S
# Mr2E89rv82y/n8we5W6z5pfBEWozlW7nWp+sdPCdUTFw/YQcqvshH6b9Rvs9qZp5
# Z+V7nHwPTH8yzKwgKzTTG1I1XEXLAK9fHnmXpaDeVeI8K6Lw3iznWZdLQe3zl+Re
# jdq5l2jU7iUfMkthfhFmi+VVYPkR/BXpV7Ub1QyyWebqkjSHJHRmv3lBYbQyk08/
# S7TlIeOr9iQ+UN57fJg4QI0yqdn6PyiehS1nSgLwKRs46T8A6hXiSn/pCXaASnds
# 0LsM5OVoKYfbgOOlWCvKfwUySWoSgrhncihSBXxH2pAuDV2vr8GOCEaePZc0Dy6O
# 1rYnKjGmqm/IRNkJghSMizr1iIOPN+23futBXAhmx8Ji/4NTmyH9K0UvXHiuA2Pa
# 3wZxxR9r9XeIUVb2V8glZay+2ULlc445CzCvVSZV01ZB6bgvCuUuBx079gCcepjn
# ZDCcEuIC5Se4F6yFaZ8RvmiJ4hgwggYUMIID/KADAgECAhB6I67aU2mWD5HIPlz0
# x+M/MA0GCSqGSIb3DQEBDAUAMFcxCzAJBgNVBAYTAkdCMRgwFgYDVQQKEw9TZWN0
# aWdvIExpbWl0ZWQxLjAsBgNVBAMTJVNlY3RpZ28gUHVibGljIFRpbWUgU3RhbXBp
# bmcgUm9vdCBSNDYwHhcNMjEwMzIyMDAwMDAwWhcNMzYwMzIxMjM1OTU5WjBVMQsw
# CQYDVQQGEwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMSwwKgYDVQQDEyNT
# ZWN0aWdvIFB1YmxpYyBUaW1lIFN0YW1waW5nIENBIFIzNjCCAaIwDQYJKoZIhvcN
# AQEBBQADggGPADCCAYoCggGBAM2Y2ENBq26CK+z2M34mNOSJjNPvIhKAVD7vJq+M
# DoGD46IiM+b83+3ecLvBhStSVjeYXIjfa3ajoW3cS3ElcJzkyZlBnwDEJuHlzpbN
# 4kMH2qRBVrjrGJgSlzzUqcGQBaCxpectRGhhnOSwcjPMI3G0hedv2eNmGiUbD12O
# eORN0ADzdpsQ4dDi6M4YhoGE9cbY11XxM2AVZn0GiOUC9+XE0wI7CQKfOUfigLDn
# 7i/WeyxZ43XLj5GVo7LDBExSLnh+va8WxTlA+uBvq1KO8RSHUQLgzb1gbL9Ihgzx
# mkdp2ZWNuLc+XyEmJNbD2OIIq/fWlwBp6KNL19zpHsODLIsgZ+WZ1AzCs1HEK6VW
# rxmnKyJJg2Lv23DlEdZlQSGdF+z+Gyn9/CRezKe7WNyxRf4e4bwUtrYE2F5Q+05y
# DD68clwnweckKtxRaF0VzN/w76kOLIaFVhf5sMM/caEZLtOYqYadtn034ykSFaZu
# IBU9uCSrKRKTPJhWvXk4CllgrwIDAQABo4IBXDCCAVgwHwYDVR0jBBgwFoAU9ndq
# 3T/9ARP/FqFsggIv0Ao9FCUwHQYDVR0OBBYEFF9Y7UwxeqJhQo1SgLqzYZcZojKb
# MA4GA1UdDwEB/wQEAwIBhjASBgNVHRMBAf8ECDAGAQH/AgEAMBMGA1UdJQQMMAoG
# CCsGAQUFBwMIMBEGA1UdIAQKMAgwBgYEVR0gADBMBgNVHR8ERTBDMEGgP6A9hjto
# dHRwOi8vY3JsLnNlY3RpZ28uY29tL1NlY3RpZ29QdWJsaWNUaW1lU3RhbXBpbmdS
# b290UjQ2LmNybDB8BggrBgEFBQcBAQRwMG4wRwYIKwYBBQUHMAKGO2h0dHA6Ly9j
# cnQuc2VjdGlnby5jb20vU2VjdGlnb1B1YmxpY1RpbWVTdGFtcGluZ1Jvb3RSNDYu
# cDdjMCMGCCsGAQUFBzABhhdodHRwOi8vb2NzcC5zZWN0aWdvLmNvbTANBgkqhkiG
# 9w0BAQwFAAOCAgEAEtd7IK0ONVgMnoEdJVj9TC1ndK/HYiYh9lVUacahRoZ2W2hf
# iEOyQExnHk1jkvpIJzAMxmEc6ZvIyHI5UkPCbXKspioYMdbOnBWQUn733qMooBfI
# ghpR/klUqNxx6/fDXqY0hSU1OSkkSivt51UlmJElUICZYBodzD3M/SFjeCP59anw
# xs6hwj1mfvzG+b1coYGnqsSz2wSKr+nDO+Db8qNcTbJZRAiSazr7KyUJGo1c+MSc
# GfG5QHV+bps8BX5Oyv9Ct36Y4Il6ajTqV2ifikkVtB3RNBUgwu/mSiSUice/Jp/q
# 8BMk/gN8+0rNIE+QqU63JoVMCMPY2752LmESsRVVoypJVt8/N3qQ1c6FibbcRabo
# 3azZkcIdWGVSAdoLgAIxEKBeNh9AQO1gQrnh1TA8ldXuJzPSuALOz1Ujb0PCyNVk
# Wk7hkhVHfcvBfI8NtgWQupiaAeNHe0pWSGH2opXZYKYG4Lbukg7HpNi/KqJhue2K
# eak6qH9A8CeEOB7Eob0Zf+fU+CCQaL0cJqlmnx9HCDxF+3BLbUufrV64EbTI40zq
# egPZdA+sXCmbcZy6okx/SjwsusWRItFA3DE8MORZeFb6BmzBtqKJ7l939bbKBy2j
# vxcJI98Va95Q5JnlKor3m0E7xpMeYRriWklUPsetMSf2NvUQa/E5vVyefQIwggaC
# MIIEaqADAgECAhA2wrC9fBs656Oz3TbLyXVoMA0GCSqGSIb3DQEBDAUAMIGIMQsw
# CQYDVQQGEwJVUzETMBEGA1UECBMKTmV3IEplcnNleTEUMBIGA1UEBxMLSmVyc2V5
# IENpdHkxHjAcBgNVBAoTFVRoZSBVU0VSVFJVU1QgTmV0d29yazEuMCwGA1UEAxMl
# VVNFUlRydXN0IFJTQSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0yMTAzMjIw
# MDAwMDBaFw0zODAxMTgyMzU5NTlaMFcxCzAJBgNVBAYTAkdCMRgwFgYDVQQKEw9T
# ZWN0aWdvIExpbWl0ZWQxLjAsBgNVBAMTJVNlY3RpZ28gUHVibGljIFRpbWUgU3Rh
# bXBpbmcgUm9vdCBSNDYwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCI
# ndi5RWedHd3ouSaBmlRUwHxJBZvMWhUP2ZQQRLRBQIF3FJmp1OR2LMgIU14g0JIl
# L6VXWKmdbmKGRDILRxEtZdQnOh2qmcxGzjqemIk8et8sE6J+N+Gl1cnZocew8eCA
# awKLu4TRrCoqCAT8uRjDeypoGJrruH/drCio28aqIVEn45NZiZQI7YYBex48eL78
# lQ0BrHeSmqy1uXe9xN04aG0pKG9ki+PC6VEfzutu6Q3IcZZfm00r9YAEp/4aeiLh
# yaKxLuhKKaAdQjRaf/h6U13jQEV1JnUTCm511n5avv4N+jSVwd+Wb8UMOs4netap
# q5Q/yGyiQOgjsP/JRUj0MAT9YrcmXcLgsrAimfWY3MzKm1HCxcquinTqbs1Q0d2V
# MMQyi9cAgMYC9jKc+3mW62/yVl4jnDcw6ULJsBkOkrcPLUwqj7poS0T2+2JMzPP+
# jZ1h90/QpZnBkhdtixMiWDVgh60KmLmzXiqJc6lGwqoUqpq/1HVHm+Pc2B6+wCy/
# GwCcjw5rmzajLbmqGygEgaj/OLoanEWP6Y52Hflef3XLvYnhEY4kSirMQhtberRv
# aI+5YsD3XVxHGBjlIli5u+NrLedIxsE88WzKXqZjj9Zi5ybJL2WjeXuOTbswB7Xj
# kZbErg7ebeAQUQiS/uRGZ58NHs57ZPUfECcgJC+v2wIDAQABo4IBFjCCARIwHwYD
# VR0jBBgwFoAUU3m/WqorSs9UgOHYm8Cd8rIDZsswHQYDVR0OBBYEFPZ3at0//QET
# /xahbIICL9AKPRQlMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MBMG
# A1UdJQQMMAoGCCsGAQUFBwMIMBEGA1UdIAQKMAgwBgYEVR0gADBQBgNVHR8ESTBH
# MEWgQ6BBhj9odHRwOi8vY3JsLnVzZXJ0cnVzdC5jb20vVVNFUlRydXN0UlNBQ2Vy
# dGlmaWNhdGlvbkF1dGhvcml0eS5jcmwwNQYIKwYBBQUHAQEEKTAnMCUGCCsGAQUF
# BzABhhlodHRwOi8vb2NzcC51c2VydHJ1c3QuY29tMA0GCSqGSIb3DQEBDAUAA4IC
# AQAOvmVB7WhEuOWhxdQRh+S3OyWM637ayBeR7djxQ8SihTnLf2sABFoB0DFR6JfW
# S0snf6WDG2gtCGflwVvcYXZJJlFfym1Doi+4PfDP8s0cqlDmdfyGOwMtGGzJ4iIm
# yaz3IBae91g50QyrVbrUoT0mUGQHbRcF57olpfHhQEStz5i6hJvVLFV/ueQ21SM9
# 9zG4W2tB1ExGL98idX8ChsTwbD/zIExAopoe3l6JrzJtPxj8V9rocAnLP2C8Q5wX
# VVZcbw4x4ztXLsGzqZIiRh5i111TW7HV1AtsQa6vXy633vCAbAOIaKcLAo/IU7sC
# lyZUk62XD0VUnHD+YvVNvIGezjM6CRpcWed/ODiptK+evDKPU2K6synimYBaNH49
# v9Ih24+eYXNtI38byt5kIvh+8aW88WThRpv8lUJKaPn37+YHYafob9Rg7LyTrSYp
# yZoBmwRWSE4W6iPjB7wJjJpH29308ZkpKKdpkiS9WNsf/eeUtvRrtIEiSJHN899L
# 1P4l6zKVsdrUu1FX1T/ubSrsxrYJD+3f3aKg6yxdbugot06YwGXXiy5UUGZvOu3l
# XlxA+fC13dQ5OlL2gIb5lmF6Ii8+CQOYDwXM+yd9dbmocQsHjcRPsccUd5E9Fisw
# EqORvz8g3s+jR3SFCgXhN4wz7NgAnOgpCdUo4uDyllU9PzGCBJEwggSNAgEBMGkw
# VTELMAkGA1UEBhMCR0IxGDAWBgNVBAoTD1NlY3RpZ28gTGltaXRlZDEsMCoGA1UE
# AxMjU2VjdGlnbyBQdWJsaWMgVGltZSBTdGFtcGluZyBDQSBSMzYCEDpSaiyEzlXm
# HWX8zBLY6YkwDQYJYIZIAWUDBAICBQCgggH5MBoGCSqGSIb3DQEJAzENBgsqhkiG
# 9w0BCRABBDAcBgkqhkiG9w0BCQUxDxcNMjUwMjI1MDIwNjU2WjA/BgkqhkiG9w0B
# CQQxMgQwOSsOWBw92WTeaip53xtNvFK93j4caSm67B7L2W1yYkoxnOEZUbT1GBUm
# 5LB9bHA6MIIBegYLKoZIhvcNAQkQAgwxggFpMIIBZTCCAWEwFgQU+GCYGab7iCz3
# 6FKX8qEZUhoWd18wgYcEFMauVOR4hvF8PVUSSIxpw0p6+cLdMG8wW6RZMFcxCzAJ
# BgNVBAYTAkdCMRgwFgYDVQQKEw9TZWN0aWdvIExpbWl0ZWQxLjAsBgNVBAMTJVNl
# Y3RpZ28gUHVibGljIFRpbWUgU3RhbXBpbmcgUm9vdCBSNDYCEHojrtpTaZYPkcg+
# XPTH4z8wgbwEFIU9Yy2TgoJhfNCQNcSR3pLBQtrHMIGjMIGOpIGLMIGIMQswCQYD
# VQQGEwJVUzETMBEGA1UECBMKTmV3IEplcnNleTEUMBIGA1UEBxMLSmVyc2V5IENp
# dHkxHjAcBgNVBAoTFVRoZSBVU0VSVFJVU1QgTmV0d29yazEuMCwGA1UEAxMlVVNF
# UlRydXN0IFJTQSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eQIQNsKwvXwbOuejs902
# y8l1aDANBgkqhkiG9w0BAQEFAASCAgA56lxPJXc4yicbH//CMAJcrNpdNYQok2oc
# w6YdAFiZjFa8ECkHq9+cprS2wY5LkZr7+JATMGmAIdxcqu/1iHqByceodVhPnCBt
# q6E3wUDz3pk3ZwNWekc8L911CheTHQm83dwAkvwDM1O19jZawxsO2bgL5ZzLBUKl
# Hoa7iiYx8rW6+dS4+nJW0jHk+7/XZ+auJFquzdZuhZQGZy1gM7Hj5No/f4Ziw17r
# camKnXhDS1A91+oXURSf2YLrg7zQ/bJ885lB6PfmYoNOFlaPQb45mO2sRXXwHh9c
# uLHsRhM896XZdlsbvbGKQt+TRI1w2U1ECVF8YnSqZKUKZ41q1+hsSL8vWfN5KRjF
# qZfIk/6v6U885n5hIfoAQKlB9ztAvzzqNEurArkn9FoIc/kDMZyovn/EebMWbtmR
# hPB44J5It7B+N2pmvnC4k4auTITaEO2AJJ5YTBDsQCQbscmYLa/3s5m5etBRQjbD
# CHJN/0Ipa+XdCW4FPuk1fKvyVDS1UxsdMlP9nJCMPlrw2TkwKGWqwYtcg696PBBd
# eWUKAb8L38viWLK/bLtuNiCIjMWXAfv217Pg4FiPQoW+PR+YzCkrcT1Z84bxqyiY
# U5FFviAanfuorqWrjTuL8z+yE/QZYAqXfn/TCrI9E5su0ayS0vjnSCicS3Ra8UuL
# p6qvHe8eVg==
# SIG # End signature block