Add Working Days to date - sql

In my quest to construct a function that can calculate the date after x working days I came across this function:
ALTER FUNCTION [dbo].[AddBusinessDays] (#Date date,#n INT)
RETURNS DATE AS BEGIN
DECLARE #d INT;
SET #d=4-SIGN(#n)*(4-DATEPART(DW,#Date));
RETURN DATEADD(D,#n+((ABS(#n)+#d-2)/5)*2*SIGN(#n)-#d/7,#Date) END
This function works however I need to link it with my holiday table so that it can omit specific holidays in my country. When I run it with today's date (26-04-2017) I get this date after 20 working days 24-05-2017, so it omitted only the weekends. How do I modify it so that it also skips the holidays?
Apologies if I am sending too many requests for one problem. I am a beginner in SQL. Thanks

Instead of relying on hard to understand calculations wouldn't it be easier to explicitly have your working dates and select from there? For example:
DECLARE #NthWorkingDay INT = 33;
DECLARE #holidays TABLE
(
holiday DATE ,
[description] VARCHAR(500)
);
INSERT #holidays
( holiday, description )
VALUES ( '20170519', '...' ),
( '20170501', '...' ),
( '20170611', '...' ),
( '20170704', '...' );
DECLARE #sunday INT ,
#saturday INT;
-- 1/1/2000 is a known date - Saturday
SET #saturday = DATEPART(WEEKDAY, DATEFROMPARTS(2000, 1, 1));
SET #sunday = DATEPART(WEEKDAY, DATEFROMPARTS(2000, 1, 2));
WITH tally
AS ( SELECT TOP 5000
ROW_NUMBER() OVER ( ORDER BY t1.object_id ) AS N
FROM master.sys.all_columns t1
CROSS JOIN master.sys.all_columns t2
),
dates ( theDate )
AS ( SELECT DATEADD(DAY, N - 1, CAST(GETDATE() AS DATE))
FROM tally
),
workDates ( workingDay, workingDate )
AS ( SELECT ROW_NUMBER() OVER ( ORDER BY theDate ) ,
theDate
FROM dates
WHERE DATEPART(WEEKDAY, theDate) NOT IN ( #saturday, #sunday )
AND theDate NOT IN ( SELECT holiday
FROM #holidays )
)
SELECT workingDate
FROM workDates
WHERE workingDay = #NthWorkingDay;

Related

Based on day fetch all dates - sql

I have start date, end date and name of days. How can fetch all dates between those two dates of that specific days in sql?
example data:
start_date:4/11/2018
end_date: 5/11/2018
days: monday, thursday
expected output: all dates between start and end date which comes on monday and thursday and store them in table
updated
my present code(not working)
; WITH CTE(dt)
AS
(
SELECT #P_FROM_DATE
UNION ALL
SELECT DATEADD(dw, 1, dt) FROM CTE
WHERE dt < #P_TO_DATE
)
INSERT INTO Table_name
(
ID
,DATE_TIME
,STATUS
,CREATED_DATE
,CREATED_BY
)
SELECT #P_ID
,(SELECT dt FROM CTE WHERE DATENAME(dw, dt) In ('tuesday','friday',null))
,'NOT SENT'
,CAST(GETDATE() AS DATE)
,#USER_ID
Another approach for generating dates between ranges can be like following query. This will be faster compared to CTE or WHILE loop.
DECLARE #StartDate DATETIME = '2018-04-11'
DECLARE #EndDate DATETIME = '2018-05-15'
SELECT #StartDate + RN AS DATE FROM
(
SELECT (ROW_NUMBER() OVER (ORDER BY (SELECT NULL)))-1 RN
FROM master..[spt_values] T1
) T
WHERE RN <= DATEDIFF(DAY,#StartDate,#EndDate)
AND DATENAME(dw,#StartDate + RN) IN('Monday','Thursday')
Note:
If the row count present in master..[spt_values] is not sufficient for the provided range, you can make a cross join with the same to get a bigger range like following.
SELECT (ROW_NUMBER() OVER (ORDER BY (SELECT NULL)))-1 RN
FROM master..[spt_values] T1
CROSS JOIN master..[spt_values] T2
By this you will be able to generate date between a range with gap of 6436369 days.
You can use a recursive common table expression (CTE) to generate a list of days. With datepart(dw, ...) you can filter for specific days of the week.
An example that creates a list of Mondays and Thursdays between March 1st and today:
create table ListOfDates (dt date);
with cte as
(
select cast('2018-03-01' as date) as dt -- First day of interval
union all
select dateadd(day, 1, dt)
from cte
where dt < getdate() -- Last day of interval
)
insert into ListOfDates
(dt)
select dt
from cte
where datepart(dw, dt) in (2, 5) -- 2=Monday and 5=Thursday
option (maxrecursion 0)
See it working at SQL Fiddle.
This will work for you:
DECLARE #table TABLE(
ID INT IDENTITY(1,1),
Date DATETIME,
Day VARCHAR(50)
)
DECLARE #Days TABLE(
ID INT IDENTITY(1,1),
Day VARCHAR(50)
)
INSERT INTO #Days VALUES ('Monday')
INSERT INTO #Days VALUES ('Thursday')
DECLARE #StartDate DATETIME='2018-01-01';
DECLARE #EndDate DATETIME=GETDATE();
DECLARE #Day VARCHAR(50)='Friday';
DECLARE #TempDate DATETIME=#StartDate;
WHILE CAST(#TempDate AS DATE)<=CAST(#EndDate AS DATE)
BEGIN
IF EXISTS (SELECT 1 FROM #Days WHERE DAY IN (DATENAME(dw,#TempDate)))
BEGIN
INSERT INTO #table
VALUES (
#TempDate, -- Date - datetime
DATENAME(dw,#TempDate) -- Day - varchar(50)
)
END
SET #TempDate=DATEADD(DAY,1,#TempDate)
END
SELECT * FROM #table
INSERT INTO TargetTab(dateCOL)
SELECT dateCOL
FROM tab
WHERE dateCOL >= startdate AND dateCOL <= enddate
AND (DATENAME(dw,dateCOL) ='Thursday' OR DATENAME(dw,dateCOL) = 'Monday')
Try this query to get your result.
Use a recursive CTE to generate your dates, then filter by week day.
SET DATEFIRST 1 -- 1: Monday, 7 Sunday
DECLARE #StartDate DATE = '2018-04-11'
DECLARE #EndDate DATE = '2018-05-15'
DECLARE #WeekDays TABLE (WeekDayNumber INT)
INSERT INTO #WeekDays (
WeekDayNumber)
VALUES
(1), -- Monday
(4) -- Thursday
;WITH GeneratingDates AS
(
SELECT
GeneratedDate = #StartDate,
WeekDay = DATEPART(WEEKDAY, #StartDate)
UNION ALL
SELECT
GeneratedDate = DATEADD(DAY, 1, G.GeneratedDate),
WeekDay = DATEPART(WEEKDAY, DATEADD(DAY, 1, G.GeneratedDate))
FROM
GeneratingDates AS G -- Notice that we are referencing a CTE that we are also declaring
WHERE
G.GeneratedDate < #EndDate
)
SELECT
G.GeneratedDate
FROM
GeneratingDates AS G
INNER JOIN #WeekDays AS W ON G.WeekDay = W.WeekDayNumber
OPTION
(MAXRECURSION 30000)
Try this:
declare #start date = '04-11-2018'
declare #end date = '05-11-2018'
declare #P_ID int = 1
declare #USER_ID int = 11
;with cte as(
select #start [date]
union all
select dateadd(DAY, 1, [date]) from cte
where [date] < #end
)
--if MY_TABLE doesn't exist
select #P_ID,
[date],
'NOT SENT',
cast(getdate() as date),
#USER_ID
into MY_TABLE
from cte
--here you can specify days: 1 - Sunday, 2 - Monday, etc.
where DATEPART(dw,[date]) in (2, 5)
option (maxrecursion 0)
--if MY_TABLE does exist
--insert into MY_TABLE
--select #P_ID,
-- [date],
-- 'NOT SENT',
-- cast(getdate() as date),
-- #USER_ID
--from cte
--where DATEPART(dw,[date]) in (2, 5)
--option (maxrecursion 0)

Conditional Count On Row_Number

I have a query that calculates the number working days within a month based on a table which stores all our public holidays.
The current output would show all working days, excluding public holidays and Saturday and Sunday, I would like to show each day of the month, but don't increment on a public holiday or Saturday or Sunday.
Is there a way to conditionally increment the row number?
Query is below:
DECLARE #startnum INT=0
DECLARE #endnum INT=365;
WITH gen AS
(
SELECT #startnum AS num
UNION ALL
SELECT num + 1
FROM gen
WHERE num + 1 <= #endnum
)
, holidays AS
(
SELECT CONVERT(DATE, transdate) AS HolidayDate
FROM WORKCALENDER w
WHERE w.CALENDARID = 'PubHoliday'
)
, allDays AS
(
SELECT DATEADD( d, num, CONVERT( DATE, '1 Jan 2016' ) ) AS DateOfYear
, DATENAME( dw, DATEADD( d, num, CONVERT( DATE, '1 Jan 2016' ))) AS [dayOfWeek]
FROM gen
)
select number = ROW_NUMBER() OVER ( ORDER BY DateOfYear )
, *
from allDays
LEFT OUTER JOIN holidays
ON allDays.DateOfYear = holidays.HolidayDate
WHERE holidays.HolidayDate IS NULL
AND allDays.dayOfWeek NOT IN ( 'Saturday', 'Sunday')
AND DateOfYear >= CONVERT( DATE, '1 ' + DATENAME( MONTH, GETDATE() ) + ' 2016' )
AND DateOfYear < CONVERT( DATE, '1 ' + DATENAME( MONTH, DATEADD( month, 1, GETDATE()) ) + ' 2016' )
option (maxrecursion 10000)
kind of pseudo code
select date, row_number() over (order by date) as num
from ( select date
from allDates
where month = x and weekday
exept
select date
from holidays
where month is x
) as t
union all
select date, null
from holidays
where month is x
order by date
You could use a windowed sum, see how the output of WorkdaySequenceInMonth is composed.
DECLARE #startDate DATE = '20160101'
, #numDays INT = 365
, #num INT = 0;
DECLARE #Holidays TABLE (Holiday DATE);
INSERT INTO #Holidays(Holiday)
VALUES ('20160101')
, ('20160115')
, ('20160714');
WITH nums AS
(
SELECT row_number() OVER (ORDER BY object_id) - 1 as num
FROM sys.columns
),
dateRange as
(
SELECT
DATEADD(DAY, num, #startDate) AS Dt
, num
FROM nums
WHERE num < #numDays
),
Parts AS
(
SELECT
R.Dt as [Date]
, Year(R.Dt) as [Year]
, Month(R.Dt) as [Month]
, Day(R.Dt) as [Day]
, Datename(weekday, R.Dt) as [Weekday]
, CASE WHEN H.Holiday IS NOT NULL
OR Datename(weekday, R.Dt) IN ('Saturday', 'Sunday')
THEN 0
ELSE 1
END AS IsWorkday
FROM dateRange R
LEFT JOIN #Holidays H ON R.Dt = H.Holiday
)
--
select
*
, sum(IsWorkday) over (PARTITION BY [Year],[month]
ORDER BY [Day]
ROWS UNBOUNDED PRECEDING) as WorkdaySequenceInMonth
from Parts
order by [Year], [Month]
Hi You can try this query, the initial part is the data generation, maybe you won't need it.
Then I generate a temp table with all the dates for the time period set in #StartYear, #EndYear
Then just simple queries to return the data
-- generate holidays table
select holiday
into #tempHolidays
from
(
select '20160101' as holiday
union all
select '20160201' as holiday
union all
select '20160205' as holiday
union all
select '20160301' as holiday
union all
select '20160309' as holiday
union all
select '20160315' as holiday
) as t
create table #tempCalendar (Date_temp date)
select * from
#tempHolidays
declare #startYear int , #endYear int, #i int, #dateStart datetime , #dateEnd datetime, #date datetime, #i = 0
Select #startYear = '2016'
,#endYear = '2016'
,#dateStart = (Select cast( (cast(#startYear as varchar(4)) +'0101') as datetime))
,#dateEnd = (Select cast( (cast(#startYear as varchar(4)) +'1231') as datetime))
,#date = #dateStart
--Insert dates of the period of time
while (#date <> #dateEnd)
begin
insert into #tempCalendar
Select #date
set #date = (select DATEADD(dd,1,#date))
end
-- Retrive Date list
Select Date_temp
from #tempCalendar
where Date_temp not in (Select holiday from #tempHolidays)
and datename(weekday,Date_temp) not in ('Saturday','Sunday')
--REtrieve sum of working days per month
select DATEPART(year,Date_temp) as year
,DATEPART(month,Date_temp) as Month
,Count(*) as CountOfWorkingDays
from #tempCalendar
where Date_temp not in (Select holiday from #tempHolidays)
and datename(weekday,Date_temp) not in ('Saturday','Sunday')
Group by DATEPART(year,Date_temp)
,DATEPART(month,Date_temp)
You should change #tempHolidays for your Holidays table, and use #StarYear and #EndYear as your time period.
Here's a simple demo that shows the use of the partition by clause to keep contiguity in your sequencing for non-holidays
IF OBJECT_ID('tempdb.dbo.#dates') IS NOT null
DROP TABLE #dates;
CREATE TABLE #dates (d DATE);
IF OBJECT_ID('tempdb.dbo.#holidays') IS NOT null
DROP TABLE #holidays;
CREATE TABLE #holidays (d DATE);
INSERT INTO [#holidays]
( [d] )
VALUES
('2016-12-25'),
('2017-12-25'),
('2018-12-25');
INSERT INTO [#dates]
( [d] )
SELECT TOP 1000 DATEADD(DAY, n, '2015-12-31')
FROM [Util].dbo.[Numbers] AS [n];
WITH holidays AS (
SELECT d.*, CASE WHEN h.d IS NULL THEN 0 ELSE 1 END AS [IsHoliday]
FROM [#dates] AS [d]
LEFT JOIN [#holidays] AS [h]
ON [d].[d] = [h].[d]
)
SELECT d, ROW_NUMBER() OVER (PARTITION BY [holidays].[IsHoliday] ORDER BY d)
FROM [holidays]
ORDER BY d;
And please forgive my marking only Christmas as a holiday!

Simplest way to fill working day table

I have a table working_days with one column date of type date
I need to fill it with working days in USA.
Can you suggest how can I so this?
Manually it is too long.
You can use a recursive CTE to accomplish this. This only excludes the weekends. Using DATEFIRST you can figure out what day is a weekend. This query should work no matter what day of the week is set to DATEFIRST.
;WITH DatesCTE
AS (
SELECT CAST('2016-01-01' AS DATE) AS [workingDays]
UNION ALL
SELECT DATEADD(DAY, 1, workingdays)
FROM DatesCTE
WHERE DATEADD(DAY, 1, workingdays) < '2017-01-01'
)
SELECT *
FROM DatesCTE
WHERE ((DATEPART(dw, workingDays) + ##DATEFIRST) % 7) NOT IN (0, 1)
OPTION (MAXRECURSION 366)
At first fill your table with all dates for year (for example 2016):
DECLARE #date_start date = '2016-01-01',
#date_end date = '2016-12-31';
WITH cte as (
SELECT #date_start as [d], 0 as Level
UNION ALL
SELECT DATEADD(day,1,[d]), [level] + 1 as [level]
from cte
WHERE [level] < DATEDIFF(day,#date_start,#date_end)
),
holidays as ( --table with holidays
SELECT * FROM (VALUES
('2016-01-01'),
('2016-01-18'),
('2016-02-15'),
('2016-05-30'),
('2016-07-04'),
('2016-09-05'),
('2016-10-10'),
('2016-11-11'),
('2016-11-24'),
('2016-12-26')) as t(d)
)
SELECT c.d
FROM cte c
LEFT JOIN holidays h on c.d=h.d
WHERE DATEPART(WEEKDAY,d) NOT IN (1,7) --will show only monday-friday
AND AND h.d is NULL
OPTION (MAXRECURSION 1000); --if you need more than 3 years get MAXRECURSION up
A simple loop will do:
declare #d date = '20160101';
while #d <= '20161231'
begin
if datepart(weekday, #d) not in (1, 7) and <#d not a holiday>
insert into working_days ("date") values (#d);
set #d = dateadd(day, 1, #d);
end

Convert Procedural Approach into Set Based Approach in Sql-Server

We are using procedural approach (while loop) for inserting records into a particular table. the insert syntax is like below,
DECLARE #CNT INT = 0,
#WEEK DATE = '2015-11-01',
#FLAG INT
CREATE TABLE #Tmpdata (officeId int,id smallint, weekDate date,startsOn varchar(10),endsOn varchar(10),flag bit);
WHILE (#CNT <7)
BEGIN
SET #WEEK = DATEADD(D,#CNT,#WEEK )
IF EXISTS
(SELECT 1
FROM YEARRANGE
WHERE #WEEK BETWEEN CONVERT(DATE,taxseasonbegin)
AND CONVERT (DATE,taxSeasonEnd)
)
BEGIN
SET #FLAG =1
END
ELSE
BEGIN
SET #FLAG = 0
END
INSERT INTO #Tmpdata
(
officeId,id,weekDate,startsOn,endsOn,flag
)
VALUES
(
5134,#lvCounter,#week,'09:00 AM','05:00 PM',#flag
);
SET #cnt=#cnt+1;
end
(NOTE : TaxSeason is from january to august).
Is it possible to re-write the above logic in set based approach?
This is making a number of assumption because you didn't post ddl or any consumable sample data. Also, there is a variable #lvCounter not defined in your code. This is perfect opportunity to use a tally or numbers table instead of a loop.
declare #lvCounter int = 42;
DECLARE #CNT INT = 0,
#WEEK DATE = '2015-11-01',
#FLAG INT;
WITH
E1(N) AS (select 1 from (values (1),(1),(1),(1),(1),(1),(1),(1),(1),(1))dt(n))
, cteTally(N) AS
(
SELECT ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) FROM E1
)
select 5134 as officeId
, #lvCounter as Id
, DATEADD(DAY, N - 1, #WEEK) as weekDate
, '09:00 AM' as startsOn
, '05:00 PM' as EndOn
, Flag
from cteTally t
cross apply
(
select CAST(count(*) as bit) as Flag
from YearRange
where DATEADD(Day, t.N, #WEEK) > CONVERT(DATE,taxseasonbegin)
AND DATEADD(Day, t.N, #WEEK) <= CONVERT (DATE,taxSeasonEnd)
) y
where t.N <= 7;
Please can you provide sample data?
You can do something like:
SELECT DateIncrement = SUM(DATEADD(D,#CNT,#WEEK)) OVER (ORDER BY officeID)
FROM...
This gets an incremented date value for each record which you can then check against your start and end dates.
You could try some Kind of this one. This gives you the data I think you Need for your insert. I do not have a table named YEARRANGE so I couldn't test it completely
DECLARE #CNT INT = 0, #WEEK DATE = '2015-11-01', #FLAG INT;
CREATE TABLE #Tmpdata (officeId int,id smallint, weekDate date,startsOn varchar(10),endsOn varchar(10),flag bit);
WITH CTE AS
(
SELECT num AS cnt,
DATEADD(D, SUM(num) OVER(ORDER BY num ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)
, #WEEK) AS [week]
FROM
(
SELECT ROW_NUMBER() OVER (ORDER BY nl) -1 AS num
FROM
(SELECT NULL AS nl UNION ALL SELECT NULL AS nl UNION ALL SELECT NULL AS nl UNION ALL SELECT NULL AS nl
UNION ALL SELECT NULL AS nl UNION ALL SELECT NULL AS nl UNION ALL SELECT NULL AS nl
) AS ni
) AS no
)
INSERT INTO #Tmpdata (officeId,id,weekDate,startsOn,endsOn,flag)
SELECT 5134 AS officeID, cnt AS id, [week],'09:00 AM' AS startsOn,'05:00 PM' AS endsOn, COALESCE(A1.flag,0) AS flag
FROM CTE
OUTER APPLY (SELECT 1
FROM YEARRANGE
WHERE [week] BETWEEN CONVERT(DATE,taxseasonbegin)
AND CONVERT (DATE,taxSeasonEnd)
) AS A1(flag);

T-SQL function loop [closed]

This question is unlikely to help any future visitors; it is only relevant to a small geographic area, a specific moment in time, or an extraordinarily narrow situation that is not generally applicable to the worldwide audience of the internet. For help making this question more broadly applicable, visit the help center.
Closed 9 years ago.
I was wondering if I could get some help on a T-SQL function I am trying to create:
Here is some sample data that needs to be queried:
Simplified table:
ID|PersonID|ValueTypeID|ValueTypeDescription|Value
1|ZZZZZ000L6|ZZZZZ00071|Start Prison Date|3/28/2012
2|ZZZZZ000L6|ZZZZZ00071|Start Prison Date|10/10/2012
3|ZZZZZ000L6|ZZZZZ00072|End Prison Date |3/29/2012
4|ZZZZZ000MD|ZZZZZ00071|Start Prison Date|1/15/2012
5|ZZZZZ000MD|ZZZZZ00072|End Prison Date |2/15/2012
6|ZZZZZ000MD|ZZZZZ00071|Start Prison Date|4/1/2012
7|ZZZZZ000MD|ZZZZZ00072|End Prison Date |4/5/2012
8|ZZZZZ000MD|ZZZZZ00071|Start Prison Date|9/3/2012
9|ZZZZZ000MD|ZZZZZ00072|End Prison Date |12/1/2012
What I need is a T-SQL function that accepts the PersonID and the Year (#PID, #YR) and returns the number of days that person has been in prison for that year.
dbo.NumDaysInPrison(#PID, #YR) as int
Example:
dbo.NumDaysInPrison('ZZZZZ000L6', 2012) returns 84
dbo.NumDaysInPrison('ZZZZZ000MD', 2012) returns 124
So far, I have come up with this query that gives me the answer sometimes.
DECLARE #Year int
DECLARE #PersonID nvarchar(50)
SET #Year = 2012
SET #PersonID = 'ZZZZZ000AA'
;WITH StartDates AS
(
SELECT
Value,
ROW_NUMBER() OVER(ORDER BY Value) AS RowNumber
FROM Prisoners
WHERE ValueTypeDescription = 'Start Prison Date' AND PersonID = #PersonID AND YEAR(Value) = #Year
), EndDates AS
(
SELECT
Value,
ROW_NUMBER() OVER(ORDER BY Value) AS RowNumber
FROM Prisoners
WHERE ValueTypeDescription = 'End Prison Date' AND PersonID = #PersonID AND YEAR(Value) = #Year
)
SELECT
SUM(DATEDIFF(d, s.Value, ISNULL(e.Value, cast(str(#Year*10000+12*100+31) as date)))) AS NumDays
FROM StartDates s
LEFT OUTER JOIN EndDates e ON s.RowNumber = e.RowNumber
This fails to capture if a record earlier in the year was left without an end date:
for example if a person has only two records:
ID|PersonID|ValueTypeID|ValueTypeDescription|Value
1|ZZZZZ000AA|ZZZZZ00071|Start Prison Date|3/28/2012
2|ZZZZZ000AA|ZZZZZ00071|Start Prison Date|10/10/2012
(3/28/2012 -> End of Year)
(10/10/2012 -> End of Year)
will returns 360, not 278.
So it seems that you have the data that you need to split out your 'start date' values and your 'end date' values. You don't really need to loop through anything, you can just pull out your start values then your end values based on your person and compare them.
The important thing is to pull out all you need to begin with and then compare the appropriate values.
Here's an example based on your data above. It would need some heavy tweaking to work with production data; it makes assumptions about the Value data. It's also a bad idea to hard-code valuetypeid as I have here; if you're making a function, you'd want to handle that, I think.
DECLARE #pid INT, #yr INT;
WITH startdatecalc AS
(
SELECT personid, CAST([value] AS date) AS startdate, DATEPART(YEAR, CAST([value] AS date)) AS startyear
FROM incarctbl
WHERE valuetypeid = 'ZZZZZ00071'
),
enddatecalc AS
(
SELECT personid, CAST([value] AS date) AS enddate, DATEPART(YEAR, CAST([value] AS date)) AS endyear
FROM incarctbl
WHERE valuetypeid = 'ZZZZZ00072'
)
SELECT CASE WHEN startyear < #yr THEN DATEDIFF(day, CAST(CAST(#yr AS VARCHAR(4)) + '-01-01' AS date), ISNULL(enddatecalc.enddate, CURRENT_TIMESTAMP))
ELSE DATEDIFF(DAY, startdate, ISNULL(enddatecalc.enddate, CURRENT_TIMESTAMP)) END AS NumDaysInPrison
FROM startdatecalc
LEFT JOIN enddatecalc
ON startdatecalc.personid = enddatecalc.personid
AND enddatecalc.enddate >= startdatecalc.startdate
AND NOT EXISTS
(SELECT 1 FROM enddatecalc xref
WHERE xref.personid = enddatecalc.personid
AND xref.enddate < enddatecalc.enddate
AND xref.enddate >= startdatecalc.startdate
AND xref.endyear < #yr)
WHERE startdatecalc.personid = #pid
AND startdatecalc.startyear <= #yr
AND (enddatecalc.personid IS NULL OR endyear >= #yr);
EDIT: Added existence check to attempt to handle if the same personid was used multiple times in the same year.
Here's my implementation with test tables and data. You'll have to change where appropriate. NOTE: i take datediff + 1 for days in prison, so if you go in on monday and leave on tuesday, that counts as two days. if you want it to count as one day, remove the "+ 1"
create table PrisonRegistry
(
id int not null identity(1,1) primary key
, PersonId int not null
, ValueTypeId int not null
, Value date
)
-- ValueTypeIDs: 1 = start prison date, 2 = end prison date
insert PrisonRegistry( PersonId, ValueTypeId, Value ) values ( 1, 1, '2012-03-28' )
insert PrisonRegistry( PersonId, ValueTypeId, Value ) values ( 1, 1, '2012-10-12' )
insert PrisonRegistry( PersonId, ValueTypeId, Value ) values ( 1, 2, '2012-03-29' )
insert PrisonRegistry( PersonId, ValueTypeId, Value ) values ( 2, 1, '2012-01-15' )
insert PrisonRegistry( PersonId, ValueTypeId, Value ) values ( 2, 2, '2012-02-15' )
insert PrisonRegistry( PersonId, ValueTypeId, Value ) values ( 2, 1, '2012-04-01' )
insert PrisonRegistry( PersonId, ValueTypeId, Value ) values ( 2, 2, '2012-04-05' )
insert PrisonRegistry( PersonId, ValueTypeId, Value ) values ( 2, 1, '2012-09-03' )
insert PrisonRegistry( PersonId, ValueTypeId, Value ) values ( 2, 2, '2012-12-1' )
go
create function dbo.NumDaysInPrison(
#personId int
, #year int
)
returns int
as
begin
declare #retVal int
set #retVal = 0
declare #valueTypeId int
declare #value date
declare #startDate date
declare #noDates bit
set #noDates = 1
set #startDate = DATEFROMPARTS( #year, 1, 1 )
declare prisonCursor cursor for
select
pr.ValueTypeId
, pr.Value
from
PrisonRegistry pr
where
DATEPART( yyyy, pr.Value ) = #year
and pr.ValueTypeId in (1,2)
and PersonId = #personId
order by
pr.Value
open prisonCursor
fetch next from prisonCursor
into #valueTypeId, #value
while ##FETCH_STATUS = 0
begin
set #noDates = 0
-- if end date, add date diff to retVal
if 2 = #valueTypeId
begin
--if #startDate is null
--begin
-- -- error: two end dates in a row
-- -- handle
--end
set #retVal = #retVal + DATEDIFF( dd, #startDate, #value ) + 1
set #startDate = null
end
else if 1 = #valueTypeId
begin
set #startDate = #value
end
fetch next from prisonCursor
into #valueTypeId, #value
end
close prisonCursor
deallocate prisonCursor
if #startDate is not null and 0 = #noDates
begin
set #retVal = #retVal + DATEDIFF( dd, #startDate, DATEFROMPARTS( #year, 12, 31 ) ) + 1
end
return #retVal
end
go
select dbo.NumDaysInPrison( 1, 2012 )
select dbo.NumDaysInPrison( 2, 2012 )
select dbo.NumDaysInPrison( 2, 2011 )
This is a complicated question. It is not so much "asking for a function" as it is dealing with two competing problems. The first is organizing the data, which is transaction-based, into records with start and stop dates for the prison period. The second is summarizing this for time spent within another given span of time (a year).
I think you need to spend some time investigating the data to understand the anomalies in it, before progressing to writing a function. The following query should help you. It does the calculate for all prisoners for a given year (which is the year in the first CTE):
with vals as (
select 2012 as yr
),
const as (
select cast(CAST(yr as varchar(255))+'-01-01' as DATE) as periodstart,
cast(CAST(yr as varchar(255))+'-12-31' as DATE) as periodend
from vals
)
select t.personId, SUM(datediff(d, (case when StartDate < const.periodStart then const.periodStart else StartDate end),
(case when EndDate > const.PeriodEnd or EndDate is NULL then const.periodEnd, else EndDate end)
)
) as daysInYear
from (select t.*, t.value as StartDate,
(select top 1 value
from t t2
where t.personId = t2.personId and t2.Value >= t.Value and t2.ValueTypeDescription = 'End Prison Date'
order by value desc
) as EndDate
from t
where valueTypeDescription = 'Start Prison Date'
) t cross join
const
where StartDate <= const.periodend and (EndDate >= const.periodstart or EndDate is NULL)
group by t.PersonId;
This query can be adapted as a function. But, I would encourage you to investigate the data before going there. Once you wrap things up in a function, it will be much more difficult to find and understand anomalies -- why did someone go in and out on the same day? How has the longest periods in prison? And so on.