PSDates.psm1

#Region '.\Classes\DateTimeExtended.ps1' -1

class DateTimeExtended {
    [datetime]$DateTime
    [datetime]$FirstDayOfYear
    [datetime]$LastDayOfYear
    [datetime]$StartOfWeek
    [datetime]$EndOfWeek
    [datetime]$StartOfMonth
    [datetime]$EndOfMonth
    [string]$WeekOfYear
    [System.TimeZoneInfo]$TimeZone
    [int]$Quater
    [datetime]$Date
    [int]$Day
    [System.DayOfWeek]$DayOfWeek
    [int]$DayOfYear
    [int]$Hour
    [System.DateTimeKind]$Kind
    [int]$Millisecond
    [int]$Minute
    [int]$Month
    [int]$Second
    [long]$Ticks
    [timespan]$TimeOfDay
    [int]$Year

    DateTimeExtended(
        [DateTime]$Date
    ) {
        $local:StartOfWeek = Get-Date $date -hour 0 -minute 0 -second 0
        $local:EndOfWeek = Get-Date $date -hour 23 -minute 59 -second 59
        $local:StartOfMonth = Get-Date $date -day 1 -hour 0 -minute 0 -second 0

        $this.DateTime = $Date
        $this.FirstDayOfYear = (Get-Date $date -hour 0 -minute 0 -second 0 -Day 1 -Month 1) 
        $this.LastDayOfYear = (Get-Date $date -hour 0 -minute 0 -second 0 -Day 31 -Month 12)
        $this.StartOfWeek = ($StartOfWeek.AddDays( - ($StartOfWeek).DayOfWeek.value__))
        $this.EndOfWeek = ($EndOfWeek.AddDays(6 - ($StartOfWeek).DayOfWeek.value__))
        $this.StartOfMonth = ($StartOfMonth)
        $this.EndOfMonth = ((($StartOfMonth).AddMonths(1).AddSeconds(-1)))
        $this.WeekOfYear = (Get-Date $date -uformat %V)
        $this.TimeZone = ([System.TimeZoneInfo]::Local)
        $this.Quater = [Math]::ceiling($Date.Month / 3)
        $this.Date = $Date.Date
        $this.Day = $Date.Day
        $this.DayOfWeek = $Date.DayOfWeek
        $this.DayOfYear = $Date.DayOfYear
        $this.Hour = $Date.Hour
        $this.Kind = $Date.Kind
        $this.Millisecond = $Date.Millisecond
        $this.Minute = $Date.Minute
        $this.Month = $Date.Month
        $this.Second = $Date.Second
        $this.Ticks = $Date.Ticks
        $this.TimeOfDay = $Date.TimeOfDay
        $this.Year = $Date.Year
    }

    # wrapper for the different datetime methods
    # had to be don this way since the datetime struct is sealed
    [DateTimeExtended] Add([timespan] $value) {
        Return [DateTimeExtended]::New($this.DateTime.Add($value))
    }

    [DateTimeExtended] AddDays([double] $value) {
        Return [DateTimeExtended]::New($this.DateTime.AddDays($value))
    }

    [DateTimeExtended] AddHours([double] $value) {
        Return [DateTimeExtended]::New($this.DateTime.AddHours($value))
    }

    [DateTimeExtended] AddMilliseconds([double] $value) {
        Return [DateTimeExtended]::New($this.DateTime.AddMilliseconds($value))
    }

    [DateTimeExtended] AddMinutes([double] $value) {
        Return [DateTimeExtended]::New($this.DateTime.AddMinutes($value))
    }

    [DateTimeExtended] AddMonths([int] $value) {
        Return [DateTimeExtended]::New($this.DateTime.AddMonths($value))
    }

    [DateTimeExtended] AddSeconds([double] $value) {
        Return [DateTimeExtended]::New($this.DateTime.AddSeconds($value))
    }

    [DateTimeExtended] AddTicks([long] $value) {
        Return [DateTimeExtended]::New($this.DateTime.AddTicks($value))
    }

    [DateTimeExtended] AddYears([int] $value) {
        Return [DateTimeExtended]::New($this.DateTime.AddYears($value))
    }

    [DateTimeExtended] ToLocalTime() {
        Return [DateTimeExtended]::New($this.DateTime.ToLocalTime())
    }

    [DateTimeExtended] ToUniversalTime() {
        Return [DateTimeExtended]::New($this.DateTime.ToUniversalTime())
    }
}
#EndRegion '.\Classes\DateTimeExtended.ps1' 104
#Region '.\Classes\DateTimeFormats.ps1' -1

