Reputation: 294
I've created a "holiday" table that has 3 columns [Date]
in date format, [BusinessDay]
that's a Y/N and [NameofDay] varchar(50)
for the Holiday or Weekend. Saturday, Sunday, Christmas are all marked for the next decade.
What I now need to do is figure out how to determine the date a request needs to be completed by not counting the business days. I've read and read and read but don't see anything that has been useful. It's always simple in my head.
What I'm trying to solve: what will the Due Date [DueDate]
be 10 business Days from the date of the request [TransactionDate]
if the request is "priority", or 5 Business Days if it's "Critical".
Upvotes: 1
Views: 1755
Reputation: 35531
Due to the recursive nature of the problem that @Andomar has brought up, I am suggesting an alternative answer (which happens to be much simpler as well, but requires windowing functions to be available). What this does is join valid business days from the calendar that are later than TransactionDate
, and then for each request id
find the 5th or 10th row as required:
;WITH cte AS
(
SELECT *,
ROW_NUMBER() OVER (PARTITION BY id ORDER BY validDeliveryDate ASC) AS rn
FROM (
SELECT requests.*, holiday.Date as validDeliveryDate
FROM requests
JOIN holiday
ON requests.TransactionDate < holiday.Date
AND DATEADD(day, 25, requests.TransactionDate) >= holiday.Date
AND holiday.BusinessDay = 'Y' ) v
)
SELECT *
FROM cte
WHERE rn = CASE WHEN critical = 1 THEN 5 ELSE 10 END
No iteration required - working sqlfiddle here
Upvotes: 4
Reputation: 1270443
I think the following works:
select t.*
from (select t.*,
row_number() over (partition by r.requestid, IsWorkDay order by seqdate) as WorkDayNum
from (select r.requestid, r.TransactionDate,
(case when critical = 1 then 5 else 10 end) as DaysToRespond,
dateadd(day, days.seqnum - 1, r.TransactionDate) as seqdate,
(case when h.date is null then 1 else 0 end) as IsWorkDay
from requests r cross join
(select top 20 ROW_NUMBER() over (order by (select NULL)) as seqnum
from information_schema.columns
) days left outer join
holidays h
on dateadd(day, days.seqnum - 1, r.TransactionDate) = h.date
) t
) t
where WorkDayNum = DaysToRespond and IsWorkDay = 1
This is untested, but here is the idea.
Well, I have tested this, and it appears to return the right result in this case:
with holidays as (
select CAST('2012-01-01' as date) as date union all
select CAST('2012-01-05' as date) as date union all
select CAST('2012-01-06' as date) as date union all
select CAST('2012-01-12' as date) as date union all
select CAST('2012-01-13' as date) as date union all
select CAST('2012-01-19' as date) as date union all
select CAST('2012-01-20' as date) as date
),
requests as (
select 1 as requestId, CAST('2012-01-02' as DATE) as TransactionDate, 1 as Critical
)
select t.*
from (select t.*,
row_number() over (partition by t.requestid, IsWorkDay order by seqdate) as WorkDayNum
from (select r.requestid, r.TransactionDate,
(case when critical = 1 then 5 else 10 end) as DaysToRespond,
dateadd(day, days.seqnum - 1, r.TransactionDate) as seqdate,
(case when h.date is null then 1 else 0 end) as IsWorkDay
from requests r cross join
(select top 20 ROW_NUMBER() over (order by (select NULL)) as seqnum
from INFORMATION_SCHEMA.columns
) days left outer join
holidays h
on dateadd(day, days.seqnum - 1, r.TransactionDate) = h.date
) t
) t
where WorkDayNum = DaysToRespond+1 and IsWorkDay = 1
This query creates a sequence of 20 days following the transaction date (Is 20 enough?). It then calculates the date for these days and compares the date to the holiday table.
To count the number of days is uses row_number()
partitioning by the request and workdays versus nonworkdays. The row to choose is the one that is a workday and the number of days after the transaction date.
Upvotes: 0
Reputation: 238186
Calculating business days requires iteration. You add a number of days, then subtract the non-business days, then add again.
One way to do that is a user-defined function:
if exists (select * from sys.objects where name ='WorkingDaysFrom' and type = 'FN')
drop function dbo.WorkingDaysFrom
go
create function dbo.WorkingDaysFrom(
@date date
, @days int)
returns date
as
begin
declare @result date = @date
declare @remaining int = @days
while @remaining > 0
begin
set @result = dateadd(day, @remaining, @result)
select @remaining = count(*)
from dbo.Holiday
where [Date] between dateadd(day, 1-@remaining, @result) and @result
and BusinessDay = 'N'
end
return @result
end
go
Live example at SQL Fiddle. This prints:
TransactionDate Priority DueDate
2013-01-01 Priority 2013-01-16
2013-01-01 Critical 2013-01-09
2013-01-03 Priority 2013-01-17
2013-01-03 Critical 2013-01-10
2013-01-06 Priority 2013-01-18
2013-01-06 Critical 2013-01-11
Upvotes: 1