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]$Quarter
    [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.Quarter = [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\GroupTimeSpan.ps1' -1

class TimeSpanGroupInfo {
    [int]      $Count 
    [datetime] $DateTime
    [object]   $Group

    TimeSpanGroupInfo([Int64]$Ticks, [int]$Count) {
        $this.Count = $Count
        $this.DateTime = (Get-Date 1/1/0001).AddTicks($Ticks)
        $this.Group = @()
    }
    TimeSpanGroupInfo([Microsoft.PowerShell.Commands.GroupInfo]$GroupInfo) {
        $this.Count = $GroupInfo.Count
        $this.DateTime = (Get-Date 1/1/0001).AddTicks($GroupInfo.Name)
        $this.Group = $GroupInfo.Group.Object
    }
}
#EndRegion '.\Classes\GroupTimeSpan.ps1' 17
#Region '.\Classes\SunTime.ps1' -1

class SunTime {
    [double]       $Latitude 
    [double]       $Longitude
    [int64]        $Now
    [double]       $JulianDate
    [double]       $JulianDay
    [double]       $MeanSolarTime
    [double]       $SolarMeanAnomaly
    [double]       $EquationOfTheCenter
    [double]       $EclipticLongitude
    [double]       $SolarTransitTime
    [double]       $HourAngle
    [DateTime]     $Sunrise
    [DateTime]     $Sunset
    [double]       $DayLength
    [TimeZoneInfo] $TimeZone

    [string] ToDegreeString([double] $value) {
        $x = [math]::Round($value * 3600)
        $num = "∠{0:N3}°" -f $value
        $rad = "∠{0:N3}rad" -f ($value * ([math]::PI / 180))
        $human = "∠{0}°{1}′{2}″" -f ($x / 3600), ($x / 60 % 60), ($x % 60)
        return "$rad = $human = $num"
    }

    [string] FromTimestamp([double]$Timestamp,
        [System.TimeZoneInfo]$TimeZone = $null) {
        $datetime = ConvertFrom-UnixTime $Timestamp
        if ($TimeZone) {
            $datetime = [System.TimeZoneInfo]::ConvertTimeFromUtc($datetime, $TimeZone)
        }
        return $datetime.ToString()
    }

    [double] JulianToTimestamp(
        [double]$Julian
    ) {
        return ($Julian - 2440587.5) * 86400
    }

    [double] TimestampToJulian (
        [double]$Timestamp
    ) {
        return $Timestamp / 86400.0 + 2440587.5
    }
}
#EndRegion '.\Classes\SunTime.ps1' 47
#Region '.\Classes\TimeSpanMeasureInfo.ps1' -1

class TimeSpanMeasureInfo {
    [datetime] $DateTime
    [int]      $Count 
    [Nullable[System.Double]] $Average
    [Nullable[System.Double]] $Sum
    [Nullable[System.Double]] $Maximum
    [Nullable[System.Double]] $Minimum
    [string] $Property

    TimeSpanMeasureInfo([datetime]$DateTime, [Microsoft.PowerShell.Commands.GenericMeasureInfo]$Measure) {
        $this.DateTime = $DateTime
        $this.Property = $Measure.Property
        $this.Count = $Measure.Count
        if($null -ne $Measure.Average){$this.Average = $Measure.Average}
        if($null -ne $Measure.Sum){$this.Sum = $Measure.Sum}
        if($null -ne $Measure.Maximum){$this.Maximum = $Measure.Maximum}
        if($null -ne $Measure.Minimum){$this.Minimum = $Measure.Minimum}       
    }

    TimeSpanMeasureInfo([datetime]$DateTime, [string]$Property, [int]$count) {
        $this.DateTime = $DateTime
        $this.Property = $Property
        $this.Count = $count      
    }
}
#EndRegion '.\Classes\TimeSpanMeasureInfo.ps1' 26
#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\Convert-ToDateTime.ps1' -1

function Convert-ToDateTime {
    <#
.SYNOPSIS
    Converts various input objects to a DateTime object.
 
.DESCRIPTION
    The `Convert-ToDateTime` function attempts to convert different types of input objects into a DateTime object.
    It supports input from various data types such as strings and objects that can be cast or converted to a DateTime.
 
.PARAMETER InputObject
    Specifies the input object to be converted to a DateTime. This parameter accepts pipeline input and is mandatory.
 
    The input can be of any type:
    - If the input is already a DateTime, it will be returned as-is.
    - If the input is a string, it attempts to parse it into a DateTime.
    - Other input types will be processed accordingly, if possible.
 
.EXAMPLE
    '2024-08-29' | Convert-ToDateTime
 
    Converts the string '2024-08-29' into a DateTime object representing the 29th of August, 2024.
 
.INPUTS
    System.Object
    The function accepts objects from the pipeline, which are attempted to be converted to DateTime.
 
.OUTPUTS
    System.DateTime
    The function outputs a DateTime object if the conversion is successful.
#>


    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [Object]$InputObject
    )

    process {
        $Return = $null
        try {
            # Attempt to convert directly if input is already a DateTime or string
            if ($InputObject -is [DateTime]) {
                $Return = $InputObject
            }
            elseif ($InputObject -is [string]) {
                try{
                    $Return = Get-Date $InputObject -ErrorAction Stop
                }
                catch{
                # Attempt to parse string input to DateTime
                $Return = [DateTime]::ParseExact($InputObject, 
                    [System.Globalization.CultureInfo]::InvariantCulture.DateTimeFormat.GetAllDateTimePatterns(), 
                    [System.Globalization.CultureInfo]::InvariantCulture, 
                    [System.Globalization.DateTimeStyles]::None) -as [DateTime]
                }
            }
            else {
                # Attempt conversion for other types using their string representation
                $Return = [DateTime]::Parse($InputObject.ToString(), 
                    [System.Globalization.CultureInfo]::InvariantCulture) -as [DateTime]
            }
        }
        catch {
            # Return error if conversion fails
            Write-Error "Unable to convert '$InputObject' to DateTime."
        }
        $Return
    }
}
#EndRegion '.\Public\Convert-ToDateTime.ps1' 70
#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 Date
   Specifies a date and time.
 
.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
 
.PARAMETER OutGrid
   Use to output time zone selects to Grid View
 
.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' 71
#Region '.\Public\Get-CronDescription.ps1' -1

Function Get-CronDescription {
<#
.SYNOPSIS
   Convert a cron expression into a human readable description
 
.DESCRIPTION
   Uses the .NET library CronExpressionDescriptor to convert cron expressions into human readable descriptions.
 
.PARAMETER Crontab
   A valid crontab string
 
.PARAMETER DayOfWeekStartIndexOne
    When used Sunday will equal 1, otherwise Sunday will be 0. (Default: Sunday = 0)
 
.PARAMETER Use24HourTimeFormat
    If true, descriptions will use a 24-hour clock (Default: false but some translations will default to true)
 
.PARAMETER Locale
    The locale to use (Default: "en")
    Supported values: cs-CZ, da, de, es, es-MX, fa, fi, fr, he-IL, hu, it, ja, ko, nb, nl, pl, pt, ro, ru, sl, sv, tr, uk, vi, zh-Hans, zh-Hant
 
.EXAMPLE
    Get-CronDescription -Crontab '0 17 * * 1'
 
    Results with default options:
    At 05:00 PM, only on Monday
 
.EXAMPLE
    Get-CronDescription -Crontab '0 17 * * 1' -DayOfWeekStartIndexOne
 
    Results with DayOfWeekStartIndexOne switch returns Sunday for the 1 instead of Monday:
    At 05:00 PM, only on Sunday
 
.EXAMPLE
    Get-CronDescription -Crontab '0 17 * * 1' -Use24HourTimeFormat
 
    Results with Use24HourTimeFormat options:
    At 17:00, only on Monday
 
.EXAMPLE
    Get-CronDescription -Crontab '0 17 * * 1' -Locale 'fr'
 
    Results with fr Locale options:
    At 05:00 PM, only on lundi
 
.OUTPUTS
   A psobject that contains the crontable, a validation value, and any error messages returned
#>

    [CmdletBinding()]
    [OutputType([string])]
    param(
        [Parameter(Mandatory = $true)]
        [string]$Crontab,
        [Parameter(Mandatory = $false)]
        [switch]$DayOfWeekStartIndexOne = $false,
        [Parameter(Mandatory = $false)]
        [switch]$Use24HourTimeFormat = $false,
        [Parameter(Mandatory = $false)]
        [ValidateSet('cs-CZ','da','de','es','es-MX','fa','fi','fr','he-IL','hu','it','ja','ko','nb','nl','pl','pt','ro','ru','sl','sv','tr','uk','vi','zh-Hans','zh-Hant')]
        [string]$Locale = 'en'
    )

    # Set options
    $options = [CronExpressionDescriptor.Options]::new()
    if($DayOfWeekStartIndexOne){
        $options.DayOfWeekStartIndexZero = $false
    }
    $options.Use24HourTimeFormat = $Use24HourTimeFormat
    $options.Locale = $Locale

    # Get Description
    [CronExpressionDescriptor.ExpressionDescriptor]::GetDescription($Crontab, $options)
}
#EndRegion '.\Public\Get-CronDescription.ps1' 74
#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 additional 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
 
.PARAMETER UnixTimeSeconds
   Date and time represented in seconds since January 1, 1970, 0:00:00.
 
.PARAMETER Year
   Specifies the year that is displayed. Enter a value from 1 to 9999
 
.PARAMETER Month
   Specifies the month that is displayed. Enter a value from 1 to 12
 
.PARAMETER Day
   Specifies the day of the month that is displayed. Enter a value from 1 to 31.
 
.PARAMETER Hour
   Specifies the hour that is displayed. Enter a value from 0 to 23.
 
.PARAMETER Minute
   Specifies the minute that is displayed. Enter a value from 0 to 59.
 
.PARAMETER Second
   Specifies the second that is displayed. Enter a value from 0 to 59.
 
.PARAMETER Millisecond
   Specifies the milliseconds in the date. Enter a value from 0 to 999.
 
.PARAMETER DisplayHint
   Determines which elements of the date and time are displayed.
 
   The accepted values are as follows:
 
      Date: displays only the date
      Time: displays only the time
      DateTime: displays the date and time
 
.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] $UnixTimeSeconds,

      [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' 114
#Region '.\Public\Get-DateFormat.ps1' -1

Function Get-DateFormat {
   <#
.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.
#>

   [alias("Get-DateFormats")]
   [CmdletBinding(DefaultParameterSetName = "Full")]
   [OutputType([DateTimeFormats], ParameterSetName = "ID")]
   [OutputType([object], ParameterSetName = "Format")]
   param(
      [Parameter(Mandatory = $false, ParameterSetName = "Full")]
      [Parameter(Mandatory = $false, ParameterSetName = "Format")]
      [datetime]$Date = $(Get-Date),

      [Parameter(Mandatory = $false, ParameterSetName = "Format")]
      [string]$Format
   )

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

   $dateFormats = [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()
   }

   if ([string]::IsNullOrEmpty($PSBoundParameters['Format'])) {
      $dateFormats
   }
   else {
      $dateFormats."$($PSBoundParameters['Format'])"
   }
}
#EndRegion '.\Public\Get-DateFormat.ps1' 88
#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\Get-SunTime.ps1' -1

function Get-SunTime {
    <#
.SYNOPSIS
Find sunrise and sunset times for any location on planet Earth.
 
.DESCRIPTION
This function finds the time of day for sunrise, sunset based on the given latitude and longitude. You can also specify time zone and elevation.
 
.PARAMETER Date
The day to find the sunrise and sunset for.
 
.PARAMETER Latitude
The Latitude entered as a decimal number representing degrees and minutes
 
.PARAMETER Longitude
The Longitude entered as a decimal number representing degrees and minutes
 
.PARAMETER Elevation
The Elevation in meters
 
.PARAMETER TimeZone
The time zone for the final results
 
.EXAMPLE
Get-SunTime -Latitude 51.501005 -Longitude -0.1445479
 
# Get the sunrise and sunset for the given coordinates for the current day
 
.EXAMPLE
$address = '1600 Pennsylvania Avenue NW'
$addr = Invoke-RestMethod "https://nominatim.openstreetmap.org/search?q=$($address)&format=json" | Select-Object -First 1
Get-SunTime -Latitude $addr.lat -Longitude $addr.lon
 
# Use the free Nominatim API get the coordinates for an address, then use those results to get the sunrise and sunset for that location.
 
.NOTES
Use can use Google Maps to find the latitude and longitude coordinates.
Right click a specific point on the Google map and you will see the latitude and longitude coordinates displayed, for example 45.51421, -122.68462.
 
#>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $false)]
        [datetime]$Date = $(Get-Date),
        [Parameter(Mandatory = $true)]
        [double]$Latitude,
        [Parameter(Mandatory = $true)]
        [double]$Longitude,
        [Parameter(Mandatory = $false)]
        [double]$Elevation = 0.0,
        [Parameter(Mandatory = $false)]
        [string]$TimeZone = $null
    )
    $suntime = [SunTime]::new()
    $datetimeOffset = [DateTimeOffset]::new($Date)
    $CurrentTimestamp = $datetimeOffset.ToUniversalTime().ToUnixTimeSeconds()

    $TimeZoneInfo = [System.TimeZoneInfo]::Local
    if(-not [string]::IsNullOrEmpty($TimeZone)){
        $TimeZoneInfo = [System.TimeZoneInfo]::FindSystemTimeZoneById($TimeZone)
    }
    

    Write-Verbose "Latitude f = $($suntime.ToDegreeString($Latitude))"
    Write-Verbose "Longitude l_w = $($suntime.ToDegreeString($Longitude))"
    Write-Verbose "Now ts = $($suntime.FromTimestamp($CurrentTimestamp, $TimeZoneInfo))"

    
    $J_date = $suntime.TimestampToJulian($CurrentTimestamp)
    Write-Verbose ("Julian date j_date = {0:N3} days" -f $J_date)
    
    # Julian day
    $n = [math]::Ceiling($J_date - (2451545.0 + 0.0009) + 69.184 / 86400.0)
    Write-Verbose ("Julian day n = {0:N3} days" -f $n)

    # Mean solar time
    $J_ = $n + 0.0009 - $Longitude / 360.0
    Write-Verbose ("Mean solar time J_ = {0:N9} days" -f $J_)

    # Solar mean anomaly
    $M_degrees = [math]::IEEERemainder(357.5291 + 0.98560028 * $J_, 360)
    $M_radians = ($M_degrees * ([math]::PI / 180))
    Write-Verbose "Solar mean anomaly M = $($suntime.ToDegreeString($M_degrees))"

    # Equation of the center
    $C_degrees = 1.9148 * [math]::Sin($M_radians) + 0.02 * [math]::Sin(2 * $M_radians) + 0.0003 * [math]::Sin(3 * $M_radians)
    Write-Verbose "Equation of the center C = $($suntime.ToDegreeString($C_degrees))"

    # Ecliptic longitude
    $L_degrees = [math]::IEEERemainder($M_degrees + $C_degrees + 180.0 + 102.9372, 360)
    Write-Verbose "Ecliptic longitude L = $($suntime.ToDegreeString($L_degrees))"

    $Lambda_radians = ($L_degrees * ([math]::PI / 180))

    # Solar transit (julian date)
    $J_transit = 2451545.0 + $J_ + 0.0053 * [math]::Sin($M_radians) - 0.0069 * [math]::Sin(2 * $Lambda_radians)
    Write-Verbose "Solar transit time J_trans = $($suntime.FromTimestamp( $suntime.JulianToTimestamp($J_transit), $TimeZoneInfo))"

    # Declination of the Sun
    $sin_d = [math]::Sin($Lambda_radians) * [math]::Sin((23.4397 * ([math]::PI / 180)))
    $cos_d = [math]::Cos([math]::Asin($sin_d))

    # Hour angle
    $some_cos = ([math]::Sin(-0.833 * [math]::PI / 180 - 2.076 * [math]::Sqrt($Elevation) / 60.0 * [math]::PI / 180) - [math]::Sin($Latitude * [math]::PI / 180) * $sin_d) / ([math]::Cos($Latitude * [math]::PI / 180) * $cos_d)
    $w0_radians = [math]::Acos($some_cos)


    $w0_degrees = $w0_radians * 180 / [math]::PI
    Write-Verbose "Hour angle w0 = $($suntime.ToDegreeString($w0_degrees))"

    $j_rise = $J_transit - $w0_degrees / 360
    $j_set = $J_transit + $w0_degrees / 360

    Write-Verbose "Sunrise j_rise = $($suntime.FromTimestamp( $suntime.JulianToTimestamp($j_rise), $TimeZoneInfo))"
    Write-Verbose "Sunset j_set = $($suntime.JulianToTimestamp($j_rise)) = $($suntime.FromTimestamp($suntime.JulianToTimestamp($j_set), $TimeZoneInfo))"
    Write-Verbose ("Day length {0:N3} hours" -f ($w0_degrees / (180 / 24)))

    [SunTime]@{
        Latitude            = $Latitude
        Longitude           = $Longitude
        Now                 = $CurrentTimestamp
        JulianDate          = $J_date
        JulianDay           = $n
        MeanSolarTime       = $J_
        SolarMeanAnomaly    = $M_degrees
        EquationOfTheCenter = $C_degrees
        EclipticLongitude   = $L_degrees
        SolarTransitTime    = $J_transit
        HourAngle           = $w0_degrees
        Sunrise             = (Get-Date $($suntime.FromTimestamp($suntime.JulianToTimestamp($j_rise), $TimeZoneInfo)))
        Sunset              = (Get-Date $($suntime.FromTimestamp($suntime.JulianToTimestamp($j_set), $TimeZoneInfo)))
        DayLength           = ($w0_degrees / (180 / 24))
        TimeZone            = $TimeZoneInfo
    }
}
#EndRegion '.\Public\Get-SunTime.ps1' 136
#Region '.\Public\Group-TimeSpan.ps1' -1

