2

I need to calculate the business hours (8am - 5pm) between different dates. Here is the scenario: Tickets get assigned to employees (Ticket_Submission (DateTime)), then the employee creates a product (Build_Date (Datetime)), now sometimes in order to create a product a medication needs to be built (Med_Date (Datetime)). In order to calculate the how long it took for the employee to create a product, I need to DATEDIFF between Ticket_submission AND Build_Date, however, if medication is involved, it had to be between Med_Date AND Build_Date. It is OK to include weekends and holidays. The following helps me to get the difference in days:

.....
,IIF((med_date = buid_date OR ticket_submission = build_date), 1 
, IIF(med_date IS NULL OR med_date < ticket_submission , (DATEDIFF(dd, ticket_submission,build_date))
, DATEDIFF(dd, med_date, build_date))) AS buildcompletedate,….

This only gives me the number of days, also, I know that if instead of DD, I put hours I can get the total number of hours between the 2 dates. But, how I can have these in business hours?

I do appreciate your help.

ticketID  Ticket_submission               Med_Date                Build_Date
1549392    2017-04-07 10:31:06:210        2017-04-08 11:31:06:210  2017-04-09 12:30:08:110
1751406    2017-06-06 4:30:08:200        2018-08-06 3:30:08:200   2018-09-10 3:30:08:200 
2583870    2019-11-20 1:20:01:100        NULL                     2019-11-23 2:20:01:100          

To help construct test sample, here is a common table expression that constructs the test sample in-memory:

; with Ticket (TicketID, Ticket_Submission, Med_Date, Build_Date)
AS
(
    SELECT TicketID = 1549392, Ticket_Submission = CAST('2017-04-07 10:31:06:210' AS DATETIME2), Med_Date = CAST('2017-04-08 11:31:06:210' AS DATETIME2), Build_Date = CAST('2017-04-09 12:30:08:110' AS DATETIME2) UNION ALL
    SELECT 1751406, CAST('2017-06-06 4:30:08:200' AS DATETIME2), CAST('2018-08-06 3:30:08:200' AS DATETIME2), CAST('2018-09-10 3:30:08:200 ' AS DATETIME2) UNION ALL
    SELECT 2583870, CAST('2019-11-20 1:20:01:100' AS DATETIME2), CAST(NULL AS DATETIME2), CAST('2019-11-23 2:20:01:100' AS DATETIME2)
)
select

    TicketID,
    0 AS RoundedBusinessHoursBetweenDates /* Update with answer code */
from Ticket

Expected Results

TicketID  RoundedBusinessHoursBetweenDates 
--------  --------------------------------
1549392   10
1751406   307
2583870   20????

Rounding

  • It's OK to round hours
    • for example, instead of 4 hours 30 min, have it as 5 hours.

Understanding the Calculation, Step-By-Step

  1. For the first row (TicketID 1549392), I expect to see:
    1. 10 hours ( about 5 hours and 30 min from 11:31 am till 5 pm (for the first day)+ 4.5 hours for 8 am till 12:30 for the second day)
  2. For the second row (TicketID 1751406), I expect to see:
    1. 2 hours for the first day (for the med_build date) + 8 hours for the last day (8 am till 3:30 pm) + 9 * remaining days between these 2 days
  3. For the third row (TicketID 2583870), if Med_Date is NULL then only the difference from ticket_submission till build_date
John Zabroski
  • 2,212
  • 2
  • 28
  • 54
nina_dev
  • 625
  • 2
  • 10
  • 19
  • If you can put your test data in text format and provide business hours it will be easier – Avi Apr 12 '20 at 16:22
  • Sample data and desired results in text form would be most helpful. – John Cappelletti Apr 12 '20 at 16:27
  • @Avi posted, thanks. – nina_dev Apr 12 '20 at 16:41
  • @JohnCappelletti, posted, thanks. – nina_dev Apr 12 '20 at 16:42
  • must take into account holidays (each country may have different days) .. A scalar function can be implemented that returns the number of working hours that have passed, with the interval of working days and their schedule, rest days and holidays – Barney CBZ Apr 12 '20 at 16:53
  • @BarneyCBZ, holidays can also be included... – nina_dev Apr 12 '20 at 16:56
  • That can add more complexity to the matter, but vacation days can be included in holidays, the problem is that the calculation is complex. but it can be implemented – Barney CBZ Apr 12 '20 at 17:01
  • @nina_dev The third example you mentioned doesn't make a lot of sense to me. 11/23/2019 is a Saturday. It's not clear to me from the requirements what the right answer here is in this case. I also formatted your question to make it easier for people to give a stab at a solution, by providing a CTE formula they can copy and paste into SSMS. – John Zabroski Apr 12 '20 at 17:33
  • @nina_dev Note, it also seems that when you say "round", you are using it in inconsistent ways. In the second example, the Med_Date is 2018-08-06 3:30:08:200 and you're saying the time between 3:30.08:200 is 2 hours. To me, rounding this implies 1 hour. However, here you're actually taking the floor of the Start Date and the Ceiling of the end date, using hour intervals. – John Zabroski Apr 12 '20 at 17:39
  • @nina_dev I edited it one last time to try to help out. However, I do think you should look it over and confirm it's as you intended. Once I'm confident the requirements are nailed down, I can answer the question. – John Zabroski Apr 12 '20 at 18:03
  • @JohnZabroski, I don't see any edits to confirm it. Thanks – nina_dev Apr 12 '20 at 18:09
  • @nina_dev the edits showed up now, i think. Please let me know if you can see them. – John Zabroski Apr 19 '20 at 13:17
  • @John Zabroski, yes, I can see the edits. Thx – nina_dev Apr 21 '20 at 03:00