class DateTimeFormats {
    [string]   $24HourTime
    [datetime] $DateTime
    [int32]    $Day
    [string]   $DayAbrv
    [string]   $DayName
    [int32]    $DayOfWeek
    [Int64]    $FileTime
    [string]   $FullDateShortTime
    [string]   $FullDateTime
    [string]   $GeneralDateShortTime
    [string]   $GeneralDateTime
    [Boolean]  $IsDaylightSavingTime
    [Boolean]  $IsLeapYear
    [string]   $ISO8601
    [string]   $ISO8601UTC
    [string]   $LongDate
    [string]   $LongDateNoDay
    [string]   $LongTime
    [int32]    $Month
    [string]   $MonthAbrv
    [string]   $MonthDay
    [string]   $MonthName
    [int32]    $Quater
    [string]   $RFC1123
    [string]   $RFC1123UTC
    [string]   $RoundTrip
    [string]   $ShortDate
    [string]   $ShortTime
    [string]   $SortableDateTime
    [string]   $SQL
    [string]   $UniversalFullDateTime
    [string]   $UniversalSortableDateTime
    [int32]    $UnixEpochTime
    [string]   $WimDatetime
    [int32]    $Year
    [string]   $YearMonth
    [string]   $YearQuater

    [string] getStringProperty() {
        return $this.StringProperty
    }
}
#EndRegion '.\Classes\DateTimeFormats.ps1' 44
#Region '.\Classes\TimeZoneConversion.ps1' -1

class TimeZoneConversion {
    [DateTime] $FromDateTime
    [String] $FromTimeZone
    [DateTime] $ToDateTime
    [String] $ToTimeZone
    [TimeSpan] $Offset

    TimeZoneConversion ($ToTimeZone, $Date, $FromTimeZone) {
        $DateTime = [DateTime]::SpecifyKind($Date, [DateTimeKind]::Unspecified)
        $from = [System.TimeZoneInfo]::FindSystemTimeZoneById($FromTimeZone)
        $to = [System.TimeZoneInfo]::FindSystemTimeZoneById($ToTimeZone)
        $utc = [System.TimeZoneInfo]::ConvertTimeToUtc($DateTime, $from)
        $newTime = [System.TimeZoneInfo]::ConvertTime($utc, $to)

        $this.FromDateTime = $Date
        $this.FromTimeZone = $FromTimeZone
        $this.ToDateTime = $newTime
        $this.ToTimeZone = $ToTimeZone
        $this.Offset = (New-TimeSpan -Start $date -End  $newTime)
    }
}
#EndRegion '.\Classes\TimeZoneConversion.ps1' 22
#Region '.\Public\Convert-TimeZone.ps1' -1

Function Convert-TimeZone {
   <#
.SYNOPSIS
   Convert a datetime value from one time zone to another
 
.DESCRIPTION
   This function will allows you to pass a date to convert from one time zone to another.
   If no date is specified the local time is used. If no FromTimeZone is passed then the
   local time zone is used.
   If you don't know the time zone ID you can use the Find-TimeZone cmdlet.
 
.PARAMETER ToTimeZone
   The time zone ID of the time zone you want to convert the date to
 
.PARAMETER date
   The date to convert. If not specified the current time will be used
 
.PARAMETER FromTimeZone
   The time zone ID of the time zone you want to convert the date from. If not specified the local time zone will be used
 
.EXAMPLE
    Convert-TimeZone -ToTimeZone "GMT Standard Time"
 
    Convert the local system time to GMT Standard Time
 
.EXAMPLE
   Convert-TimeZone -date '11/17/2017 12:34 AM' -FromTimeZone "China Standard Time" -ToTimeZone "US Mountain Standard Time"
 
   Converts the date and time 11/17/2017 12:34 AM from 'China Standard Time' to 'US Mountain Standard Time'
 
.OUTPUTS
   A PSObject object containing the time zone conversion data
#>

   [CmdletBinding()]
   [OutputType([TimeZoneConversion])]
   param(
      [parameter(Mandatory = $True)]
      [Validatescript( { try { $id = $_; [System.TimeZoneInfo]::FindSystemTimeZoneById($_) }
            catch { throw("'$Id' is not a valid time zone Id. Use the Find-TimeZone cmdlet to find the valid time zone Id.") } })]
      [string]$ToTimeZone,

      [Parameter(Mandatory = $false)]
      [datetime]$Date = $(Get-Date),

      [parameter(Mandatory = $false)]
      [Validatescript( { try { $id = $_; [System.TimeZoneInfo]::FindSystemTimeZoneById($_) }
            catch { throw("'$Id' is not a valid time zone Id. Use the Find-TimeZone cmdlet to find the valid time zone Id.") } })]
      [string]$FromTimeZone = [System.TimeZoneInfo]::Local.Id.ToString()
   )

   [TimeZoneConversion]::new($ToTimeZone, $Date, $FromTimeZone)
}
#EndRegion '.\Public\Convert-TimeZone.ps1' 53
#Region '.\Public\ConvertFrom-UnixTime.ps1' -1

