How to create the greek orthodox easter calendar in sql server? - sql

I am a newbie in SQL and i really need your help for my project. So, i had to create a master calendar table (which i have already created with the name "DimDate") but the problem is that i can't define right the function for the Easter Holidays in Greece.
I'm trying to create a function which will return the Easter Holidays (e.g Easter Sunday, Easter Monday, Good Friday) using the Meeus's Julian algorithm and not the Gregorian.
The function i have already tried, concerns the Gregorian calendar and as a result i get wrong dates for most of Easter Sundays. For example, for the year 2015, i see from the result of my query that the Easter Sunday was on 05/04/2015 (dd/MM/yyy) BUT the truth is that on 2015 the real date of Easter Sunday in Greece was 12/04/2015. And this is because i used the Gregorian algorithm as you can see below in the code i used.
CREATE FUNCTION dbo.GetEasterHolidays(#year INT)
RETURNS TABLE
WITH SCHEMABINDING
AS
RETURN
(
WITH x AS
(
SELECT [Date] = CONVERT(DATE, RTRIM(#year) + '0' + RTRIM([Month])
+ RIGHT('0' + RTRIM([Day]),2))
FROM (SELECT [Month], [Day] = DaysToSunday + 28 - (31 * ([Month] / 4))
FROM (SELECT [Month] = 3 + (DaysToSunday + 40) / 44, DaysToSunday
FROM (SELECT DaysToSunday = paschal - ((#year + #year / 4 + paschal - 13) % 7)
FROM (SELECT paschal = epact - (epact / 28)
FROM (SELECT epact = (24 + 19 * (#year % 19)) % 30)
AS epact) AS paschal) AS dts) AS m) AS d
)
SELECT [Date], HolidayName = 'Easter Sunday' FROM x
UNION ALL SELECT DATEADD(DAY,-2,[Date]), 'Good Friday' FROM x
UNION ALL SELECT DATEADD(DAY, 1,[Date]), 'Easter Monday' FROM x
);
--- Now 'updating' my "DimDate" calendar table---
;WITH x AS
(
SELECT d.[Date], d.IsHolidayEU, d.HolidayEU, h.HolidayName
FROM dbo.DimDate AS d
CROSS APPLY dbo.GetEasterHolidays(d.[Year]) AS h
WHERE d.[Date] = h.[Date]
)
UPDATE x SET IsHolidayEU = 1, HolidayEU = HolidayName;
I have searched the internet extensively and i found what i wanted but in HANA, not sql. I am confused since i tried to convert the HANA Code i found into sql but didn't manage it.
Also, here is the link from the HANA code i found, it contains 2 pictures.
I think i need to convert the second (image) code into sql server statements.
https://blogs.sap.com/2015/04/08/happy-easter-folks-easter-date-calculation/
Please help me if you can...and thank you in advance.
*Sorry for the looks of the code, i am new here, i couldn't find the way to make it better in order to be whole inside the "grey" area

It is a great function that works great at least until 2100.
In order to not have performance issues create a temp table with the given dates like this:
WITH estdates as
(SELECT c.fd EasterSunday
, c.[Year]
, DATEADD(dd,-2,c.fd) EasterFriday
, DATEADD(dd,1,c.fd) EasterMonday
FROM
(
SELECT dbo.UDF_GetEaster_Orthodox( ur.YR) fd,ur.YR [Year]
FROM
(
SELECT ROW_NUMBER() OVER (ORDER BY N1.N) +1999 AS YR
FROM (VALUES(1),(1),(1),(1),(1),(1),(1),(1)) AS N1(N)
CROSS JOIN (VALUES(1),(1),(1),(1),(1),(1),(1),(1)) AS N2(N)
) AS ur
) c
)
select *
into #EasterHolidays
from
(SELECT EasterFriday EasterHolidays FROM estdates d
UNION ALL
SELECT EasterMonday FROM estdates d ) a

I finally managed to get the right results with the following code.
The first function gives the Easter Sunday:
CREATE FUNCTION dbo.Easter_Orthodox22(#YEAR INT)
RETURNS DATE
AS
BEGIN
DECLARE #EasterMonth INT,
#EasterDay INT,
#Easter NVARCHAR(10),
#EYear VARCHAR(4),
#EMonth VARCHAR(2),
#EDay VARCHAR(2),
#EasterDate DATE,
#a INT,
#b INT,
#c INT,
#d INT,
#e INT;
SET #a = #YEAR%4;
SET #b = #YEAR%7;
SET #c = #YEAR%19;
SET #d = (19 * #c + 15)%30;
SET #e = (2 * #a + 4 * #b - #d +34)%7;
SET #EasterDay = (((#d + #e + 114)%31)+1);
SET #EasterMonth = CAST(((#d + #e + 114)/31) AS INT);
SET #EYear = CAST( #YEAR AS VARCHAR(4));
SET #EMonth = CAST( #EasterMonth AS VARCHAR(2));
SET #EDay = CAST( #EasterDay AS VARCHAR(2));
SET #Easter = CONCAT( CONCAT( #EYear, RIGHT( CONCAT('0', #EMonth),2)), RIGHT ( CONCAT('0', #EDay),2));
SET #EasterDate = CAST( #Easter AS DATE);
SET #EasterDate = DATEADD( day, 13, #EasterDate);
RETURN #EasterDate;
END
GO
Then, i created two more functions in order to find the Easter Monday and Great Friday:
CREATE FUNCTION dbo.GetEasterMonday2 (#YEAR INT)
RETURNS DATE
AS
BEGIN
RETURN (SELECT convert(date,convert(datetime,dbo.Easter_Orthodox2(#YEAR)) + 1))
END
GO
CREATE FUNCTION dbo.GetGoodFriday2 (#YEAR INT)
RETURNS DATE
AS
BEGIN
RETURN (SELECT convert(date,convert(datetime,dbo.Easter_Orthodox2(#YEAR)) - 2))
END
GO
But, now i am facing a different problem! A performance one, when i try to update my master calendar. I do the following in order to update the calendar:
UPDATE [dbo].[DimDate]
SET IsHolidayEU = 1,
HolidayEU = 'Easter Sunday'
WHERE [DATE] = dbo.Easter_Orthodox22(#CurrentYear)
UPDATE [dbo].[DimDate]
SET IsHolidayEU = 1,
HolidayEU = 'Good Friday'
WHERE [DATE] = dbo.GetGoodFriday2(#CurrentYear)
UPDATE [dbo].[DimDate]
SET IsHolidayEU = 1,
HolidayEU = 'Easter Monday'
WHERE [DATE] = dbo.GetEasterMonday2(#CurrentYear)
These Update expression are written inside a while loop inside my calendar code..I think that i should have used the "SCHEMABINDING" and/or "CROSS APPLY" with "with-query" that is exhibited in the primary code but don't know how to change my code into that in order to improve the performance..Any help appreciated guys!

Related

Sum of age between two dates in Years Month Days format

I need to calculate age in years, months and days format between two dates (DateFrom and DateTo) in a way that:
If day of month in DateFrom is 1st then take it as a whole month
If day of month in DateFrom is not 1st then count days till the end of the month.
Example:
DateFrom='2010-02-01', DateTo='2011-03-11', Age= 1 Years, 1 Months 11 Days
DateFrom='2010-02-02', DateTo='2011-03-11', Age= 1 Years, 1 Months 8 Days
After calculating the age I'd need to sum the ages assuming that month is 30 days - in the above example I'd expect result: 2 Years, 2 Months, 19 Days.
My assumption would be that you have have a table containing "DateFrom" and "DateTo" columns so the query would be something like this:
DECLARE #TotalDiffInDays int = (SELECT AVG(DATEDIFF(DAY, DateFrom, DateTo)) AS [TotalDays] FROM #t)
SELECT #TotalDiffInDays
DECLARE #DaysInMonth int = 30;
DECLARE #DaysInYear int = 365;
SELECT
#TotalDiffInDays / 365 AS AvgYearsDiff,
(#TotalDiffInDays / #DaysInMonth - #TotalDiffInDays / #DaysInYear * 12) AS AvgMonthsDiff,
#TotalDiffInDays - ((#TotalDiffInDays / 365) * #DaysInYear + (#TotalDiffInDays / #DaysInMonth - #TotalDiffInDays / #DaysInYear * 12) * #DaysInMonth) AS AvgDaysDiff
Note that in this case I am using INT division to get correct numbers.
In addition, if you just want to get dates difference in the format you desribed above you can use this query:
SELECT
DATEDIFF(YEAR, DateFrom, DateTo) AS [Years],
-- Add year diff to get corret months diff
DATEDIFF(MONTH, DATEADD(YEAR, DATEDIFF(YEAR, DateFrom, DateTo), DateFrom), DateTo) AS [Months],
-- Add months diff to get correct days diff
DATEDIFF(DAY, DATEADD(MONTH, DATEDIFF(MONTH, DateFrom, DateTo), DateFrom), DateTo) AS [Days]
FROM #t
I hope this will helps.
Yeah, just forgot about your rule of the 1st and not 1st day of the month, you can easily modify those queries with adding the DATEPART(DAY , your_date) function to check if it's a 1st date of the month and perform DETEADD() to apply this logic or just add a value of 1 before AVG.
So basically since this custom date maths, you'd need to implement your own functions to get desired results.
See the TSQL below
CREATE FUNCTION dbo.getDateSum(#d1 varchar(100), #d2 varchar(100))
RETURNS varchar(100)
WITH EXECUTE AS CALLER
AS
BEGIN
DECLARE #y int, #m int, #d int
Select
#d1= REPLACE(REPLACE(REPLACE(#d1,' years, ', '-'),' months, ','-'),' days',''),
#d2= REPLACE(REPLACE(REPLACE(#d2,' years, ', '-'),' months, ','-'),' days','')
Select
#y= CAST(LEFT(#d1,CHARINDEX('-',#d1)-1) AS INT)+CAST(LEFT(#d2,CHARINDEX('-',#d2)-1) AS INT),
#m= CAST(SUBSTRING(#d1,CHARINDEX('-',#d1)+1,LEN(#d1)-CHARINDEX('-',REVERSE(#d1))-CHARINDEX('-',#d1))AS INT)+CAST(SUBSTRING(#d2,CHARINDEX('-',#d2)+1,LEN(#d2)-CHARINDEX('-',REVERSE(#d2))-CHARINDEX('-',#d2)) AS INT),
#d= CAST(LEFT(REVERSE(#d1),CHARINDEX('-',REVERSE(#d1))-1)AS INT)+CAST(LEFT(REVERSE(#d2),CHARINDEX('-',REVERSE(#d2))-1) AS INT)
IF(#d>30)
BEGIN
SET #d=#d%30
SET #m=#m+CAST(#d/30 as INT)
END
IF(#m>30)
BEGIN
SET #m=#m%12
SET #y=#y+CAST(#m/12 as INT)
END
RETURN (cast(#y as varchar)+ ' years, ' + cast(#m as varchar) +' months, '+ cast(#d as varchar) + ' days' );
END
go
CREATE FUNCTION dbo.getDateDiff(#df date, #dt date)
RETURNS varchar(100)
WITH EXECUTE AS CALLER
AS
BEGIN
declare #y int, #m int, #d int
Select #y= YEAR(#dt)- YEAR(#df),#m= MONTH(#dt)- MONTH(#df),#d=CASE WHEN DAY(#df)=1 THEN DAY(#dt)- DAY(#df)+1 ELSE DAY(#dt)- DAY(#df) -1 END
If (#d<0)
BEGIN
Set #m=#m-1
set #d=#d + DATEDIFF(d, #dt, EOMONTH(#dt))
END
IF(#m<0)
BEGIN
Set #y=#y-1
Set #m=#m+ 12
END
RETURN (cast(#y as varchar)+ ' years, ' + cast(#m as varchar) +' months, '+ cast(#d as varchar) + ' days' )
END
go
SELECT dbo.getDateSum(dbo.getDateDiff('2010-02-01','2011-03-11'),dbo.getDateDiff('2010-02-02', '2011-03-11'))
go

How To Get Elapsed Time Between Two Dates, Ignoring Some Time Range?

I have a table with a DATETIME column 'Start' and a DATETIME column 'End'. I want to return the number of minutes between the start and the end (End is always after than Start). Usually I'd just use 'DateDiff()' but this time I need to exclude another date range. For example - From Tuesday at 9am until Wednesday at 6pm, of each week, should be ignored.
If a row has a Start of Tuesday at 8am and an End of Wednesday at 7pm - the elapsed time should be two hours (120 minutes) - because of the ignored date range.
I'm having trouble coming up with a decent way of doing this and my searching online hasn't found quite what I'm looking for. Can someone help me along?
Try This:
--total time span to calculate difference
DECLARE #StartDate DATETIME = '2015-11-10 8:00:00AM',
#EndDate DATETIME = '2015-11-11 7:00:00PM'
--get the day of week (-1 because sunday is counted as first weekday)
DECLARE #StartDayOfWeek INT = (SELECT DATEPART(WEEKDAY, #StartDate)) -1
DECLARE #EndDayOfWeek INT = (SELECT DATEPART(WEEKDAY, #EndDate)) -1
--set the time span to exclude
DECLARE #InitialDOWToExclude TINYINT = 2
DECLARE #InitialTODToExclude VARCHAR(100) = '9:00:00 AM'
DECLARE #EndDOWToExclude TINYINT = 3
DECLARE #EndTODToExclude VARCHAR(100) = '6:00:00 PM'
--this will be the final output in hours
DECLARE #ElapsedHours INT = (SELECT DATEDIFF(HOUR, #StartDate, #EndDate))
DECLARE #WeeksBetween INT = (SELECT DATEDIFF(WEEK, #StartDate, #EndDate))
DECLARE #Iterator INT = 0
WHILE (#Iterator <= #WeeksBetween)
BEGIN
DECLARE #InitialDaysBetween INT = #StartDayOfWeek - #InitialDOWToExclude
DECLARE #StartDateToExclude DATETIME = (SELECT DATEADD(DAY, #InitialDaysBetween, DATEADD(WEEK, #Iterator, #StartDate)))
SET #StartDateToExclude =CAST(DATEPART(YEAR, #StartDateToExclude) AS VARCHAR(100))
+ CAST(DATEPART(MONTH, #StartDateToExclude) AS VARCHAR(100))
+ CAST(DATEPART(DAY, #StartDateToExclude) AS VARCHAR(100))
+ ' '
+ CAST(#InitialTODToExclude AS VARCHAR(100))
DECLARE #EndDaysBetween INT = #EndDayOfWeek - #EndDOWToExclude
DECLARE #EndDateToExclude DATETIME = (SELECT DATEADD(DAY, #EndDaysBetween, DATEADD(WEEK, #Iterator, #EndDate)))
SET #EndDateToExclude =CAST(DATEPART(YEAR, #EndDateToExclude) AS VARCHAR(100))
+ CAST(DATEPART(MONTH, #EndDateToExclude) AS VARCHAR(100))
+ CAST(DATEPART(DAY, #EndDateToExclude) AS VARCHAR(100))
+ ' '
+ CAST(#EndTODToExclude AS VARCHAR(100))
SET #ElapsedHours = #ElapsedHours - DATEDIFF(HOUR, #StartDateToExclude, #EndDateToExclude)
SET #Iterator = #Iterator + 1
END
SELECT #ElapsedHours
This might get you pretty close..
DECLARE #Table1 TABLE ([Id] INT, [Start] DATETIME, [End] DATETIME)
INSERT INTO #Table1 VALUES
(1, '2015-11-08 00:00:00', '2015-11-10 21:45:38'),
(2, '2015-11-09 00:00:00', '2015-11-11 21:45:38')
;
-- hours to exclude
WITH excludeCTE AS
(
SELECT *
FROM (VALUES('Tuesday', 9, 0), ('Wednesday', 0, 0)) AS T([Day], [Hour], [Amount])
UNION ALL
SELECT [Day], [Hour] + 1, [Amount]
FROM excludeCTE
WHERE ([Day] = 'Tuesday' AND [Hour] < 23) OR ([Day] = 'Wednesday' AND [Hour] < 18)
),
-- all hours between start and end
dateCTE AS
(
SELECT [Id],
[Start],
[End],
DATENAME(weekday, [Start])[Day],
DATENAME(hour, [Start])[Hour]
FROM #Table1 t
UNION ALL
SELECT cte.[Id],
DATEADD(HOUR, 1, cte.[Start]),
cte.[End],
DATENAME(weekday, DATEADD(HOUR, 1, cte.[Start]))[Day],
DATENAME(hour, DATEADD(HOUR, 1, cte.[Start]))[Hour]
FROM #Table1 t
JOIN dateCTE cte ON t.Id = cte.Id
WHERE DATEADD(HOUR, 1, cte.[Start]) <= t.[End]
)
SELECT t.[Id],
t.[Start],
t.[End],
SUM(COALESCE(e.[Amount], 1)) [Hours]
FROM #Table1 t
INNER JOIN dateCTE d ON t.[Id] = d.[Id]
LEFT JOIN excludeCTE e ON d.[Day] = e.[Day] AND d.[Hour] = e.[Hour]
GROUP BY t.[Id],
t.[Start],
t.[End]
OPTION (MAXRECURSION 0) -- allow more than 100 hours
Putting the additional constraint that there can only be one excluded range between any two date
CREATE TABLE worktable (
_Id INT
, _Start DATETIME
, _End DATETIME
);
INSERT INTO worktable VALUES
(1, '2015-11-09 00:00:00', '2015-11-09 00:45:00') -- Start and End before excluded range
, (2, '2015-11-09 00:00:00', '2015-11-11 21:45:00') -- Start before, End after
, (3, '2015-11-09 00:00:00', '2015-11-10 21:00:00') -- Start before, End between
, (4, '2015-11-10 10:00:00', '2015-11-11 10:00:00') -- Start between, End between
, (5, '2015-11-10 10:00:00', '2015-11-11 21:45:00') -- Start between, End after
With getDates As (
SELECT _Id
, a = _Start
, b = _End
, c = DATEADD(hh, 9
, DATEADD(DAY,DATEDIFF(DAY, 0, _Start) / 7 * 7
+ 7 * Cast(Sign(1 - DatePart(dw, _Start)) + 1 as bit), 1))
, d = DATEADD(hh, 18
, DATEADD(DAY,DATEDIFF(DAY, 0, _Start) / 7 * 7
+ 7 * Cast(Sign(1 - DatePart(dw, _Start)) + 1 as bit), 2))
FROM worktable
), getDiff As (
SELECT c_a = DATEDIFF(mi, a, c)
, c_b = DATEDIFF(mi, b, c)
, b_d = DATEDIFF(mi, d, b)
, a, b, c, d, _id
FROM getDates
)
Select _id
, (c_a + ABS(c_a)) / 2
- (c_b + ABS(c_b)) / 2
+ (b_d + ABS(b_d)) / 2
FROM getDiff;
c is the date of the first Tuesday after the start date (Find the next occurance of a day of the week in SQL) you may need to adjust the last value depending from DATEFIRST
d is the date of the first Wednesday after the start date in the same week of c
Cast(Sign(a - b) + 1 as bit) is 1 if a is more than or equal b, 0 otherwise
(x + ABS(x)) / 2 is x if not negative, otherwise 0
Given that the formula to get the elapsed time with the excluded range is:
+ (Exclusion Start - Start) If (Start < Exclusion Start)
- (Exclusion Start - End) If (End < Exclusion Start)
+ (End - Exclusion End) If (Exclusion End < End)
-- excluded range (weekday numbers run from 1 to 7)
declare #x datetime = /*ignore*/ '1900012' + /*start day # and time*/ '3 09:00am';
declare #y datetime = /*ignore*/ '1900012' + /* end day # and time*/ '4 06:00pm';
-- normalize date to 1900-01-21, which was a Sunday
declare #s datetime =
dateadd(day, 19 + datepart(weekday, #start), cast(cast(#start as time) as datetime));
declare #e datetime =
dateadd(day, 19 + datepart(weekday, #end), cast(cast(#end as time) as datetime));
-- split range into two parts, one before #x and the other after #y
-- each part collapses to zero when #s and #e respectively fall between #x and #y
select (
datediff(second, -- diff in minutes would truncate so count seconds
case when #s < #x then #s else #x end, -- minimum of #s, #x
case when #e < #x then #e else #x end -- minimum of #e, #x
) +
datediff(second,
case when #s > #y then #s else #y end, -- maximum of #s, #y
case when #e > #y then #e else #y end -- maximum of #e, #y
)
) / 60; -- convert seconds to minutes, truncating with integer division
I glanced at the earlier answers and I thought that surely there was something more straightforward and elegant. Perhaps this is easier to understand and one clear advantage over some solutions is that it's trivial to change the excluded range and that range doesn't have to be limited to a single day.
I'm assuming that your dates never span more than one regular calendar week. It wouldn't be too difficult to extend it to handle more though. One approach would be to handle starting and ending partial weeks plus the full weeks in the middle.
Imagine that your start time is 8:59:30am and your end time is 6:00:30pm. In such a case I'm figuring that you'd want to accumulate the half minutes on each side to get a full minute in total after subtracting the 9-6 block. If you use datediff(minute, ...) you would be truncating the partial minutes and never get the chance to add them together: so that's why I count seconds and then divide by sixty at the end. Of course, if you're only dealing in whole minutes then you won't need to do it that way.
I've chosen my reference date somewhat arbitrarily. At first I thought it might possibly be handy to look at a real and convenient date on the calendar but ultimately it only really matters that it falls on a Sunday. So I settled on the first Sunday falling on a date ending in the digit 1.
Note that the solution also relies on datefirst being set to Sunday. That could be tweaked or made more portable if necessary.

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.

Summing and grouping the number of records in a month

I have the following table which has employees' absence:
RecordId EmpID ActivityCode DateFrom DateTo
---------------------------------------------------------------
666542 1511 AB 29/01/2011 02/02/2011
666986 1511 AB 11/11/2011 11/11/2011
666996 1511 EL 13/11/2011 17/11/2011
755485 1787 SL 01/11/2011 14/11/2011
758545 1787 SL 15/11/2011 03/12/2011
796956 1954 AB 09/11/2011 09/11/2011
799656 1367 AB 09/11/2011 09/11/2011
808845 1527 EL 16/11/2011 16/11/2011
823323 1527 EL 17/11/2011 17/11/2011
823669 1527 EL 18/11/2011 18/11/2011
899555 1123 AB 09/11/2011 09/12/2011
990990 1511 AB 12/11/2011 12/11/2011
Now I want a report generated by a stored proc to sum all the absence days for a specific absence code for each month in a given year, for example If i want to know the totals of absence in 2011 from the previous table I will get something similar to:
Month TotalDays
---------------------------------
JAN 2011 201
FEB 2011 36
MAR 2011 67
APR 2011 91
....
The stored proc will have two params (#Year INT, #AbsCode NVARCHAR(3)).
Please Note, Sometimes a record overlaps another month (like the first row in the example table) and that should be counted separately for each month. I have tried using loops but with no luck. I am so weak in TSQL.
UPDATE
Right now I am using a scalar value user function and a stored procedure to do the job, Its ugly and hard to trace. Here it is any way:
The User function:
ALTER FUNCTION [dbo].[GetActivityTotalDaysInMonth]
(
#ActivityCode CHAR(3)
,#Year INT
,#Month INT
)
RETURNS INT
AS
BEGIN
DECLARE #FirstDayOfMonth DATETIME
DECLARE #LastDayOfMonth DATETIME
SET #FirstDayOfMonth = CAST(CAST(#Year AS varchar) + '-' + CAST(#Month AS varchar) + '-' + CAST(1 AS varchar) AS DATETIME)
SET #LastDayOfMonth = DATEADD(s, -1, DATEADD(M, 1, #FirstDayOfMonth))
DECLARE #TotalDays INT
SELECT #TotalDays =
SUM(DATEDIFF(DAY,
(CASE WHEN ActivityDateFrom < #FirstDayOfMonth THEN #FirstDayOfMonth ELSE ActivityDateFrom END)
, (CASE WHEN ActivityDateTo > #LastDayOfMonth THEN #LastDayOfMonth ELSE ActivityDateTo END))+1)
FROM Activities
WHERE
ActivityCode=#ActivityCode
AND ((ActivityDateFrom < #FirstDayOfMonth AND ActivityDateTo >= #FirstDayOfMonth)
OR (ActivityDateFrom >= #FirstDayOfMonth AND ActivityDateTo <= #LastDayOfMonth)
OR (ActivityDateFrom <= #LastDayOfMonth AND ActivityDateTo > #LastDayOfMonth))
RETURN #TotalDays
END
Now, I call this function inside a loop in a stored procedure:
ALTER PROCEDURE GetAnnualActivityTotalDays
(
#ActivityCode CHAR(3)
,#Year INT
)
AS
BEGIN
SET NOCOUNT ON;
DECLARE #Stats TABLE
([Month] NVARCHAR(50), TotalDays INT)
DECLARE #MonthNo INT
DECLARE #Month DATETIME
SET #MonthNo = 1
WHILE #MonthNo <= 12
BEGIN
SET #Month = CAST(CAST(#Year AS varchar) + '-' + CAST(#MonthNo AS varchar) + '-' + CAST(1 AS varchar) AS DATETIME)
INSERT INTO #Stats ([Month], TotalDays)
SELECT UPPER(SUBSTRING(DATENAME(mm, #Month), 1, 3)) + ', ' + CAST(#Year AS NVARCHAR),
dbo.GetActivityTotalDaysInMonth(#ActivityCode
,#Year
,#MonthNo
,#Base)
SET #MonthNo = #MonthNo + 1
END
SELECT * FROM #Stats
END
As you can see, this is ugly code which I believe it can be done in an easier way.. Any suggestions?
You'd need to create a calendar table which will allow you to easily count the days for each month that the start and end dates encompass. For example recordid = 666542 has 3 days in January and 2 days in February. You'd be able to get that number by a query like
select calyear, calmonth, caldate
from calendar
join activities on calendar.caldate between activities.activitydatefrom and activities.activitydateto
where activitycode = 'AB'
If you wrap that in a common table expression you can perform aggregation queries afterwards on the CTE.
with mycte as (
select calyear, calmonth, caldate
from calendar
join activities on calendar.caldate between activities.activitydatefrom and activities.activitydateto
where activitycode = 'AB'
)
select calyear, calmonth, count(caldate)
from mycte
group by calyear, calmonth
order by calyear, calmonth
To generate the calendar table you can use code similar to
create table calendar (calyear, calmonth, caldate)
declare #numdays int --number of days to generate in the calendar
declare #datestart datetime --the date to begin from in the calendar
set #numdays = 365
set #datestart = 'jan 1 2011';
with num as (
select 0 number
union
select 1 number
union
select 2 number
union
select 3 number
union
select 4 number
union
select 5 number
union
select 6 number
union
select 7 number
union
select 8 number
union
select 9 number
),
numberlist as (
select ((hundreds.number * 100) + (tens.number * 10) + ones.number) n
from num hundreds
cross join num tens
cross join num ones
where ((hundreds.number * 100) + (tens.number * 10) + ones.number) < #numdays
)
insert into calendar (calyear, calmonth, caldate)
select
datepart(yy,dateadd(dd,n,#datestart)) calyear,
datepart(mm,dateadd(dd,n,#datestart)) calmonth,
dateadd(dd,n,#datestart)caldate
from numberlist

t-sql select get all Months within a range of years

I need a select to return Month and year Within a specified date range where I would input the start year and month and the select would return month and year from the date I input till today.
I know I can do this in a loop but I was wondering if it is possible to do this in a series selects?
Year Month
---- -----
2010 1
2010 2
2010 3
2010 4
2010 5
2010 6
2010 7
and so on.
Gosh folks... using a "counting recursive CTE" or "rCTE" is as bad or worse than using a loop. Please see the following article for why I say that.
http://www.sqlservercentral.com/articles/T-SQL/74118/
Here's one way to do it without any RBAR including the "hidden RBAR" of a counting rCTE.
--===== Declare and preset some obviously named variables
DECLARE #StartDate DATETIME,
#EndDate DATETIME
;
SELECT #StartDate = '2010-01-14', --We'll get the month for both of these
#EndDate = '2020-12-05' --dates and everything in between
;
WITH
cteDates AS
(--==== Creates a "Tally Table" structure for months to add to start date
-- calulated by the difference in months between the start and end date.
-- Then adds those numbers to the start of the month of the start date.
SELECT TOP (DATEDIFF(mm,#StartDate,#EndDate) + 1)
MonthDate = DATEADD(mm,DATEDIFF(mm,0,#StartDate)
+ (ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) -1),0)
FROM sys.all_columns ac1
CROSS JOIN sys.all_columns ac2
)
--===== Slice each "whole month" date into the desired display values.
SELECT [Year] = YEAR(MonthDate),
[Month] = MONTH(MonthDate)
FROM cteDates
;
I know this is an old question, but I'm mildly horrified at the complexity of some of the answers. Using a CTE is definitely the simplest way to go for selecting these values:
WITH months(dt) AS
(SELECT getdate() dt
UNION ALL
SELECT dateadd(month, -1, dt)
FROM months)
SELECT
top (datediff(month, '2017-07-01' /* start date */, getdate()) + 1)
YEAR(months.dt) yr, MONTH(months.dt) mnth
FROM months
OPTION (maxrecursion 0);
Just slap in whichever start date you'd like in place of the '2017-07-01' above and you're good to go with an efficient and easily-integrated solution.
Edit: Jeff Moden's answer quite effectively advocates against using rCTEs. However, in this case it appears to be a case of premature optimization - we're talking about 10's of records in all likelihood, and even if you span back to 1900 from today, it's still a minuscule hit. Using rCTEs to achieve code maintainability seems to be worth the trade if the expected result set is small.
You can use something like this: Link
To generate the equivalent of a numbers table using date ranges.
But could you please clarify your inputs and outputs?
Do you want to input a start date, for example, '2010-5-1' and end date, for example, '2010-8-1' and have it return every month between the two? Do you want to include the start month and end month, or exclude them?
Here's some code that I wrote that will quickly generate an inclusive result of every month between two dates.
--Inputs here:
DECLARE #StartDate datetime;
DECLARE #EndDate datetime;
SET #StartDate = '2010-1-5 5:00PM';
SET #EndDate = GETDATE();
--Procedure here:
WITH RecursiveRowGenerator (Row#, Iteration) AS (
SELECT 1, 1
UNION ALL
SELECT Row# + Iteration, Iteration * 2
FROM RecursiveRowGenerator
WHERE Iteration * 2 < CEILING(SQRT(DATEDIFF(MONTH, #StartDate, #EndDate)+1))
UNION ALL
SELECT Row# + (Iteration * 2), Iteration * 2
FROM RecursiveRowGenerator
WHERE Iteration * 2 < CEILING(SQRT(DATEDIFF(MONTH, #StartDate, #EndDate)+1))
)
, SqrtNRows AS (
SELECT *
FROM RecursiveRowGenerator
UNION ALL
SELECT 0, 0
)
SELECT TOP(DATEDIFF(MONTH, #StartDate, #EndDate)+1)
DATEADD(month, DATEDIFF(month, 0, #StartDate) + A.Row# * POWER(2,CEILING(LOG(SQRT(DATEDIFF(MONTH, #StartDate, #EndDate)+1))/LOG(2))) + B.Row#, 0) Row#
FROM SqrtNRows A, SqrtNRows B
ORDER BY A.Row#, B.Row#;
Code below generates the values for the range between 21 Jul 2013 and 15 Jan 2014.
I usually use it in SSRS reports for generating lookup values for the Month parameter.
declare
#from date = '20130721',
#to date = '20140115';
with m as (
select * from (values ('Jan', '01'), ('Feb', '02'),('Mar', '03'),('Apr', '04'),('May', '05'),('Jun', '06'),('Jul', '07'),('Aug', '08'),('Sep', '09'),('Oct', '10'),('Nov', '11'),('Dec', '12')) as t(v, c)),
y as (select cast(YEAR(getdate()) as nvarchar(4)) [v] union all select cast(YEAR(getdate())-1 as nvarchar(4)))
select m.v + ' ' + y.v [value_field], y.v + m.c [label_field]
from m
cross join y
where y.v + m.c between left(convert(nvarchar, #from, 112),6) and left(convert(nvarchar, #to, 112),6)
order by y.v + m.c desc
Results:
value_field label_field
---------------------------
Jan 2014 201401
Dec 2013 201312
Nov 2013 201311
Oct 2013 201310
Sep 2013 201309
Aug 2013 201308
Jul 2013 201307
you can do the following
SELECT DISTINCT YEAR(myDate) as [Year], MONTH(myDate) as [Month]
FROM myTable
WHERE <<appropriate criteria>>
ORDER BY [Year], [Month]
---Here is a version that gets the month end dates typically used for accounting purposes
DECLARE #StartDate datetime;
DECLARE #EndDate datetime;
SET #StartDate = '2010-1-1';
SET #EndDate = '2020-12-31';
--Procedure here:
WITH RecursiveRowGenerator (Row#, Iteration)
AS ( SELECT 1, 1
UNION ALL
SELECT Row# + Iteration, Iteration * 2
FROM RecursiveRowGenerator
WHERE Iteration * 2 < CEILING(SQRT(DATEDIFF(MONTH, #StartDate, #EndDate)+1))
UNION ALL SELECT Row# + (Iteration * 2), Iteration * 2
FROM RecursiveRowGenerator
WHERE Iteration * 2 < CEILING(SQRT(DATEDIFF(MONTH, #StartDate, #EndDate)+1)) )
, SqrtNRows AS ( SELECT * FROM RecursiveRowGenerator
UNION ALL SELECT 0, 0 )
SELECT TOP(DATEDIFF(MONTH, #StartDate, #EndDate)+1)
DateAdd(d,-1,DateAdd(m,1, DATEADD(month, DATEDIFF(month, 0, #StartDate) + A.Row# * POWER(2,CEILING(LOG(SQRT(DATEDIFF(MONTH, #StartDate, #EndDate)+1))/LOG(2))) + B.Row#, 0) ))
Row# FROM SqrtNRows A, SqrtNRows B ORDER BY A.Row#, B.Row#;
DECLARE #Date1 DATE
DECLARE #Date2 DATE
SET #Date1 = '20130401'
SET #Date2 = DATEADD(MONTH, 83, #Date1)
SELECT DATENAME(MONTH, #Date1) "Month", MONTH(#Date1) "Month Number", YEAR(#Date1) "Year"
INTO #Month
WHILE (#Date1 < #Date2)
BEGIN
SET #Date1 = DATEADD(MONTH, 1, #Date1)
INSERT INTO #Month
SELECT DATENAME(MONTH, #Date1) "Month", MONTH(#Date1) "Month Number", YEAR(#Date1) "Year"
END
SELECT * FROM #Month
ORDER BY [Year], [Month Number]
DROP TABLE #Month
declare #date1 datetime,
#date2 datetime,
#date datetime,
#month integer,
#nm_bulan varchar(20)
create table #month_tmp
( bulan integer null, keterangan varchar(20) null )
select #date1 = '2000-01-01',
#date2 = '2000-12-31'
select #month = month(#date1)
while (#month < 13)
Begin
IF #month = 1
Begin
SELECT #date = CAST( CONVERT(VARCHAR(25),DATEADD(dd,-(DAY(DATEADD(mm,0,#date1))-1),DATEADD(mm,0,#date1)),111) + ' 00:00:00' as DATETIME )
End
ELSE
Begin
SELECT #date = CAST( CONVERT(VARCHAR(25),DATEADD(dd,-(DAY(DATEADD(mm,#month -1,#date1))-1),DATEADD(mm,#month -1,#date1)),111) + ' 00:00:00' as DATETIME )
End
select #nm_bulan = DATENAME(MM, #date)
insert into #month_tmp
select #month as nilai, #nm_bulan as nama
select #month = #month + 1
End
select * from #month_tmp
drop table #month_tmp
go