How to turn this script into a UDF in BigQuery - google-bigquery

This should be easy to do but I'm struggling.
I have the below script that calculates a number of business dates before or after a date. I need to change to UDF as I'll be using it in multiple views. It is for Bigquery:
DECLARE Date DATE;
DECLARE DAYS_EXTEND INT64;
DECLARE COUNTER INT64;
SET Date = '2021-12-23';
SET DAYS_EXTEND = 3;
SET COUNTER = DAYS_EXTEND;
BEGIN
WHILE COUNTER > 0 Do
SET COUNTER = COUNTER -1;
SET Date = Date +1;
IF Extract( DAYOFWEEK from Date) in (1,7) or Date in ('2021-01-01','2021-04-02','2021-04-05','2021-05-03','2021-05-31','2021-08-30','2021-12-27','2021-12-28','2022-01-03','2022-04-15','2022-04-18','2022-05-02','2022-06-02','2022-06-03','2022-08-29','2022-12-26','2022-12-27')
Then
BEGIN
SET DAYS_EXTEND = DAYS_EXTEND +1;
SET COUNTER = COUNTER +1;
END;
END IF;
END WHILE;
WHILE COUNTER <0 DO
SET COUNTER = COUNTER +1;
SET Date = Date -1;
IF Extract( DAYOFWEEK from Date) in (1,7) or Date in ('2021-01-01','2021-04-02','2021-04-05','2021-05-03','2021-05-31','2021-08-30','2021-12-27','2021-12-28','2022-01-03','2022-04-15','2022-04-18','2022-05-02','2022-06-02','2022-06-03','2022-08-29','2022-12-26','2022-12-27')
Then
BEGIN
SET DAYS_EXTEND = DAYS_EXTEND - 1;
SET COUNTER = COUNTER - 1;
END;
END IF;
END WHILE;
END;
I'm just not sure how to turn it into a Select statement or if it is possible to do UDF without a Select statement with the loops remaining.
I'd appreciate any help.

For what I understand from your question, you are trying to create a function/procedure that make uses of selects and loops to identify business days. Personally I would avoid complex scripting (usually as last resort). I think It can be achieve by just using selects.
Here is a sample code, that can assist you identifying business days:
DECLARE MyDate DATE;
DECLARE DAYS_RANGE INT64;
SET MyDate = '2021-11-22';
SET DAYS_RANGE = 10;
/*if it fits you, create a table for special dates and holidays*/
create temp table offdays (
nonworkingdays date
);
insert into offdays values('2021-11-23');
insert into offdays values('2021-11-19');
with working_dates as(
select dafter,
case when EXTRACT(DAYOFWEEK FROM dafter) not in (1,7) then 1 else 0 end as isweekdays_after,
case when dafter in (select nonworkingdays from offdays) then 1 else 0 end as isoffdays_after,
dbefore,
case when EXTRACT(DAYOFWEEK FROM dbefore) not in (1,7) then 1 else 0 end as isweekdays_before,
case when dbefore in (select nonworkingdays from offdays) then 1 else 0 end as isoffdays_before
from (
select date_add(MyDate, INTERVAL days_to_count DAY) as dafter,
date_add(MyDate, INTERVAL -days_to_count DAY) as dbefore
from (SELECT days_to_count FROM UNNEST(GENERATE_ARRAY(1, DAYS_RANGE)) AS days_to_count)
)
)
select * from working_dates
If you run it, you will get a main table working_dates with rows equals to the range given and columns that will help you identify weekdays and offdays.
So, You can use this code to create a function or procedure where you can pass parameters and calculate if you either want days after or before and return the days or the count of days filtered by the columns weekdays and offdays.
Take this as a sample function derived from above script:
CREATE TEMP FUNCTION GetBusinessDaysAfter(fnDate Date, days_range INT64)
RETURNS INT64
AS ((
select count(d.dafter) from(
select dafter,
case when EXTRACT(DAYOFWEEK FROM dafter) not in (1,7) then 1 else 0 end as isweekdays_after,
case when dafter in ('2021-11-23') then 1 else 0 end as isoffdays_after,
from (
select date_add(fnDate, INTERVAL days_to_count DAY) as dafter
from (SELECT days_to_count FROM UNNEST(GENERATE_ARRAY(1, days_range)) AS days_to_count)
)) as d
where d.isweekdays_after=1 and d.isoffdays_after=0
));
select GetBusinessDaysAfter('2021-11-22',3)
I can use it to retrieve how many business days will pop up in the next 3 days. Turns out to be just 2 (for the sake of the sample, I put a fixed value in offdays, you can replace it making a reference to your holidays table in your dataset).
For more information about scripting, functions and procedures, here are some useful links:
Data definition language
Working with arrays
Working with dates functions