Function Group-TimeSpan {
    <#
.SYNOPSIS
Groups objects by a specified time span.
 
.DESCRIPTION
The `Group-TimeSpan` function takes a collection of objects and groups them based on a specified time span.
It supports grouping by properties such as days, hours, minutes, etc., allowing for flexible data grouping.
 
.PARAMETER InputObject
Specifies the input objects to be grouped. This parameter accepts pipeline input.
 
.PARAMETER Property
Specifies the property name of the InputObject to use for grouping. The property should be of a DateTime type.
 
.PARAMETER Years
Specifies the number of years to group by.
 
.PARAMETER Months
Specifies the number of months to group by.
 
.PARAMETER Days
Specifies the number of days to group by.
 
.PARAMETER Hours
Specifies the number of hours to group by.
 
.PARAMETER Minutes
Specifies the number of minutes to group by.
 
.PARAMETER Seconds
Specifies the number of seconds to group by.
 
.EXAMPLE
Get-ChildItem $PSHOME | Group-TimeSpan -Property CreationTime -Hours 1
 
Groups the files by each hour based on their CreationTime.
 
.EXAMPLE
Get-ChildItem $PSHOME | Group-TimeSpan -Property CreationTime -Days 7
 
Groups the files by 7 days based on their CreationTime.
 
.OUTPUTS
TimeSpanGroupInfo[]
Returns an array of TimeSpanGroupInfo objects.
#>

    [CmdletBinding()]
    [OutputType([TimeSpanGroupInfo[]])]
    param (
        [Parameter(
            ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true,
            Mandatory = $true
        )]
        [Alias("PSObject")]
        [object]$InputObject,

        [Parameter(Mandatory = $false)]
        [string]$Property = $null,

        [Parameter(Mandatory = $false)]
        [switch]$IncludeAllTimes = $false,

        [Parameter(Mandatory = $true, ParameterSetName = "Years")]
        [int]$Years,
        [Parameter(Mandatory = $true, ParameterSetName = "Months")]
        [int]$Months,
        [Parameter(Mandatory = $true, ParameterSetName = "Days")]
        [int]$Days,
        [Parameter(Mandatory = $true, ParameterSetName = "Hours")]
        [int]$Hours,
        [Parameter(Mandatory = $true, ParameterSetName = "Minutes")]
        [int]$Minutes,
        [Parameter(Mandatory = $true, ParameterSetName = "Seconds")]
        [int]$Seconds
    )

    begin {
        $Null = $Years,  $Months # Prevent PSReviewUnusedParameter false positive
        [Collections.Generic.List[PSObject]] $objects = @()
        switch ($PsCmdlet.ParameterSetName) {
            "Days" { $ticks = 36000000000 * 24 * $Days }
            "Hours" { $ticks = 36000000000 * $Hours }
            "Minutes" { $ticks = 600000000 * $Minutes }
            "Seconds" { $ticks = 10000000 * $Seconds }
        }
    }


    process {
        $InputObject | Foreach-Object { 
            if (-not [string]::IsNullOrEmpty( $Property )) {
                $timeValue = $_.$Property
            }
            else {
                $timeValue = $_
            }
            $objects.Add([pscustomobject]@{
                    TimeProperty = $timeValue | Convert-ToDateTime
                    Object       = $_
                })
        }
    }


    end {
        $min = [datetime]::MinValue
        $groupedDates = $objects | Group-Object {
            if ($PSBoundParameters['Months']) {
                # Monthly
                $monthsDifference = (($_.TimeProperty.Year - $min.Year) * 12 + $_.TimeProperty.Month - $min.Month)
                $groupStartMonth = [math]::Floor($monthsDifference / $Months) * $Months
                $month = $min.AddMonths($groupStartMonth)
                (Get-Date -Year $month.Year -Month $month.Month -day 1 -hour 0 -minute 0 -second 0 -Millisecond 0).Ticks
            }
            elseif ($PSBoundParameters['Years']) {
                # Yearly
                $yearDifference = $_.TimeProperty.Year - $min.Year
                $groupStartYear = [math]::Floor($yearDifference / $Years) * $Years
                $year = $min.AddMonths($groupStartYear * 12)
                (Get-Date -Year $year.Year -Month 1 -day 1 -hour 0 -minute 0 -second 0 -Millisecond 0).Ticks
            }
            else {
                $min.Ticks - (($min.Ticks - $_.TimeProperty.Ticks) - (($min.Ticks - $_.TimeProperty.Ticks) % $ticks))
            }
        }

        [TimeSpanGroupInfo[]]$output = $groupedDates | ForEach-Object {
            [TimeSpanGroupInfo]::new($_)
        }

        if($IncludeAllTimes){
            $FirstTime = $output | Sort-Object DateTime | Select-Object -First 1 -ExpandProperty DateTime | Select-Object -ExpandProperty Ticks
            $LastTime = $output | Sort-Object DateTime | Select-Object -Last 1 -ExpandProperty DateTime | Select-Object -ExpandProperty Ticks
            $blankTimes = while($FirstTime -lt $LastTime){
                $toAdd = [TimeSpanGroupInfo]::new($FirstTime, 0)
                if($output.DateTime -notcontains $toAdd.DateTime){
                    $toAdd
                }
                if ($PSBoundParameters['Months']) {
                    $FirstTime = $toAdd.DateTime.AddMonths($Months).Ticks
                }
                elseif ($PSBoundParameters['Years']) {
                    $FirstTime = $toAdd.DateTime.AddMonths($Years * 12).Ticks
                }
                else{
                    $FirstTime += $ticks
                }
            }
            $output = @($output) + $($blankTimes)
        }

        $output | Sort-Object DateTime
    }
}
#EndRegion '.\Public\Group-TimeSpan.ps1' 157
#Region '.\Public\Measure-TimeSpan.ps1' -1

