Splitting time + duration into intervals in t-sql - tsql

Does anyone know of a simple method for solving this?
I have a table which consists of start times for events and the associated durations. I need to be able to split the event durations into thirty minute intervals. So for example if an event starts at 10:45:00 and the duration is 00:17:00 then the returned set should allocate 15 minutes to the 10:30:00 interval and 00:02:00 minutes to the 11:00:00 interval.
I'm sure I can figure out a clumsy approach but would like something a little simpler. This must come up quite often I'd imagine but Google is being unhelpful today.
Thanks,
Steve

You could create a lookup table with just the times (over 24 hours), and join to that table. You would need to rebase the date to that used in the lookup. Then perform a datediff on the upper and lower intervals to work out their durations. Each middle interval would be 30 minutes.
create table #interval_lookup (
from_date datetime,
to_date datetime
)
declare #time datetime
set #time = '00:00:00'
while #time < '2 Jan 1900'
begin
insert into #interval_lookup values (#time, dateadd(minute, 30, #time))
set #time = dateadd(minute, 30, #time)
end
declare #search_from datetime
declare #search_to datetime
set #search_from = '10:45:00'
set #search_to = dateadd(minute, 17, #search_from)
select
from_date as interval,
case
when from_date <= #search_from and
#search_from < to_date and
from_date <= #search_to and
#search_to < to_date
then datediff(minute, #search_from, #search_to)
when from_date <= #search_from and
#search_from < to_date
then datediff(minute, #search_from, to_date)
when from_date <= #search_to and
#search_to < to_date then
datediff(minute, from_date, #search_to)
else 30
end as duration
from
#interval_lookup
where
to_date > #search_from
and from_date <= #search_to

Create TVF that splits single event:
ALTER FUNCTION dbo.TVF_TimeRange_Split_To_Grid
(
#eventStartTime datetime
, #eventDurationMins float
, #intervalMins int
)
RETURNS #retTable table
(
intervalStartTime datetime
,intervalEndTime datetime
,eventDurationInIntervalMins float
)
AS
BEGIN
declare #eventMinuteOfDay int
set #eventMinuteOfDay = datepart(hour,#eventStartTime)*60+datepart(minute,#eventStartTime)
declare #intervalStartMinute int
set #intervalStartMinute = #eventMinuteOfDay - #eventMinuteOfDay % #intervalMins
declare #intervalStartTime datetime
set #intervalStartTime = dateadd(minute,#intervalStartMinute,cast(floor(cast(#eventStartTime as float)) as datetime))
declare #intervalEndTime datetime
set #intervalEndTime = dateadd(minute,#intervalMins,#intervalStartTime)
declare #eventDurationInIntervalMins float
while (#eventDurationMins>0)
begin
set #eventDurationInIntervalMins = cast(#intervalEndTime-#eventStartTime as float)*24*60
if #eventDurationMins<#eventDurationInIntervalMins
set #eventDurationInIntervalMins = #eventDurationMins
insert into #retTable
select #intervalStartTime,#intervalEndTime,#eventDurationInIntervalMins
set #eventDurationMins = #eventDurationMins - #eventDurationInIntervalMins
set #eventStartTime = #intervalEndTime
set #intervalStartTime = #intervalEndTime
set #intervalEndTime = dateadd(minute,#intervalMins,#intervalEndTime)
end
RETURN
END
GO
Test:
select getdate()
select * from dbo.TVF_TimeRange_Split_To_Grid(getdate(),23,30)
Test Result:
2008-10-31 11:28:12.377
intervalStartTime intervalEndTime eventDurationInIntervalMins
----------------------- ----------------------- ---------------------------
2008-10-31 11:00:00.000 2008-10-31 11:30:00.000 1,79372222222222
2008-10-31 11:30:00.000 2008-10-31 12:00:00.000 21,2062777777778
Sample usage:
select input.eventName, result.* from
(
select
'first' as eventName
,cast('2008-10-03 10:45' as datetime) as startTime
,17 as durationMins
union all
select
'second' as eventName
,cast('2008-10-05 11:00' as datetime) as startTime
,17 as durationMins
union all
select
'third' as eventName
,cast('2008-10-05 12:00' as datetime) as startTime
,100 as durationMins
) input
cross apply dbo.TVF_TimeRange_Split_To_Grid(input.startTime,input.durationMins,30) result
Sample usage result:
eventName intervalStartTime intervalEndTime eventDurationInIntervalMins
--------- ----------------------- ----------------------- ---------------------------
first 2008-10-03 10:30:00.000 2008-10-03 11:00:00.000 15
first 2008-10-03 11:00:00.000 2008-10-03 11:30:00.000 2
second 2008-10-05 11:00:00.000 2008-10-05 11:30:00.000 17
third 2008-10-05 12:00:00.000 2008-10-05 12:30:00.000 30
third 2008-10-05 12:30:00.000 2008-10-05 13:00:00.000 30
third 2008-10-05 13:00:00.000 2008-10-05 13:30:00.000 30
third 2008-10-05 13:30:00.000 2008-10-05 14:00:00.000 10
(7 row(s) affected)

Related

generate series based on particular day in each month -postgresql

i have following query in postgresql for dates between 2 ranges.
select generate_series('2019-04-01'::timestamp, '2020-03-31', '1 month')
as g_date
I need to generate specific date in every month .i.e 15 th of every month. Following is my query to generate series
DO $$
DECLARE
compdate date = '2019-04-15';
BEGIN
CREATE TEMP TABLE tmp_table ON COMMIT DROP AS
select *,
case
when extract('day' from d) <> extract('day' from compdate) then 0
when ( extract('month' from d)::int - extract('month' from compdate)::int ) % 1 = 0 then 1
else 0
end as c
from generate_series('2019-04-01'::timestamp, '2020-03-31', '1 day') d;
END $$;
SELECT * FROM tmp_table
where c=1;
;
But every thing is perfect if input date between (1..29)-04-2019 ..
2019-04-25
2019-05-25
2019-06-25
2019-07-25
2019-08-25
2019-09-25
2019-10-25
2019-11-25
2019-12-25
2020-01-25
2020-02-25
2020-03-25
but if i give compdate: 31-04-2019 or 30-04-2019 giving out put:
2019-05-31
2019-07-31
2019-08-31
2019-10-31
2019-12-31
2020-01-31
2020-03-31
Expected Output:
date flag
2019-04-01 0 ----start_date
2019-04-30 1
2019-05-31 1
2019-06-30 1
2019-07-31 1
2019-08-31 1
2019-09-30 1
2019-10-31 1
2019-11-30 1
2019-12-31 1
2020-01-31 1
2020-02-29 1
2020-03-31 0 ---end_date
If matched day not found in the result it should take last day of that month..i.e if 31 not found in month of feb it
should take 29-02-2019 and also in april month instead of 31 it should take 2019-04-30.
Please suggest.
to generate the last days of the month, just generate first days & subtract a 1 day interval
example: the following generates all last day of month in the year 2010
SELECT x - interval '1 day' FROM
GENERATE_SERIES('2010-02-01', '2011-01-01', interval '1 month') x
You cannot accomplish what you want with generate_series. This results due to that process applying a fixed increment from the previous generated value. Your case 1 month. Now Postgres will successfully compute correct end-of-month date from 1 month to the next. So for example 1month from 31-Jan yields 28-Feb (or 29), because 31-Feb would be an invalid date, Postgres handles it. However, that same interval from 28-Feb gives the valid date 28-Mar so no end-of-month adjustment is needed. Generate_Series will return 28th of the month from then on. The same applies to 30 vs. 31 day months.
But you can achieve what your after with a recursive CTE by employing a varying interval to the same initial start date. If the resulting date is invalid for date the necessary end-of-month adjustment will be made. The following does that:
create or replace function constant_monthly_date
( start_date timestamp
, end_date timestamp
)
returns setof date
language sql strict
as $$
with recursive date_set as
(select start_date ds, start_date sd, end_date ed, 1 cnt
union all
select (sd + cnt*interval '1 month') ds, sd, ed, cnt+1
from date_set
where ds<end_date
)
select ds::date from date_set;
$$;
-- test
select * from constant_monthly_date(date '2020-01-15', date '2020-12-15' );
select * from constant_monthly_date(date '2020-01-31', date '2020-12-31' );
Use the least function to get the least one between the computed day and end of month.
create or replace function test1(day int) returns table (t timestamptz) as $$
select least(date_trunc('day', t) + make_interval(days => day-1), date_trunc('day', t) + interval '1 month' - interval '1 day') from generate_series('2019-04-01', '2020-03-31', interval '1 month') t
$$ language sql;
select test1(31);

How does one query a DateTimeOffset column to get all rows for a particular day?

So let's say you have a table with the column below (type is datetimeoffset(3)).
DTO_Created
2017-04-28 03:16:56.942 -05:00
2017-05-01 00:20:54.925 -05:00
2017-05-01 12:17:52.752 -05:00
2017-05-01 23:21:00.198 -05:00
2017-05-02 01:19:23.254 -05:00
How would you query to only get the rows created on 2017-05-01? (3 rows total) I am attempting this, but am not getting all 3.
SELECT * FROM MyTable WHERE DTO_Created >= '2017-05-01 00:00:00.000' AND DTO_Created <= '2017-05-01 23:59:59.999'
The problem seems to be caused by the type (datetimeoffset) because this doesn't happen with regular datetime columns.
Environment: SQL Server 2016
You need to use datetimeoffset for the where clause as well.
First, create and populate sample table (Please save us this step in your future questions):
DECLARE #T AS TABLE
(
DTO_Created DateTimeOffset
)
INSERT INTO #T(DTO_Created) VALUES
('2017-04-28 03:16:56.942 -05:00'),
('2017-05-01 00:20:54.925 -05:00'),
('2017-05-01 12:17:52.752 -05:00'),
('2017-05-01 23:21:00.198 -05:00'),
('2017-05-02 01:19:23.254 -05:00')
The query:
SELECT *
FROM #T
WHERE DTO_Created >= '2017-05-01 00:00:00.000 -05:00'
AND DTO_Created < '2017-05-02 00:00:00.000 -05:00'
Results:
DTO_Created
2017-05-01 00:20:54.925 -05:00
2017-05-01 12:17:52.752 -05:00
2017-05-01 23:21:00.198 -05:00
Another option for 2016 or higher version is to use AT TIME ZONE, but you'll have to add the hours difference to the search value, and beware of daylight savings time:
SELECT *
FROM #T
WHERE DTO_Created >= CAST('2017-05-01 05:00:00' AS DateTime2) AT TIME ZONE 'Easter Island Standard Time' -- -05:00
AND DTO_Created < CAST('2017-05-02 05:00:00' AS DateTime2) AT TIME ZONE 'Easter Island Standard Time'
If you want to ignore local time and treat the UTC part of the DateTimeOffset as if it was DateTime you can simply cast the DateTimeOffset to Date, though that would be a non-sargable predicate:
SELECT *
FROM #T
WHERE CAST(DTO_Created As Date) = '2017-05-01'
Try this
CONVERT(DATETIME, DTO_Created, 1) >= '2017-05-01 00:00:00.000' AND CONVERT(DATETIME, DTO_Created, 1) <= '2017-05-01 23:59:59.999'
OR this
cast(DTO_Created as datetime) >= '2017-05-01 00:00:00.000' AND cast(DTO_Created as datetime) <= '2017-05-01 23:59:59.999'

Group Records By Time Period

I'm trying to take a time frame the user selects and then group the selection into time periods - in this case: 2 weeks.
For instance, today is 5/4/2018 and if I set that as my start date and 5/31/2018 as my end date, I get the following:
DECLARE #StartDate DATE ,
#EndDate DATE ,
#ToDate DATE;
SET #StartDate = GETDATE ();
SET #EndDate = '20180531';
SET #ToDate = DATEADD ( DAY, 1, #EndDate );
SELECT dd.Date ,
ROW_NUMBER () OVER ( ORDER BY DATEPART ( WEEK, dd.Date )) AS [rownumb]
FROM dbo.DateDimension AS [dd]
WHERE dd.Date >= #StartDate
AND dd.Date < #ToDate;
And the results look like:
Date rownumb
2018-05-04 1
2018-05-05 2
2018-05-06 3
2018-05-07 4
2018-05-08 5
2018-05-09 6
2018-05-10 7
2018-05-11 8
2018-05-12 9
2018-05-13 10
2018-05-14 11
2018-05-15 12
2018-05-16 13
2018-05-17 14
2018-05-18 15
2018-05-19 16
2018-05-20 17
2018-05-21 18
2018-05-22 19
2018-05-23 20
2018-05-24 21
2018-05-25 22
2018-05-26 23
2018-05-27 24
2018-05-28 25
2018-05-29 26
2018-05-30 27
2018-05-31 28
I was playing around with ROW_NUMBER ( along with RANK and DENSE_RANK ) but I have not been able to get these functions to accomplish what I am looking for but what I am hoping to do is have an additional column called "TimePeriod" where the dates are grouped together in 2-week increments ( or 14 days ) so that 5/4/18 through 5/17/18 have a value of 1 for the "TimePeriod" column and 5/18/18 through 5/31/18 would have a value of 2 for the "TimePeriod" column. And this should be dynamic so that wider date ranges are grouped in periods of two weeks with each period increasing by 1.
Suggestions?
If there's no requirement to use the ordering and ranking functions in sql, you can easily implement as below.
get the total number of days between the start and end date
for each date you subtract the days difference of the current date from the total days difference, then divide this by 14
so this basically will give you the interval (2 weeks) to which the current date belongs, it's zero based so you might want to add a 1 to it
DECLARE #StartDate DATE ,
#EndDate DATE ,
#ToDate DATE;
DECLARE #DaysDiff INT;
SET #StartDate = GETDATE ();
SET #EndDate = '20180531';
SET #ToDate = DATEADD ( DAY, 1, #EndDate );
--GET the difference in days between the start and end date
SET #DaysDiff = DATEDIFF( Day, #StartDate,#ToDate )
SELECT dd.Date ,
( #DaysDiff - DATEDIFF(Day,dd.Date,#ToDate) )/14
FROM dbo.DateDimension AS [dd]
WHERE dd.Date >= #StartDate
AND dd.Date < #ToDate;

SQL Server not showing correct week numbers for given date

SQL Server is showing week 53 for first week of 2011 except 1th of January, and needs to be week 1.
Below is the query and output:
declare #T table (dt datetime)
insert into #T values
('2010-12-26'),
('2010-12-27'),
('2010-12-28'),
('2010-12-29'),
('2010-12-30'),
('2010-12-31'),
('2011-01-01'),
('2011-01-02'),
('2011-01-03'),
('2011-01-04'),
('2011-01-05'),
('2011-01-06'),
('2011-01-07'),
('2011-01-08')
select dt,DATEPART(wk,dt) from #T
Output:
2010-12-26 00:00:00.000 53
2010-12-27 00:00:00.000 53
2010-12-28 00:00:00.000 53
2010-12-29 00:00:00.000 53
2010-12-30 00:00:00.000 53
2010-12-31 00:00:00.000 53
2011-01-01 00:00:00.000 1
2011-01-02 00:00:00.000 2
2011-01-03 00:00:00.000 2
2011-01-04 00:00:00.000 2
2011-01-05 00:00:00.000 2
2011-01-06 00:00:00.000 2
2011-01-07 00:00:00.000 2
2011-01-08 00:00:00.000 2
I want SQL Server to show week 1 from Dec 26th - Jan 1th. Does anybody know how to accomplish this?
Thanks and regards,
Aschwin.
It was alot harder than I first expected. I am comparing the end of last year to see if it is qualified to be part of the new year. If so i set the week as 1, otherwise i just use the normal week.
declare #T table (dt datetime)
insert into #T values
('2010-12-25'),
('2010-12-26'),
('2010-12-27'),
('2010-12-28'),
('2010-12-29'),
('2010-12-30'),
('2010-12-31'),
('2011-01-01'),
('2011-01-02'),
('2011-01-03'),
('2011-01-04'),
('2011-01-05'),
('2011-01-06'),
('2011-01-07'),
('2011-01-08'),
('2011-12-31'),
('2012-01-01')
select dt,
week = case when dt + 6 - datediff(day, -1, dt) % 7 = dateadd(year, datediff(year,-1, dt), 0)
then 1 else datepart(week, dt) end from #t
Proof:
https://data.stackexchange.com/stackoverflow/q/110527/
I am not sure it holds for all years (but it looks like it) but you could solve this using a CASE statement.
SELECT dt
, CASE WHEN DATEPART(wk, dt) <> 53
THEN DATEPART(wk, dt)
ELSE 1
END
FROM #T
The new ISO_WEEK datepart doesn't apply to your requested output.
I Created 2 functions to deal with this issue
1) to get First or last day of the week
2) to get the week number or year
function 1
CREATE FUNCTION [dbo].[fn_GetDayOf]
(
#Date datetime,
--#FirstDayOfWeek int = 7,
#Mode int =1
)
/*
Mode 1: First Day Of Week
Mode 2: Last Day Of Week
*/
RETURNS datetime
WITH EXECUTE AS CALLER
BEGIN
Declare #Return datetime
--SET DATEFIRST #FirstDayOfWeek
IF #Mode = 1
BEGIN
select #Return = dateadd(day,-(datepart(weekday,#date)-1),convert(date,#date))
END
ELSE IF #Mode = 2
BEGIN
select #Return = dateadd(SECOND,-1,convert(datetime,dateadd(day,(datepart(weekday,#date)),convert(date,#date))))
END
ELSE
BEGIN
SET #Return = #Date
END
--SET DATEFIRST 7
RETURN #Return
END
Function 2
CREATE FUNCTION [dbo].[fn_GetYearWeek]
(
#Date datetime,
--#FirstDayOfWeek int = 7,
#Mode int =1
)
/*
Mode 1 = Week Number
Mode 2 = Year
*/
RETURNS INT
BEGIN
declare #Return int
IF #Mode = 1
BEGIN
select #Return = case when datepart(week,[dbo].[fn_GetDayOf] (#Date,1)) <> datepart(week,[dbo].[fn_GetDayOf] (#Date,2)) then datepart(week,[dbo].[fn_GetDayOf] (#Date,1)) else datepart(week,[dbo].[fn_GetDayOf] (#Date,2)) end
END
ELSE IF #Mode = 2
BEGIN
select #Return = case when datepart(WEEK,[dbo].[fn_GetDayOf] (#Date,1)) <> datepart(week,[dbo].[fn_GetDayOf] (#Date,2)) then datepart(YEAR,[dbo].[fn_GetDayOf] (#Date,1)) else datepart(YEAR,[dbo].[fn_GetDayOf] (#Date,2)) end
END
ELSE
BEGIN
SET #Return = -1
END
Return #Return
END
Running Example
declare #T table (dt datetime)
insert into #T values
('2010-12-25'),
('2010-12-26'),
('2010-12-27'),
('2010-12-28'),
('2010-12-29'),
('2010-12-30'),
('2010-12-31'),
('2011-01-01'),
('2011-01-02'),
('2011-01-03'),
('2011-01-04'),
('2011-01-05'),
('2011-01-06'),
('2011-01-07'),
('2011-01-08'),
('2011-12-31'),
('2012-01-01'),
('2012-01-02'),
('2012-12-31'),
('2013-01-01')
select
dt,
datepart(week,dt),
--case when datepart(week,[dbo].[fn_GetDayOf] (dt,1)) <> datepart(week,[dbo].[fn_GetDayOf] (dt,2)) then datepart(week,[dbo].[fn_GetDayOf] (dt,1)) else datepart(week,[dbo].[fn_GetDayOf] (dt,2)) end
[dbo].[fn_GetYearWeek] (dt,1),
[dbo].[fn_GetYearWeek] (dt,2)
from #T
result:
Another way to retrieve the total number of weeks in current year:
DECLARE #LASTDAY DATETIME
DECLARE #weeks INT
SET #LASTDAY = DATEADD(ms,-3,DATEADD(yy,0,DATEADD(yy,DATEDIFF(yy,0,GETDATE())+1,0)))
SELECT #weeks = CASE DATEname(dw,#LASTDAY)
WHEN 'MONDAY' THEN DATEPART(WK, DATEADD(wk,DATEDIFF(wk,7,#LASTDAY),5))
WHEN 'TUESDAY' THEN DATEPART(WK, DATEADD(wk,DATEDIFF(wk,7,#LASTDAY),5))
WHEN 'WEDNESDAY' THEN DATEPART(WK, DATEADD(wk,DATEDIFF(wk,7,#LASTDAY),5))
ELSE DATEPART(WK, #LASTDAY)
END
select #weeks

Is it possible to set start of week for T-SQL DATEDIFF function?

I use DATEDIFF function to filter records added this week only:
DATEDIFF(week, DateCreated, GETDATE()) = 0
and I noticed what it's assumed what week starts on Sunday. But in my case I would prefer to set start of week on Monday. Is it possible somehow in T-SQL?
Thanks!
Update:
Below is an example showing what DATEDIFF doesn't check ##DATEFIRST variable so I need another solution.
SET DATEFIRST 1;
SELECT
DateCreated,
DATEDIFF(week, DateCreated, CAST('20090725' AS DATETIME)) AS D25,
DATEDIFF(week, DateCreated, CAST('20090726' AS DATETIME)) AS D26
FROM
(
SELECT CAST('20090724' AS DATETIME) AS DateCreated
UNION
SELECT CAST('20090725' AS DATETIME) AS DateCreated
) AS T
Output:
DateCreated D25 D26
----------------------- ----------- -----------
2009-07-24 00:00:00.000 0 1
2009-07-25 00:00:00.000 0 1
(2 row(s) affected)
26 Jul 2009 is Sunday, and I want DATEDIFF returns 0 in third column too.
Yes it possible
SET DATEFIRST 1; -- Monday
from http://msdn.microsoft.com/en-us/library/ms181598.aspx
It appears datediff doesn't respect the Datefirst, so make it do so run it like this
create table #testDates (id int identity(1,1), dateAdded datetime)
insert into #testDates values ('2009-07-09 15:41:39.510') -- thu
insert into #testDates values ('2009-07-06 15:41:39.510') -- mon
insert into #testDates values ('2009-07-05 15:41:39.510') -- sun
insert into #testDates values ('2009-07-04 15:41:39.510') -- sat
SET DATEFIRST 7 -- Sunday (Default
select * from #testdates where datediff(ww, DATEADD(dd,-##datefirst,dateadded), DATEADD(dd,-##datefirst,getdate())) = 0
SET DATEFIRST 1 -- Monday
select * from #testdates where datediff(ww, DATEADD(dd,-##datefirst,dateadded), DATEADD(dd,-##datefirst,getdate())) = 0
Stolen from
http://social.msdn.microsoft.com/Forums/en-US/transactsql/thread/8cc3493a-7ae5-4759-ab2a-e7683165320b
I have another solution.
This should be easier to understand, correct me if I am wrong
SET DATEFIRST 1
select DATEDIFF(week, 0, DATEADD(day, -##DATEFIRST, '2018-04-15 00:00:00.000'))
We subtract '-1' from date and Sunday will become Saturday (which is 7nth day of week)
and Mondфy(2) will first day of week
So if i'm getting this correctly,
the only thing we need to do is remove 1 day from both dates on our datediff as following :
DATEDIFF(week,dateadd(day,-1,cast(GETDATE() as date)),
dateadd(day,-1,cast([Date] as date))) as RollingWeek