3 Answers3

2

I am not sure your data is valid. I assume the date for the 2nd row should be 2018-09-10 15:30:08:200 as 2018-09-10 3:30:08:200 is not within working hours. Anyway, you'll need to calculate the hours in 3 separate parts. The hours from the start DateTime to 5 pm and the hours from 8 am of the last date to the real last DateTime, plus the working hours in between.

It's not pretty, but here is the sample code. The sum of these 3 columns is your total hours.

declare @tblTemp table(ticketId int, ticket_submission datetime, med_date datetime, build_date datetime)
insert into @tblTemp
values(1549392,'2017-04-07 10:31:06:210','2017-04-08 11:31:06:210','2017-04-09 12:30:08:110'),
    (1751406,'2017-06-06 16:30:08:200','2018-08-06 15:30:08:200','2018-09-10 15:30:08:200'),
    (2583870,'2019-11-20 13:20:01:100',null,'2019-11-23 14:20:01:100')

select ticketId, datediff(hour, coalesce(med_date, ticket_submission), convert(varchar(10), coalesce(med_date, ticket_submission), 120) + ' 17:00:00') -- first day hours
    ,(datediff(day, coalesce(med_date, ticket_submission), build_date) - 1) * (17-8) -- hours in between
    ,datediff(hour, convert(varchar(10), build_date, 120) + ' 08:00:00', build_date) -- last day hours
from @tblTemp

To exclude weekends, see this so answer or this one. If you want to exclude weekends and holidays, you might need a calendar table.

Weihui Guo
  • 3,669
  • 5
  • 34
  • 56
  • I do appreciate your time! Can you please help me understand the script? for the following lines: select datediff(hour, coalesce(med_date, ticket_submission), convert(varchar(19), convert(date, coalesce(med_date, ticket_submission))) + ' 17:00:00') -- first day hours ,(datediff(day, coalesce(med_date, ticket_submission), build_date) - 1) * (17-8) -- hours in between ,datediff(hour, convert(varchar(19), convert(date, build_date)) + ' 08:00:00', build_date) -- last day hours @Weihui Guo – nina_dev Apr 12 '20 at 19:45
  • Thanks, can you please explain the first line, and the third line, as well? I mean : select datediff(hour, coalesce(med_date, ticket_submission), convert(varchar(19), convert(date, coalesce(med_date, ticket_submission))) + ' 17:00:00') and datediff(hour, convert(varchar(19), convert(date, build_date)) + ' 08:00:00', build_date) @Weihui Guo – nina_dev Apr 12 '20 at 19:52
  • I am sorry for not being clear, I am trying to understand what convert(varchar(19), convert(date, coalesce(med_date, ticket_submission))) + ' 17:00:00') does I know that coalesce, gives me the first item that is not null, but why convert(varchar(19)... comes after it and then again date... – nina_dev Apr 12 '20 at 19:59
  • Thank you! just my last question, please. why do I need to add ' 17:00:00'' or add ' 08:00:00' since I am actually counting the hours till 5 pm, shouldn't I subtract it? this is the part that's confusing for me... adding these 2 numbers. Thanks again, you saved my life! – nina_dev Apr 12 '20 at 20:07
  • Perfectly makes sense! I do appreciate your time and expertise! – nina_dev Apr 12 '20 at 20:13
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/211501/discussion-between-weihui-guo-and-nina-dev). – Weihui Guo Apr 12 '20 at 20:13
  • Can you please explain what 121 does here : coalesce(med_date, ticket_submission), 121) on the first line... and why we used varchar(19) not 20 or 25? Thanks, @Weihui Guo – nina_dev Apr 14 '20 at 15:45