Function Measure-TimeSpan {
    <#
.SYNOPSIS
    Measures statistical properties (such as sum, average, and maximum) of a specified property within grouped time spans.
 
.DESCRIPTION
    The `Measure-TimeSpan` function calculates various statistical measures (sum, average, maximum, minimum) for a specified property across a collection of grouped time spans.
    It is designed to work with objects grouped by the `Group-TimeSpan` function, focusing on numerical properties for aggregation.
 
.PARAMETER TimeSpanGroupInfo
    Specifies the input objects that represent grouped time spans. This parameter accepts pipeline input and is mandatory.
 
.PARAMETER Property
    Specifies the property name of the TimeSpanGroupInfo objects to measure. This property should be numeric and is mandatory.
 
.PARAMETER Sum
    Switch parameter that, when specified, calculates the sum of the specified property across all input objects.
 
.PARAMETER Average
    Switch parameter that, when specified, calculates the average of the specified property across all input objects.
 
.PARAMETER Maximum
    Switch parameter that, when specified, calculates the maximum value of the specified property across all input objects.
 
.PARAMETER Minimum
    Switch parameter that, when specified, calculates the minimum value of the specified property across all input objects.
 
.EXAMPLE
    $groupedData = Get-EventLog -LogName System | Group-TimeSpan -Property TimeGenerated -Days 1
    $groupedData | Measure-TimeSpan -Property Count -Sum
 
    Measures the sum of the 'Count' property for each grouped time span in the system event log.
 
.EXAMPLE
    $groupedData = Get-ChildItem $PSHOME | Group-TimeSpan -Property CreationTime -Hours 1
    $groupedData | Measure-TimeSpan -Property Length -Average -Sum
 
    Measures the sum and average size of files grouped by each hour based on their CreationTime.
 
.INPUTS
    TimeSpanGroupInfo[]
    The function accepts grouped time span objects from the pipeline.
 
.OUTPUTS
    TimeSpanMeasureInfo[]
    Returns the calculated statistical value(s) based on the input and specified parameters.
#>


    [CmdletBinding()]
    [OutputType([TimeSpanMeasureInfo[]])]
    param (
        [Parameter(
            ValueFromPipeline = $true,
            Mandatory = $true
        )]
        [TimeSpanGroupInfo[]]$TimeSpanGroupInfo,

        [Parameter(Mandatory = $true)]
        [string]$Property,

        [Parameter(Mandatory = $false)]
        [switch]$Sum = $false,
        [Parameter(Mandatory = $false)]
        [switch]$Average = $false,
        [Parameter(Mandatory = $false)]
        [switch]$Maximum = $false,
        [Parameter(Mandatory = $false)]
        [switch]$Minimum = $false
    )

    begin {
        [Collections.Generic.List[TimeSpanMeasureInfo]] $objects = @()
        $MeasureParameters = @{}
        $PSBoundParameters.GetEnumerator() | Where-Object { $_.Key -ne 'TimeSpanGroupInfo' } | ForEach-Object {
            $MeasureParameters.Add($_.Key, $_.Value)
        }
    }

    process {
        $TimeSpanGroupInfo | Foreach-Object { 
            $m = $_.Group | Measure-Object @MeasureParameters
            if($m){
                $objects.Add([TimeSpanMeasureInfo]::new($_.DateTime, $m))
            }
            else{
                $blankFill = [TimeSpanMeasureInfo]::new($_.DateTime, $Property, $_.Count)
                if($Average){$blankFill.Average = 0}
                if($Sum){$blankFill.Sum = 0}
                if($Maximum){$blankFill.Maximum = 0}
                if($Minimum){$blankFill.Minimum = 0}
                $objects.Add($blankFill)
            }

             
        }
    }

    end {
        $objects
    }
}
#EndRegion '.\Public\Measure-TimeSpan.ps1' 102
#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
 
.PARAMETER Years
Specifies the number for yearly interval
 
.PARAMETER Months
Specifies the number for monthly interval
 
.PARAMETER Days
Specifies the number for daily interval
 
.PARAMETER Hours
Specifies the number for hourly interval
 
.PARAMETER Minutes
Specifies the number for minute interval
 
.PARAMETER Seconds
Specifies the number for second interval
 
.PARAMETER Weeks
Specifies the number for weekly interval
 
.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' 127
#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
# Argument Completers
$ArgumentCompleters = Join-Path $PSScriptRoot 'Resources\ArgumentCompleters.ps1'
. $ArgumentCompleters