January 29, 2007 at 12:42 pm
Hi All,
Firstly sorry if i have posted in the wrong forum here, i'm new to this place so go easy!
I have a problem whereby i need to work out a date given the starting date and the number of workdays (5 day working week - monday to friday).
Searching around i have found out how to work out the number of workdays between two dates, but am struggling to find any help on a solution to my problem. If anyone could point me to an article they know of i would be extremely grateful!
John
January 29, 2007 at 3:13 pm
I don't understand what the problem you are trying to solve is.
Please explain and give examples.
January 29, 2007 at 3:22 pm
I am working on a critical path project where i know the date i should be starting a step. Knowing that this step will take 10 working days i want to know the finish date of the step.
So if the first step starts on the 1st Jan 2007 and takes 10 working days, using dateadd (-1) would give me 10th Jan 2007, although the date i am looking to calculate would give me 12th Jan 2007 because of the two weekend days on the 6th and 7th.
Does this make any more sense?
January 29, 2007 at 11:03 pm
Try this function:
drop function fn_Last_Workday go create function fn_Last_Workday (@start_day datetime, @work_days int) returns datetime as begin
declare @end_day datetime declare @prior_workday datetime declare @weeks int declare @remaining_days int
-- Return null if input parameters are invalid if @start_day is null or @work_days is null or @work_days < 1 begin return null end
-- Find first workday on or before the start date set @prior_workday = case datediff(dd,'17530101',@start_day)%7 when 6 then dateadd(dd,-2,@start_day) when 0 then dateadd(dd,-3,@start_day) else dateadd(dd,-1,@start_day) end
-- Find number of whole weeks select @weeks = @work_days/5 -- Find the remaining days after subtracting whole weeks set @remaining_days = @work_days-(@weeks*5) -- Find the last workday of whole weeks set @end_day = dateadd(dd,(@weeks*7)+@remaining_days,@prior_workday)
set @end_day = -- Add 2 days if adding remaining days includes a weekend case when datediff(dd,'17530101',@end_day)%7 in (5,6) then dateadd(dd,2,@end_day) when datediff(dd,'17530101',@end_day)%7 = 0 and @remaining_days > 0 then dateadd(dd,2,@end_day) when datediff(dd,'17530101',@end_day)%7 = 1 and @remaining_days > 1 then dateadd(dd,2,@end_day) when datediff(dd,'17530101',@end_day)%7 = 2 and @remaining_days > 2 then dateadd(dd,2,@end_day) when datediff(dd,'17530101',@end_day)%7 = 3 and @remaining_days > 3 then dateadd(dd,2,@end_day) else @end_day end
return @end_day
end go
select Last_Date=dbo.fn_Last_Workday('20010101',10)
January 30, 2007 at 12:30 am
What about holidays?
Take a look at this article
http://www.sqlservercentral.com/columnists/plarsson/howmanymoremondaysuntiliretire.asp
declare @startdate datetime,
@enddate datetime,
@workdays smallint
select @startdate = '20070101',
@workdays = 10
select @enddate = dateadd(day, 3 + 1.4 * @workdays, @startdate)
select seqdate
from dbo.fnSeqDates(@startdate, @enddate)
where datepart(dw, seqdate) in (2, 3, 4, 5, 6)
and row_number() over (order by seqdate) = @workdays
N 56°04'39.16"
E 12°55'05.25"
January 30, 2007 at 3:17 am
Thanks very much dclark! works a treat.
Peter - Many thanks for your reply as well, although at the moment we are not bothered about holidays as the company it is for generally work bank hols.
January 30, 2007 at 9:57 pm
If you have a number of these kinds of calculations to make, you may want to make a calendar table. Many different examples on SQLServerCentral... do a search. Well worth the effort.
--Jeff Moden
Change is inevitable... Change for the better is not.
January 30, 2007 at 10:45 pm
If you want a date table, you can use my own personal favorite.
Date Table Function F_TABLE_DATE:
February 25, 2007 at 1:02 am
I am trying to accomplish something similar to the original post however I would like to use a business day calendar. I need to take a date like 2/21/2007 and add it against number -5 to return date 2/14/2007, or add against 4 to return day 2/27/2007. I have succesfully created the calendar table and the structure of it is below. I am able to calculate the number of business days between two days using the calendar but I am stuck with the above task. Any help would be greatly appreciated.
Date isWeekday isWorkday
2007-02-21 00:00:00 1 1
2007-02-22 00:00:00 1 1
2007-02-23 00:00:00 1 1
2007-02-24 00:00:00 0 0
2007-02-25 00:00:00 0 0
2007-02-26 00:00:00 1 1
2007-02-27 00:00:00 1 1
2007-02-28 00:00:00 1 1
February 25, 2007 at 10:49 am
I see you're online right now, Jeremy... gimme a couple minutes more and I'll show you one way to do this...
--Jeff Moden
Change is inevitable... Change for the better is not.
February 25, 2007 at 11:19 am
Lot's of folks will do a calculation like this...
--===== A way without the WorkDayCountColumn
SELECT c.Date
FROM #Calendar c
WHERE
c.IsWorkDay = 1
AND 4+1 = (
SELECT COUNT(*)
FROM #Calendar c2
WHERE c2.Date >= '20070221'
AND c2.Date <= c.Date
AND c2.IsWorkDay=1
)
... where the "4" is the number of business days you want to look ahead. If you want to look back, you're out of luck unless you write another chunk of logic. And, although very small, the method above uses a triangular self join in a correlated sub-query... my experience has been that they don't perform as well as direct set-based logic because of the number of internal rows they generate to solve the problem. The formula for the number of rows touched by a triangular join is (N2+N)/2... when looking for 4 (really, 5 in this case, 4+1), then the triangular join has to internally generate (52+5)/2 or 15 rows... why the hell would I want to burden the CPU with looking at 15 rows when I really need to look at just 2? Think of the performance gain if you could look at only two rows instead of 15 a couple of thousand times... and that's just for 5 days!
As you can tell, I'm pretty sour on triangular joins and correlated sub-queries... there are a few places (like, finding ranges of missing IDs) where they can be VERY fast, but in most cases, their performance is worse than a cursor
Soooooooooo.... with all that in mind, when I create a calendar table for a client, I usually add a column called "WorkDayCount" which is nothing more than a running count of workdays starting at the earliest date in the calendar table... here's a very small hard-coded example...
--===== Create a small example Calendar table
-- Do notice the primary key constraint
CREATE TABLE #Calendar
(
DATE DATETIME,
IsWeekday INT,
IsWorkday INT,
WorkDayCount INT,
CONSTRAINT PK_Calendar_Date
PRIMARY KEY CLUSTERED (Date)
WITH FILLFACTOR = 100
)
--===== Populate it with a small sample of data (all of Feb 2007)
INSERT INTO #Calendar (DATE,IsWeekday,IsWorkday)
SELECT '2007-02-01 00:00:00',1,1 UNION ALL
SELECT '2007-02-02 00:00:00',1,1 UNION ALL
SELECT '2007-02-03 00:00:00',0,0 UNION ALL
SELECT '2007-02-04 00:00:00',0,0 UNION ALL
SELECT '2007-02-05 00:00:00',1,1 UNION ALL
SELECT '2007-02-06 00:00:00',1,1 UNION ALL
SELECT '2007-02-07 00:00:00',1,1 UNION ALL
SELECT '2007-02-08 00:00:00',1,1 UNION ALL
SELECT '2007-02-09 00:00:00',1,1 UNION ALL
SELECT '2007-02-10 00:00:00',0,0 UNION ALL
SELECT '2007-02-11 00:00:00',0,0 UNION ALL
SELECT '2007-02-12 00:00:00',1,1 UNION ALL
SELECT '2007-02-13 00:00:00',1,1 UNION ALL
SELECT '2007-02-14 00:00:00',1,1 UNION ALL
SELECT '2007-02-15 00:00:00',1,1 UNION ALL
SELECT '2007-02-16 00:00:00',1,1 UNION ALL
SELECT '2007-02-17 00:00:00',0,0 UNION ALL
SELECT '2007-02-18 00:00:00',0,0 UNION ALL
SELECT '2007-02-19 00:00:00',1,1 UNION ALL
SELECT '2007-02-20 00:00:00',1,1 UNION ALL
SELECT '2007-02-21 00:00:00',1,1 UNION ALL
SELECT '2007-02-22 00:00:00',1,1 UNION ALL
SELECT '2007-02-23 00:00:00',1,1 UNION ALL
SELECT '2007-02-24 00:00:00',0,0 UNION ALL
SELECT '2007-02-25 00:00:00',0,0 UNION ALL
SELECT '2007-02-26 00:00:00',1,1 UNION ALL
SELECT '2007-02-27 00:00:00',1,1 UNION ALL
SELECT '2007-02-28 00:00:00',1,1
Notice that the WorkDayCount column hasn't been populated, yet... and most folks would do some form of triangular join or a cursor or a while loop to populate that column. Me? I use a proprietary form of the UPDATE statement to do it and it's lightning quick...
--===== Now, populate the WorkDayCount column with a running count
-- using a little SQL Server prestidigitation 😉
-- Do notice the WITH (INDEX()) notation... it's important
DECLARE @CurrentCount INT
SET @CurrentCount = 0
UPDATE #Calendar
SET @CurrentCount = WorkDayCount = CASE
WHEN IsWorkDay = 1
THEN @CurrentCount + 1
ELSE @CurrentCount
END
FROM #Calendar WITH (INDEX(PK_Calendar_Date))
--===== Just verify what we have in the calendar table
SELECT * FROM #Calendar
From then on, the kinds of calculations you're asking for become very simple and very fast... recommend you create a function for it but here's an almost hard-coded example to solve the two problems you posted... and more...
--===== Setup a couple of variables to simulate what could be done in a function...
DECLARE @GivenDate DATETIME
DECLARE @NumberOfDays INT
SET @GivenDate = '20070221'
--===== Solve the first problem: Find business day 5 days BEFORE given date
SET @NumberOfDays = -5
SELECT DATE
FROM #Calendar
WHERE WorkDayCount = (SELECT WorkdayCount FROM #Calendar WHERE <A href="mailtoate=@GivenDate)+@NumberOfDays">Date=@GivenDate)+@NumberOfDays
AND IsWorkDay = 1
--===== Solve the second problem: Find business day 4 days AFTER given date
-- (Notice that the SELECT is exactly the same so we could turn this into a function)
SET @NumberOfDays = 4
SELECT DATE
FROM #Calendar
WHERE WorkDayCount = (SELECT WorkdayCount FROM #Calendar WHERE <A href="mailtoate=@GivenDate)+@NumberOfDays">Date=@GivenDate)+@NumberOfDays
AND IsWorkDay = 1
--===== Solve an additional problem: Find the next business day after given date
-- (Notice that the SELECT is exactly the same so we could turn this into a function)
SET @GivenDate = '20070217' --A SATURDAY
SET @NumberOfDays = 1
SELECT DATE
FROM #Calendar
WHERE WorkDayCount = (SELECT WorkdayCount FROM #Calendar WHERE <A href="mailtoate=@GivenDate)+@NumberOfDays">Date=@GivenDate)+@NumberOfDays
AND IsWorkDay = 1
--===== Solve an additional problem: Find the next business day after given date
-- (Notice that the SELECT is exactly the same so we could turn this into a function)
SET @GivenDate = '20070216' --A FRIDAY
SET @NumberOfDays = 1
SELECT DATE
FROM #Calendar
WHERE WorkDayCount = (SELECT WorkdayCount FROM #Calendar WHERE <A href="mailtoate=@GivenDate)+@NumberOfDays">Date=@GivenDate)+@NumberOfDays
AND IsWorkDay = 1
--Jeff Moden
Change is inevitable... Change for the better is not.
February 26, 2007 at 5:32 pm
Hello Jeff,
Thank you for this, it works great.
I was hoping you could point me in the right direction to using this in my query. I have a Orders table that contains the date factor and is joined to transit time table that contains the number factor(-1,-5,4,3 etc). How can I pass this against the query the uses the calendar table to calculate the dates?
Jeremy
February 26, 2007 at 7:08 pm
Like I said... make a function using the SELECT examples I gave... then just do a joined select and use the function passing the date from one table and the transit time from the other...
Since you haven't given me any info about how your two tables relate, I can be much more of a help than that.
--Jeff Moden
Change is inevitable... Change for the better is not.
February 26, 2007 at 7:33 pm
Jeff,
I have the fuction portion written:
create Function dbo.CalcDate
(
@GivenDate DATETIME,
@NumberOfDays INT
)
Returns table
as
Return (SELECT dt
FROM dbo.workcalendar
WHERE WorkDayCount = (SELECT WorkdayCount FROM dbo.workcalendar WHERE dt=@GivenDate)+@NumberOfDays
AND IsWorkDay = 1)
My tables are Order, TransitTime, and WorkCalendar
Below is the query I am using:
SELECT order.Order_Num, order.CustomerNumber, order.ItemNum, order.RequireDate, Order.State, TransitTime.PrepTime, WorkCalendar.date
FROM Order LEFT OUTER JOIN
WorkCalendar ON order.RequireDate = WorkCalendar.date LEFT OUTER JOIN
TransitTime ON Order.State = TransitTime.State AND order.CustomerNumber = TransitTime.CustomerNumber
February 26, 2007 at 7:59 pm
Don't need the WorkCalendar join anymore... you have the function...
SELECT o.Order_Num,
o.CustomerNumber,
o.ItemNum,
o.RequireDate,
o.State,
tt.PrepTime,
dbo.CalcDate(o.RequireDate,-tt.PrepTime) AS RequiredShipDate
FROM dbo.Order o
LEFT OUTER JOIN dbo.TransitTime tt
ON o.State = tt.State
AND o.CustomerNumber = tt.CustomerNumber
--Jeff Moden
Change is inevitable... Change for the better is not.
Viewing 15 posts - 1 through 15 (of 17 total)
You must be logged in to reply to this topic. Login to reply