Function ConvertFrom-UnixTime {
   <#
.SYNOPSIS
   Converts a Unix Time value to a datetime value
 
.DESCRIPTION
   This function will return the datetime based on the unix epoch time.
 
.PARAMETER UnixTime
   The UnixTime value to return the datetime for
 
.EXAMPLE
   ConvertFrom-UnixTime -UnixTime 1509512400
   Gets datetime for the Unix time 1509512400
 
.OUTPUTS
   The datetime value based on the unix time
 
#>

   [CmdletBinding()]
   [OutputType([datetime])]
   param(
      [Parameter(Mandatory = $true)]
      [double]$UnixTime
   )

   (Get-Date '1970-01-01T00:00:00.000Z').ToUniversalTime().AddSeconds($UnixTime)
}
#EndRegion '.\Public\ConvertFrom-UnixTime.ps1' 29
#Region '.\Public\ConvertFrom-WmiDateTime.ps1' -1

Function ConvertFrom-WmiDateTime {
   <#
.SYNOPSIS
   Converts a Wmi Time value to a datetime value
 
.DESCRIPTION
   This function will return the datetime based on a WMI datetime string.
 
.PARAMETER WmiTime
   The WmiTime value to return the datetime for
 
.EXAMPLE
   ConvertFrom-WmiDateTime -WmiTime '20190912173652.000000-300'
   Gets datetime for the Wmi time 20190912173652.000000-300
 
.OUTPUTS
   The datetime value based on the wmi time
#>

   [CmdletBinding()]
   [OutputType([datetime])]
   param(
      [Parameter(Mandatory = $true)]
      [string]$WmiTime
   )

   # Extract individual components from the WMI DateTime string
   $year = [int]$WmiTime.Substring(0, 4)
   $month = [int]$WmiTime.Substring(4, 2)
   $day = [int]$WmiTime.Substring(6, 2)
   $hour = [int]$WmiTime.Substring(8, 2)
   $minute = [int]$WmiTime.Substring(10, 2)
   $second = [int]$WmiTime.Substring(12, 2)
   $millisecond = [int]$WmiTime.Substring(15, 6)

   # Create a DateTime object
   $dateTime = [datetime]::SpecifyKind(([datetime]"$year-$month-$day $($hour):$($minute):$second.$millisecond"), 'Utc')

   # Create a TimeSpan object for the UTC offset
   if ($WmiTime -match '\+') {
      $offsetMinutes = [int]$WmiTime.Split('+')[-1]
      $offset = New-TimeSpan -Minutes $offsetMinutes
      # Adjust for the UTC offset
      $dateTime = $dateTime.Add(-$offset)
   }
   elseif ($WmiTime -match '\-') {
      $offsetMinutes = [int]$WmiTime.Split('-')[-1]
      $offset = New-TimeSpan -Minutes $offsetMinutes
      # Adjust for the UTC offset
      $dateTime = $dateTime.Add($offset)
   }
   
   # Convert to local time and output
   $dateTime.ToLocalTime()
}
#EndRegion '.\Public\ConvertFrom-WmiDateTime.ps1' 55
#Region '.\Public\ConvertTo-UnixTime.ps1' -1

Function ConvertTo-UnixTime {
   <#
.SYNOPSIS
   Converts a datetime value to Unix Time
.DESCRIPTION
   This function will return the unix time based on the unix epoch time. If no date is passed in the current date and time is used.
.PARAMETER Date
   The datetime value to return the unix time for
.EXAMPLE
   ConvertTo-UnixTime
   Gets unix time for the current time
.EXAMPLE
   ConvertTo-UnixTime -date "11/17/2017"
   Gets unix time for a specific date
.OUTPUTS
   The int32 value of the unix time
#>

   [CmdletBinding()]
   [OutputType([int32])]
   param(
      [Parameter(Mandatory = $false)]
      [datetime]$date = $(Get-Date)
   )

   [int][double]::Parse((Get-Date ($date).touniversaltime() -UFormat %s))
}
#EndRegion '.\Public\ConvertTo-UnixTime.ps1' 27
#Region '.\Public\ConvertTo-WmiDateTime.ps1' -1