0

Try this:

; with Ticket (TicketID, Ticket_Submission, Med_Date, Build_Date)
AS
(
    SELECT TicketID = 1549392, Ticket_Submission = CAST('2017-04-07 10:31:06:210' AS DATETIME2), Med_Date = CAST('2017-04-08 11:31:06:210' AS DATETIME2), Build_Date = CAST('2017-04-09 12:30:08:110' AS DATETIME2) UNION ALL
    SELECT 1751406, CAST('2017-06-06 4:30:08:200' AS DATETIME2), CAST('2018-08-06 3:30:08:200' AS DATETIME2), CAST('2018-09-10 3:30:08:200 ' AS DATETIME2) UNION ALL
    SELECT 2583870, CAST('2019-11-20 1:20:01:100' AS DATETIME2), CAST(NULL AS DATETIME2), CAST('2019-11-23 2:20:01:100' AS DATETIME2)
)
select

    TicketID,
    CASE
        WHEN DATEDIFF(Hour,ISNULL(Med_Date,ticket_submission),build_date)>24
        THEN (DATEDIFF(Hour,ISNULL(Med_Date,ticket_submission),build_date)-15)/24
    END AS DAYS,
    CASE
        WHEN DATEDIFF(Hour,ISNULL(Med_Date,ticket_submission),build_date)>24 AND (DATEDIFF(Hour,ISNULL(Med_Date,ticket_submission),build_date)-15)/24 >0
        THEN DATEDIFF(Hour,ISNULL(Med_Date,ticket_submission),build_date) -(15 * DATEDIFF(DAY,ISNULL(Med_Date,ticket_submission),build_date))-9
        ELSE DATEDIFF(Hour,ISNULL(Med_Date,ticket_submission),build_date) -(15 * DATEDIFF(DAY,ISNULL(Med_Date,ticket_submission),build_date))
    END
from Ticket

The Total_Hours Column will Return the Total number of Columns Between the Two Days and the Days and Hours Column Return the Days and Hours Differences between those Columns.

Hope this Code Works Successfully for your Rows and Sorry for my previous answer i misunderstood your Question.

Anyway Thanks John Zabroski and Martin Smith

John Zabroski
  • 2,212
  • 2
  • 28
  • 54
Thiyagu
  • 1,260
  • 1
  • 5
  • 14
  • This is objectively not correct based on @nina_dev stating the first row should be 10 hours difference. You are not taking into account weekends and holidays. – John Zabroski Apr 12 '20 at 17:15
  • @JohnZabroski the questioner doesn't care about those "It is OK to include weekends and holidays." – Martin Smith Apr 12 '20 at 17:22
  • In that case, the answer is still wrong, but I guessed at the wrong reason why it was wrong. Please see my updated formatting to nina_dev's question. If you plug in Thiyagu's calculation, it does not give the expected result, objectively. – John Zabroski Apr 12 '20 at 17:35
  • 1
    @JohnZabroski I have corrected the code.can u Check and update me whether they are correct or not – Thiyagu Apr 12 '20 at 18:54
  • @Thiyagu I think your new answer is very close to correct. I think nina_dev needs to elaborate on how rounding works, and then it is likely correct. I believe you need to take the "FLOOR" of the nearest hour on the start date, and the "CEILING" to the nearest hour on the end date. – John Zabroski Apr 12 '20 at 19:29
0

Several aspects were analyzed to implement the solution.

  1. Schedule (with the days and hours per available for business)
  2. The Holidays and religious (List Day)

by means of a function, it establishes the days related to the schedule

and if any of the days worked are holidays, evaluating each of the time ranges

already with these relations it is calculated by day how much I worked and calculates the total hours

with the code you can understand it better

