<# Public domain. Like any other piece of software (and information generally), this script comes with NO WARRANTY. This is version 20210426 of holidays.ps1. #> <# .SYNOPSIS Print a list of holidays for a given year and other good times to take a day off from work .DESCRIPTION Days off aren't just for weekends. Use this calendar of holidays to figure out the best days to take off from work. .PARAMETER Year The desired year to use when calculating holidays. .PARAMETER TimeZone The target time zone to use when calculating when Daylight Saving Time begins. #> Param( [Parameter(Mandatory=$False)] [UInt64] $Year = (Get-Date).Year, [Parameter(Mandatory=$False)] [String] $TimeZone = 'Pacific Standard Time' ) $ErrorActionPreference = 'Stop' Set-StrictMode -Version:2 $holidays = @( @{ 'Name' = "New Year's"; 'Month' = 1; 'Day' = 1; 'SpanFuture' = 1; }, @{ 'Name' = 'Martin Luther King, Jr. Weekend'; 'Month' = 1; 'DayOfWeek' = 1; 'OrdinalWeek' = 3; 'SpanFuture' = 1; 'SpanPast' = 1; }, @{ 'Name' = "Valentine's Eve"; 'Month' = 2; 'Day' = 13; }, @{ 'Name' = "Presidents' Day Weekend"; 'Month' = 2; 'DayOfWeek' = 1; 'OrdinalWeek' = 3; 'SpanFuture' = 0; 'SpanPast' = 1; }, @{ 'Name' = 'Leap Day'; 'Month' = 2; 'Day' = 29; }, @{ 'Name' = "St. Patrick's Day"; 'Month' = 3; 'Day' = '17'; }, @{ 'Name' = "Monday after Mother's Day"; 'Month' = 5; 'DayOfWeek' = 0; 'OrdinalWeek' = 2; 'SpanFuture' = 1; 'SpanPast' = 0; }, @{ 'Name' = "Memorial Day Weekend"; 'Month' = 5; 'DayOfWeek' = 1; 'OrdinalWeek' = 'L'; 'SpanFuture' = 1; 'SpanPast' = 1; }, @{ 'Name' = "Monday after Father's Day"; 'Month' = 6; 'DayOfWeek' = 0; 'OrdinalWeek' = 3; 'SpanFuture' = 1; 'SpanPast' = 0; }, @{ 'Name' = 'Independence Day'; 'Month' = 7; 'Day' = 4; 'SpanFuture' = 1; 'SpanPast' = 1; }, @{ 'Name' = 'Labor Day Weekend'; 'Month' = 9; 'DayOfWeek' = 1; 'OrdinalWeek' = 1; 'SpanFuture' = 1; 'SpanPast' = 1; }, @{ 'Name' = 'Columbus Day Weekend'; 'Month' = 10; 'DayOfWeek' = 1; 'OrdinalWeek' = 2; 'SpanFuture' = 1; 'SpanPast' = 1; }, @{ 'Name' = 'Halloween Eve'; 'Month' = 10; 'Day' = 30; }, @{ 'Name' = 'Veterans Day'; 'Month' = 11; 'Day' = 11; 'SpanFuture' = 1; 'SpanPast' = 1; }, @{ 'Name' = 'Thanksgiving Weekend'; 'Month' = 11; 'DayOfWeek' = 4; 'OrdinalWeek' = 4; 'SpanFuture' = 2; 'SpanPast' = 1; } @{ 'Name' = 'Christmas'; 'Month' = 12; 'Day' = 25; 'SpanFuture' = 1; 'SpanPast' = 1; } @{ 'Name' = "New Year's Eve"; 'Month' = 12; 'Day' = 31; } ) # Ported from libtai-0.60 Function Easter { Param( [Parameter(Mandatory=$False)] [UInt64] $Year ) [Int32] $c = [Math]::Truncate($Year / 100) + 1 [Int32] $j = $Year % 19 [Int32] $t = 210 - ([Math]::Truncate(($c * 3) / 4) % 210) [Int32] $n = 57 - ((14 + $j * 11 + [Math]::Truncate(($c * 8 + 5) / 25) + $t) % 30) if (($n -eq 56) -and ($j -gt 10)) { $n-- } if ($n -eq 57) { $n-- } $n -= ([Math]::Truncate((($Year % 28) * 5) / 4) + $t + $n + 2) % 7 if ($n -lt 32) { $month = 3 $day = $n } else { $month = 4 $day = $n - 31 } $d = New-Object DateTime $Year, $month, $day Return $d } Function Main { # Easter is always a Sunday $d = Easter -Year:$Year $holidays += @{ 'Name' = 'Easter Weekend'; 'Month' = $d.Month; 'Day' = $d.Day; 'SpanFuture' = 1; 'SpanPast' = 1; } # DST sucks $tz = [System.TimeZoneInfo]::FindSystemTimeZoneById($TimeZone) foreach ($rule in $tz.GetAdjustmentRules()) { if (($Year -ge $rule.DateStart.Year) -and ($Year -lt $rule.DateEnd.Year)) { if (-not $rule.DaylightTransitionStart.IsFixedDateRule) { $s = $rule.DaylightTransitionStart $holidays += @{ 'Name' = 'Monday after Daylight Saving Time'; 'Month' = $s.Month; 'OrdinalWeek' = $s.Week; 'DayOfWeek' = $s.Day - 1; 'SpanFuture' = 1; } } } } $h = @() :LOOP foreach ($holiday in $holidays) { if (-not ([String]::IsNullOrEmpty($holiday['Day']))) { try { $t = New-Object DateTime $Year, $holiday['Month'], $holiday['Day'] } catch { continue } } if (-not ([String]::IsNullOrEmpty($holiday['DayOfWeek']))) { $t = New-Object DateTime $Year, $holiday['Month'], 1 if ('L' -eq $holiday['OrdinalWeek']) { $t = $t.AddMonths(1) } $n = [Byte] $t.DayOfWeek # 0 = Sun, 1 = Mon, 2 = Tue, etc. $o = ((7 + $holiday['DayOfWeek']) - $n) % 7 if ('L' -eq $holiday['OrdinalWeek']) { $t = $t.AddDays(-1 * (7 - $o)) } else { $t = $t.AddDays($o + (7 * ($holiday['OrdinalWeek']-1))) } } [Byte] $dow = $t.DayOfWeek if ((0 -ne $dow) -and (6 -ne $dow)) { $h += New-Object -TypeName:'PSObject' -Property:@{ 'Name' = $holiday['Name']; 'DT' = $t; 'Date' = $t.ToString('D'); } } if (-not ([String]::IsNullOrEmpty($holiday['SpanFuture']))) { $i = 0; $u = $t while ($i -lt $holiday['SpanFuture']) { $u = $u.AddDays(1) if ((0 -lt [Byte]$u.DayOfWeek) -and (6 -gt [Byte]$u.DayOfWeek)) { $h += New-Object -TypeName:'PSObject' -Property:@{ 'Name' = $holiday['Name']; 'DT' = $u; 'Date' = $u.ToString("D"); } $i++ } } } if (-not ([String]::IsNullOrEmpty($holiday['SpanPast']))) { $i = 0; $u = $t while ($i -lt $holiday['SpanPast']) { $u = $u.AddDays(-1) if ((0 -lt [Byte]$u.DayOfWeek) -and (6 -gt [Byte]$u.DayOfWeek)) { $h += New-Object -TypeName:'PSObject' -Property:@{ 'Name' = $holiday['Name']; 'DT' = $u; 'Date' = $u.ToString("D"); } $i++ } } } } # LOOP $out = @() $h | foreach { $out += New-Object -TypeName:PSObject -Property:@{ 'Name' = $_.Name; 'DT' = $_.DT; 'Date' = $_.DT.ToString('D'); } } Return ($out | Sort-Object -Property:'DT' | Select-Object -Property:Name,Date) } . Main Exit