Function ConvertTo-WmiDateTime {
   <#
.SYNOPSIS
   Converts a datetime value to a Wmi datetime string
 
.DESCRIPTION
   This function will return the WMI datetime string based on a datetime passed.
 
.PARAMETER WmiTime
   The WmiTime value to return the datetime for
 
.EXAMPLE
   ConvertTo-WmiDateTime -Date '06/25/2019 16:17'
 
   Return the WMI datetime string for the datetime of "06/25/2019 16:17"
 
.OUTPUTS
   The string value based on the datetime
#>

   [CmdletBinding()]
   [OutputType([string])]
   param(
      [Parameter(Mandatory = $false)]
      [datetime]$Date = (Get-Date)
   )

   $wmiString = $Date.ToString("yyyyMMddHHmmss.ffffff")
   if($Date.Kind -eq 'Utc'){
      $wmiString += '+000'
   }
   else{
      $offset = ([System.TimeZoneInfo]::Local).BaseUtcOffset.TotalMinutes
      $wmiString += "$($offset)"
   }
   $wmiString
}
#EndRegion '.\Public\ConvertTo-WmiDateTime.ps1' 37
#Region '.\Public\Find-TimeZone.ps1' -1

Function Find-TimeZone {
   <#
.SYNOPSIS
   Returns Time Zone information
 
.DESCRIPTION
   This function will return the information for the system time zones. You can search by name and/or hour offsets.
   You can also return the local time zone.
 
.PARAMETER Name
   All or part of the time zone name. Will be used to perform a wildcard search on the time zones
 
.PARAMETER Offset
   The number of hours the time zone is offset from UTC
 
.PARAMETER local
   Use to return the time zone of the current system
 
.EXAMPLE
   Find-TimeZone -local
   Return the time zone of the local system
 
.EXAMPLE
   Find-TimeZone -Name "GMT"
   Search for time zones with 'GMT' in the name
 
.EXAMPLE
   Find-TimeZone -Name "central" -Offset -6
   Search for time zones with 'Central' in the name and have a UTC offset of -6 hours
 
.OUTPUTS
   The TimeZoneInfo value or values found
#>

   [CmdletBinding()]
   [OutputType([System.TimeZoneInfo])]
   param(
      [parameter(Mandatory = $false)][string]$Name,
      [parameter(Mandatory = $false)][int]$Offset,
      [parameter(Mandatory = $false)][switch]$Local,
      [parameter(Mandatory = $false)][switch]$OutGrid
   )

   if ($Local) {
      [System.TimeZoneInfo]::Local
   }
   else {
      $TimeZones = [System.TimeZoneInfo]::GetSystemTimeZones()

      if ($Name) {
         $TimeZones = $TimeZones | Where-Object { $_.DisplayName -like "*$($Name)*" -or $_.DaylightName -like "*$($Name)*" -or
            $_.StandardName -like "*$($Name)*" -or
            $_.Id -like "*$($Name)*" }
      }

      if ($Offset) {
         $TimeZones = $TimeZones | Where-Object { $_.BaseUtcOffset.Hours -eq $Offset }
      }

      if ($OutGrid) {
         $TimeZones | Out-Gridview -Title "Select the timezone(s) to return" -PassThru
      }
      else {
         $TimeZones
      }

   }
}
#EndRegion '.\Public\Find-TimeZone.ps1' 68
#Region '.\Public\Get-CronNextOccurrence.ps1' -1

