PSBusinessTime.psm1
#region Private function GetElapsedTime { [CmdletBinding()] param ( [Parameter(Mandatory)] [DateTime]$StartDate, [Parameter(Mandatory)] [DateTime]$EndDate, [Parameter(Mandatory)] [DateTime]$StartHour, [Parameter(Mandatory)] [DateTime]$FinishHour ) $Subtractor = New-TimeSpan $StartHourDifference = $StartHour.TimeOfDay - $StartDate.TimeOfDay if ($StartHourDifference -gt 0) { $Subtractor += $StartHourDifference } $FinishHourDifference = $EndDate.TimeOfDay - $FinishHour.TimeOfDay if ($FinishHourDifference -gt 0) { $Subtractor += $FinishHourDifference } (New-TimeSpan -Start $StartDate -End $EndDate) - $Subtractor } #endregion #region Public function Add-WorkingDays { <# .SYNOPSIS Add a number of working days onto a given date. .DESCRIPTION Add a number of working days onto a given date. What constitutes a "working day" in terms of day of the week, or calendar date, is arbitrary and completely customisable. In other words, the default parameters dictate normal working days, which are Monday through Friday. You can also specify particular dates, or days of the week, to be regarded as non-working dates via the -NonWorkingDates and -NonWorkingDaysOfWeek parameters. .PARAMETER Date The starting date used for calculation. The default value is the current datetime. .PARAMETER Days The number of working days to add onto from the given date. Number can be negative in order to substract from the given date, too. .PARAMETER NonWorkingDaysOfWeek The days of the week, representated as strings e.g. 'Monday','Tuesday','Wednesday','Thursday','Friday','Saturday','Sunday', which denotes non-working days of the week. Days specified in this parameter will not be considered as working days. Default values are Saturday and Sunday. .PARAMETER NonWorkingDates An array of datetime objects which denote specific non-working dates. Dates specified in this parameter will not be considered as working days. .EXAMPLE Add-WorkingDays -Days 3 Adds 3 working days onto the current date. For example, if today's date is 2022-04-07, then the returned datetime object will be 2022-04-12. .EXAMPLE Add-WorkingDays -Days -3 Minuses 3 working days from the current date. For example, if today's date is 2022-04-07, then the returned datetime object will be 2022-04-04. .EXAMPLE Add-WorkingDays -Date (Get-Date '2022-04-14') -Days 5 -NonWorkingDates (Get-Date '2022-04-15'),(Get-Date '2022-04-18') Add 5 working days from 2022-04-14, discounting 2022-04-15 (Good Friday) and 2022-04-18 (Easter Monday) as working days. The returned datetime object will be 2022-04-25. .EXAMPLE Add-WorkingDays -Days 1 -NonWorkingDaysOfWeek 'Friday','Saturday','Sunday' Add 1 working day onto the current date. For example, if today's date is 2022-04-07, then the returned datetime object will be 2022-04-11. .INPUTS This function does not accept pipeline input. .OUTPUTS System.DateTime .NOTES Chris Dent (@indented-automation) wrote this in the WinAdmins Discord https://discord.com/channels/618712310185197588/618857671608500234/913855890384371712 #> [CmdletBinding()] param ( [Parameter()] [object]$Date = (Get-Date), [Parameter(Mandatory)] [int]$Days, [Parameter()] [ValidateSet('Monday','Tuesday','Wednesday','Thursday','Friday','Saturday','Sunday')] [String[]]$NonWorkingDaysOfWeek = @('Saturday','Sunday'), [Parameter()] [DateTime[]]$NonWorkingDates ) $increment = $Days / [Math]::Abs($Days) do { $Date = $Date.AddDays($increment) if ($NonWorkingDaysOfWeek -notcontains $Date.DayOfWeek -And $NonWorkingDates -notcontains $Date.Date) { $Days -= $increment } } while ($Days) return $Date } function Get-WorkingDates { <# .SYNOPSIS Return all the working dates between two given datetimes. .DESCRIPTION Return all the working dates between two given datetimes. This is helpful to identify the specific dates between two dates which are considered to be "working day(s)". What constitutes a "working day" in terms of day of the week, or calendar date, including working hours, is arbitrary and completely customisable. In other words, the default parameters dictate normal working days, which are Monday through Friday. You can also specify particular dates, or days of the week, to be regarded as non-working dates via the -NonWorkingDates and -NonWorkingDaysOfWeek parameters. This function does not consider the time, only the date, when determining whether it is a working date or not. .PARAMETER StartDate The datetime object to identify all the working dates from. It must be an older datetime than -EndDate. .PARAMETER EndDate The datetime object to identify all the working dates to. It must be a newer datetime than -StartDate. .PARAMETER NonWorkingDaysOfWeek The days of the week, representated as strings e.g. 'Monday','Tuesday','Wednesday','Thursday','Friday','Saturday','Sunday', which denotes non-working days of the week. Days specified in this parameter will not be considered as working days. Default values are Saturday and Sunday. .PARAMETER NonWorkingDates An array of datetime objects which denote specific non-working dates. Dates specified in this parameter will not be considered as working days. .EXAMPLE Get-WorkingDates -StartDate (Get-Date '2022-04-11') -EndDate (Get-Date '2022-04-11') The function will return a datetime object of date '2022-04-11' because '2022-04-11' is considered a working date, as it is a date within the default parameters, and is the only working date within the provided range. '2022-04-11' is a Monday. .EXAMPLE Get-WorkingDates -StartDate (Get-Date '2022-04-09') -EndDate (Get-Date '2022-04-09') The function will not produce any output (null) because '2022-04-09' is not considered a working day within the default parameters and the given range. '2022-04-09' is a Saturday. .EXAMPLE Get-WorkingDates -StartDate (Get-Date '2022-04-04') -EndDate (Get-Date '2022-04-17') The function will return an array of 10 datetime objects for '2022-04-04' through to '2022-04-08', and '2022-04-11' through to '2022-04-15'. These are considered working dates within the default parameters. '2022-04-04' through to '2022-04-08' is Monday through to Friday, and '2022-04-11' through to '2022-04-15' is Monday through to Friday. .EXAMPLE Get-WorkingDates -StartDate (Get-Date '2022-04-04') -EndDate (Get-Date '2022-04-17') -NonWorkingDates (Get-Date '2022-04-05') The function will return an array of 9 datetime objects for '2022-04-04', '2022-04-06' through to '2022-04-08', and and '2022-04-11' through to '2022-04-15'. These are considered working dates within the defined parameters. '2022-04-05' is considered a non-working date, whereas every other date inbetween the range is considered a working date. .EXAMPLE Get-WorkingDates -StartDate (Get-Date '2022-04-04') -EndDate (Get-Date '2022-04-17') -NonWorkingDaysOfWeek 'Saturday','Sunday','Monday' The function will return an array of 8 datetime objects for '2022-04-05' through to '2022-04-08', and '2022-04-12' through to '2022-04-15'. These are considered working dates within the defined parameters. Saturdays, Sundays, and Mondays, are considered non-working days, therefore every other date inbetween the range is considered a working date. .INPUTS This function does not accept pipeline input. .OUTPUTS System.DateTime[] #> [CmdletBinding()] param ( [Parameter(Mandatory)] [DateTime]$StartDate, [Parameter(Mandatory)] [ValidateScript({ if ($StartDate -gt $_) { throw "-StartDate must be less than -EndDate." } else { return $true } })] [DateTime]$EndDate, [Parameter()] [ValidateSet('Monday','Tuesday','Wednesday','Thursday','Friday','Saturday','Sunday')] [String[]]$NonWorkingDaysOfWeek = @('Saturday','Sunday'), [Parameter()] [DateTime[]]$NonWorkingDates ) # This can return 1 less than intended if we do not do this change. # For example, if the dates in between are 3 working days, # but the time span between them are 2 whole days, # this returns 2 instead of 3 $StartDate = $StartDate.Date $EndDate = $EndDate.Date.AddSeconds(1) $TimeSpan = New-TimeSpan -Start $StartDate -End $EndDate $Days = [Math]::Ceiling($TimeSpan.TotalDays) $Date = $StartDate $WorkingDays = do { if ($NonWorkingDaysOfWeek -notcontains $Date.DayOfWeek -And $NonWorkingDates -notcontains $Date.Date) { $Date.Date } $Date = $Date.AddDays(1) $Days-- } while ($Days) $WorkingDays } function New-BusinessTimeSpan { <# .SYNOPSIS Get the elapsed time between two dates, where the time measured is only inbetween "business hours". .DESCRIPTION Get the elapsed time between two dates, where the time measured is only inbetween "business hours". This is helpful to measure the amount of time past from a start datetime, to an end datetime, while only considering "business hours". What constitutes "business hours" in terms of day of the week, or calendar date, including working hours, is arbitrary and completely customisable. In other words, the default parameters dictate normal working days, which are Monday through Friday and 08:00 through 17:00. You can also specify particular dates, or days of the week, to be regarded as non-working dates via the -NonWorkingDates and -NonWorkingDaysOfWeek parameters. This function does consider both date and time while calculating the elapsed time. .PARAMETER Start The datetime object to start calculating the elapsed time from. It must be an older datetime than -End. .PARAMETER End The datetime object to end calculating the elapsed time to. It must be a newer datetime than -Start. .PARAMETER StartHour The starting hour of a typical working day. The default starting hour is 08:00 (AM). Note: this parameter is a datetime object is, however only the time is used for calculation. The date is ignored. .PARAMETER FinishHour The final hour of a typical working day. The default final hour is 17:00. Note: this parameter is a datetime object is, however only the time is used for calculation. The date is ignored. .PARAMETER NonWorkingDaysOfWeek The days of the week, representated as strings e.g. 'Monday','Tuesday','Wednesday','Thursday','Friday','Saturday','Sunday', which denotes non-working days of the week. Days specified in this parameter will not be considered as working days. Default values are Saturday and Sunday. .PARAMETER NonWorkingDates An array of datetime objects which denote specific non-working dates. Dates specified in this parameter will not be considered as working days. .EXAMPLE New-BusinessTimeSpan -Start (Get-Date '2022-04-11 10:00:00') -End (Get-Date '2022-04-11 10:37:00') The function will return a timespan object of 37 minutes. 2022-04-11 is a Monday and the whole time inbetween the date range given is within the default parameters. .EXAMPLE New-BusinessTimeSpan -Start (Get-Date '2022-04-11 08:00:00') -End (Get-Date '2022-04-12 08:00:00') The function will return a timespan object of 9 hours. 2022-04-11 is a Monday and 2022-04-12 is a Tuesday, and only 9 hours is considered "working hours" within the default parameters. .EXAMPLE New-BusinessTimeSpan -Start (Get-Date '2022-04-11 13:00:00') -End (Get-Date '2022-04-13 13:00:00') The function will return a timespan object of 18 hours. 2022-04-11 through 2022-04-13 is Monday through Wednesday, and only 18 hours is considered "working hours" within the default parameters. .EXAMPLE New-BusinessTimeSpan -Start (Get-Date '2022-04-01 00:00:00') -End (Get-Date '2022-04-30 23:59:59') -NonWorkingDates (Get-Date '2022-04-15'), (Get-Date '2022-04-18') The function will return a timespan object of 162 hours. 2022-04-01 through 2022-04-30 is an entire calendar month, and only 162 hours is considered "working hours" within the defined parameters. '2022-04-15' and '2022-04-18' are considered non-working dates. .EXAMPLE New-BusinessTimeSpan -Start (Get-Date '2022-01-01 00:00:00') -End (Get-Date '2022-12-31 23:59:59') -NonWorkingDates (Get-Date '2022-01-03'), (Get-Date '2022-04-15'), (Get-Date '2022-04-18'), (Get-Date '2022-05-02'), (Get-Date '2022-06-02'), (Get-Date '2022-06-03'), (Get-Date '2022-08-29'), (Get-Date '2022-12-26'), (Get-Date '2022-12-27') The function will return a timespan object of 2259 hours. 2022-01-01 through 2022-12-31 is an entire year, and only 2259 hours is considered "working hours" within the defined parameters. All dates passed to -NonWorkingDates are considered non-working dates (public holidays in the UK for 2022). .EXAMPLE New-BusinessTimeSpan -Start (Get-Date '2022-01-01 00:00:00') -End (Get-Date '2022-12-31 23:59:59') -NonWorkingDaysOfWeek 'Monday','Tuesday','Wednesday','Thursday','Friday','Saturday','Sunday' The function will return a timespan object of 0 hours. 2022-01-01 through 2022-12-31 is an entire year, and 0 hours is considered "working hours" within the defined parameters. All days passed to -NonWorkingDaysOfWeek are considered non-working days, hence the result of 0 hours. .INPUTS This function does not accept pipeline input. .OUTPUTS System.TimeSpan #> [CmdletBinding()] param( [Parameter(Mandatory)] [datetime]$Start, [Parameter(Mandatory)] [ValidateScript({ if ($Start -gt $_) { throw "-Start must be less than -End." } else { return $true } })] [datetime]$End, [Parameter()] [DateTime]$StartHour = '08:00:00', [Parameter()] [ValidateScript({ if ($StartHour -gt $_) { throw "-StartHour must be less than -FinishHour." } else { return $true } })] [DateTime]$FinishHour = '17:00:00', [Parameter()] [ValidateSet('Monday','Tuesday','Wednesday','Thursday','Friday','Saturday','Sunday')] [String[]]$NonWorkingDaysOfWeek = @('Saturday','Sunday'), [Parameter()] [DateTime[]]$NonWorkingDates ) $CommonParams = @{ NonWorkingDaysOfWeek = $NonWorkingDaysOfWeek NonWorkingDates = $NonWorkingDates } $WorkingHours = New-TimeSpan -Start $StartHour -End $FinishHour $WorkingDays = Get-WorkingDates -StartDate $Start -EndDate $End @CommonParams if ($null -eq $WorkingDays) { New-TimeSpan } elseif ($WorkingDays.Count -eq 1) { $Params = @{ StartHour = $StartHour FinishHour = $FinishHour } $_Start = Get-Date ('{0}/{1}/{2} {3}:{4}:{5}' -f $WorkingDays.Year, $WorkingDays.Month, $WorkingDays.Day, $StartHour.Hour, $StartHour.Minute, $StartHour.Second) if ($Start -le $_Start) { $Params["StartDate"] = $_Start } else { $Params["StartDate"] = $Start } $_End = Get-Date ('{0}/{1}/{2} {3}:{4}:{5}' -f $WorkingDays.Year, $WorkingDays.Month, $WorkingDays.Day, $FinishHour.Hour, $FinishHour.Minute, $FinishHour.Second) if ($End -gt $_End) { $Params["EndDate"] = $_End } else { $Params["EndDate"] = $End } $Result = GetElapsedTime @Params if ($Result.Ticks -le 0) { New-TimeSpan } else { $Result } } else { $NumberOfWorkingDays = $WorkingDays.Count $ElapsedTime = New-TimeSpan $InBetweenHours = New-TimeSpan $FirstDayEnd = Get-Date ('{0}/{1}/{2} {3}:{4}:{5}' -f $Start.Year, $Start.Month, $Start.Day, $FinishHour.Hour, $FinishHour.Minute, $FinishHour.Second) if (Test-WorkingDay -Date $Start -StartHour $StartHour -FinishHour $FinishHour @CommonParams) { $Params = @{ StartDate = $Start EndDate = $FirstDayEnd StartHour = $StartHour FinishHour = $FinishHour } $ElapsedTime += (GetElapsedTime @Params) $NumberOfWorkingDays-- } elseif ($Start -gt $FirstDayEnd) { $NumberOfWorkingDays-- } $LastDayStart = Get-Date ('{0}/{1}/{2} {3}:{4}:{5}' -f $End.Year, $End.Month, $End.Day, $StartHour.Hour, $StartHour.Minute, $StartHour.Second) if (Test-WorkingDay -Date $End -StartHour $StartHour -FinishHour $FinishHour @CommonParams) { $Params = @{ StartDate = $LastDayStart EndDate = $End StartHour = $StartHour FinishHour = $FinishHour } $ElapsedTime += (GetElapsedTime @Params) $NumberOfWorkingDays-- } elseif ($End -lt $LastDayStart) { $NumberOfWorkingDays-- } $InBetweenHours = New-TimeSpan -Seconds ($NumberOfWorkingDays * $WorkingHours.TotalSeconds) $InBetweenHours + $ElapsedTime } } function Test-WorkingDay { <# .SYNOPSIS Determine whether a given datetime is a working day. .DESCRIPTION Determine whether a given datetime is a working day. What constitutes a "working day" in terms of day of the week, or calendar date, including working hours, is arbitrary and completely customisable. In other words, the default parameters dictate normal working days, which are Monday through Friday, and normal working hours are 08:00 through 17:00. You can also specify particular dates, or days of the week, to be regarded as non-working dates via the -NonWorkingDates and -NonWorkingDaysOfWeek parameters. If the datetime of -Date falls outside of these parameters, you'll receive a boolean result. .PARAMETER Date The datetime to determine whether it is a working day or not. .PARAMETER StartHour The starting hour of a typical working day. The default starting hour is 08:00 (AM). Note: this parameter is a datetime object is, however only the time is used for calculation. The date is ignored. .PARAMETER FinishHour The final hour of a typical working day. The default final hour is 17:00. Note: this parameter is a datetime object is, however only the time is used for calculation. The date is ignored. .PARAMETER NonWorkingDaysOfWeek The days of the week, representated as strings e.g. 'Monday','Tuesday','Wednesday','Thursday','Friday','Saturday','Sunday', which denotes non-working days of the week. Days specified in this parameter will not be considered as working days. Default values are Saturday and Sunday. .PARAMETER NonWorkingDates An array of datetime objects which denote specific non-working dates. Dates specified in this parameter will not be considered as working days. .EXAMPLE Test-WorkingDay -Date (Get-Date '2022-04-11 09:00:00') The function will return true because the datetime is within the default parameters. 2022-04-11 is a Monday, and 09:00 is between 08:00 and 17:00. .EXAMPLE Test-WorkingDay -Date (Get-Date '2022-04-10 11:00:00') The function will return false because the datetime is outside the default parameters. 2022-04-10 is a Sunday, and therefore is not a working day, regardless of the hour/minute. .EXAMPLE Test-WorkingDay -Date (Get-Date '2022-04-11 22:00:00') -StartHour (Get-Date '08:00:00') -FinishHour Get-Date '23:00:00' The function will return true because the datetime is within the defined parameters. 2022-04-11 is a Monday, and 22:00 is between 08:00 and 23:00. Note: a datetime object is passed for both -StartHour and -FinishHour, however only the time is used for calculation. The date is ignored. .EXAMPLE Test-WorkingDay -Date (Get-Date '2022-04-11 09:00:00') -NonWorkingDaysOfWeek 'Saturday','Sunday','Monday' The function will return false because the datetime is outside the defined parameters. 2022-04-11 is a Monday, and is considered a non working day of the week. .EXAMPLE Test-WorkingDay -Date (Get-Date '2022-04-11 09:00:00') -NonWorkingDates (Get-Date '2022-04-11') The function will return false because the datetime is outside the defined parameters. 2022-04-11 is a Monday, and is considered a non working date. .INPUTS This function does not accept pipeline input. .OUTPUTS System.Boolean #> [CmdletBinding()] param ( [Parameter(Mandatory)] [DateTime]$Date, [Parameter()] [DateTime]$StartHour = '08:00:00', [Parameter()] [ValidateScript({ if ($StartHour -gt $_) { throw "-StartHour must be less than -FinishHour." } else { return $true } })] [DateTime]$FinishHour = '17:00:00', [Parameter()] [ValidateSet('Monday','Tuesday','Wednesday','Thursday','Friday','Saturday','Sunday')] [String[]]$NonWorkingDaysOfWeek = @('Saturday','Sunday'), [Parameter()] [DateTime[]]$NonWorkingDates ) $NonWorkingDaysOfWeek -notcontains $Date.DayOfWeek -And $Date.TimeOfDay -ge $StartHour.TimeOfDay -And $Date.TimeOfDay -lt $FinishHour.TimeOfDay -And $NonWorkingDates -notcontains $Date.Date } #endregion |