Comparing Week of Year with "overflow" - sql

So I want to be able to look at a specific week of the year and look at all data in the preceding and following 6 weeks.
WHERE t1.weeknum >= week-6 AND t1.weeknum <=week+6
So if week is 20, I want to return everything between 14 and 26.
The problem is weeks >=47 and <=6. For instance, if week is 4, I want the range to be 50 through 10. Years are a separate dimension and I am including all data regardless of year.
I think this would be similar to a compass heading. Say you are at 350 degrees and turn right 30 degrees. 350+30 = 20 degree bearing.
I'm using SQL Server Express

The Modulus operator (%) seems to be what you want. Since you are using the range 1 to 52 to represent week numbers you need to shift the range to 0 to 51 while calculating:
select WeekNumber,
( WeekNumber - 1 + 52 - 6 ) % 52 + 1 as WeekFrom,
( WeekNumber - 1 + 6 ) % 52 + 1 as WeekTo
from ( values ( 1 ), ( 4 ), ( 6 ), ( 20 ), ( 50 ), ( 52 ) ) as Samples( WeekNumber );
By way of explanation:
Start with WeekNumber.
Subtract 1 to shift it to a zero-based range: 0 to 51.
For "from" values add 52 so that the result is always non-negative. This assumes that the input values are always in the range 1 to 52.
Add or subtract the desired offset (6).
Use modulus to calculate the value with wraparound.
Add 1 to shift the result back to a one-based range: 1 to 52.

