I have a booking table with the following information:
BookingID,(unique, not null)
StartDate, (not null)
EndDate (not null)
I need to calculate the number of nights someone remained in residence which I can do with a DATEDIFF between EndDate and StartDate. However, if someone is in residence for the entire month during a 31 day month we only charge them 30.
I'm not sure how to do this in SQL. I was thinking I would have to create a variable, calculate on a monthly basis and add to the variable, but that seems like it would be messy and take a very long time, especially towards the end of year. This needs to be done for about 5,000 records on a daily basis.
So:
If someone starts on 7/25/14 and ends 9/2/14, the nights is 38 not 39.
If someone starts on 10/2/14 and ends on 11/1/14, the nights is 30.
If someone starts on 10/2/14 and ends on 10/31/14, the nights is 29.
We will be calculating into the future so it doesn't matter if the end date is greater than the day the report is being ran.
Does anyone have any ideas how to accomplish this in the best way?
I would first to create a lookup table with all the month with 31 days
Such as
DECLARE #month TABLE (start_date date,end_date date)
INSERT INTO #month VALUES ('2014-07-01','2014-07-31'),('2014-08-01','2014-08-31'),('2014-10-01','2014-10-31'),('2014-12-01','2014-12-31')
//populate all months in your calculate range
Then you can calculate the value with
DECLARE #start DATE = '2014-07-25', #end DATE = '2014-09-02'
SELECT DATEDIFF(day,#start,#end) -
(SELECT COUNT(*) FROM #month WHERE start_date >= #start AND end_date <= #end)
Retrieve the integer part of the datediff divided by 31 :
SELECT DATEDIFF(day,'2014-07-25', '2014-09-02') - DATEDIFF(day,'2014-07-25', '2014-09-02') / 31
The SQLish solution is to create a calendar table that holds all the dates you will ever care about and any business meaning that those dates may have, such as "Is this a holiday?", "Is this off-season?", or "do we charge for this day?"
This may sound insane to someone accustomed to other programming languages, but it is perfectly sensible in the database world. Business rules and business logic get stored as data, not as code.
Make this table and populate it:
CREATE TABLE Calendar (
[date] date
,[is_charged] bit
)
and then you can write code that is nearly plain English:
SELECT
[BookingID]
,COUNT([date])
FROM BookingTable
INNER JOIN Calendar
ON [date] >= [StartDate]
AND [date] < [EndDate]
WHERE [is_charged] = 1
GROUP BY [BookingId]
When your business rules change, you just update the calendar instead of rewriting the code.
If I've read your question correctly then you can't actually use those solutions above which consist of a table of billable and non billable days because the 31st is billable unless the whole month was booked.
I reckon this is probably a job for a user defined function. Which runs up a total starting with the month that the start date is in and finishing with the month that the end date is in.
CREATE FUNCTION dbo.FN_BillableDays (#StartDate date, #EndDate date)
returns int
AS
BEGIN
IF #StartDate > #EndDate
BEGIN
return null --Error
END
DECLARE #Next date
DECLARE #MonthStart date
DECLARE #MonthEnd date
DECLARE #NextMonthStart date
DECLARE #n int =0
SET #Next = #StartDate
SET #MonthStart = DATEADD(day,1-DAY(#Next),#Next)
SET #NextMonthStart = DATEADD(month,1,#MonthStart )
SET #MonthEnd = DATEADD(day,-1,#NextMonthStart)
WHILE DATEDIFF(month,#Next,#EndDate) >0
BEGIN
SET #n = #n +
CASE
WHEN DAY(#next) = 1 AND DAY(#MonthEnd) = 31 THEN 30
WHEN DAY(#next) = 1 THEN DAY(#MonthEnd)
ELSE 1+DAY(#MonthEnd) -DAY(#next) END
SET #MonthStart = #NextMonthStart
SET #NextMonthStart = DATEADD(month,1,#MonthStart )
SET #MonthEnd = DATEADD(day,-1,#NextMonthStart)
SET #Next = #NextMonthStart
END
--Month of the EndDate
SET #n = #n +
CASE
WHEN DAY(#next) = 1 AND DAY(#EndDate) = 31 THEN 29
WHEN DAY(#next) = 1 THEN DAY(#EndDate)-1
ELSE DAY(#MonthEnd) -DAY(#EndDate) END
return #n
END
I tried it with some test dates
SELECT
b.BookingID,
b.StartDate,
b.EndDate,
dbo.FN_BillableDays (b.StartDate,b.EndDate) AS BillableDays
FROM dbo.Booking b
And got the following
BookingID StartDate EndDate BillableDays
----------- ---------- ---------- ------------
1 2013-12-31 2014-01-02 2
2 2013-12-31 2014-01-30 30
3 2014-01-01 2014-01-30 29
4 2014-01-01 2014-01-31 29
5 2014-01-01 2014-02-01 30
6 2014-01-01 2014-02-02 31
7 2014-02-02 2014-02-01 NULL
(7 row(s) affected)
Which matches my understanding of the logic you want to implement but you may want to tweak the last bit which adds on the days for the final month. If they leave on the 31st do you want to give them their last night (30th to 31st) for free.
If you don't then delete the line
WHEN DAY(#next) = 1 AND DAY(#EndDate) = 31 THEN 29
Related
I have a SQL Server query that runs at noon every day and looks for records entered between the previous day and today.
CreatedDate
RecordIdent
06/18/2022
123456
DECLARE #ReportDate = 6/21/2022
SELECT *
FROM SampleTable
WHERE CreatedDate BETWEEN DATEADD(D,-1,#ReportDate) AND GETDATE()
It works fine for Tuesday through Saturday. Where I hit a brick wall is when the report runs on Mondays or the day after a holiday. I need a way to make the increment int of the DATEADD formula dynamic so that it changes to account for Sunday and/or a holiday. Note Saturdays are considered working days for us So if today the report date is Tuesday 6/21 the increment int would be -3.
Thanks in advance for the help.
to get the 1 working day before #ReportDate
DATEADD(D,-1,#ReportDate)
It will be
SELECT MAX(CalDate)
FROM CALENDAR
WHERE CalDate < #ReportDate
AND IsHoliday = 0 -- Not a holiday
AND IsWorkingDay = 1 -- is a working day
Incorporate into your query
DECLARE #ReportDate DATE = '2022-06-20' -- a Monday
SELECT *
FROM SampleTable
WHERE CreatedDate BETWEEN (SELECT MAX(CalDate)
FROM CALENDAR
WHERE CalDate < #ReportDate
AND IsHoliday = 0
AND IsWorkingDay = 1)
AND GETDATE()
I've seen different version of this kind of function for other coding languages (Python, jQuery, etc.) but not for SQL. I have a procedure that needs to have a date calculated that is 65 days from the creation date, but it cannot include weekend or holidays. We already have a function that is able to add only working days to a date, but not take into account holidays. We have a holiday table that lists all the holiday dates, tblHolidayDates with a column HolidayDate in standard date format.
How would I do this? I'd also consider maybe just creating a Calendar table as well if someone could give me a CREATE TABLE query for that - all it would need is dates, weekday, and holiday columns.
Below I have given the current loop function that adds business days, but it's missing holidays. Any help would be greatly appreciated!!
ALTER FUNCTION [dbo].[AddWorkDaysToDate]
(
#fromDate datetime,
#daysToAdd int
)
RETURNS datetime
AS
BEGIN
DECLARE #toDate datetime
DECLARE #daysAdded integer
-- add the days, ignoring weekends (i.e. add working days)
set #daysAdded = 1
set #toDate = #fromDate
while #daysAdded <= #daysToAdd
begin
-- add a day to the to date
set #toDate = DateAdd(day, 1, #toDate)
-- only move on a day if we've hit a week day
if (DatePart(dw, #toDate) != 1) and (DatePart(dw, #toDate) != 7)
begin
set #daysAdded = #daysAdded + 1
end
end
RETURN #toDate
END
I wrote a SQL function years ago to build a dynamic holiday table as a table function. The link is below:
http://www.joebooth-consulting.com/sqlServer/sqlServer.html#CalendFunc
Hope it helps...
You can access the table function (or your own holiday table) to determine number of holidays via a SQL statement like below
SELECT count(*) FROM holiday_date(2013)
WHERE holiday_date BETWEEN #fromDate AND #toDate
then add the count to the returned date using dateAdd().
If you might have holidays that fall on a weekend, add the following to the WHERE clause
AND DatePart(dw, Holiday_date) != 1) and (DatePart(dw, holiday_date) != 7)
I've always achieved this with a static table of dates from roughly 5 years in the past to 10 in the future, each date being marked with 'working day' status and sometimes other flags as required. Previously using MS-SL server I would achieve this quickly with a WHILE loop, I think MySQL supports the same syntax
WHILE (condition)
BEGIN
INSERT date
END
To create the table either use the Enterprise Manager UI or something like
CREATE TABLE DateTable
(
actual_date datetime NOT NULL,
is_holiday bit NOT NULL
)
Assuming we have a DateTable with columns actual_date (date) and is_holiday (bit) , containing all dates and where all workdays have is_holiday=0:
SELECT actual_date from
(
SELECT actual_date, ROW_NUMBER() OVER(ORDER BY actual_date) AS Row
FROM DateTable
WHERE is_holiday= 0 and actual_date > '2013-12-01'
) X
WHERE row = 65
This will find 2013-12-01 plus 65 workdays, skipping holidays.
I am working on an attendance software in asp.net, in it i have to make a report which will tell the user about the hours and everything...so far i have created the basic functionality of the system, i.e. the user can check in and check out...i am stuck at making the report...
I have to calculate the working hours for every month, so the user can compare his hours with the total hours...what i had in mind was to create a stored procedure which when given a month name and a year, returns an int containing working hours for that month....but i can seem to get at it....
so far i found out how to create a date from a given month and a date, and found out the last day of that month, using which i can find out the total days in month...now i cant seem to figure out how do i know how much days to subtract for getting the working days.
here's the so far code..
declare
#y int,
#m int,
#d int,
#date datetime
set #y = 2012
set #m = 01
set #d = 01
----To create the date first
select #date = dateadd(mm,(#y-1900)* 12 + #m - 1,0) + (#d-1)
----Last Day of that date
SELECT DATEADD(s,-1,DATEADD(mm, DATEDIFF(m,0,#date)+1,0))
any help will be appreciated guys, thanks in advance....
The #theDate is any date on the month you want to calculate the work days. This approach does not take care about holidays.
DECLARE #theDate DATETIME = GETDATE()
SELECT MONTH(#theDate) [Month], 20 + COUNT(*) WorkDays
FROM (
SELECT DATEADD(MONTH, DATEDIFF(MONTH, 0, #theDate), 28) AS theDate
UNION
SELECT DATEADD(MONTH, DATEDIFF(MONTH, 0, #theDate), 29)
UNION
SELECT DATEADD(MONTH, DATEDIFF(MONTH, 0, #theDate), 30)
) AS d
WHERE DATEPART(DAY, theDate) > 28
AND DATEDIFF(DAY, 0, theDate) % 7 < 5
Here you can consider the below sql server code to get the first and
last day of the given month and also ignore all the Saturdays and Sundays.
DECLARE #curr_date datetime=getdate()
DECLARE #st_date datetime,#ed_date datetime
select #st_date=DATEADD(mm,datediff(mm,0,#curr_date),0),#ed_date = DATEADD(mm,datediff(mm,-1,#curr_date),-1)
--select #st_date as first_day,#ed_date as last_day
SET DATEFIRST 1 --Monday as first day of week
select DATEADD(dd,number,#st_date) from master..spt_values
where DATEDIFF(dd,DATEADD(dd,number,#st_date),#ed_date) >= 0 and type='P'
and DATEPART(DW,DATEADD(dd,number,#st_date)) <> 6
and DATEPART(DW,DATEADD(dd,number,#st_date)) <> 7
But inorder to calculate the actual working hours, you will have to take into the consideration of following thigs
1.Calculate the time interval between swipe-in and swipe-outs between start and end time for a day.
2.Exclude all the time gap(employee not in office)
3.Consider the company holidays.
etc
Here is a UDF to count work days. You can pass any date of a month to this function. But usually you should use actual "calendar" table to calculate work days and insert in this table weekends, holidays,... and so on.
CREATE FUNCTION dbo.WorkDaysCount (#Date datetime)
RETURNS int AS
BEGIN
DECLARE #BeginOfMonth datetime
SET #BeginOfMonth=DATEADD(DAY,-DAY(#Date)+1,#Date);
DECLARE #EndOfMonth datetime
SET #EndOfMonth=DATEADD(Day,-1,DATEADD(Month,1,#BeginOfMonth));
DECLARE #cDate datetime
set #cDate=#BeginOfMonth
Declare #WorkDaysCount int
SET #WorkDaysCount=0
while #cDate<=#EndOfMonth
begin
if DATEPART(dw,#cDate) not in (1,7) SET #WorkDaysCount=#WorkDaysCount+1 -- not a Sunday or Saturday change (1,7) to (6,7) if you have other week start day (Monday).
set #cDate=#cDate+1;
end;
return (#WorkDaysCount);
END
I want to find the result as such like
I want to take current date from system .
I want to retrieve corresponding day of that date.
and go back 90 days back to the current date and want to try to find out that how many times similar day had occured.
want to find out total patients visisted on those days to clinic. (ex.COUNT(VisitId)) from my PtientVisist table.
and finally want to calaculate the average of patients visited on that day.
like-> if I get todays date 8 jun 2012 and retrieved today day as Friday . so want to find out
like since from last 90 days from todays date how many fridays had occured and how many patiens visited on that total fridays and want to count AVG = Total patients visisted/ total friday.
Please give some assistance Thanks in advance.
Air code to get the # of days of the week in a timespan:
DECLARE #enddate datetime
SET #enddate = GETDATE()
DECLARE #dayofweek nvarchar(50)
SELECT #dayofweek = DATENAME(dw, getdate())
DECLARE #startdate datetime
SELECT #startdate = DATEADD(d,-90, getdate())
DECLARE #count int
SET #count = 0
WHILE (#startdate < #enddate)
BEGIN
IF (DATENAME(dw, #startdate) = #dayofweek)
BEGIN
SET #count = #count + 1
END
SET #startdate = DATEADD(d, 1, #startdate)
END
PRINT #count
You can do something with the actual date in the loop in order to get your final query.
I am attempting to populate a table based on 2 and 3 week intervals for a semi-monthly pay period in TSQL. The table should populate,
2 week date
2 week date
3 week date
2 week date
2 week date
3 week date
..based on the first date I supply, subsequently adding 2 or 3 weeks to the last date supplied. I should be able to supply a start date and end date. It may be that it's just early in the morning, but can't think of an elegant way to accomplish this task. Any pointers?
Thanks!
George
WITH dates (d, n) AS
(
SELECT #mydate, 1
UNION ALL
SELECT DATEADD(week, CASE n % 3 WHEN 0 THEN 3 ELSE 2 END, d), n + 1
FROM dates
WHERE d < #enddate
)
INSERT
INTO mytable
SELECT d
FROM dates
OPTION (MAXRECURSION 0)
Horrid brute force approach - because the 2,2,3 is difficult to loop just adding it regardless into the temp table and then filtering at the end incase a couple extra entries go in - not the most efficient but if you are needing to just get a range one off then it works.
So the caveat here is: ok for one off, I wouldn't use in production :)
declare #start datetime
declare #end datetime
declare #calculated datetime
set #start = '20010101'
set #end = '20011231'
set #calculated = #start
Create Table #Dates (PayDate datetime)
while #calculated <= #end
begin
set #calculated = DateAdd(wk,2,#calculated)
insert into #Dates(paydate) values (#calculated)
set #calculated = DateAdd(wk,2,#calculated)
insert into #Dates(paydate) values (#calculated)
set #calculated = DateAdd(wk,3,#calculated)
insert into #Dates(paydate) values (#calculated)
end
select * from #Dates where paydate >= #start and paydate <= #end
drop table #dates
So you have a 7-week cycle -- figure out which 7-week period you're in from some known starting point and then which week of this group of 7 you are.