Hoping someone can tell me where I am going wrong here, I started with a Business Hours query found at Calculate business hours between two dates, it's the second top answer because the top answer was not providing the correct output upon testing. The issue I am having is when I cross over to a new day and the end datetime exceeds the Business Day End. Here is my test:
select dbo.BusinessSeconds('09:00:00','17:00:00','2014-12-24 16:59:59','2014-12-26 18:00:00');
So if I go from 1 second before COB to the start of the next day, I get 1 second, which is correct. If I go from the same start to the end of the day the next day, I get 28801, or 1 full day and 1 second - which is also correct. However, if I extend the date end time to 17:00:01 through 17:59:59 it also includes those seconds but if I hit 18:00:00, the time truncates back to the original 17:00:00 end of day. So I will get correct data for any given end time unless it is during the 17:00:00-17:59:59 timeframes and then I potentially get up to an extra hour of time that I should not have.
Any help would be greatly appreciated as I have a deliverable on this by tomorrow.
ALTER Function [dbo].[BusinessSeconds](#BusinessDayStart TIME,#BusinessDayEnd TIME,#StartDate DATETIME,#EndDate DATETIME) Returns Int
AS
Begin
--TEST: select dbo.BusinessSeconds('09:00:00','17:00:00','2014-12-24 16:59:59','2014-12-26 18:00:00');
Declare #WorkMin INT = 0 -- Initialize counter
Declare #Reverse BIT -- Flag to hold if direction is reverse
Declare #StartHour TIME = #BusinessDayStart -- Start of business hours (can be supplied as an argument if needed)
Declare #EndHour TIME = #BusinessDayEnd -- End of business hours (can be supplied as an argument if needed)
Declare #Holidays Table (HDate DateTime) -- Table variable to hold holidayes
-- If dates are in reverse order, switch them and set flag
If #StartDate>#EndDate
Begin
Declare #TempDate DateTime=#StartDate
Set #StartDate=#EndDate
Set #EndDate=#TempDate
Set #Reverse=1
End
Else Set #Reverse = 0
-- Get country holidays from table based on the country code (Feel free to remove this or modify as per your DB schema)
-- Insert Into #Holidays(HDate) select DATEADD(month,((LEFT(CAL_ID,4)-1900)*12)+LEFT(RIGHT(CAL_ID,4),2)-1,RIGHT(CAL_ID,2)-1) from B_CALENDAR where CAL_DAY_PK = 263 and HDATE >= DateAdd(dd, DateDiff(dd,0,#StartDate), 0)
Insert Into #Holidays (HDate) Select HolidayDate from V_BUS_HOL Where HolidayDate >= DateAdd(dd, DateDiff(dd,0,#StartDate), 0)
If CAST(#StartDate as TIME) < #StartHour Set #StartDate = CAST(CAST(#StartDate as DATE) as DATETIME) + CAST(#StartHour as DATETIME) -- If Start time is less than start hour, set it to start hour
If CAST(#StartDate as TIME) >= DATEADD(HOUR,1,#EndHour) Set #StartDate = CAST(CAST(DATEADD(DAY,1,#StartDate) as DATE) as DATETIME) + CAST(#StartHour as DATETIME) -- If Start time is after end hour, set it to start hour of next day
If CAST(#EndDate as TIME) >= DATEADD(HOUR,1,#EndHour) Set #EndDate = CAST(CAST(#EndDate as DATE) as DATETIME) + CAST(#EndHour as DATETIME) -- If End time is after end hour, set it to end hour
If CAST(#EndDate as TIME) < #StartHour Set #EndDate = CAST(CAST(DATEADD(DAY,-1,#EndDate) as DATE) as DATETIME) + CAST(#EndHour as DATETIME) -- If End time is before start hour, set it to end hour of previous day
If #StartDate>#EndDate Return 0
-- If Start and End is on same day
If DateDiff(Day,#StartDate,#EndDate) <= 0
Begin
If Datepart(dw,#StartDate)>1 And DATEPART(dw,#StartDate)<7 -- If day is between sunday and saturday
If (Select Count(*) From #Holidays Where HDATE=DateAdd(dd, DateDiff(dd,0,#StartDate), 0)) = 0 -- If day is not a holiday
If #EndDate<#StartDate Return 0 Else Set #WorkMin=DATEDIFF(SECOND, #StartDate, #EndDate) -- Calculate difference
Else Return 0
Else Return 0
End
Else Begin
Declare #Partial int=1 -- Set partial day flag
While DateDiff(Day,#StartDate,#EndDate) > 0 -- While start and end days are different
Begin
If Datepart(dw,#StartDate)>1 And DATEPART(dw,#StartDate)<7 -- If this is a weekday
Begin
If (Select Count(*) From #Holidays Where HDATE=DateAdd(dd, DateDiff(dd,0,#StartDate), 0)) = 0 -- If this is not a holiday
Begin
If #Partial=1 -- If this is the first iteration, calculate partial time
Begin
Set #WorkMin=#WorkMin + DATEDIFF(SECOND, #StartDate, DateAdd(hour, DATEPART(HOUR,#EndHour), DateDiff(DAY, 0, #StartDate)))
Set #StartDate=DateAdd(hour, DATEPART(HOUR,#StartHour)+24, DateDiff(DAY, 0, #StartDate))
Set #Partial=0
End
Else Begin -- If this is a full day, add full minutes
Set #WorkMin=#WorkMin + (DATEPART(HOUR,#EndHour)-DATEPART(HOUR,#StartHour))*60
Set #StartDate = DATEADD(DD,1,#StartDate)
End
End
Else Set #StartDate = DATEADD(DD,1,#StartDate)
End
Else Set #StartDate = DATEADD(DD,1,#StartDate)
End
If Datepart(dw,#StartDate)>1 And DATEPART(dw,#StartDate)<7 -- If last day is a weekday
If (Select Count(*) From #Holidays Where HDATE=DateAdd(dd, DateDiff(dd,0,#StartDate), 0)) = 0 -- And it is not a holiday
If #Partial=0 Set #WorkMin=#WorkMin + DATEDIFF(SECOND, #StartDate, #EndDate) Else Set #WorkMin=#WorkMin + DATEDIFF(SECOND, DateAdd(hour, DATEPART(HOUR,#StartHour), DateDiff(DAY, 0, #StartDate)), #EndDate)
End
If #Reverse=1 Set #WorkMin=-#WorkMin
Return #WorkMin
End
This one is an old one, however I believe that your problem is datetime/time data types. You didn't state the version of SQL Server you are using but if you have 2008 or above, you may want to adjust your tables and this function to use the datetime2 format. It is more accurate. datetime/smalldatetime both are only accurate to a certain point, then they round which could cause the problem you are looking at.
Related
I'm working on SQL-Server 2012 and have the following code example to get workdays between two dates
DECLARE #StartDate AS date
DECLARE #EndDate AS date
SET #StartDate = '2019/02/18' -- this is a monday
SET #EndDate = '2019/02/23' -- this is a saturday
SELECT
DATEDIFF(DD, #StartDate, #EndDate)
- (DATEDIFF(WK, #StartDate,#EndDate) * 2)
- CASE WHEN DATEPART(DW, #StartDate) = 1 THEN 1 ELSE 0 END
- CASE WHEN DATEPART(DW, #EndDate) = 1 THEN 1 ELSE 0 END
the result is 4, which is correct...
But If I put 2019/02/24 (sunday) for the EndDate I'm getting 3... ????
I'm getting crazy here...
You're validating your Enddate to be Sunday instead of Saturday. I had a similar function available that is independent of date settings.
SELECT ISNULL((((DATEDIFF(dd,#StartDate,#EndDate)) --Start with total number of days including weekends
- (DATEDIFF(wk,#StartDate,#EndDate)*2) --Subtact 2 days for each full weekend
- (1-SIGN(DATEDIFF(dd,6,#StartDate)%7)) --If StartDate is a Sunday, Subtract 1
- (1-SIGN(DATEDIFF(dd,5,#EndDate) %7)))) , 0) --If StartDate is a Saturday, Subtract 1
WHERE #StartDate <= #EndDate
The answer by #Luis Cazares works great except that when the start date or end date fall on a Saturday or Sunday, then the number is 1 off. Also, holidays are not accounted for (if that falls within the discussion).
Here's my way to construct a function.
CREATE FUNCTION [dbo].[getWorkDays] (#startDate date, #endDate date)
RETURNS int
AS
BEGIN
DECLARE #daysct int = 0;
DECLARE #currentDate date = #startDate;
IF #endDate>#startDate
GOTO COUNTIT;
ELSE
GOTO ZERO;
COUNTIT:
BEGIN
--Pre-process: if the startDate is holiday or weekend, then step in until the next business day
WHILE datepart(weekday, #currentDate) in (1, 7) OR EXISTS (SELECT holiday_date FROM dbo.holidays WHERE holiday_date = #currentDate) --if weekend or holiday
BEGIN
SET #currentDate = dateadd(day, 1, #currentDate);
SET #daysct=1;
END
WHILE #currentDate <= #endDate
BEGIN
IF #currentDate=#endDate
BREAK;
ELSE
IF datepart(weekday, #currentDate) not in (1, 7) AND NOT EXISTS (SELECT holiday_date FROM dbo.holidays WHERE holiday_date = #currentDate) --if not weekend and not holiday
BEGIN
SET #daysct=#daysct + 1;
END
SET #currentDate=dateadd(day, 1, #currentDate);
END
--Post-process: if the end date DOES NOT fall on a weekend or holiday, then #daysct is systematically 1 below. Add 1 to straighten it.
IF datepart(weekday, #endDate) in (1, 7) OR EXISTS (SELECT holiday_date FROM dbo.holidays WHERE holiday_date = #endDate)
SET #daysct = #daysct - 1;
RETURN(#daysct);
END
ZERO:
BEGIN
SET #daysct=0;
RETURN(#daysct);
END
END;
It's not the most efficient code but it's naturally the process how I would hand-count it. The catch is you need to declare a variable or constant, otherwise it will significantly slow down your queries.
DECLARE #days_worked int = dbo.getWorkDays(dateadd(day, 1, eomonth(getdate() AT TIME ZONE 'Pacific Standard Time', -1)),getdate() AT TIME ZONE 'Pacific Standard Time');
Also, before you can use it, make sure you have a table for the holidays. My table is constructed this way.
enter image description here
DECLARE #StartDate DATETIME
DECLARE #EndDate DATETIME
SET #StartDate = '2019/02/18'
SET #EndDate = '2019/02/23'
SELECT
(DATEDIFF(dd, #StartDate, #EndDate) + 1)
-(DATEDIFF(wk, #StartDate, #EndDate) * 2)
-(CASE WHEN DATENAME(dw, #StartDate) = 'Sunday' THEN 1 ELSE 0 END)
-(CASE WHEN DATENAME(dw, #EndDate) = 'Saturday' THEN 1 ELSE 0 END)
When looking for a solution for my question I found the following sql (Calculate business hours between two dates).
However, I want the starting point to be 8:30 instead of 9 and the end point 17:30 instead of 17. Currently I am using an integer. Can anyone help me with this? Thanks in advance!
Create Function GetWorkingMin(#StartDate DateTime, #EndDate DateTime, #Country Varchar(2)) Returns Int
AS
Begin
Declare #WorkMin int = 0 -- Initialize counter
Declare #Reverse bit -- Flag to hold if direction is reverse
Declare #StartHour int = 9 -- Start of business hours (can be supplied as an argument if needed)
Declare #EndHour int = 17 -- End of business hours (can be supplied as an argument if needed)
Declare #Holidays Table (HDate DateTime) -- Table variable to hold holidayes
-- If dates are in reverse order, switch them and set flag
If #StartDate>#EndDate
Begin
Declare #TempDate DateTime=#StartDate
Set #StartDate=#EndDate
Set #EndDate=#TempDate
Set #Reverse=1
End
Else Set #Reverse = 0
-- Get country holidays from table based on the country code (Feel free to remove this or modify as per your DB schema)
Insert Into #Holidays (HDate) Select HDate from HOLIDAY Where COUNTRYCODE=#Country and HDATE>=DateAdd(dd, DateDiff(dd,0,#StartDate), 0)
If DatePart(HH, #StartDate)<#StartHour Set #StartDate = DateAdd(hour, #StartHour, DateDiff(DAY, 0, #StartDate)) -- If Start time is less than start hour, set it to start hour
If DatePart(HH, #StartDate)>=#EndHour+1 Set #StartDate = DateAdd(hour, #StartHour+24, DateDiff(DAY, 0, #StartDate)) -- If Start time is after end hour, set it to start hour of next day
If DatePart(HH, #EndDate)>=#EndHour+1 Set #EndDate = DateAdd(hour, #EndHour, DateDiff(DAY, 0, #EndDate)) -- If End time is after end hour, set it to end hour
If DatePart(HH, #EndDate)<#StartHour Set #EndDate = DateAdd(hour, #EndHour-24, DateDiff(DAY, 0, #EndDate)) -- If End time is before start hour, set it to end hour of previous day
If #StartDate>#EndDate Return 0
-- If Start and End is on same day
If DateDiff(Day,#StartDate,#EndDate) <= 0
Begin
If Datepart(dw,#StartDate)>1 And DATEPART(dw,#StartDate)<7 -- If day is between sunday and saturday
If (Select Count(*) From #Holidays Where HDATE=DateAdd(dd, DateDiff(dd,0,#StartDate), 0)) = 0 -- If day is not a holiday
If #EndDate<#StartDate Return 0 Else Set #WorkMin=DATEDIFF(MI, #StartDate, #EndDate) -- Calculate difference
Else Return 0
Else Return 0
End
Else Begin
Declare #Partial int=1 -- Set partial day flag
While DateDiff(Day,#StartDate,#EndDate) > 0 -- While start and end days are different
Begin
If Datepart(dw,#StartDate)>1 And DATEPART(dw,#StartDate)<7 -- If this is a weekday
Begin
If (Select Count(*) From #Holidays Where HDATE=DateAdd(dd, DateDiff(dd,0,#StartDate), 0)) = 0 -- If this is not a holiday
Begin
If #Partial=1 -- If this is the first iteration, calculate partial time
Begin
Set #WorkMin=#WorkMin + DATEDIFF(MI, #StartDate, DateAdd(hour, #EndHour, DateDiff(DAY, 0, #StartDate)))
Set #StartDate=DateAdd(hour, #StartHour+24, DateDiff(DAY, 0, #StartDate))
Set #Partial=0
End
Else Begin -- If this is a full day, add full minutes
Set #WorkMin=#WorkMin + (#EndHour-#StartHour)*60
Set #StartDate = DATEADD(DD,1,#StartDate)
End
End
Else Set (at)StartDate = DATEADD(HOUR, (at)StartHour, CAST(CAST(DATEADD(DD,1,(at)StartDate) AS DATE) AS DATETIME))
End
Else Set (at)StartDate = DATEADD(HOUR, (at)StartHour, CAST(CAST(DATEADD(DD,1,(at)StartDate) AS DATE) AS DATETIME))
End
If Datepart(dw,#StartDate)>1 And DATEPART(dw,#StartDate)<7 -- If last day is a weekday
If (Select Count(*) From #Holidays Where HDATE=DateAdd(dd, DateDiff(dd,0,#StartDate), 0)) = 0 -- And it is not a holiday
If #Partial=0 Set #WorkMin=#WorkMin + DATEDIFF(MI, #StartDate, #EndDate) Else Set #WorkMin=#WorkMin + DATEDIFF(MI, DateAdd(hour, #StartHour, DateDiff(DAY, 0, #StartDate)), #EndDate)
End
If #Reverse=1 Set #WorkMin=-#WorkMin
Return #WorkMin
End
Use TIME for start time. Then create INT variable to put converted value to minute to use for conditions.
I have created sample for you.
DECLARE #StartDate DATETIME='2018-12-10 10:00'
DECLARE #ExpectedStartTime TIME='08:30'
DECLARE #ExpectedStartMin INT=DATEPART(HOUR,#ExpectedStartTime)*60+DATEPART(MINUTE,#ExpectedStartTime)
DECLARE #ActualStartMin INT=DATEPART(HOUR,#StartDate)*60+DATEPART(MINUTE,#StartDate)
--Check before change the StartDate
SELECT #ExpectedStartMin, #ActualStartMin, #StartDate
If #ExpectedStartMin<#ActualStartMin Set #StartDate = CAST(CAST(#StartDate AS DATE) AS DATETIME)+#ExpectedStartTime
-- same way to the other conditions.
--
--
--Check after change the StartDate
SELECT #ExpectedStartMin, #ActualStartMin, #StartDate
i have an ssis Package which runs on business days (mon-Fri). if i receive file on tuesday , background(DB), it takes previous business day date and does some transactions. If i run the job on friday, it has to fetch mondays date and process the transactions.
i have used the below query to get previous business date
Select Convert(varchar(50), Position_ID) as Position_ID,
TransAmount_Base,
Insert_Date as InsertDate
from tblsample
Where AsOfdate = Dateadd(dd, -1, Convert(datetime, Convert(varchar(10), '03/28/2012', 101), 120))
Order By Position_ID
if i execute this query i'll get the results of yesterdays Transactios. if i ran the same query on monday, it has to fetch the Fridays transactions instead of Sundays.
SELECT DATEADD(DAY, CASE DATENAME(WEEKDAY, GETDATE())
WHEN 'Sunday' THEN -2
WHEN 'Monday' THEN -3
ELSE -1 END, DATEDIFF(DAY, 0, GETDATE()))
I prefer to use DATENAME for things like this over DATEPART as it removes the need for Setting DATEFIRST And ensures that variations on time/date settings on local machines do not affect the results. Finally DATEDIFF(DAY, 0, GETDATE()) will remove the time part of GETDATE() removing the need to convert to varchar (much slower).
EDIT (almost 2 years on)
This answer was very early in my SO career and it annoys me everytime it gets upvoted because I no longer agree with the sentiment of using DATENAME.
A much more rubust solution would be:
SELECT DATEADD(DAY, CASE (DATEPART(WEEKDAY, GETDATE()) + ##DATEFIRST) % 7
WHEN 1 THEN -2
WHEN 2 THEN -3
ELSE -1
END, DATEDIFF(DAY, 0, GETDATE()));
This will work for all language and DATEFIRST settings.
This function returns last working day and takes into account holidays and weekends. You will need to create a simple holiday table.
-- =============================================
-- Author: Dale Kilian
-- Create date: 2019-04-29
-- Description: recursive function returns last work day for weekends and
-- holidays
-- =============================================
ALTER FUNCTION dbo.fnGetWorkWeekday
(
#theDate DATE
)
RETURNS DATE
AS
BEGIN
DECLARE #importDate DATE = #theDate
DECLARE #returnDate DATE
--Holidays
IF EXISTS(SELECT 1 FROM dbo.Holidays WHERE isDeleted = 0 AND #theDate = Holiday_Date)
BEGIN
SET #importDate = DATEADD(DAY,-1,#theDate);
SET #importDate = (SELECT dbo.fnGetWorkWeekday(#importDate))
END
--Satruday
IF(DATEPART(WEEKDAY,#theDate) = 7)
BEGIN
SET #importDate = DATEADD(DAY,-1,#theDate);
SET #importDate = (SELECT dbo.fnGetWorkWeekday(#importDate))
END
--Sunday
IF(DATEPART(WEEKDAY,#theDate) = 1)
BEGIN
SET #importDate = DATEADD(DAY,-2,#theDate);
SET #importDate = (SELECT dbo.fnGetWorkWeekday(#importDate))
END
RETURN #importDate;
END
GO
Then how about:
declare #dt datetime='1 dec 2012'
select case when 8-##DATEFIRST=DATEPART(dw,#dt)
then DATEADD(d,-2,#dt)
when (9-##DATEFIRST)%7=DATEPART(dw,#dt)%7
then DATEADD(d,-3,#dt)
else DATEADD(d,-1,#dt)
end
The simplest solution to find the previous business day is to use a calendar table with a column called IsBusinessDay or something similar. The your query is something like this:
select max(BaseDate)
from dbo.Calendar c
where c.IsBusinessDay = 0x1 and c.BaseDate < #InputDate
The problem with using functions is that when (not if) you have to create exceptions for any reason (national holidays etc.) the code quickly becomes unmaintainable; with the table, you just UPDATE a single value. A table also makes it much easier to answer questions like "how many business days are there between dates X and Y", which are quite common in reporting tasks.
You can easily make this a function call, adding a second param to replace GetDate() with whatever date you wanted.
It will work for any day of the week, at any date range, if you change GetDate().
It will not change the date if the day of week is the input date (GetDate())
Declare #DayOfWeek As Integer = 2 -- Monday
Select DateAdd(Day, ((DatePart(dw,GetDate()) + (7 - #DayOfWeek)) * -1) % 7, Convert(Date,GetDate()))
More elegant:
select DATEADD(DAY,
CASE when datepart (dw,Getdate()) < 3 then datepart (dw,Getdate()) * -1 + -1 ELSE -1 END,
cast(GETDATE() as date))
select
dateadd(dd,
case DATEPART(dw, getdate())
when 1
then -2
when 2
then -3
else -1
end, GETDATE())
thanks for the tips above, I had a slight variant on the query in that my user needed all values for the previous business date. For example, today is a Monday so he needs everything between last Friday at midnight through to Saturday at Midnight. I did this using a combo of the above, and "between", just if anyone is interested. I'm not a massive techie.
-- Declare a variable for the start and end dates.
declare #StartDate as datetime
declare #EndDate as datetime
SELECT #StartDate = DATEADD(DAY, CASE DATENAME(WEEKDAY, GETDATE())
WHEN 'Sunday' THEN -2
WHEN 'Monday' THEN -3
ELSE -1 END, DATEDIFF(DAY, 0, GETDATE()))
select #EndDate = #StartDate + 1
select #StartDate , #EndDate
-- Later on in the query use "between"
and mydate between #StartDate and #EndDate
I want to use the following function for scheduling club meetings which occur on a monthly basis based on the week and weekday of the month. In the example below I have a (to be) function that returns the Third Wednesday of the month. If that day occurs in the past then it returns the next month's 3rd Wednesday.
I want to get away from the Loops and I feel that there is a better method for calculation. Is there a more OO process? Your opinion?
--CREATE FUNCTION NextWeekDayofMonth
DECLARE
--(
#WEEK INT,
#WEEKDAY INT,
#REFERENCEDATE DATETIME
--)
--RETURNS DATETIME
--AS
-------------------------------
--Values for testing - Third Wednesday of the Month
set #WEEK = 3 --Third Week
set #WEEKDAY = 4 --Wednesday
set #REFERENCEDATE = '08/20/2011'
-------------------------------
BEGIN
DECLARE #WEEKSEARCH INT
DECLARE #FDOM DATETIME
DECLARE #RETURNDATE DATETIME
SET #FDOM = DATEADD(M,DATEDIFF(M,0,#REFERENCEDATE),0)
SET #RETURNDATE = DATEADD(M,0,#FDOM)
WHILE (#RETURNDATE < #REFERENCEDATE)
--If the calculated date occurs in the past then it
--finds the appropriate date in the next month
BEGIN
SET #WEEKSEARCH = 1
SET #RETURNDATE = #FDOM
--Finds the first weekday of the month that matches the provided weekday value
WHILE ( DATEPART(DW,#RETURNDATE) <> #WEEKDAY)
BEGIN
SET #RETURNDATE = DATEADD(D,1,#RETURNDATE)
END
--Iterates through the weeks without going into next month
WHILE #WEEKSEARCH < #WEEK
BEGIN
IF MONTH(DATEADD(WK,1,#RETURNDATE)) = MONTH(#FDOM)
BEGIN
SET #RETURNDATE = DATEADD(WK,1,#RETURNDATE)
SET #WEEKSEARCH = #WEEKSEARCH+1
END
ELSE
BREAK
END
SET #FDOM = DATEADD(M,1,#FDOM)
END
--RETURN #RETURNDATE
select #ReturnDate
END
IMO, the best process is to store important business information as rows in tables in a database. If you build a calendar table, you can get all the third Wednesdays by a simple query. Not only are the queries simple, they can be seen to be obviously correct.
select cal_date
from calendar
where day_of_week_ordinal = 3
and day_of_week = 'Wed';
The third Wednesday that's on or after today is also simple.
select min(cal_date)
from calendar
where day_of_week_ordinal = 3
and day_of_week = 'Wed'
and cal_date >= CURRENT_DATE;
Creating a calendar table is straightforward. This was written for PostgreSQL, but it's entirely standard SQL (I think) except for the columns relating to ISO years and ISO weeks.
create table calendar (
cal_date date primary key,
year_of_date integer not null
check (year_of_date = extract(year from cal_date)),
month_of_year integer not null
check (month_of_year = extract(month from cal_date)),
day_of_month integer not null
check (day_of_month = extract(day from cal_date)),
day_of_week char(3) not null
check (day_of_week =
case when extract(dow from cal_date) = 0 then 'Sun'
when extract(dow from cal_date) = 1 then 'Mon'
when extract(dow from cal_date) = 2 then 'Tue'
when extract(dow from cal_date) = 3 then 'Wed'
when extract(dow from cal_date) = 4 then 'Thu'
when extract(dow from cal_date) = 5 then 'Fri'
when extract(dow from cal_date) = 6 then 'Sat'
end),
day_of_week_ordinal integer not null
check (day_of_week_ordinal =
case
when day_of_month >= 1 and day_of_month <= 7 then 1
when day_of_month >= 8 and day_of_month <= 14 then 2
when day_of_month >= 15 and day_of_month <= 21 then 3
when day_of_month >= 22 and day_of_month <= 28 then 4
else 5
end),
iso_year integer not null
check (iso_year = extract(isoyear from cal_date)),
iso_week integer not null
check (iso_week = extract(week from cal_date))
);
You can populate that table with a spreadsheet or with a UDF. Spreadsheets usually have pretty good date and time functions. I have a UDF, but it's written for PostgreSQL (PL/PGSQL), so I'm not sure how much it would help you. But I'll post it later if you like.
Here's a date math way to accomplish what you want without looping:
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
-- =============================================
-- Description: Gets the nth occurrence of a given weekday in the month containing the specified date.
-- For #dayOfWeek, 1 = Sunday, 2 = Monday, 3 = Tuesday, 4 = Wednesday, 5 = Thursday, 6 = Friday, 7 = Saturday
-- =============================================
CREATE FUNCTION GetWeekdayInMonth
(
#date datetime,
#dayOfWeek int,
#nthWeekdayInMonth int
)
RETURNS datetime
AS
BEGIN
DECLARE #beginMonth datetime
DECLARE #offSet int
DECLARE #firstWeekdayOfMonth datetime
DECLARE #result datetime
SET #beginMonth = DATEADD(DAY, -DATEPART(DAY, #date) + 1, #date)
SET #offSet = #dayOfWeek - DATEPART(dw, #beginMonth)
IF (#offSet < 0)
BEGIN
SET #firstWeekdayOfMonth = DATEADD(d, 7 + #offSet, #beginMonth)
END
ELSE
BEGIN
SET #firstWeekdayOfMonth = DATEADD(d, #offSet, #beginMonth)
END
SET #result = DATEADD(WEEK, #nthWeekdayInMonth - 1, #firstWeekdayOfMonth)
IF (NOT(MONTH(#beginMonth) = MONTH(#result)))
BEGIN
SET #result = NULL
END
RETURN #result
END
GO
DECLARE #nextMeetingDate datetime
SET #nextMeetingDate = dbo.GetWeekdayInMonth(GETDATE(), 4, 3)
IF (#nextMeetingDate IS NULL OR #nextMeetingDate < GETDATE())
BEGIN
SET #nextMeetingDate = dbo.GetWeekDayInMonth(DATEADD(MONTH, 1, GETDATE()), 4, 3)
END
SELECT #nextMeetingDate
Here's another function based solution using date math that returns the Next Nth Weekday on or after a given date. It doesn't use any looping to speak of, but it may recurs by at most one iteration if the next Nth weekday is in the next month.
This function takes the DATEFIRST setting into account for environments that use a value other than the default.
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
-- =============================================
-- Author: David Grimberg
-- Create date: 2015-06-18
-- Description: Gets the next Nth weekday
-- #param Date is any date in a month
-- #param DayOfWeek is the weekday of interest ranging
-- from 1 to 7 with ##DATEFIRST being the
-- first day of the week
-- #param NthWeekday represents which ordinal weekday to return.
-- Positive values return dates relative to the start
-- of the month. Negative values return dates relative
-- to the end of the month. Values > 4 indicate the
-- last week, values < -4 indicate the first week.
-- Zero is assumed to be 1.
-- =============================================
ALTER FUNCTION dbo.xxGetNextNthWeekday
(
#Date date,
#NthWeekday smallint,
#DayOfWeek tinyint
)
RETURNS date
AS
BEGIN
DECLARE #FirstOfMonth date
DECLARE #inc int
DECLARE #Result date
-- Clamp the #NthWeekday input to valid values
set #NthWeekday = case when #NthWeekday = 0 then 1
when #NthWeekday > 4 then -1
when #NthWeekday < -4 then 1
else #NthWeekday
end
-- Normalize the requested day of week taking
-- ##DATEFIRST into consideration.
set #DayOfWeek = (##DATEFIRST + 6 + #DayOfWeek) % 7 + 1
-- Gets the first of the current month or the
-- next month if #NthWeekday is negative.
set #FirstOfMonth = dateadd(month, datediff(month,0,#Date)
+ case when #NthWeekday < 0 then 1 else 0 end
, 0)
-- Add and/or subtract 1 week depending direction of search and the
-- relationship of #FirstOfMonth's Day of the Week to the #DayOfWeek
-- of interest
set #inc = case when (datepart(WEEKDAY, #FirstOfMonth)+##DATEFIRST-1)%7+1 > #DayOfWeek
then 0
else -1
end
+ case when #NthWeekday < 0 then 1 else 0 end
-- Put it all together
set #Result = dateadd( day
, #DayOfWeek-1
, dateadd( WEEK
, #NthWeekday + #inc
, dateadd( WEEK -- Gets 1st Sunday on or
, datediff(WEEK, -1, #FirstOfMonth)
,-1 ) ) ) -- before #FirstOfMonth
-- [Snip here] --
if #Result < #Date
set #Result = dbo.xxGetNextNthWeekday( dateadd(month, datediff(month, 0, #Date)+1, 0)
, #NthWeekday, #DayOfWeek)
-- [to here for no recursion] --
Return #Result
END
If you want the past or future Nth weekday of the current month based on the #Date parameter rather than the next Nth weekday snip out the recursive part as indicated above.
I seem to be asking a lot of SQL questions at the moment. (I would normally write a program to sort the data into a report table, however at the moment that is not possible, and needs to be done using SQL)
The Question
I need a where clause that returns results with the next working day. i.e. Monday 01/01/01 the next working day would be Tuesday 02/01/01 which could be achieved with a simple date add. However on a Friday 05/01/01 the next working day is Monday 08/01/01. Is there anything built in to help cope with this easily?
Thanks for your advice.
The key is to use the DATEPART(weekday,#date) function, it'll return the day of the week, so if it's saturday or sunday you just add one or two to the current date to get the desired result.
You can create a user defined function to do so easily, for example Pinal Dave has this
CREATE FUNCTION dbo.udf_GetPrevNextWorkDay (#dtDate DATETIME, #strPrevNext VARCHAR(10))
RETURNS DATETIME
AS
BEGIN
DECLARE #intDay INT
DECLARE #rtResult DATETIME
SET #intDay = DATEPART(weekday,#dtDate)
--To find Previous working day
IF #strPrevNext = 'Previous'
IF #intDay = 1
SET #rtResult = DATEADD(d,-2,#dtDate)
ELSE
IF #intDay = 2
SET #rtResult = DATEADD(d,-3,#dtDate)
ELSE
SET #rtResult = DATEADD(d,-1,#dtDate)
--To find Next working day
ELSE
IF #strPrevNext = 'Next'
IF #intDay = 6
SET #rtResult = DATEADD(d,3,#dtDate)
ELSE
IF #intDay = 7
SET #rtResult = DATEADD(d,2,#dtDate)
ELSE
SET #rtResult = DATEADD(d,1,#dtDate)
--Default case returns date passed to function
ELSE
SET #rtResult = #dtDate
RETURN #rtResult
END
GO
CREATE FUNCTION dbo.uf_GetNextWorkingDay (#givenDate DATETIME)
RETURNS DATETIME
AS
BEGIN
DECLARE #workingDate DATETIME
IF (DATENAME(dw , #givenDate) = 'Friday')
BEGIN
SET #workingDate = DATEADD(day, 3, #givenDate)
END
ELSE IF (DATENAME(dw , #givenDate) = 'Saturday')
BEGIN
SET #workingDate = DATEADD(day, 2, #givenDate)
END
ELSE
BEGIN
SET #workingDate = DATEADD(day, 1, #givenDate)
END
RETURN #workingDate
END
A good article http://ryanfarley.com/blog/archive/2005/02/14/1685.aspx
You could do this with a simple day check and add 3 days rather than one if its a Friday. However, do you need to take into consideration public holidays though?
All Credit to Cashif -- I modified his to include a Holiday table (tblHolidays with a date field HolDate) which is very lightweight -- about 10 rows a year if you're lucky enough to get that many days off!
My version returns date type (which I find easier to work with). I also run thru this more than once -- if Thursday is your startday and Friday is a holiday, you have to again add 2 more days to get to Monday (or the next working day). Then it checkes to make sure the next week doesn't start with a holiday.
CREATE FUNCTION [dbo].[GetNextWorkingDay] (#givenDate DATE)
RETURNS DATE
AS
BEGIN
DECLARE #workingDate DATETIME
IF (DATENAME(dw , #givenDate) = 'Friday')
BEGIN
SET #workingDate = DATEADD(day, 3, #givenDate)
END
ELSE IF (DATENAME(dw , #givenDate) = 'Saturday')
BEGIN
SET #workingDate = DATEADD(day, 2, #givenDate)
END
ELSE
BEGIN
SET #workingDate = DATEADD(day, 1, #givenDate)
END
while ((Select count(*) from tblHolidays where holdate = #workingDate) > 0)
begin
set #workingDate = dateadd(dd,1,#WorkingDate)
end
-- if adding a day makes it a Saturday, add 2 more to get to Monday (and test to make sure the week doesn't start with a holiday)
IF (DATENAME(dw , #workingDate) = 'Saturday')
BEGIN
SET #workingDate = DATEADD(day, 2, #workingDate)
END
while ((Select count(*) from tblHolidays where holdate = #workingDate) > 0)
begin
set #workingDate = dateadd(dd,1,#WorkingDate)
end
RETURN #workingDate
END
... of course you should refactor so the code doesn't repeat, and include a while clause to repeat only as many times as needed to get to a working day, but I have a deadline... that'll be for another less hectic day.
Here's what I did for a one-off application:
WHILE EXISTS (SELECT * FROM Holidays WHERE CONVERT(VARCHAR, HolidayDate,101) = CONVERT(VARCHAR,#DateVariable 101 ) OR DATENAME(WEEKDAY, #DateVariable)='SATURDAY' OR DATENAME(WEEKDAY, #DateVariable)='SUNDAY')
BEGIN
SET #DateVariable = DateAdd(day,1,#DateVariable)
PRINT #DateVariable -- If you want
END
Note that, our Holidays table stores all holidays for the past and future years.
You could do this with a simple case statement.
select case when datepart(dw, getdate()) >= 6 then getdate() + (9 - datepart(dw, getdate())) else getdate() + 1 end
What you need is a calendar table. Getting the next working days is not so simple if you need to account for holidays other than the weekend. The table basically contains just two columns Date and an integer field indicating whether it is a working/non working day- but there can be other columns for - example quarter etc as well.
This table is populated once a year and then maintained as necessary. Getting the result for next working day then becomes as simple as a query like this.
SELECT field1,filed2 from your table T where your date_Field = (SELECT min(date) From calendar table where WorkingDay = 1 and date > GetDate())
/P
How about somethin like this?
select * from table
where (date = dateadd(dd,1,#today) and datepart(weekday,#today) not in (6,0) ) --its not friday or saturday
or (date = dateadd(dd,2,#today) and datepart(weekday,#today) = 0) -- its saturday
or (date = dateadd(dd,3,#today) and datepart(weekday,#today) = 6) --its friday
the date attribute should have the same time as #today or else you have to also use between
It's been basically answered above, but it took me a while to get it and I thought maybe this will help somebody. Is used a simple CASE-WHEN.
This is a check in order to find out which days are weekdays
DATEPART(dw, date) -> MON through FRI = 1,2,3,4,5 SAT = 6, SUN = 7
This is the CASE-WHEN:
CASE
WHEN DATEPART(weekday, date) <= 5 THEN date -- weekdays, no change
WHEN DATEPART(weekday, date) = 6 THEN date + 2 -- SAT + 2 = MON
WHEN DATEPART(weekday, date) = 6 THEN date + 1 -- SUN + 1 = MON
END AS 'WEEKDAY_DATES'
Check, if the statement actually gets you weekdays only:
CASE
WHEN DATEPART(weekday, date) <= 5 THEN DATENAME(weekday, date)
WHEN DATEPART(weekday, date) = 6 THEN DATENAME(weekday, date + 2)
WHEN DATEPART(weekday, date) = 6 THEN DATENAME(weekday, date + 1)
END AS 'WEEKDAY_NAMES'