Function Get-CronNextOccurrence {
    <#
.SYNOPSIS
   Get the next occurrence for a crontab
 
.DESCRIPTION
   This function will either return the next occurrence, or if an end date is supplied, it will return
   all the occurrences between the start and end date.
 
.PARAMETER Crontab
   A valid crontab string
 
.PARAMETER StartTime
   The datetime object to find the next occurrence from. Uses current time if not supplied.
 
.PARAMETER EndTime
   The datetime object to stop finding occurrences for from the StartTime
 
.EXAMPLE
    Get-CronNextOccurrence -Crontab '0 17 * * *'
 
    Will return the next occurrence of the crontab from the current time
 
.EXAMPLE
    $Date = Get-Date '12/14/2032'
    Get-CronNextOccurrence -Crontab '0 17 * * *' -StartTime $Date
 
    Will return the next occurrence of the crontab from the time provided
 
.EXAMPLE
    Get-CronNextOccurrence -Crontab '0 17 * * *' -StartTime $Date -EndTime $Date.AddDays(3)
 
    Will return the all occurrences of the crontab between the two times
 
.OUTPUTS
   A datetime object for every occurrence returned
#>

    [CmdletBinding()]
    [OutputType('datetime')]
    param(
        [Parameter(Mandatory = $true)]
        [string]$Crontab,
        [Parameter(Mandatory = $false)]
        [datetime]$StartTime = (Get-Date),
        [Parameter(Mandatory = $false)]
        [datetime]$EndTime
    )
    # validat crontab
    $Schedule = Test-CrontabSchedule -Crontab $Crontab

    # if no end date, just get next occurrence, else find all occurrences between start and end
    if ($Schedule.valid -eq $true -and $null -eq $EndTime) {
        $schedule.schedule.GetNextOccurrence($StartTime)
    }
    elseif ($Schedule.valid -eq $true) {
        $schedule.schedule.GetNextOccurrences($StartTime, $EndTime)
    }
    else {
        throw $Schedule.ErrorMsg
    }


}
#EndRegion '.\Public\Get-CronNextOccurrence.ps1' 64
#Region '.\Public\Get-DateExtended.ps1' -1

