How can I calculate business hours between two dates? For example we have two dates; 01/01/2010 15:00 and 04/01/2010 12:00 And we have working hours 09:00 to 17:00 in weekda
I know this is post is very old but here is a function I wrote recently to calculate Business Hours/Minutes between any two events. It also takes into account any holidays which must be defined in a table.
The function returns the interval in minutes - you can divide by 60 to get hours as required.
This has been tested on SQL Server 2008. Hope it helps someone.
Create Function GetWorkingMin(@StartDate DateTime, @EndDate DateTime, @Country Varchar(2)) Returns Int
AS
Begin
Declare @WorkMin int = 0 -- Initialize counter
Declare @Reverse bit -- Flag to hold if direction is reverse
Declare @StartHour int = 9 -- Start of business hours (can be supplied as an argument if needed)
Declare @EndHour int = 17 -- End of business hours (can be supplied as an argument if needed)
Declare @Holidays Table (HDate DateTime) -- Table variable to hold holidayes
-- If dates are in reverse order, switch them and set flag
If @StartDate>@EndDate
Begin
Declare @TempDate DateTime=@StartDate
Set @StartDate=@EndDate
Set @EndDate=@TempDate
Set @Reverse=1
End
Else Set @Reverse = 0
-- Get country holidays from table based on the country code (Feel free to remove this or modify as per your DB schema)
Insert Into @Holidays (HDate) Select HDate from HOLIDAY Where COUNTRYCODE=@Country and HDATE>=DateAdd(dd, DateDiff(dd,0,@StartDate), 0)
If DatePart(HH, @StartDate)<@StartHour Set @StartDate = DateAdd(hour, @StartHour, DateDiff(DAY, 0, @StartDate)) -- If Start time is less than start hour, set it to start hour
If DatePart(HH, @StartDate)>=@EndHour+1 Set @StartDate = DateAdd(hour, @StartHour+24, DateDiff(DAY, 0, @StartDate)) -- If Start time is after end hour, set it to start hour of next day
If DatePart(HH, @EndDate)>=@EndHour+1 Set @EndDate = DateAdd(hour, @EndHour, DateDiff(DAY, 0, @EndDate)) -- If End time is after end hour, set it to end hour
If DatePart(HH, @EndDate)<@StartHour Set @EndDate = DateAdd(hour, @EndHour-24, DateDiff(DAY, 0, @EndDate)) -- If End time is before start hour, set it to end hour of previous day
If @StartDate>@EndDate Return 0
-- If Start and End is on same day
If DateDiff(Day,@StartDate,@EndDate) <= 0
Begin
If Datepart(dw,@StartDate)>1 And DATEPART(dw,@StartDate)<7 -- If day is between sunday and saturday
If (Select Count(*) From @Holidays Where HDATE=DateAdd(dd, DateDiff(dd,0,@StartDate), 0)) = 0 -- If day is not a holiday
If @EndDate<@StartDate Return 0 Else Set @WorkMin=DATEDIFF(MI, @StartDate, @EndDate) -- Calculate difference
Else Return 0
Else Return 0
End
Else Begin
Declare @Partial int=1 -- Set partial day flag
While DateDiff(Day,@StartDate,@EndDate) > 0 -- While start and end days are different
Begin
If Datepart(dw,@StartDate)>1 And DATEPART(dw,@StartDate)<7 -- If this is a weekday
Begin
If (Select Count(*) From @Holidays Where HDATE=DateAdd(dd, DateDiff(dd,0,@StartDate), 0)) = 0 -- If this is not a holiday
Begin
If @Partial=1 -- If this is the first iteration, calculate partial time
Begin
Set @WorkMin=@WorkMin + DATEDIFF(MI, @StartDate, DateAdd(hour, @EndHour, DateDiff(DAY, 0, @StartDate)))
Set @StartDate=DateAdd(hour, @StartHour+24, DateDiff(DAY, 0, @StartDate))
Set @Partial=0
End
Else Begin -- If this is a full day, add full minutes
Set @WorkMin=@WorkMin + (@EndHour-@StartHour)*60
Set @StartDate = DATEADD(DD,1,@StartDate)
End
End
Else Set @StartDate = DATEADD(DD,1,@StartDate)
End
Else Set @StartDate = DATEADD(DD,1,@StartDate)
End
If Datepart(dw,@StartDate)>1 And DATEPART(dw,@StartDate)<7 -- If last day is a weekday
If (Select Count(*) From @Holidays Where HDATE=DateAdd(dd, DateDiff(dd,0,@StartDate), 0)) = 0 -- And it is not a holiday
If @Partial=0 Set @WorkMin=@WorkMin + DATEDIFF(MI, @StartDate, @EndDate) Else Set @WorkMin=@WorkMin + DATEDIFF(MI, DateAdd(hour, @StartHour, DateDiff(DAY, 0, @StartDate)), @EndDate)
End
If @Reverse=1 Set @WorkMin=-@WorkMin
Return @WorkMin
End
ALTER FUNCTION WorkTime_fn (@StartDate DATETIME, @FinishDate DATETIME)
RETURNS VARCHAR(9)
AS
BEGIN
DECLARE @Temp BIGINT
SET @Temp=0
DECLARE @FirstDay VARCHAR(9)
SET @FirstDay = CONVERT(VARCHAR(9),@StartDate, 112)
DECLARE @LastDay VARCHAR(9)
SET @LastDay = CONVERT(VARCHAR(9),@FinishDate, 112)
DECLARE @StartTime VARCHAR(9)
SET @StartTime = CONVERT(VARCHAR(9),@StartDate, 108)
DECLARE @FinishTime VARCHAR(9)
SET @FinishTime = CONVERT(VARCHAR(9),@FinishDate, 108)
DECLARE @WorkStart VARCHAR(9)
SET @WorkStart = '09:30:00'
DECLARE @WorkFinish VARCHAR(9)
SET @WorkFinish = '17:30:00'
IF (@StartTime<@WorkStart)
BEGIN
SET @StartTime = @WorkStart
END
IF (@FinishTime>@WorkFinish)
BEGIN
SET @FinishTime=@WorkFinish
END
DECLARE @CurrentDate VARCHAR(9)
SET @CurrentDate = CONVERT(VARCHAR(9),@StartDate, 112)
DECLARE @LastDate VARCHAR(9)
SET @LastDate = CONVERT(VARCHAR(9),@FinishDate, 112)
WHILE(@CurrentDate<=@LastDate)
BEGIN
IF (DATEPART(dw, @CurrentDate)!=1 AND DATEPART(dw, @CurrentDate)!=7)
BEGIN
IF (@CurrentDate!=@FirstDay) AND (@CurrentDate!=@LastDay)
BEGIN
SET @Temp = (@Temp + (8*60))
END
ELSE IF (@CurrentDate=@FirstDay) AND (@CurrentDate!=@LastDay)
BEGIN
SET @Temp = @Temp + DATEDIFF(MINUTE, @StartTime, @WorkFinish)
END
ELSE IF (@CurrentDate!=@FirstDay) AND (@CurrentDate=@LastDay)
BEGIN
SET @Temp = @Temp + DATEDIFF(MINUTE, @WorkStart, @FinishTime)
END
ELSE IF (@CurrentDate=@FirstDay) AND (@CurrentDate=@LastDay)
BEGIN
SET @Temp = DATEDIFF(MINUTE, @StartTime, @FinishTime)
END
END
SET @CurrentDate = CONVERT(VARCHAR(9),DATEADD(day, 1, @CurrentDate),112)
END
Return @TEMP
END
-- =============================================
-- Author: Baran Kaynak
-- Create date: 14.03.2011
-- Description: 09:30 ile 17:30 arasındaki iş saatlerini hafta sonlarını almayarak toplar.
-- =============================================
CREATE FUNCTION [dbo].[WorkTime]
(
@StartDate DATETIME,
@FinishDate DATETIME
)
RETURNS BIGINT
AS
BEGIN
DECLARE @Temp BIGINT
SET @Temp=0
DECLARE @FirstDay DATE
SET @FirstDay = CONVERT(DATE, @StartDate, 112)
DECLARE @LastDay DATE
SET @LastDay = CONVERT(DATE, @FinishDate, 112)
DECLARE @StartTime TIME
SET @StartTime = CONVERT(TIME, @StartDate)
DECLARE @FinishTime TIME
SET @FinishTime = CONVERT(TIME, @FinishDate)
DECLARE @WorkStart TIME
SET @WorkStart = '09:30'
DECLARE @WorkFinish TIME
SET @WorkFinish = '17:30'
IF (@StartTime<@WorkStart)
BEGIN
SET @StartTime = @WorkStart
END
IF (@FinishTime>@WorkFinish)
BEGIN
SET @FinishTime=@WorkFinish
END
DECLARE @CurrentDate DATE
SET @CurrentDate = CONVERT(DATE, @StartDate, 112)
DECLARE @LastDate DATE
SET @LastDate = CONVERT(DATE, @FinishDate, 112)
WHILE(@CurrentDate<=@LastDate)
BEGIN
IF (DATEPART(dw, @CurrentDate)!=1 AND DATEPART(dw, @CurrentDate)!=7)
BEGIN
IF (@CurrentDate!=@FirstDay) AND (@CurrentDate!=@LastDay)
BEGIN
SET @Temp = (@Temp + (9*60))
END
--IF it starts at startdate and it finishes not this date find diff between work finish and start as minutes
ELSE IF (@CurrentDate=@FirstDay) AND (@CurrentDate!=@LastDay)
BEGIN
SET @Temp = @Temp + DATEDIFF(MINUTE, @StartTime, @WorkFinish)
END
ELSE IF (@CurrentDate!=@FirstDay) AND (@CurrentDate=@LastDay)
BEGIN
SET @Temp = @Temp + DATEDIFF(MINUTE, @WorkStart, @FinishTime)
END
--IF it starts and finishes in the same date
ELSE IF (@CurrentDate=@FirstDay) AND (@CurrentDate=@LastDay)
BEGIN
SET @Temp = DATEDIFF(MINUTE, @StartDate, @FinishDate)
END
END
SET @CurrentDate = DATEADD(day, 1, @CurrentDate)
END
-- Return the result of the function
IF @Temp<0
BEGIN
SET @Temp=0
END
RETURN @Temp
END
GO
Here is an inline version Start/EndDateTime like 2015-03-16 09:52:24.000 Start/EndTime (businesshours) like 07:00:00 It is bulky but works in your select statement
I will post it in Function version as well.
Case when <StartDate>= <EndDate> then 0
When Convert(date,<StartDate>) = Convert(date,<EndDate>) Then
IIF( DATEPART(Dw,<StartDate>) in(1,7)
or Convert(time,<StartDate>) > Convert(time,<EndTime>)
or Convert(time,<EndDate>) < Convert(time,<StartTime>),0,
DateDiff(S,IIF(Convert(time,<StartDate>) < Convert(time,<StartTime>),Convert(time,<StartTime>),Convert(time,<StartDate>))
,IIF(Convert(time,<EndDate>) > Convert(time,<EndTime>), Convert(time,<EndTime>), Convert(time,<EndDate>))))
when Convert(date,<StartDate>) <> Convert(date,<EndDate>) then
IIF(DATEPART(Dw,<StartDate>) in(1,7) or Convert(time,<StartDate>) > Convert(time,<EndTime>),0 ,DateDiff(S,IIF(Convert(time,<StartDate>) < Convert(time,<StartTime>),Convert(time,<StartTime>),Convert(time,<StartDate>)), Convert(time,<EndTime>)))
+ IIF(DATEPART(Dw,<EndDate>) in(1,7) or Convert(time,<EndDate>) < Convert(time,<StartTime>),0,DateDiff(S,Convert(time,<StartTime>),IIF(Convert(time,<EndDate>) > Convert(time,<EndTime>), Convert(time,<EndTime>), Convert(time,<EndDate>))))
else -333
end --as pday
+IIF(DatePart(wEEk,<StartDate>) = DatePart(wEEk,<EndDate>)
,0, (DateDiff(wk,dateadd(d,-datepart(dw,<StartDate>),dateadd(ww,1,<StartDate>)),DATEADD(wk, DATEDIFF(wk, 6, <EndDate>), 6)-1) * 5)) * Datediff(S, Convert(time,<StartTime>),Convert(time,<EndTime>)) --Fullweek_days
+Case When Convert(date,<StartDate>) = Convert(date,<EndDate>) then 0
When DatePart(wEEk,<StartDate>) <> DatePart(wEEk,<EndDate>) then
IIF( datepart(dw,<StartDate>) = 7,0,DateDIFF(DAY,<StartDate>+1,dateadd(d,-datepart(dw,<StartDate>),dateadd(ww,1,<StartDate>)))) -- beginFulldays
+IIF( datepart(dw,<EndDate>) = 1,0,DateDIFF(DAY,DATEADD(wk, DATEDIFF(wk, 6, <EndDate>), 6),<EndDate> -1)) --Endfulldays
When DatePart(wEEk,<StartDate>) = DatePart(wEEk,<EndDate>) then
DateDiff(DAY,<StartDate>+1,<EndDate> )
ELSE -333 END * Datediff(S, Convert(time,<StartTime>),Convert(time,<EndTime>))
Here is the Function Version:
CREATE FUNCTION [dbo].[rsf_BusinessTime]
(
@startDateTime Datetime,
@endDateTime Datetime ,
@StartTime VarChar(12),
@EndTime VarChar(12) )
RETURNS BIGINT
As
BEGIN
Declare @totalSeconds BigInt,
@SecondsInDay int,
@dayStart Time = Convert(time,@StartTime),
@dayEnd Time =Convert(time,@EndTime),
@SatAfterStart Datetime = dateadd(d,-datepart(dw,@startDateTime),dateadd(ww,1,@startDateTime)),
@Sunbeforend Datetime = DATEADD(wk, DATEDIFF(wk, 6, @endDateTime), 6)
-- This function calculates the seconds between the start and end dates provided for business hours.
-- It only returns the time between the @start and @end time (hour of day) of the work week.
-- Weekend days are removed.
-- Holidays are not considered.
Set @SecondsInDay = Datediff(S, @dayStart,@dayEnd)
Set @totalSeconds =
--first/last/sameday
Case when @startDateTime= @endDateTime then 0
When Convert(date,@startDateTime) = Convert(date,@endDateTime) Then
IIF( DATEPART(Dw,@startDateTime) in(1,7)
or Convert(time,@startDateTime) > @dayEnd
or Convert(time,@endDateTime) < @dayStart,0,
DateDiff(S,IIF(Convert(time,@startDateTime) < @dayStart,@dayStart,Convert(time,@startDateTime))
,IIF(Convert(time,@endDateTime) > @dayEnd, @dayEnd, Convert(time,@endDateTime))))
when Convert(date,@startDateTime) <> Convert(date,@endDateTime) then
IIF(DATEPART(Dw,@startDateTime) in(1,7) or Convert(time,@startDateTime) > @dayEnd,0 ,DateDiff(S,IIF(Convert(time,@startDateTime) < @dayStart,@dayStart,Convert(time,@startDateTime)), @dayEnd))
+ IIF(DATEPART(Dw,@endDateTime) in(1,7) or Convert(time,@endDateTime) < @dayStart,0,DateDiff(S,@dayStart,IIF(Convert(time,@endDateTime) > @dayEnd, @dayEnd, Convert(time,@endDateTime))))
else -333
end --as pday
+IIF(DatePart(wEEk,@startDateTime) = DatePart(wEEk,@endDateTime)
,0, (DateDiff(wk,@SatAfterStart,@Sunbeforend-1) * 5)) * @SecondsInDay --Fullweek_days
+Case When Convert(date,@startDateTime) = Convert(date,@endDateTime) then 0
When DatePart(wEEk,@startDateTime) <> DatePart(wEEk,@endDateTime) then
IIF( datepart(dw,@startDateTime) = 7,0,DateDIFF(DAY,@startDateTime+1,@SatAfterStart)) -- beginFulldays
+IIF( datepart(dw,@endDateTime) = 1,0,DateDIFF(DAY,@Sunbeforend,@endDateTime -1)) --Endfulldays
When DatePart(wEEk,@startDateTime) = DatePart(wEEk,@endDateTime) then
DateDiff(DAY,@startDateTime+1,@endDateTime )
ELSE -333 END * @SecondsInDay
Return @totalSeconds
END
What do you think about this solution?
Without using loop "While".
create function dbo.WorkingHoursBetweenDates ( @StartDate datetime, @EndDate datetime, @StartTime time, @EndTime time )
returns decimal ( 10, 2 )
as
begin
return
case
when @EndTime < @StartTime or @EndDate < @StartDate then
0
else
round
( ( dbo.WorkingDaysBetweenDates(@StartDate, @EndDate) -
( dbo.WorkingDaysBetweenDates(@StartDate, @StartDate) *
case
when cast ( @StartDate as time ) > @EndTime then
1
else
datediff
( mi,
@StartTime
, case
when @StartTime > cast ( @StartDate as time ) then
@StartTime
else
cast ( @StartDate as time )
end
) /
( datediff ( mi, @StartTime, @EndTime ) + 0.0 )
end
) -
( dbo.WorkingDaysBetweenDates(@EndDate, @EndDate) *
case
when cast ( @EndDate as time ) < @StartTime then
1
else
datediff
( mi,
case
when @EndTime < cast ( @EndDate as time ) then
@EndTime
else
cast ( @EndDate as time )
end,
@EndTime
) /
( datediff ( mi, @StartTime, @EndTime ) + 0.0 )
end
)
) *
( datediff ( mi, @StartTime, @EndTime ) / 60.0 ), 2
)
end
end
------
create function dbo.WorkingDaysBetweenDates ( @StartDate date, @EndDate date )
returns int
as
begin
return
( datediff(dd, @StartDate, @EndDate) + 1 ) -
( datediff(wk, @StartDate, @EndDate) * 2 ) -
( case when datename(dw, @StartDate) = 'Sunday' then 1 else 0 end ) -
( case when datename(dw, @EndDate) = 'Saturday' then 1 else 0 end ) -
( select
count ( 1 )
from
dbo.Tb_Holidays
where
HDate between @StartDate and @EndDate
and datename(dw, HDate) not in ( 'Sunday', 'Saturday' )
)
end
The question says that public holidays should not be considered, so this answer does just that - calculates business hours taking weekends into account, but ignoring possible public holidays.
It also assumes that the given start and end date/times are during the business hours.
With this assumption the code doesn't care about the time when the business day starts or ends, it cares only about the total number of business hours per day. In your example, there are 8 business hours between 09:00 and 17:00. It doesn't have to be a whole number. The formula below calculates it with one minute precision, but it is trivial to make it one second or any other precision.
If you need to take public holidays into account you'd need to have a separate table which would list dates for public holidays, which may differ from year to year and from state to state or country to country. The main formula may stay the same, but you'd need to subtract from its result hours for public holidays that fall within the given range of dates.
The formula
SELECT
DATEDIFF(minute, StartDT, EndDT) / 60.0
- DATEDIFF(day, StartDT, EndDT) * 16
- DATEDIFF(week, StartDT, EndDT) * 16 AS BusinessHours
FROM T
To understand how it works let's create a table with some sample data that covers various cases:
DECLARE @T TABLE (StartDT datetime2(0), EndDT datetime2(0));
INSERT INTO @T VALUES
('2012-03-05 09:00:00', '2012-03-05 15:00:00'), -- simple part of the same day
('2012-03-05 10:00:00', '2012-03-06 10:00:00'), -- full day across the midnight
('2012-03-05 11:00:00', '2012-03-06 10:00:00'), -- less than a day across the midnight
('2012-03-05 10:00:00', '2012-03-06 15:00:00'), -- more than a day across the midnight
('2012-03-09 16:00:00', '2012-03-12 10:00:00'), -- over the weekend, less than 7 days
('2012-03-06 16:00:00', '2012-03-15 10:00:00'), -- over the weekend, more than 7 days
('2012-03-09 16:00:00', '2012-03-19 10:00:00'); -- over two weekends
The query
SELECT
StartDT,
EndDT,
DATEDIFF(minute, StartDT, EndDT) / 60.0
- DATEDIFF(day, StartDT, EndDT) * 16
- DATEDIFF(week, StartDT, EndDT) * 16 AS BusinessHours
FROM @T;
produces the following result:
+---------------------+---------------------+---------------+
| StartDT | EndDT | BusinessHours |
+---------------------+---------------------+---------------+
| 2012-03-05 09:00:00 | 2012-03-05 15:00:00 | 6.000000 |
| 2012-03-05 10:00:00 | 2012-03-06 10:00:00 | 8.000000 |
| 2012-03-05 11:00:00 | 2012-03-06 10:00:00 | 7.000000 |
| 2012-03-05 10:00:00 | 2012-03-06 15:00:00 | 13.000000 |
| 2012-03-09 16:00:00 | 2012-03-12 10:00:00 | 2.000000 |
| 2012-03-06 16:00:00 | 2012-03-15 10:00:00 | 50.000000 |
| 2012-03-09 16:00:00 | 2012-03-19 10:00:00 | 42.000000 |
+---------------------+---------------------+---------------+
It works, because in SQL Server DATEDIFF returns the count of the specified datepart boundaries crossed between the specified startdate and enddate.
Each day has 8 business hours. I calculate total number of hours between two dates, then subtract the number of midnights multiplied by 16 non-business hours per day, then subtract the number of weekends multiplied by 16 (8+8 business hours for Sat+Sun).