Use the StartDate and EndDate in your WHERE clause. Please mark as answer if this is what you need.
declare #StartDayOfYear int
, #EndDayOfYear int
, #WeekNo int
, #YearNo int
, #WeekDate date
, #YearText varchar(4)
, #Start int
, #WeekOfStart date
, #WeekOfEnd date
set #WeekNo = 2
set #YearNo = year(getdate())
set #YearText = #YearNo
set #WeekDate = (select dateadd(day,1 - datepart(dw, #YearText + '-01-01') + (#WeekNo-1) * 7,#YearText + '-01-01'))
set #Start = (select datepart(dayofyear,#WeekDate))
set #StartDayOfYear = (select #Start - 42)
set #EndDayOfYear = (select #Start + 42)
set #WeekOfStart = (select Convert (date,dateadd (day, #StartDayOfYear, #WeekDate)))
set #WeekOfEnd = (select Convert (date,dateadd (day, #EndDayOfYear, #WeekDate)))
select #StartDayOfYear
, #EndDayOfYear
, #WeekDate as StartWeek
, CAST(DATEADD(wk, 0, DATEADD(DAY, 1-DATEPART(WEEKDAY, #WeekOfStart), DATEDIFF(dd, -1, #WeekOfStart))) AS DATE) as StartDate
, CAST(DATEADD(wk, 1, DATEADD(DAY, 0-DATEPART(WEEKDAY, #WeekOfEnd), DATEDIFF(dd, 0, #WeekOfEnd))) AS DATE) as EndDate

Related

How to get date difference in SQL Server and return value

How to get day difference from when the user registered to current date? I have this scenario:
I have some fixed value in master table like [0, 6, 12, 18, 24, 30, 36, 42 .....]
and suppose
day difference is greater or equal than 1 and less than 6 then It should be return 1.
day difference is greater than 6 and less than 12 then it should return 2 and so on.
day difference is greater than 12 and less than 18 then return 3.
day difference is greater than 18 and less than 24 then return 4.
.
.
.
And so on.
I don't want to use case statements because values in master table can not be fix but value pattern will be fix. table value pattern is like that:
common difference between two consecutive values is 6 i.e.
if n=0 then
n+1 = (0 + 6) => 6
Thanks
declare #day int;
declare #regdate datetime = '2019-12-09 19:24:19.623';
declare #currentDate datetime = GETDATE();
SET #day = (SELECT DATEDIFF(day, #regdate, #currentDate) % 6 FROM tblMembers WHERE Id = 1)
PRINT #day
I think that you are looking for integer division, not modulo. This is the default behavior in SQL Server when both arguments are integers, so, since DATEDIFF returns an integer, this should do it:
1 + DATEDIFF(day, #regdate, #currentDate) / 6
Here's approach you can build your solution on:
declare #masterTable table (id int, col int);
insert into #masterTable values
(1,0) ,
(2,6) ,
(3,12),
(4,18),
(5,24),
(6,30),
(7,36),
(8,42),
(9,48);
-- test data
declare #day int;
declare #regdate datetime = '2019-12-09 19:24:19.623';
declare #currentDate datetime = GETDATE();
select #day = datediff(day, #regdate, #currentDate)
;with cte as (
select id,
col lowerBound,
-- here we need to provide some fallback value for last record
coalesce(lead(col) over (order by id), 1000) upperBound
from #masterTable
)
select id from (values (#day)) [day]([cnt])
join cte on [day].[cnt] between cte.lowerBound and cte.upperBound

Count each days of week between two dates without loop

I can do it with loop, but if many day is slow. So I need do without loop.
Here is my code:
DECLARE
#FRDT date = '01-SEP-2019'
,#TODT date = '30-SEP-2019'
,#N int
,#SUN int = 0
,#MON int = 0
,#TUE int = 0
,#WED int = 0
,#THU int = 0
,#FRI int = 0
,#SAT int = 0
WHILE #FRDT <= #TODT
BEGIN
SET #N = DATEPART(WEEKDAY, #FRDT)
IF #N = 1
SET #SUN = #SUN + 1
ELSE IF #N = 2
SET #MON = #MON + 1
ELSE IF #N = 3
SET #TUE = #TUE + 1
ELSE IF #N = 4
SET #WED = #WED + 1
ELSE IF #N = 5
SET #THU = #THU + 1
ELSE IF #N = 6
SET #FRI = #FRI + 1
ELSE IF #N = 7
SET #SAT = #SAT + 1
SET #FRDT = DATEADD(DAY, 1, #FRDT)
END
SELECT 1 AS [NO], 'Sunday' AS [DAYNAME], #SUN AS [NUMBEROFDAY]
UNION SELECT 2, 'Monday', #MON
UNION SELECT 3, 'Tuesday', #TUE
UNION SELECT 4, 'Wednesday', #WED
UNION SELECT 5, 'Thursday', #THU
UNION SELECT 6, 'Friday', #FRI
I want to result like code above, but not use loop for better performance.
The date range is 30 days, dividing by 7 gives quotient 4 and remainder 2.
So every day of the week gets 4 and two days need an additional one. These are the ones corresponding to #start_date and the following day in this case.
SQL to implement this approach is below (demo)
SELECT DATENAME(WEEKDAY,base_date),
quotient + IIF(Nums.N < remainder, 1, 0)
FROM (VALUES
(0),
(1),
(2),
(3),
(4),
(5),
(6)) Nums(N)
CROSS APPLY(SELECT 1 + DATEDIFF(DAY,#start_date,#end_date)) DC(day_count)
CROSS APPLY(SELECT DATEADD(DAY, Nums.N, #start_date), day_count/7, day_count% 7) D(base_date, quotient, remainder)
ORDER BY DATEPART(DW,base_date)
You can do it with using recursive CTE as below-
DECLARE #start_date DATE= '01-SEP-2019', #end_date DATE= '30-SEP-2019';
WITH cte
AS (
SELECT #start_date AS date_
UNION ALL
SELECT CAST(DATEADD(day, 1, date_) AS DATE)
FROM cte
WHERE date_ < #end_date
)
SELECT DATEPART(DW,date_) No,
DATENAME(DW,date_) Day_Name,
COUNT(*) Num_Day
FROM cte
GROUP BY DATEPART(DW,date_),DATENAME(DW,date_)
ORDER BY DATEPART(DW,date_)
OPTION(MAXRECURSION 0);
Output-
No Day_Name Num_Day
1 Sunday 5
2 Monday 5
3 Tuesday 4
4 Wednesday 4
5 Thursday 4
6 Friday 4
7 Saturday 4
For such situation you need to have a Number table or Date Table.
In my example I am using a Number table. You can create number table anyway you want and it will help in many situations.
Create Table tblNumber(Number int primary key)
insert into tblNumber (Number) values(1),(2)...... till thousands or millions
Edit: You could generate the numbers for this number table using:
INSERT INTO tblNumber
SELECT TOP 100000 ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) AS [N]
FROM dbo.syscolumns tb1,dbo.syscolumns tb2
Keep this table permanently as it is useful.
DECLARE #FromDT DATETIME= '2019-09-01';
DECLARE #ToDT DATETIME= '2019-09-30';
SELECT COUNT(*), wkday
FROM
(
SELECT DATEname(weekday, DATEADD(day, number, #FromDT)) wkday
FROM tblNumber
WHERE number BETWEEN DATEPART(day, #FromDT) AND DATEPART(day, #ToDT)
) tbl
GROUP BY wkday;
If you have a Date table then it is more efficient in this situation.

Calculate total working days left of the month

Below is my code to calculate the total amount of working days in the current month.
DECLARE #my int
DECLARE #myDeduct int
DECLARE #day INT
DECLARE #mydate DATETIME
SET #mydate = getDate()
SET #myDeduct = 0
SET DateFirst 1 -- Set it monday=1 (value)
--Saturday and Sunday on the first and last day of a month will Deduct 1
IF (DATEPART(weekday,(DATEADD(dd,-(DAY(#mydate)-1),#mydate))) > 5)
SET #myDeduct = #myDeduct + 1
IF (DATEPART(weekday,(DATEADD(dd,-(DAY(DATEADD(mm,1,#mydate))),
DATEADD(mm,1,#mydate)))) > 5)
SET #myDeduct = #myDeduct + 1
SET #my = day(DATEADD(dd,-(DAY(DATEADD(mm,1,#mydate))),DATEADD(mm,1,#mydate)))
select (((#my/7) * 5 + (#my%7)) - #myDeduct) as Working_Day_per_month
How can I now calculate how many of those working days are left for the current month?
Not worried about public holidays.
try this:
This Query will give you working days in the current month..Assuming non working days are Saturdays and Sundays. If you have a table which contains list of working days/holidays then you can join that table also with the query to get the correct result..
SELECT number+1 as 'day'
FROM master..spt_values
WHERE type='p'
AND number <datepart(dd, DateAdd(day,-1,DateAdd(Month,1,DateAdd(Month,
DateDiff(Month, 0, GETDATE()),0))))
AND datename(WEEKDAY,DateAdd(Month, DateDiff(Month, 0, GetDate()),
number) ) not in ('Saturday','Sunday')
I used Joe G Joseph answer and altered it like this!
Declare #DaysLeft as INT
;With MyCTE AS
(
SELECT number + 1 as 'CurrentDay'
FROM master..spt_values
WHERE type='p'
AND number < datepart(dd, DateAdd(day,-1,DateAdd(Month,1,DateAdd(Month,
DateDiff(Month, 0, GETDATE()),0))))
AND datename(WEEKDAY,DateAdd(Month, DateDiff(Month, 0, GETDATE()),
number) ) not in ('Saturday','Sunday')
)
Select #DaysLeft = (Select COUNT(*) As MyTotal from MyCTE
WHERE CurrentDay >= DATEPART(DD, GETDATE()))
Select #DaysLeft

Possible recursive CTE query using date ranges

Not sure how to even phrase the title on this one!
I have the following data:
IF OBJECT_ID ('tempdb..#data') IS NOT NULL DROP TABLE #data
CREATE TABLE #data
(
id UNIQUEIDENTIFIER
,reference NVARCHAR(30)
,start_date DATETIME
,end_date DATETIME
,lapse_date DATETIME
,value_received DECIMAL(18,3)
)
INSERT INTO #data VALUES ('BE91B9C1-C02F-46F7-9B63-4D0B25D9BA2F','168780','2006-05-01 00:00:00.000',NULL,'2011-09-27 00:00:00.000',537.42)
INSERT INTO #data VALUES ('B538F123-C839-447A-B300-5D16EACF4560','320858','2011-08-08 00:00:00.000',NULL,NULL,0)
INSERT INTO #data VALUES ('1922465D-2A55-434D-BAAA-8E15D681CF12','306597','2011-04-08 00:00:00.000','2011-06-22 13:14:40.083','2011-08-07 00:00:00.000',12)
INSERT INTO #data VALUES ('7DF8FBCC-B490-4892-BDC5-8FD2D73B0323','321461','2011-07-01 00:00:00.000',NULL,'2011-09-25 00:00:00.000',8.44)
INSERT INTO #data VALUES ('1EC2E754-F325-4313-BDFC-9010E255F6FE','74215','2000-10-31 00:00:00.000',NULL,'2011-08-30 00:00:00.000',258)
INSERT INTO #data VALUES ('9E59B09C-0198-48AC-8EEC-A0D76CEA9385','169194','2008-06-25 00:00:00.000',NULL,'2011-09-25 00:00:00.000',1766.4)
INSERT INTO #data VALUES ('97CF6C0F-324A-49A6-B9D8-AC848A1F821A','288039','2010-09-01 00:00:00.000','2011-07-29 00:00:00.000','2011-08-21 00:00:00.000',55)
INSERT INTO #data VALUES ('97CF6C0F-324A-49A6-B9D8-AC848A1F821A','324423','2011-08-01 00:00:00.000',NULL,'2011-09-25 00:00:00.000',5)
INSERT INTO #data VALUES ('D5E5197A-E8E1-468C-9991-C8712224C2BF','323395','2011-08-25 00:00:00.000',NULL,NULL,0)
INSERT INTO #data VALUES ('0EC4976C-16B9-4C99-BD07-D0CBDF014D32','323741','2011-08-25 00:00:00.000',NULL,NULL,0)
And I want to be able to group all references into a category of 'active', 'lapsed' or 'new' based upon the following criteria:
Active has a start date that is less than the last date of the reference month, a lapse date after the last day of the prior month and a value_received > 0;
New has a start date which falls within the reference month;
Lapsed has a lapse date which falls within the reference month.
And to then apply these definitions for each reference for a rolling 13 months (so from Now going back as far as July 2010) so that for each month I can see how many references fall into each group.
I am able to use the following to define this for the current month:
select
id
,reference
,start_date
,end_date
,lapse_date
,value_received
,CASE WHEN start_date < DATEADD(month,DATEPART(Month,GETDATE()) + 1,DATEADD(year,DATEPART(year,GETDATE())-1900,0)) --next month start date
AND lapse_date > DATEADD(ms,-3,DATEADD(mm,DATEDIFF(mm,0,GETDATE())+1,0)) --last day of current month
AND value_received > 0
THEN 'Active'
WHEN lapse_date < DATEADD(month,DATEPART(Month,GETDATE()) + 1,DATEADD(year,DATEPART(year,GETDATE())-1900,0)) --next month start
AND lapse_date > DATEADD(ms,-3,DATEADD(mm,DATEDIFF(mm,0,GETDATE()),0)) --last day of prior month
THEN 'lapse'
WHEN start_date < DATEADD(month,DATEPART(Month,GETDATE()) + 1,DATEADD(year,DATEPART(year,GETDATE())-1900,0)) --next month start date
AND start_date > DATEADD(ms,-3,DATEADD(mm,DATEDIFF(mm,0,GETDATE()),0)) --last day of prior month
THEN 'New'
ELSE 'Not applicable'
END AS [type]
from #data
But I can't see a nice / efficient way of doing this (other than to repeat this query 13 times and union the results, which I know is just awful)
Would this be a case for using the current month as an anchor and using recursion (if so, some pointers would be most appreciated)?
Any help most appreciated as always :)
* Edited to include actual solution *
In case it's of interest to anyone, this is the final query I used:
;WITH Months as
(
SELECT DATEADD(ms,-3,DATEADD(mm,DATEDIFF(mm,0,GETDATE())+1,0)) as month_end
,0 AS level
UNION ALL
SELECT DATEADD(month, -1, month_end)as month_end
,level + 1 FROM Months
WHERE level < 13
)
SELECT
DATENAME(Month,month_end) + ' ' + DATENAME(YEAR,month_end) as date
,SUM(CASE WHEN start_date <= month_end
AND Month(start_date) <> MONTH(Month_end)
AND lapse_date > Month_end
THEN 1 ELSE 0 END) AS Active
,SUM(CASE WHEN start_date <= Month_end
AND DATENAME(MONTH,start_date) + ' ' + DATENAME(YEAR,start_date) =
DATENAME(MONTH,month_end) + ' ' + DATENAME(YEAR,month_end)
THEN 1 ELSE 0 END) AS New
,SUM(CASE WHEN lapse_date <= Month_end
AND Month(lapse_date) = MONTH(Month_end)
THEN 1 ELSE 0 END) AS lapse
FROM #data
CROSS JOIN Months
WHERE id IS NOT NULL
AND start_date IS NOT NULL
GROUP BY DATENAME(Month,month_end) + ' ' + DATENAME(YEAR,month_end)
ORDER by MAX(level) ASC
You don't need a "real" recursive CTE here. You can use one for the month references though:
;WITH Months
as
(
SELECT DATEADD(day, -DATEPART(day, GETDATE())+1, GETDATE()) as 'MonthStart'
UNION ALL
SELECT DATEADD(month, -1, MonthStart) as 'MonthStart'
FROM Months
)
Then you can JOIN to SELECT TOP 13 * FROM Months in your above query.
I'm not going to try to parse all your CASE statements, but essentially you can use a GROUP BY on the date and the MonthStart fields, like:
GROUP BY Datepart(year, monthstart), Datepart(month, monthstart)
and aggregate by month. It will probably be easiest to have all your options (active, lapsed, etc) as columns and calculate each with a SUM(CASE WHEN ... THEN 1 ELSE 0 END) as it will be easier with a GROUP BY.
You can cross join your request with a recursive CTE, this is a good idea.
WITH thirteenMonthBack(myDate, level) as
(
SELECT GETDATE() as myDate, 0 as level
UNION ALL
SELECT DATEADD(month, -1, myDate), level + 1
FROM thirteenMonthBack
WHERE level < 13
)
SELECT xxx
FROM youQuery
CROSS JOIN thirteenMonthBack
DECLARE #date DATE = GETDATE()
;WITH MonthsCTE AS (
SELECT 1 [Month], DATEADD(DAY, -DATEPART(DAY, #date)+1, #date) as 'MonthStart'
UNION ALL
SELECT [Month] + 1, DATEADD(MONTH, 1, MonthStart)
FROM MonthsCTE
WHERE [Month] < 12 )
SELECT * FROM MonthsCTE
/*
| The below SELECT statements show TWO examples of how this can be useful.
| Example 1 SELECT: Simple example of showing how to generate 12 days ahead based on date entered
| Example 2 SELECT: This example shows how to generate 12 months ahead based on date entered
| This example tries to mimic as best it can Oracles use of LEVEL and CONNECT BY LEVEL
*/
WITH dynamicRecords(myDate, level) AS
(
SELECT GETDATE() AS myDate, 1 AS level
UNION ALL
SELECT myDate + 1, level + 1 /* 12 Days - WHERE level < 12 */
--SELECT DATEADD(month, 1, myDate), level + 1 /* 12 Months - WHERE level < 12 */
FROM dynamicRecords
WHERE level < 12
)
SELECT *
FROM dynamicRecords
Option (MaxRecursion 0) /* The default MaxRecursion setting is 100. Generating more than 100 dates using this method will require the Option (MaxRecursion N) segment of the query, where N is the desired MaxRecursion setting. Setting this to 0 will remove the MaxRecursion limitation altogether */
Screenshots:
/* Original T-SQL Solution I found here: https://riptutorial.com/sql-server/example/11098/generating-date-range-with-recursive-cte
| The below provides an example of how to generate the days within a date range of the dates entered.
| The below SELECT statements show TWO examples of how this can be useful.
| Example 1 SELECT: Uses static dates to display ALL of the dates within the range for the dates entered
| Example 2 SELECT: This example uses GETDATE() and then obtains the FOM day and the EOM day of the dates
| beging entered to then show all days in the month of the dates entered.
*/
With DateCte AS
(
SELECT CAST('2021-04-21' AS DATE) AS BeginDate, CAST('2022-05-02' AS DATE) AS EndDate
--SELECT CAST( GETDATE() - Day(GETDATE()) + 1 AS DATE ) AS BeginDate, CAST(EOMONTH(GETDATE()) AS DATE) AS EndDate
UNION ALL
SELECT DateAdd(Day, 1, BeginDate), EndDate
FROM DateCte
WHERE BeginDate < EndDate
)
Select BeginDate AS Dates
From DateCte
Option (MaxRecursion 0) /* The default MaxRecursion setting is 100. Generating more than 100 dates using this method will require the Option (MaxRecursion N) segment of the query, where N is the desired MaxRecursion setting. Setting this to 0 will remove the MaxRecursion limitation altogether */
;
Screenshot:

simulating Excel networkdays in sql

I am sure somebody has worked out to simulate Excel networkdays function in other programming language.
I'd appreciate if you could share.
Thanks
The idea is to calculate the weekdays between the start of each date's week then applying various offsets.
Find the number of days between the Saturday before each date
Divide by 7 and multiply by 5 to get the number of weekdays
Offset the total for whether the start date is after the end date
Offset again for whether the start is after the end and the start is a Saturday
Again for whether the start is after and the end is Sunday
Again for whether the start is not after and the start is a Sunday
Again for whether the start is not after and the end is a Saturday
Add some random dates into a table.
declare #t table ([start] datetime, [end] datetime)
insert into #t values ('2088-01-14 11:56:23','2011-11-10 03:34:09')
insert into #t values ('2024-09-24 10:14:29','2087-09-16 15:52:06')
Then calcuate NETWORKDAYS for those dates.
select [start],[end]
,((datediff(day,0,[end])-datepart(dw,[end]))-(datediff(day,0,[start])-datepart(dw,[start])))/7*5 --[weekdays]
+ datepart(dw,[end]) - datepart(dw,[start]) --[weekday diff]
+ case when datediff(day,0,[start]) > datediff(day,0,[end]) then -1 else 1 end --[start after]
+ case when datediff(day,0,[start]) > datediff(day,0,[end]) and datepart(dw,[start]) = 7 then 1 else 0 end --[start after and start saturday]
+ case when datediff(day,0,[start]) > datediff(day,0,[end]) and datepart(dw,[end]) = 1 then 1 else 0 end --[start after and end sunday]
+ case when datediff(day,0,[start]) <= datediff(day,0,[end]) and datepart(dw,[start]) = 1 then -1 else 0 end --[start not after and start sunday]
+ case when datediff(day,0,[start]) <= datediff(day,0,[end]) and datepart(dw,[end]) = 7 then -1 else 0 end --[start not after and end saturday]
as [networkdays]
from #t
A SQL solution as per SO question "Equivalent of Excel’s NETWORKDAYS function with Jet ADO"
Hope this helps someone out there but this works for me. Requires you to create a dbo.FederalHolidays table (which can be easily populated with online date sources).
Cleanest way I could come up with that is easily scalable.
--Simulate Excel Formula =NETWORKDAYS() excludes weekend days and federal holidays. Requires a Federal Holiday Table, easy to create.
DECLARE #d1 date, #d2 date
SET #d1 = '4/12/2019'
SET #d2 = '4/23/2019'
SELECT
DATEDIFF(dd,#d1,#d2) +1 --First calculate regular date difference +1 to count the start date; does not exclude weekends/holidays.
- (SELECT COUNT(*) --Calculate number of holidays between the date range and subtract it from DATEDIFF().
FROM [dbo].[FederalHolidays]
WHERE DATE BETWEEN #d1 AND #d2)
- (SELECT (DATEDIFF(wk, #d1, #d2) * 2) --Calculate number of weekend days between the date range and subtract it from DATEDIFF().
+(CASE WHEN DATENAME(dw, #d1) = 'Sunday' THEN 1 ELSE 0 END)
+(CASE WHEN DATENAME(dw, #d2) = 'Saturday' THEN 1 ELSE 0 END)) as NetWorkDays
For what it's worth, I created the following function for mysql that assumes Mon-Fri are business days:
DROP FUNCTION IF EXISTS BusinessDays;
DELIMITER //
CREATE FUNCTION BusinessDays (startDate DATE, endDate DATE)
RETURNS INT
DETERMINISTIC
BEGIN
DECLARE startWeekDay INT;
DECLARE allDays INT;
DECLARE fullWeekCount INT;
DECLARE remainderDays INT;
DECLARE maxPossibleRemainderWeekendDays INT;
DECLARE soloSundays INT;
DECLARE totalBusinessDays INT;
SET startWeekDay = WEEKDAY(startDate);
SET allDays = ABS(DATEDIFF(endDate, startDate)) + 1;
SET fullWeekCount = FLOOR(allDays/7);
SET remainderDays = allDays - (fullWeekCount * 7);
SET maxPossibleRemainderWeekendDays = ROUND(2*(startWeekDay+remainderDays-6)/(ABS(2*(startWeekDay+remainderDays-6))+1))+1;
SET soloSundays = ROUND(2*(startWeekDay-6)/(ABS(2*(startWeekDay-6))+1))+1;
SET totalBusinessDays = allDays - (fullWeekCount * 2) - maxPossibleRemainderWeekendDays + soloSundays;
RETURN totalBusinessDays;
END //
DELIMITER ;
Perhaps this will also help, I created a formula (in Excel) that will simulate the NETWORKDAYS function:
= 1 + ( ( B1 - A1) * 5 - ( WEEKDAY(A1) - WEEKDAY(B1) ) * 2 ) / 7 + IF(A1<=B1,IF(WEEKDAY(B1)=7,-1,0) + IF(WEEKDAY(A1)=1,-1,0), IF(WEEKDAY(B1)<>1,-1,0) + IF(WEEKDAY(A1)<>7,-1,0) )
NOTE: WEEKDAY() has Sunday as 0 to Saturday as 6
I have been looking for this capability for quite some time, so went ahead and just created it on my own.
Usage: This is a function you can create in SQL. You can easily modify this to work outside a function if needed, but I prefer functions to take care of these types of calculations.
I added quite a few comments in case anyone was wondering how it worked. What this does is take two inputs:
#startDate - the beginning date you want to add workday to.
#addDays - the whole number of days you want to add to the #startDate.
Calculation process:
It loops repeatedly until the # of working days has been reached. For example, if you enter 365 days, it will cycle around 500 times before it's able to predict the # of working days to add. I've added #weekendCount in case anyone needs to know the # of weekend days excluded before reaching the final end date.
Once completed, the integer #recCounter essentially is the # of days that must be added to the #startDate before the number of working days is reached. I am sure someone can write this better than I, or perhaps someone can make use of this. Hope this helps! :)
CREATE FUNCTION [dbo].[addNetworkDays](#startDate AS DATETIME, #addDays AS Int)
RETURNS DATETIME
AS
BEGIN
DECLARE #recCounter Int
SET #recCounter = 0
DECLARE #weekendCount Int
SET #weekendCount = 0
DECLARE #workdayCount Int
SET #workdayCount = 0
DECLARE #newDate DateTime
WHILE #addDays > #workdayCount
BEGIN
-- Add another day to the start date
SET #recCounter = #recCounter + 1
-- Cumuluate the weekend vs. workday counter, based on the day of the week. This loop will repeat until #workdayCount has reached the #addDays.
-- Note that #weekendCount is not used in any capacity, can be used if you need to know how many weekend days there are.
IF DATEPART(dw, DATEADD(d, #recCounter, #startDate)) IN (1, 7) SET #weekendCount = #weekendCount + 1
IF DATEPART(dw, DATEADD(d, #recCounter, #startDate)) IN (2, 3, 4, 5, 6) SET #workdayCount = #workdayCount + 1
END
-- At this point, the script has completed the cycle, stopping when the detected # of workdays has reached the count of days the user specified.
-- Calculate the new date, based on the # of cycles *days* detected above.
SET #newDate = DATEADD(d, #recCounter, #startDate)
-- If the new/adjusted date falls on a Saturday or Sunday, add additional days to compensate.
IF DATEPART(dw, #newDate) = 1 SET #newDate = DATEADD(d, 1, #newDate)
IF DATEPART(dw, #newDate) = 7 SET #newDate = DATEADD(d, 1, #newDate)
RETURN CAST(#newDate AS DATETIME)
NETWORKDAYS for Australia
WITH PH_NET_WORKDAY AS
(
----BUILD UNDERLYING WORKDAY DATA TABLE
SELECT
DT,
STATE,
CASE
WHEN WORKDAY-PUBLIC_HOLIDAY <0
THEN 0
ELSE WORKDAY-PUBLIC_HOLIDAY
END AS WORKDAY
FROM
(
SELECT
DT,
STATE,
WORKDAY,
----PUBLIC HOLIDAY INFORMATION HERE
CASE
WHEN STATE = 'NSW'
THEN
CASE
WHEN DT IN (
'01-Oct-2018',
'25-Dec-2018',
'26-Dec-2018',
'01-Jan-2019',
'28-Jan-2019',
'19-Apr-2019',
'20-Apr-2019',
'21-Apr-2019',
'22-Apr-2019',
'25-Apr-2019',
'10-Jun-2019',
'07-Oct-2019',
'25-Dec-2019',
'26-Dec-2019'
)
THEN 1
ELSE 0
END
WHEN STATE = 'SA'
THEN
CASE
WHEN DT IN (
'01-Oct-2018',
'24-Dec-2018',
'25-Dec-2018',
'26-Dec-2018',
'31-Dec-2018',
'01-Jan-2019',
'28-Jan-2019',
'11-Mar-2019',
'19-Apr-2019',
'20-Apr-2019',
'22-Apr-2019',
'25-Apr-2019',
'10-Jun-2019',
'07-Oct-2019',
'24-Dec-2019',
'25-Dec-2019',
'26-Dec-2019'
)
THEN 1
ELSE 0
END
WHEN STATE = 'QLD'
THEN
CASE
WHEN DT IN (
'01-Oct-2018',
'25-Dec-2018',
'26-Dec-2018',
'01-Jan-2019',
'28-Jan-2019',
'19-Apr-2019',
'20-Apr-2019',
'21-Apr-2019',
'22-Apr-2019',
'25-Apr-2019',
'06-May-2019',
'07-Oct-2019',
'25-Dec-2019',
'26-Dec-2019'
)
THEN 1
ELSE 0
END
WHEN STATE = 'VIC'
THEN
CASE
WHEN DT IN (
'28-Sep-2018',
'06-Nov-2018',
'25-Dec-2018',
'26-Dec-2018',
'01-Jan-2019',
'28-Jan-2019',
'11-Mar-2019',
'19-Apr-2019',
'20-Apr-2019',
'21-Apr-2019',
'22-Apr-2019',
'25-Apr-2019',
'10-Jun-2019',
'05-Nov-2019',
'25-Dec-2019',
'26-Dec-2019'
)
THEN 1
ELSE 0
END
WHEN STATE = 'TAS'
THEN
CASE
WHEN DT IN (
'25-Dec-2018',
'26-Dec-2018',
'01-Jan-2019',
'28-Jan-2019',
'11-Mar-2019',
'19-Apr-2019',
'22-Apr-2019',
'23-Apr-2019',
'25-Apr-2019',
'10-Jun-2019',
'25-Dec-2019',
'26-Dec-2019'
)
THEN 1
ELSE 0
END
ELSE 0
END AS PUBLIC_HOLIDAY
FROM
(
SELECT
DT.*,
ST.*
FROM
(
SELECT
TRUNC (TO_DATE('01-JAN-2021','DD-MON-YYYY') - ROWNUM) AS DT,
TRIM(TO_CHAR( TRUNC (TO_DATE('01-JAN-2021','DD-MON-YYYY') - ROWNUM) , 'DAY')) AS WEEK_DAY,
CASE
WHEN TRIM(TO_CHAR( TRUNC (TO_DATE('01-JAN-2021','DD-MON-YYYY') - ROWNUM) , 'DAY')) = 'SATURDAY'
THEN 0
WHEN TRIM(TO_CHAR( TRUNC (TO_DATE('01-JAN-2021','DD-MON-YYYY') - ROWNUM) , 'DAY')) = 'SUNDAY'
THEN 0
----BUILD STATE PUBLIC HOLIDAY DATE SET HERE, WE CAN EXCLUDE PUBLIC HOLIDAYS
ELSE 1
END AS WORKDAY
FROM DUAL CONNECT BY ROWNUM < (365.25*8+1)
) DT
,
(
SELECT 'NSW' AS STATE FROM DUAL
UNION
SELECT 'QLD' AS STATE FROM DUAL
UNION
SELECT 'SA' AS STATE FROM DUAL
UNION
SELECT 'ACT' AS STATE FROM DUAL
UNION
SELECT 'VIC' AS STATE FROM DUAL
UNION
SELECT 'ACT' AS STATE FROM DUAL
) ST
)
)
),
----A SAMPLE DATA SET FOR SAME DATES BETWEEN DIFFERENT STATE TO TEST PUBLIC HOLIDAY DIFFERENCES
SAMPLE_DATA AS
(
SELECT
'01-FEB-2019' AS D1,
'11-FEB-2019' AS D2,
'NSW' AS STATE
FROM DUAL
UNION
SELECT
'01-FEB-2019' AS D1,
'11-FEB-2019' AS D2,
'SA' AS STATE
FROM DUAL
UNION
SELECT
'19-APR-2019' AS D1,
'26-APR-2019' AS D2,
'NSW' AS STATE
FROM DUAL
)
----SELECT WORKDAYS FROM PH TABLE AND INSERT INTO SAPLE TABLE
SELECT
SAMPLE_DATA.*,
(
SELECT SUM(WORKDAY)
FROM PH_NET_WORKDAY
WHERE
STATE = SAMPLE_DATA.STATE
AND DT>=SAMPLE_DATA.D1
AND DT<=SAMPLE_DATA.D2
)
AS WORKDAYS
FROM SAMPLE_DATA