Function Get-DateExtended {
   <#
.SYNOPSIS
   Gets common extended date values that are not included by default with the Get-Date cmdlet
 
.DESCRIPTION
   This function includes added values for:
    FirstDayOfYear : First day of the year
    LastDayOfYear : Last day of the year
    StartOfWeek : First day of the week
    EndOfWeek : Last day of the week
    StartOfMonth : First day of the month
    EndOfMonth : Last day of the month
    TimeZone : Current machine timezone
    Quater : The quarter of the year.
 
   All dates are based on the date passed. If no date is passed in the current date and time are used.
 
.PARAMETER Date
   The datetime value to return the information for
 
.EXAMPLE
   Get-DateExtended
   Gets extended date and time information based on the current time
 
.EXAMPLE
   Get-DateExtended "11/17/2017"
   Gets extended date and time information for a specific date
 
.OUTPUTS
   A PSObject containing extended values for the date.
#>

   [CmdletBinding()]
   [OutputType('DateTimeExtended')]
   param(
      [Parameter(Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
      [Alias("LastWriteTime")]
      [DateTime]$Date = [DateTime]::Now,

      [Parameter()]
      [switch] $FromUnixTime,

      [Parameter()]
      [ValidateRange(1, 9999)]
      [int] $Year,

      [Parameter()]
      [ValidateRange(1, 12)]
      [int] $Month,

      [Parameter()]
      [ValidateRange(1, 31)]
      [int] $Day,

      [Parameter()]
      [ValidateRange(0, 23)]
      [int] $Hour,

      [Parameter()]
      [ValidateRange(0, 59)]
      [int] $Minute,

      [Parameter()]
      [ValidateRange(0, 59)]
      [int] $Second,

      [Parameter()]
      [ValidateRange(0, 999)]
      [int] $Millisecond,

      [Parameter()]
      [ValidateSet('Date', 'Time', 'DateTime')]
      [string] $DisplayHint
   )

   process{
      [DateTimeExtended]::New((Get-Date @PSBoundParameters))
   }

}
#EndRegion '.\Public\Get-DateExtended.ps1' 81
#Region '.\Public\Get-DateFormat.ps1' -1

Function Get-DateFormat {
   [alias("Get-DateFormats")]
   <#
.SYNOPSIS
   Returns common date and time formats
.DESCRIPTION
   This function format date and time values into multiple different common formats. All dates are based on the date passed.
   If no date is passed in the current date and time are used.
 
.PARAMETER Date
   The datetime value to return the formats for
 
.EXAMPLE
   Get-DateFormats
 
   Gets formatted date and time information based on the current time
.EXAMPLE
   Get-DateFormats -Date "11/17/2017"
 
   Gets formatted date and time information for a specific date
 
.OUTPUTS
   A PSObject containing the diffent values for the datetime formats.
#>

   [CmdletBinding()]
   [OutputType([DateTimeFormats])]
   param(
      [Parameter(Mandatory = $false)]
      [datetime]$Date = $(Get-Date)
   )

   $offset = ([System.TimeZoneInfo]::Local).BaseUtcOffset.ToString()
   $offset = $offset.Substring(0, $offset.LastIndexOf(':'))

   [DateTimeFormats]@{
      DateTime                  = $Date.DateTime
      RFC1123UTC                = $Date.ToUniversalTime().ToString('r')
      SQL                       = $Date.ToString("yyyy-MM-dd HH:mm:ss.fff")
      ISO8601UTC                = $Date.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss.fffZ")
      ISO8601                   = $Date.ToString("yyyy-MM-ddTHH:mm:ss.fff") + $offset
      ShortDate                 = $Date.ToString('d')
      LongDate                  = $Date.ToString('D')
      LongDateNoDay             = $Date.ToString('D').Substring($Date.ToString('D').IndexOf(',')+2)
      FullDateShortTime         = $Date.ToString('f')
      FullDateTime              = $Date.ToString('F')
      GeneralDateShortTime      = $Date.ToString('g')
      GeneralDateTime           = $Date.ToString('G')
      MonthDay                  = $Date.ToString('M')
      RoundTrip                 = $Date.ToString('o')
      RFC1123                   = $Date.ToString('r')
      SortableDateTime          = $Date.ToString('s')
      ShortTime                 = $Date.ToString('t')
      LongTime                  = $Date.ToString('T')
      UniversalSortableDateTime = $Date.ToString('u')
      UniversalFullDateTime     = $Date.ToString('U')
      YearMonth                 = $Date.ToString('Y')
      "24HourTime"              = $Date.ToString("HH:mm")
      Day                       = $Date.Day
      DayAbrv                   = (Get-Culture).DateTimeFormat.GetAbbreviatedDayName($Date.DayOfWeek.value__)
      DayName                   = $Date.DayOfWeek.ToString()
      DayOfWeek                 = $Date.DayOfWeek.value__
      Month                     = $Date.Month
      MonthName                 = (Get-Culture).DateTimeFormat.GetMonthName($Date.Month)
      MonthAbrv                 = (Get-Culture).DateTimeFormat.GetAbbreviatedMonthName($Date.Month)
      Quater                    = [Math]::ceiling($Date.Month / 3)
      YearQuater                = "$($Date.Year)$("{0:00}" -f [Math]::ceiling($Date.Month/3) )"
      Year                      = $Date.Year
      WimDatetime               = (ConvertTo-WmiDateTime $Date)
      UnixEpochTime             = (ConvertTo-UnixTime $Date)
      IsDaylightSavingTime      = $Date.IsDaylightSavingTime()
      IsLeapYear                = [datetime]::IsLeapYear($Date.Year)
      FileTime                  = $Date.ToFileTime()
   }
}
#EndRegion '.\Public\Get-DateFormat.ps1' 75
#Region '.\Public\Get-Easter.ps1' -1

Function Get-Easter {
    <#
    .SYNOPSIS
    This function offers a generic Easter computing method for any given year, using Western, Orthodox or Julian algorithms.
 
    .DESCRIPTION
    Shamelessly stolen from python dateutil (https://github.com/dateutil/dateutil/blob/master/src/dateutil/easter.py)
 
    .PARAMETER Year
    The year to get Easter from
 
    .PARAMETER Calendar
    Gregorian : is the default and valid from 1583 to 4099
    Orthodox : valid from 1583 to 4099
    Julian : valid from 326
 
    .EXAMPLE
    Get-Easter -Year 2024
 
    #>

    [CmdletBinding()]
    [OutputType([datetime])]
    param(
        [Parameter(Mandatory = $false)]
        [int]$year = (Get-Date).Year,

        [Parameter(Mandatory = $false)]
        [ValidateSet('Gregorian', 'Julian', 'Orthodox')]
        [string]$Calendar = 'Gregorian'
    )

    # Golden year - 1
    $g = $year % 19
    $e = 0
    if ($Calendar -ne 'Gregorian') {
        # Old method
        $i = (19 * $g + 15) % 30
        $j = ($year + [math]::floor($year / 4) + $i) % 7
        if ($Calendar -eq 'Orthodox') {
            # Extra dates to convert Julian to Gregorian date
            $e = 10
            if ($year -gt 1600) {
                $e = $e + [math]::floor([math]::floor($year / 100) - 16 - ([math]::floor($year / 100) - 16) / 4)
            }
        }
    }
    else {
        # Century
        $c = [math]::floor($year / 100)
        # (23 - Epact) mod 30
        $h = ($c - [math]::floor($c / 4) - [math]::floor((8 * $c + 13) / 25) + 19 * $g + 15) % 30
        # Number of days from March 21 to Paschal Full Moon
        $i = $h - ([math]::floor($h / 28)) * (1 - ([math]::floor($h / 28)) * ([math]::floor(29 / ($h + 1))) * ([math]::floor((21 - $g) / 11)))
        # Weekday for PFM (0=Sunday, etc)
        $j = ($year + [math]::floor($year / 4) + $i + 2 - $c + [math]::floor($c / 4)) % 7
    }



    # Number of days from March 21 to Sunday on or before PFM
    $p = $i - $j + $e
    $d = 1 + ($p + 27 + [math]::floor(($p + 6) / 40)) % 31
    $m = 3 + [math]::floor(($p + 26) / 30)

    [datetime]::new($year, $m, $d)
}
#EndRegion '.\Public\Get-Easter.ps1' 67
#Region '.\Public\Get-PatchTuesday.ps1' -1

Function Get-PatchTuesday {
   <#
.SYNOPSIS
   Returns the second Tuesday of the month
 
.DESCRIPTION
   This function allow you to pass a date, or a month/year combination to find the second Tuesday (aka Patch Tuesday) of any month
 
.PARAMETER Date
   The datetime value to return the second Tuesday for the month
 
.PARAMETER Month
   The month to return the second Tuesday for. Enter a value from 1 to 12.
 
.PARAMETER Year
   The year to return the second Tuesday for. Enter a value from 1 to 9999
 
.EXAMPLE
   Get-PatchTuesday
   Returns the second Tuesday for the current month
 
.EXAMPLE
   Get-PatchTuesday -Date "11/17/2021"
   Returns the second Tuesday for November 2021
 
.EXAMPLE
   Get-PatchTuesday -Month 6 -Year 2020
   Returns the second Tuesday for June 2020
 
.EXAMPLE
   Get-PatchTuesday -Month 4
   Returns the second Tuesday for April of the current year
 
.OUTPUTS
   A datetime object of the second Tuesday.
#>

   [CmdletBinding(DefaultParameterSetName = 'Date')]
   [OutputType([datetime])]
   param(
      [Parameter(Mandatory = $false, ParameterSetName = "Date")]
      [datetime]$Date = $(Get-Date),
      [Parameter(Mandatory = $false, ParameterSetName = "MonthYear")]
      [ValidateRange(1, 12)]
      [int]$Month = $(Get-Date).Month,
      [Parameter(Mandatory = $false, ParameterSetName = "MonthYear")]
      [ValidateRange(1, 9999)]
      [int]$Year = $(Get-Date).Year
   )
   
   if ($PsCmdlet.ParameterSetName -eq "MonthYear") {
      $date = (Get-Date -Day 1 -Month $Month -Year $Year).Date
   }
   
   # Get the first day of the month
   $StartOfMonth = Get-Date $date.Date -Day 1

   # Get every Tuesday, and return the second one
   $ptdate = (0..30 | Foreach-Object { $StartOfMonth.adddays($_) } | Where-Object { $_.dayofweek.value__ -eq 2 })[1]
   $ptdate.Date
}
#EndRegion '.\Public\Get-PatchTuesday.ps1' 61
#Region '.\Public\New-Duration.ps1' -1

Function New-Duration {
    <#
    .SYNOPSIS
    Calculates the time span between two dates and returns the duration in the ISO 8601 format
 
    .DESCRIPTION
    Calculates the timespan between two dates and returns the duration in the ISO 8601 format
 
    https://en.wikipedia.org/wiki/ISO_8601#Durations
 
    .PARAMETER Start
    Specifies the start of a time span.
 
    .PARAMETER End
    Specifies the end of a time span. End date must be greater than the start date
 
    .EXAMPLE
    New-Duration -Start '2/3/2023' -End (Get-Date)
 
    .EXAMPLE
    New-Duration -Days 1 -Hours 4
 
    .EXAMPLE
    New-Duration -Weeks 3
    #>

    [CmdletBinding()]
    [OutputType([string])]
    param(
        [Parameter(Mandatory = $true,
            ParameterSetName = "datetime")]
        [datetime]$Start,

        [Parameter(Mandatory = $true,
            ParameterSetName = "datetime")]
        [datetime]$End,

        [Parameter(Mandatory = $false,
            ParameterSetName = "numbers")]
        [int]$Years = 0,

        [Parameter(Mandatory = $false,
            ParameterSetName = "numbers")]
        [int]$Months = 0,

        [Parameter(Mandatory = $false,
            ParameterSetName = "numbers")]
        [int]$Days = 0,

        [Parameter(Mandatory = $false,
            ParameterSetName = "numbers")]
        [int]$Hours = 0,

        [Parameter(Mandatory = $false,
            ParameterSetName = "numbers")]
        [int]$Minutes = 0,

        [Parameter(Mandatory = $false,
            ParameterSetName = "numbers")]
        [int]$Seconds = 0,

        [Parameter(Mandatory = $false,
            ParameterSetName = "week")]
        [int]$Weeks = 0
    )

    if ($Start -gt $End) {
        throw "Start date must be before the end date"
    }
    if ($PSCmdlet.ParameterSetName -eq 'datetime') {
        # If start date is later in the month offset by 1
        $daysOffset = if ($start.Day -gt $End.Day) { 1 }else { 0 }
        # Get the total months between dates
        $TotalMonths = ($End.Month - $start.Month - $daysOffset) + ($End.Year - $start.Year) * 12
        # Get the number of years
        $Years = [math]::floor($TotalMonths / 12)
        # Get the number of months less the years
        $Months = $TotalMonths % 12
        # Calculate the remaining timespan
        $TimeSpan = New-TimeSpan -Start $start.AddYears($Years).AddMonths($Months) -End $End

        # Set variables to build the string
        $Days = $TimeSpan.Days
        $Hours = $TimeSpan.Hours
        $Minutes = $TimeSpan.Minutes
        $Seconds = $TimeSpan.Seconds
    }

    $Duration = 'P'
    if ($Years -ne 0) { $Duration += "$($Years)Y" }
    if ($Weeks -ne 0) { $Duration += "$($Weeks)W" }
    if ($Months -ne 0) { $Duration += "$($Months)M" }
    if ($Days -ne 0) { $Duration += "$($Days)D" }
    if (($Hours + $Minutes + $Seconds) -ne 0) {
        $Duration += "T"
        if ($Hours -ne 0) { $Duration += "$($Hours)H" }
        if ($Minutes -ne 0) { $Duration += "$($Minutes)M" }
        if ($Seconds -ne 0) { $Duration += "$($Seconds)S" }
    }

    if($Duration -eq 'P'){
        $Duration = 'PT0S'
    }

    $Duration
}
#EndRegion '.\Public\New-Duration.ps1' 106
#Region '.\Public\Test-CrontabSchedule.ps1' -1

Function Test-CrontabSchedule {
    <#
.SYNOPSIS
   Tests that a crontab string is valid
 
.DESCRIPTION
   This function attempts to parse a crontab string to ensure it is valid.
 
.PARAMETER Crontab
   The datetime value to return the second Tuesday for the month
 
.EXAMPLE
   Test-CrontabSchedule -crontab '0 17 * * *'
 
    Valid schedule that returns:
    Crontab Valid
    ------- -----
    0 17 * * * True
 
.EXAMPLE
    Test-CrontabSchedule -crontab '0 17 * 13 *'
 
    Invalid schedule that returns:
    Crontab Valid ErrorMsg
    ------- ----- --------
    0 17 * 13 * False 13 is higher than the maximum allowable value for the [Month] field. Value must be between 1 and 12 (all inclusive).
 
.OUTPUTS
   A psobject that contains the crontable, a validation value, and any error messages returned
#>

    [CmdletBinding()]
    [OutputType([string])]
    param(
        [Parameter(Mandatory = $true)]
        [string]$Crontab
    )

    $Result = [ordered]@{
        Schedule = $Crontab
        Valid    = $false
    }

    try {
        $Result['Schedule'] = [NCrontab.CrontabSchedule]::Parse($Crontab)
        $Result['Valid'] = $true
    }
    catch {
        $ErrorMsg = $_.Exception.ErrorRecord.ToString()
        $ErrorMsg = $ErrorMsg.Substring($ErrorMsg.IndexOf(': "') + 3)
        $ErrorMsg = $ErrorMsg.Substring(0, $ErrorMsg.Length - 1)
        $Result.Add('ErrorMsg', $ErrorMsg)
    }

    [PSCustomObject]$Result

}
#EndRegion '.\Public\Test-CrontabSchedule.ps1' 57