Related

Generate n consecutive dates with even day-part after today's date (for example for 2020-06-08 it will be 2020-06-10, 2020-06-12, 2020-06-14 ect.)

i need a bit help i dont know wat is wrong in my code . Plz help.
create procedure consecutive_N_even_day
#n int
as
begin
declare #na int
set #na = 0
declare #nexndate date
declare #date date
set #date=GETDATE()
declare #datepart int
while(#na<#n)
begin
set #nexndate=DATEADD(DAY,#na,#date)
set #datepart=DATEPART(day,#nexndate)
if #datepart%2=0
begin
print #nexndate;
end
set #na=#na+1
end
end
You can use a recursive query for this:
with cte as (
select dateadd(day, day(getdate()) % 2, cast(getdate() as date)) dt, 0 n
union all
select dateadd(day, 2, dt), n + 1 from cte where n < #n
)
select * from cte
The query starts from today (if it is an even day) or tomorrow, and then produces a series of dates with a 2 days increment. It iterates #n times.
You don't specify what to do if we reach the end of a month that has an uneven number of days. It is always possible to adapt the logic of the recursive part of the query to what you actually need in this case.
If #n is greater than 100, you need to add option (maxrecursion 0) at the very end of the query.

BQ scripting: Writing results of a loop to a table

I am working with BigQuery scripting, I have written a simple WHILE loop which iterates through daily Google Analytics tables and sums the visits, now I'd like to write these results out to a table.
I've gotten as far as creating the table, but I can't capture the value of visits from my SQL query to populate the table. Date works fine, because it is defined outside of the SQL. I tried to DECLARE the value of visits with a new variable, but again this does not work because it's not known outside of the statement.
SET vis = visits;
How can I correctly write my results out to a table?
DECLARE d DATE DEFAULT DATE_SUB(CURRENT_DATE(), INTERVAL 1 DAY);
DECLARE pfix STRING DEFAULT REGEXP_REPLACE(CAST(d AS STRING),"-","");
DECLARE vis INT64;
CREATE OR REPLACE TABLE test.looped_results (Date DATE, Visits INT64);
WHILE d > '2019-10-01' DO
SELECT d, SUM(totals.visits) AS visits
FROM `project.dataset.ga_sessions_*`
WHERE _table_suffix = pfix
GROUP BY Date;
SET d = DATE_SUB(d, INTERVAL 1 DAY);
SET vis = visits;
INSERT INTO test.looped_results VALUES (d, visits);
END WHILE;
Update: I also tried an alternative solution, assigning visits to it's own variable, but this produces the same error:
WHILE d > '2019-10-01' DO
SET vis_count = (SELECT SUM(totals.visits) AS visits
FROM `mindful-agency-136314.43786551.ga_sessions_*`
WHERE _table_suffix = pfix);
INSERT INTO test.looped_results VALUES (d, vis_count);
SET d = DATE_SUB(d, INTERVAL 1 DAY);
END WHILE;
Results:
In my results I see the correct number of rows created, with the correct dates, but the value of visits for each is the value for the most recent day.
I would also move INSERT INTO outside of the WHILE loop by collecting result into result variable (along with few other minor changes) as in below example
DECLARE d DATE DEFAULT DATE_SUB(CURRENT_DATE(), INTERVAL 1 DAY);
DECLARE pfix STRING;
DECLARE vis_count INT64;
DECLARE result ARRAY<STRUCT<vis_date DATE, vis_count INT64>> DEFAULT [];
CREATE OR REPLACE TABLE test.looped_results (Date DATE, Visits INT64);
WHILE d > '2019-10-01' DO
SET pfix = REGEXP_REPLACE(CAST(d AS STRING),"-","");
SET vis_count = (SELECT SUM(totals.visits) AS visits
FROM `project.dataset.ga_sessions_*`
WHERE _table_suffix = pfix);
SET result = ARRAY_CONCAT(result, [STRUCT(d, vis_count)]);
SET d = DATE_SUB(d, INTERVAL 1 DAY);
END WHILE;
INSERT INTO test.looped_results SELECT * FROM UNNEST(result);
Note: I hope your example is for scripting learning purpose and not for production as whenever possible we should stick with set based processing which can be easily done in your case
Here is a better way which is faster and without using a loop.
Basically, you form an array of suffix and do SELECT/INSERT in single query:
DECLARE date_range ARRAY<DATE> DEFAULT
GENERATE_DATE_ARRAY(DATE '2019-10-01', DATE '2019-10-10', INTERVAL 1 DAY);
DECLARE suffix_array ARRAY<STRING>
DEFAULT (SELECT ARRAY_AGG(REGEXP_REPLACE(CAST(dates AS STRING),"-",""))
FROM UNNEST(date_range) dates);
CREATE OR REPLACE TABLE test.looped_results (Date DATE, Visits INT64);
INSERT INTO test.looped_results
SELECT Date, SUM(totals.visits)
FROM `project.dataset.ga_sessions_*`
WHERE _table_suffix in UNNEST(suffix_array);
GROUP BY Date;
Actually, you need to update the pfix variable in there. Also, it is a good idea to instantiate the visits. Finally, your GROUPBY doesn't necessarily need a dimension if you are providing it with a pfix constraint.
This should do it:
DECLARE d DATE DEFAULT DATE_SUB(CURRENT_DATE(), INTERVAL 1 DAY);
DECLARE pfix STRING DEFAULT REGEXP_REPLACE(CAST(d AS STRING),'-','');
DECLARE visits int64;
SET visits = 0;
CREATE OR REPLACE TABLE project.dataset.looped_results (Date DATE, Visits INT64);
WHILE d > '2019-10-01' DO
SET visits = (SELECT SUM(totals.visits) FROM `project.dataset.ga_sessions_*` WHERE _table_suffix = pfix);
SET d = DATE_SUB(d, INTERVAL 1 DAY);
SET pfix = REGEXP_REPLACE(CAST(d AS STRING),"-","");
INSERT INTO dataset.looped_results VALUES (d, visits);
END WHILE;
Hope it helps.
Having reviewed my code (several times!) I realized that I wasn't refreshing the variable which transforms the data into the table prefix within the loop.
Here is a working version of the script, where I set pfix at the end of the loop:
DECLARE d DATE DEFAULT DATE_SUB(CURRENT_DATE(), INTERVAL 1 DAY);
DECLARE pfix STRING DEFAULT REGEXP_REPLACE(CAST(d AS STRING),"-","");
DECLARE vis_count INT64;
CREATE OR REPLACE TABLE test.looped_results (Date DATE, Visits INT64);
WHILE d > '2019-10-01' DO
SET vis_count = (SELECT SUM(totals.visits) AS visits
FROM `project.dataset.ga_sessions_*`
WHERE _table_suffix = pfix);
INSERT INTO test.looped_results VALUES (d, vis_count);
SET d = DATE_SUB(d, INTERVAL 1 DAY);
SET pfix = REGEXP_REPLACE(CAST(d AS STRING),"-","");
END WHILE;
I would actually not use a while loop but rather a group by
SELECT date, SUM(totals.visits) AS visits
FROM `mindful-agency-136314.43786551.ga_sessions_*`
GROUP BY Date;
It will give you your results as the table that you want, you don't need to loop on your table.
Depending on your set up but you can set the query to be ran every day so every new day you will have the new values

Oracle months_between function unexpected result

Oracle's months_between function returns unexpected results.
For example, the difference between 20191001 and 20190916 is 15 days, so the return value of the function is expected to be (1/31) * 15 = 0.4838 ....
SELECT months_between('20191001','20190916')
from dual
.5161290322580645161290322580645161290323
Is this a specification? Or is it a bug?
The function MONTHS_BETWEEN works with 31 day months when the day-of-the-month is not the last day of the month.
SELECT MONTHS_BETWEEN
(TO_DATE('20190201','YYYYMMDD'),
TO_DATE('20190131','YYYYMMDD') ) "Months"
FROM DUAL;
will return 0.032 (rounded) but
SELECT MONTHS_BETWEEN
(TO_DATE('20190301','YYYYMMDD'),
TO_DATE('20190228','YYYYMMDD') ) "Months"
FROM DUAL;
will return 0.129
For a more complete description of MONTHS_BETWEN see the documentation
Here is a version for SQL Server which is accurate.
/***************************************************************************
Name : fsMonthsBetweenDates
Purpose : Mimics the Months_Between function in Oracle; except this one is 100% accurate
Author : Gordon de Rouyan
Date : 2022-09-24
***************************************************************************/
CREATE OR ALTER FUNCTION dbo.fsMonthsBetweenDates
(
#Date1 datetime,
#Date2 datetime,
#InclusiveFirstDay bit = 1
)
RETURNS numeric(10,5)
WITH SCHEMABINDING -- , INLINE = ON -- compatibility_level = 150
AS
BEGIN
-- Using all floats for numeric values
DECLARE #Months numeric(10,5) = 0;
IF #Date1 IS NOT NULL AND #Date2 IS NOT NULL
BEGIN
-- Include the first day or not; e.g. 2021-02-01 and 2021-02-01
-- Do you want to consider this combination as one day 2021-02-01#00:00:00 to 2021-02-01#23:59:59?
DECLARE #FirstDayInclusive int = CASE
WHEN #InclusiveFirstDayFlag = 1 THEN 1
ELSE 0
END;
-- Determine start date
DECLARE #StartDt date = CASE
WHEN CAST(#Date2 AS date) > CAST(#Date1 AS date) THEN #Date1
ELSE #Date2
END;
-- Determine end date
DECLARE #EndDt date = CASE
WHEN CAST(#Date2 AS date) > CAST(#Date1 AS date) THEN #Date2
ELSE #Date1
END;
DECLARE -- The DAY portion of the dates
#StartDtDay int = DAY(#StartDt),
#EndDtDay int = DAY(#EndDt),
-- Number of days in each month specific to each supplied date; exact with leap year
#StartDtMonthDays numeric(7,5) = DAY(EOMONTH(#StartDt)),
#EndDtMonthDays numeric(7,5) = DAY(EOMONTH(#EndDt));
DECLARE -- The fraction of the first month
#FirstMonthFraction numeric(6,5) = (#StartDtMonthDays - #StartDtDay + #FirstDayInclusive) / #StartDtMonthDays,
-- The fraction of the last month
#LastMonthFraction numeric(6,5) = #EndDtDay / #EndDtMonthDays;
SET #Months = DATEDIFF(MONTH, #StartDt, #EndDt) - 1 + (#FirstMonthFraction + #LastMonthFraction);
END;
RETURN #Months;
END;
Here is a version for SQL Server which mimics the same output as Oracle.
/***************************************************************************
Name : fsOracleMonths_Between
Purpose : Mimics the Months_Between function in Oracle; exactly the same values
Author : Gordon de Rouyan
Date : 2022-09-24
***************************************************************************/
CREATE OR ALTER FUNCTION [dbo].[fsOracleMonths_Between]
(
#Date1 datetime,
#Date2 datetime
)
RETURNS numeric(10,5)
WITH SCHEMABINDING -- , INLINE = ON -- SQL Server 2019, compatibility_level = 150
AS
BEGIN
DECLARE #Months numeric(10,5) = 0,
#OmbDate1 date = #Date1,
#OmbDate2 date = #Date2;
IF #Date1 IS NOT NULL AND #Date2 IS NOT NULL
AND #OmbDate1 <> #OmbDate2
BEGIN
-- Compare the datetime values
DECLARE #Date2GreaterThanDate1Flag bit = CASE
WHEN #OmbDate1 <= #OmbDate2 THEN 1
ELSE 0
END;
-- This ensures the correct value comes out based on the numeric(7,5)
-- int / numeric(7,5) = numeric(7,5)
-- numeric(7,5) / 31 = error
DECLARE #ConstantDay numeric(7,5) = 31;
-- The DAY portion of the dates
DECLARE #Date1Day int = DAY(#OmbDate1),
#Date2Day int = DAY(#OmbDate2);
-- Number of days in each month specific to each supplied date; exact with leap year
DECLARE #Date1MonthDays int = DAY(EOMONTH(#OmbDate1)),
#Date2MonthDays int = DAY(EOMONTH(#OmbDate2));
-- The fraction of the start month
DECLARE #StartMonthFraction numeric(6,5) = CASE
WHEN #Date2GreaterThanDate1Flag = 1 THEN (#ConstantDay - #Date1Day) / #ConstantDay
ELSE (#ConstantDay - #Date2Day) / #ConstantDay
END;
-- The fraction of the end month
DECLARE #EndMonthFraction numeric(6,5) = CASE
WHEN #Date2GreaterThanDate1Flag = 1 THEN #Date2Day / #ConstantDay
ELSE #Date1Day / #ConstantDay
END;
SET #Months = CASE
WHEN #Date2GreaterThanDate1Flag = 1 THEN DATEDIFF(MONTH, #OmbDate2, #OmbDate1 ) + 1 - (#StartMonthFraction + #EndMonthFraction)
ELSE DATEDIFF(MONTH, #OmbDate2, #OmbDate1) - 1 + (#StartMonthFraction + #EndMonthFraction)
END;
END;
RETURN #Months;
END;

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

SQL - WHERE clause Return results with next working day

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'