CREATE FUNCTION TimeInBusiness
(
    @StarDate as Datetime,
    @EndDate as Datetime,
    @Schedule NVARCHAR(MAX),
    @HolyDay as varchar(3000)
)
RETURNS decimal(18,2)
AS 
BEGIN
    declare @Globalization as varchar(6)='en-US'
    declare @Returnvalue as varchar(4000);
    DECLARE @ListHolyDay TABLE(HolyDay Date);
    DECLARE @ListDays TABLE(Day Date,DayWeek varchar(20),StarTime Time,EndTime Time);

    INSERT INTO @ListHolyDay(HolyDay)
    SELECT try_cast(value as date)
    FROM  STRING_SPLIT(@HolyDay, ',')
    where try_cast(value as date) is not null

    DECLARE @ListBusinessDay TABLE(DayWeek varchar(20),InveralTimeStar Time,InveralTimeEnd Time);
    INSERT INTO @ListBusinessDay(DayWeek,InveralTimeStar,InveralTimeEnd)
    SELECT DayWeek,cast(InveralTimeStar as time) as InveralTimeStar,cast(InveralTimeEnd as time) as InveralTimeEnd
    FROM OPENJSON(@Schedule)
      WITH (
        DayWeek NVARCHAR(20) '$.Interval.DayWeek',
        InveralTimeStar NVARCHAR(20) '$.Interval.InveralTimeStar',
        InveralTimeEnd NVARCHAR(20) '$.Interval.InveralTimeEnd'
      );

    declare @SecondsHour as float=3600
    declare @IntervalStarDate as Date
    declare @IntervalEndDate as Date
    set @IntervalStarDate=cast(@StarDate as date)
    set @IntervalEndDate=cast(@EndDate as date)
    declare @StarTime as varchar(8)=''
    declare @EndTime as varchar(8)=''

    WHILE (@IntervalStarDate<=@IntervalEndDate) 
    BEGIN
        if (@IntervalStarDate<@StarDate)
        begin
            set @StarTime=FORMAT( @StarDate, 'HH:mm:ss', @Globalization )
        end
        else
        begin
            set @StarTime=FORMAT( @IntervalStarDate, 'HH:mm:ss', @Globalization )
        end
        if (@IntervalStarDate<@IntervalEndDate)
        begin
            set @EndTime='23:59:59'
        end
        else
        begin
            set @EndTime=FORMAT( @EndDate, 'HH:mm:ss', @Globalization )
        end
        INSERT INTO @ListDays(Day,DayWeek,StarTime,EndTime )VALUES (@IntervalStarDate, FORMAT( @IntervalStarDate, 'ddd', @Globalization ),cast(@StarTime as time),cast(@EndTime as time)) 
    set @IntervalStarDate=DATEADD(Day,1,@IntervalStarDate)
    END;

    with ForDayTime as 
    (
    select ListDays.Day,ListBusinessDay.DayWeek ,ListBusinessDay.InveralTimeStar,ListBusinessDay.InveralTimeEnd,
    ListDays.StarTime,ListDays.EndTime,
    CASE
        WHEN ListBusinessDay.InveralTimeStar<=ListDays.StarTime THEN ListDays.StarTime
        ELSE ListBusinessDay.InveralTimeStar
    END as StarTimeDay
    ,
    CASE
        WHEN ListBusinessDay.InveralTimeEnd>=ListDays.EndTime THEN ListDays.EndTime
        ELSE ListBusinessDay.InveralTimeEnd
    END as EndTimeDay
    from @ListDays as ListDays  
     inner join @ListBusinessDay as ListBusinessDay on ListDays.DayWeek =ListBusinessDay.DayWeek 
    where (ListDays.Day not in (select HolyDay from @ListHolyDay))
    )
    select 
@Returnvalue=isnull(sum(datediff(SECOND,  StarTimeDay,EndTimeDay)) /@SecondsHour,0) 
    from ForDayTime
    where (StarTimeDay<EndTimeDay)
    RETURN @Returnvalue;
    END;

Example Use

declare @StarDate as Datetime='2020-04-10 10:00:00'
declare @EndDate as Datetime='2020-04-20 12:59:00'
DECLARE @Schedule NVARCHAR(MAX);
SET @Schedule = N'[
{"Interval": {"DayWeek": "Mon", "InveralTimeStar": "09:30", "InveralTimeEnd": "11:30"}},
{"Interval": {"DayWeek": "Mon", "InveralTimeStar": "13:30", "InveralTimeEnd": "16:30"}},
{"Interval": {"DayWeek": "Tue", "InveralTimeStar": "08:30", "InveralTimeEnd": "16:30"}},
{"Interval": {"DayWeek": "Wed", "InveralTimeStar": "08:30", "InveralTimeEnd": "16:30"}},
{"Interval": {"DayWeek": "Thu", "InveralTimeStar": "08:30", "InveralTimeEnd": "16:30"}},
{"Interval": {"DayWeek": "Fri", "InveralTimeStar": "08:30", "InveralTimeEnd": "12:30"}}
]';
declare @HolyDay as varchar(3000)='2020-04-16,2020-04-17'
select 
dbo.TimeInBusiness('2020-04-10 10:00:00','2020-04-20 12:59:00',@Schedule,@HolyDay) as result,
dbo.TimeInBusiness('2020-04-10 10:00:00','2020-04-20 12:59:00',@Schedule,'') as result2,
dbo.TimeInBusiness('2020-04-01 10:00:00','2020-04-20 12:59:00',@Schedule,@HolyDay) as result
Go

example used funtion

Barney CBZ
  • 101
  • 7