Business Hours Logic and exceptions to Opening Hours - sql

I Have created a date dimension from using the following:
https://www.codeproject.com/Articles/647950/Create-and-Populate-Date-Dimension-for-Data-Wareho
This is just a standard one and I have added 2 columns for OpeningTime and Closing Time with the value of Opening being 08:00:00 and Closing being 18:00:00.
I also have a computed field that works out the difference in minutes between the start time and end time as a non persistent computed field.
I have the following Logic below, but before give you this, let me set the scene of showing the context of the situation of the business, so they are normally open from 8am to 6pm Mon - Fri and also Sat at 8am - 1pm
However there can be exception to the rules as they can open for longer or even on sundays. The Code i have below for some reason falls over by showing less on a sunday given that on rare occasions if they were to be open on a sunday for argument sake 8am to 1pm. Obviously for whatever extending opening hours they are open for even on a Sunday, I would manually have to add it this to the Dim_Date calender which for starttime and enddate I have 00:00:00 for both giving me a 0 for datediff in min by default unless the business tell me otherwise.
But the following Code does NOT take that into account and calculates less than what it should be, something wrong with the code if someone can please provide a solution, basically I just want the code to be flexible so I only make changes to the calender even for extended hours or unusual business hours and the code to reflect that. Thank you.
declare #Date1 datetime = '2017-08-01 08:00:00'
declare #Date2 datetime = '2017-08-07 09:10:00'
declare #StartTime time = cast(#Date1 as time)
declare #EndTime time = cast(#Date2 as time)
declare #CCStartTime time = (select StartTime from dim_date where id = convert(nvarchar(8),#Date1,112))
declare #CCEndTime time = (select EndTime from dim_date where id = convert(nvarchar(8),#Date2,112))
declare #ActualStart time = (select case
when datename(DW,#Date1)='Sunday' then '00:00:00'
when #StartTime between '00:00:00' and #CCStartTime then #CCStartTime
when #StartTime between #CCStartTime and #CCEndTime then #StartTime
when #StartTime between #CCEndTime and '23:59:59' then #CCEndTime end)
declare #ActualEnd time = (select case
when datename(DW,#Date2)='Sunday' then '00:00:00'
when #EndTime between '00:00:00' and #CCStartTime then #CCStartTime
when #EndTime between #CCStartTime and #CCEndTime then #EndTime
when #EndTime between #CCEndTime and '23:59:59' then #CCEndTime end)
declare #DiffrenceStart int = isnull(DATEDIFF(minute,#CCStartTime,#ActualStart),0)
declare #DiffrenceEnd int = isnull(DATEDIFF(minute,#ActualEnd,#CCEndTime),0)
/*
select #StartTime as StartDate
select #EndTime as EndDate
select #CCStartTime as CCStartDate
select #CCEndTime as CCEndDate
select #ActualStart as ActualStart
select #ActualEnd as ActualEnd
select abs(#DiffrenceStart) as DiffrenceStart
select abs(#DiffrenceEnd) as DifrenceEnd
*/
select sum(Min)- (#DiffrenceStart + #DiffrenceEnd)
from dim_date
where id between convert(nvarchar(8),#Date1,112) and convert(nvarchar(8),#Date2,112)

Look at this:
declare #ActualEnd time = (select case
when datename(DW,#Date2)='Sunday' then '00:00:00'
souldn't it be:
declare #ActualEnd time = (select case
when datename(DW,#Date2)='Sunday' then '23:59:59'

Related

Multiple DATEADD functions in one query - TSQL

I'm trying to make multiple edits on a random date to leave a full random datetime within a range of dates (3 months back, 3 months forward) but setting the Hours/Minutes/Seconds/Milliseconds to 0.
I'm doing this so I can then add a random amount of time to create activity start and end times that will always fit within office working hours. The script below sets a variable for the start time of the activity, then carries out 5 separate edits to this variable to zero the time element.
Is there an easier way to carry out multiple DATEADD edits, it seems clunky!
DECLARE #STARTTIME DATETIME
DECLARE #ENDTIME DATETIME
SET #STARTTIME = (SELECT DATEADD(DAY,ABS(CHECKSUM(NEWID()) % 180), (select dateadd(dd, -90, getdate()) )))
SET #STARTTIME = (SELECT DATEADD(HH, - (SELECT DATEPART(HH,#STARTTIME)),#STARTTIME))
SET #STARTTIME = (SELECT DATEADD(MI, - (SELECT DATEPART(MI,#STARTTIME)),#STARTTIME))
SET #STARTTIME = (SELECT DATEADD(SS, - (SELECT DATEPART(SS,#STARTTIME)),#STARTTIME))
SET #STARTTIME = (SELECT DATEADD(MS, - (SELECT DATEPART(MS,#STARTTIME)),#STARTTIME))
SET #STARTTIME = (SELECT DATEADD(hh,(SELECT FLOOR(RAND()*(15-08)+08)), #STARTTIME))
SET #ENDTIME = (SELECT DATEADD(hh,(SELECT FLOOR(RAND()*(3-1)+1)), #STARTTIME))
SELECT
#STARTTIME AS 'STARTTIME',
#ENDTIME AS 'ENDTIME'
Results
STARTIME 2017-04-02 13:00:00.000
ENDTIME 2017-04-02 15:00:00.000
There is not a need to use newid() in T-SQL code. It is only needed within a query to generate multiple random numbers.
So:
set #starttime = cast(datedd(day, floor(rand() * 180 - 90), getdate()) as date);
set #starttime = dateadd(hour, floor(rand()*(15-08)+08), #starttime);
set #enddtime = dateadd(hour, floor(rand()*(3-1)+1), #starttime);
Notes:
This uses cast(. . . as date) to remove the time component of the date.
There is no need to have nested select statements.
For T-SQL code, you can use rand(), rather than the newid() work-around (that is needed within a single query to generate multiple random values).
Don't use date part abbreviations such as "hh". Just spell out the date part. The code is much easier to write and maintain.
You convert it to a Date data type would set the time part to 00:00:000
DECLARE #STARTTIME DATETIME
DECLARE #ENDTIME DATETIME
SET #STARTTIME = cast((SELECT DATEADD(DAY,ABS(CHECKSUM(NEWID()) % 180), (select dateadd(dd, -90, getdate()) ))) as date)
SET #STARTTIME = (SELECT DATEADD(hh,(SELECT FLOOR(RAND()*(15-08)+08)), #STARTTIME))
SET #ENDTIME = (SELECT DATEADD(hh,(SELECT FLOOR(RAND()*(3-1)+1)), #STARTTIME))
SELECT
#STARTTIME AS 'STARTTIME',
#ENDTIME AS 'ENDTIME'

Query of set time range relative to current date

I come in to work 6.30am, and need to audit what happened from 5pm when I left to 6.30am this morning. I have used code to search 13.5 hours back from any given time:
SELECT * FROM TRANSACTION_HISTORY
WHERE TRANSACTION_HISTORY.ACTIVITY_DATE_TIME > (SELECT DATEADD(hour,-13.5,(SELECT MAX (TRANSACTION_HISTORY.ACTIVITY_DATE_TIME) FROM TRANSACTION_HISTORY)))
Problem is if I run query later, I lose time from the start, eg. If I run query at 7am, I only get results from 5.30pm onwards. Rather than change criteria every day, I wanted to able to search from 6.30am of the current day, back to 5.30pm of the previous day. Can this be done?
Normally, I don't advise using between for datetime, because the boundary conditions can cause confusion. I don't think that is the case.
Here is one method:
SELECT *
FROM TRANSACTION_HISTORY th
WHERE th.ACTIVITY_DATE_TIME between cast(cast(getdate() -1 as date) as datetime) + 17.5/24.0
cast(cast(getdate() as date) as datetime) + 6.5/24.0;
The expression cast(cast(getdate() as date) as datetime) is truncating the datetime value to midnight. As a datetime, SQL Server lets you add a number which is understood as a fraction of a day. Hence, 17.5/24 represents "5:30". This addition doesn't work for the date data type.
You can do this.
DECLARE #dt datetime
DECLARE #dt1 datetime
DECLARE #dt2 datetime
SET #dt = CONVERT(datetime, CONVERT(VARCHAR(10), getdate(), 101 ), 101)
SET #dt1 = dateadd(mi, 30, dateadd(hh, 6, #dt))
SET #dt2 = dateadd(mi, -27*30, #dt1)
SELECT #dt1 as StartDate, #dt2 as EndDate
For additional details what this script does you may check these pages.
http://technet.microsoft.com/en-us/library/ms174450%28v=sql.105%29.aspx
http://technet.microsoft.com/en-us/library/ms186819%28v=sql.105%29.aspx

SQL Group by Minute- Expanded

I am working on something similar to this post here:
TS SQL - group by minute
However mine is pulling from an message queue, and I need to see an accurate count of the amount of traffic the Message Queue is creating/ sending, and at what time
Select * From MessageQueue mq
My expanded version of this though is the following:
A) User defines a start time and an end time (Easy enough using Declare's #StartTime and #EndTime
B) Give the user the option of choosing the "grouping". Will it be broken out by 1 minutes, 5 minutes, 15 minutes, or 30 minutes (Max). (I had thought to do this with a CASE statement, but my test problems fall apart on me.)
C) Display the data to accurately show a count of what happened during the interval (Grouping) selected.
This is where I am at so far
SQL Blob:
DECLARE #StartTime datetime
DECLARE #EndTime datetime
SELECT DATEPART(n, mq.cre_date)/5 as Time --Trying to just sort by 5 minute intervals
,CONVERT(VARCHAR(10),mq.Cre_Date,101)
,COUNT(*) as results
FROM dbo.MessageQueue mq
WHERE mq.cre_date BETWEEN #StartDate AND #EndDate
GROUP BY DATEPART(n, mq.cre_date)/5 --Trying to just sort by 5 minute intervals
, eq.Cre_Date
This is the output I would like to achieve:
[Time] [Date] [Message Count]
1300 06/26/2012 5
1305 06/26/2012 1
1310 06/26/2012 100
Here is a way to do what you want:
DECLARE #StartTime DATETIME, #EndTime DATETIME
DECLARE #Interval INT
SET #StartTime = '20130625'
SET #EndTime = '20130627'
SET #Interval = 5
SELECT CONVERT(VARCHAR(10),mq.Cre_Date,101) [Date],
CONVERT(TIME,DATEADD(MINUTE,DATEDIFF(MINUTE,0,mq.Cre_Date)/#Interval*#Interval,0)) [Time],
COUNT(*) Results
FROM dbo.MessageQueue mq
WHERE mq.cre_date >= #StartDate
AND mq.cre_date <= #EndDate
GROUP BY CONVERT(VARCHAR(10),mq.Cre_Date,101),
CONVERT(TIME,DATEADD(MINUTE,DATEDIFF(MINUTE,0,mq.Cre_Date)/#Interval*#Interval,0))

Add Date parameter and time parameter to single date time

I'm modifying a store procedure in SQL Server 2008. The original procedure took in two dates
#StartDate DATETIME
#EndDate DATETIME
And would convert the time portion to the Earliest and latest possible times respectively.
I have added two additional parameters to accept Time Portions.
#StartTime DATETIME
#EndTime DATETIME
The Time portions are optional.
An RDL report file generates the report online for the users. The logic needs to occur in the Stored Proc.
What I have so far is not much, as I'm a C# programmer leaving my element.
IF (#StartTime IS NULL)
SET #StartDate = fn_SetBeginningTime(#StartDate) -- Sets time portion to earliest value
ELSE
-- This is where I don't know how to add the time from #StartTime to the time portion of the datetime value #StartDate
IF (#EndTime IS NULL)
-- This will do the same as the start time/start date
Assuming:
By earliest you mean 00:00:00 on the start date
By latest you mean 00:00:00 on the day after enddate (for use with <=)
Should there be any, this will ignore the time from a #*Date param and the date from a#*Time param.
declare #StartDate DATETIME = '15 jul 2010'
declare #EndDate DATETIME = '15 jul 2010'
declare #StartTime DATETIME = '06:06:06'
declare #EndTime DATETIME = null
--always make #StartDate/#EndDate's time 00:00:00
SET #StartDate = CAST(#StartDate AS DATE)
SET #EndDate = CAST(#EndDate AS DATE)
IF (#StartTime IS NOT NULL) -- set #StartDate's time to #StartTime
SET #StartDate += CAST(#StartTime AS TIME)
IF (#EndTime IS NULL)
SET #EndDate += 1 --set it to midnight
ELSE
SET #EndDate += CAST(#EndTime AS TIME) --set #EndDate's time to #EndTime
select #StartDate, #EndDate
>>> 2010-07-15 06:06:06.000,2010-07-16 00:00:00.000
For DATE / TIME;
declare #StartDate DATE = '15 jul 2010'
declare #EndDate DATE = '15 jul 2010'
declare #StartTime TIME = null
declare #EndTime TIME = '22:22:22'
declare #start datetime = cast(#StartDate as datetime) + coalesce(#StartTime, cast('00:00:00' as time))
declare #end datetime = cast(#EndDate as datetime) + coalesce(#EndTime, cast('23:59:59' as time))
select #start, #end

Calculate time difference (only working hours) in minutes between two dates

I need to calculate the number of "active minutes" for an event within a database. The start-time is well known.
The complication is that these active minutes should only be counted during a working day - Monday-Friday 9am-6.30pm, excluding weekends and (known) list of holiday days
The start or "current" time may be outside working hours, but still only the working hours are counted.
This is SQL Server 2005, so T-SQL or a managed assembly could be used.
If you want to do it pure SQL here's one approach
CREATE TABLE working_hours (start DATETIME, end DATETIME);
Now populate the working hours table with countable periods, ~250 rows per year.
If you have an event(#event_start, #event_end) that will start off hours and end off hours then simple query
SELECT SUM(end-start) as duration
FROM working_hours
WHERE start >= #event_start AND end <= #event_end
will suffice.
If on the other hand the event starts and/or ends during working hours the query is more complicated
SELECT SUM(duration)
FROM
(
SELECT SUM(end-start) as duration
FROM working_hours
WHERE start >= #event_start AND end <= #event_end
UNION ALL
SELECT end-#event_start
FROM working_hours
WHERE #event_start between start AND end
UNION ALL
SELECT #event_end - start
FROM working_hours
WHERE #event_end between start AND end
) AS u
Notes:
the above is untested query, depending on your RDBMS you might need date/time functions for aggregating and subtracting datetime (and depending on the functions used the above query can work with any time precision).
the query can be rewritten to not use the UNION ALL.
the working_hours table can be used for other things in the system and allows maximum flexibility
EDIT:
In MSSQL you can use DATEDIFF(mi, start, end) to get the number of minutes for each subtraction above.
Using unreason's excellent starting point, here is a TSQL implementation for SQL Server 2012.
This first SQL populates a table with our work days and times excluding weekends and holidays:
declare #dteStart date
declare #dteEnd date
declare #dtStart smalldatetime
declare #dtEnd smalldatetime
Select #dteStart = '2016-01-01'
Select #dteEnd = '2016-12-31'
CREATE TABLE working_hours (starttime SMALLDATETIME, endtime SMALLDATETIME);
while #dteStart <= #dteEnd
BEGIN
IF datename(WEEKDAY, #dteStart) <> 'Saturday'
AND DATENAME(WEEKDAY, #dteStart) <> 'Sunday'
AND #dteStart not in ('2016-01-01' --New Years
,'2016-01-18' --MLK Jr
,'2016-02-15' --President's Day
,'2016-05-30' --Memorial Day
,'2016-07-04' --Fourth of July
,'2016-09-05' --Labor Day
,'2016-11-11' --Veteran's Day
,'2016-11-24' --Thanksgiving
,'2016-11-25' --Day after Thanksgiving
,'2016-12-26' --Christmas
)
BEGIN
select #dtStart = SMALLDATETIMEFROMPARTS(year(#dteStart),month(#dteStart),day(#dteStart),8,0) --8:00am
select #dtEnd = SMALLDATETIMEFROMPARTS(year(#dteStart),month(#dteStart),day(#dteStart),17,0) --5:00pm
insert into working_hours values (#dtStart,#dtEnd)
END
Select #dteStart = DATEADD(day,1,#dteStart)
END
Now here is the logic that worked to return the minutes as an INT:
declare #event_start datetime2
declare #event_end datetime2
select #event_start = '2016-01-04 8:00'
select #event_end = '2016-01-06 16:59'
SELECT SUM(duration) as minutes
FROM
(
SELECT DATEDIFF(mi,#event_start,#event_end) as duration
FROM working_hours
WHERE #event_start >= starttime
AND #event_start <= endtime
AND #event_end <= endtime
UNION ALL
SELECT DATEDIFF(mi,#event_start,endtime)
FROM working_hours
WHERE #event_start >= starttime
AND #event_start <= endtime
AND #event_end > endtime
UNION ALL
SELECT DATEDIFF(mi,starttime,#event_end)
FROM working_hours
WHERE #event_end >= starttime
AND #event_end <= endtime
AND #event_start < starttime
UNION ALL
SELECT SUM(DATEDIFF(mi,starttime,endtime))
FROM working_hours
WHERE starttime > #event_start
AND endtime < #event_end
) AS u
This correctly returns 1 minute shy of three 9 hour work days
I came here looking for an answer to a very similar question - I needed to get the minutes between 2 dates excluding weekends and excluding hours outside of 08:30 and 18:00. After a bit of hacking around, I think i have it sorted. Below is how I did it. thoughts are welcome - who knows, maybe one day I'll sign up to this site :)
create function WorkingMinutesBetweenDates(#dteStart datetime, #dteEnd datetime)
returns int
as
begin
declare #minutes int
set #minutes = 0
while #dteEnd>=#dteStart
begin
if datename(weekday,#dteStart) <>'Saturday' and datename(weekday,#dteStart)<>'Sunday'
and (datepart(hour,#dteStart) >=8 and datepart(minute,#dteStart)>=30 )
and (datepart(hour,#dteStart) <=17)
begin
set #minutes = #minutes + 1
end
set #dteStart = dateadd(minute,1,#dteStart)
end
return #minutes
end
go
I started working with what Unreason posted and was a great start. I tested this is SQL Server and found not all time was being captured. I think the problem was primarily when the event started and ended the same day. This solution seems to be working well enough for me
CREATE TABLE [dbo].[working_hours](
[wh_id] [int] IDENTITY(1,1) NOT FOR REPLICATION NOT NULL,
[wh_starttime] [datetime] NULL,
[wh_endtime] [datetime] NULL,
PRIMARY KEY CLUSTERED
(
[wh_id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO
CREATE FUNCTION [dbo].[udFWorkingMinutes]
(
#startdate DATETIME
,#enddate DATETIME
)
RETURNS INT
AS
BEGIN
DECLARE #WorkingHours INT
SET #WorkingHours =
(SELECT
CASE WHEN COALESCE(SUM(duration),0) < 0 THEN 0 ELSE SUM(Duration)
END AS Minutes
FROM
(
--All whole days
SELECT ISNULL(DATEDIFF(mi,wh_starttime,wh_endtime),0) AS Duration
FROM working_hours
WHERE wh_starttime >= #startdate AND wh_endtime <= #enddate
UNION ALL
--All partial days where event start after office hours and finish after office hours
SELECT ISNULL(DATEDIFF(mi,#startdate,wh_endtime),0) AS Duration
FROM working_hours
WHERE #startdate > wh_starttime AND #enddate >= wh_endtime
AND (CAST(wh_starttime AS DATE) = CAST(#startdate AS DATE))
AND #startdate < wh_endtime
UNION ALL
--All partial days where event starts before office hours and ends before day end
SELECT ISNULL(DATEDIFF(mi,wh_starttime,#enddate),0) AS Duration
FROM working_hours
WHERE #enddate < wh_endtime
AND #enddate >= wh_starttime
AND #startdate <= wh_starttime
AND (CAST(wh_endtime AS DATE) = CAST(#enddate AS DATE))
UNION ALL
--Get partial day where intraday event
SELECT ISNULL(DATEDIFF(mi,#startdate,#enddate),0) AS Duration
FROM working_hours
WHERE #startdate > wh_starttime AND #enddate < wh_endtime
AND (CAST(#startdate AS DATE)= CAST(wh_starttime AS DATE))
AND (CAST(#enddate AS DATE)= CAST(wh_endtime AS DATE))
) AS u)
RETURN #WorkingHours
END
GO
Alls that is left to do is populate the working hours table with something like
;WITH cte AS (
SELECT CASE WHEN DATEPART(Day,'2014-01-01 9:00:00 AM') = 1 THEN '2014-01-01 9:00:00 AM'
ELSE DATEADD(Day,DATEDIFF(Day,0,'2014-01-01 9:00:00 AM')+1,0) END AS myStartDate,
CASE WHEN DATEPART(Day,'2014-01-01 5:00:00 PM') = 1 THEN '2014-01-01 5:00:00 PM'
ELSE DATEADD(Day,DATEDIFF(Day,0,'2014-01-01 5:00:00 PM')+1,0) END AS myEndDate
UNION ALL
SELECT DATEADD(Day,1,myStartDate), DATEADD(Day,1,myEndDate)
FROM cte
WHERE DATEADD(Day,1,myStartDate) <= '2015-01-01'
)
INSERT INTO working_hours
SELECT myStartDate, myEndDate
FROM cte
OPTION (MAXRECURSION 0)
delete from working_hours where datename(dw,wh_starttime) IN ('Saturday', 'Sunday')
--delete public holidays
delete from working_hours where CAST(wh_starttime AS DATE) = '2014-01-01'
My first post! Be merciful.
Globally, you'd need:
A way to capture the end-time of the event (possibly through notification, or whatever started the event in the first place), and a table to record this beginning and end time.
A helper table containing all the periods (start and end) to be counted. (And then you'd need some supporting code to keep this table up to date in the future)
A stored procedure that will:
iterate over this helper table and find the 'active' periods
calculate the minutes within each active period.
(Note that this assumes the event can last multiple days: is that really likely?)
A different method would be to have a ticking clock inside the event, which checks every time whether the event should be counted at that time, and increments (in seconds or minutes) every time it discovers itself to be active during the relevant period. This would still require the helper table and would be less auditable (presumably).