(Oracle 11g DB) Calculate Number of buisiness days between current time and a date while excluding holidays in a view - oracle

So I have this working SQL script that take a date and returns the age from current time to the given date excluding dates defined in a table called exclude dates
SELECT
COUNT(*)
FROM
(
SELECT
ROWNUM rnum
FROM
all_objects
WHERE
ROWNUM <= CAST(current_timestamp AS DATE) - to_date('&2') + 1
)
WHERE
to_char(to_date('&2') + rnum - 1, 'DY') NOT IN ( 'SAT', 'SUN' )
AND NOT EXISTS (
SELECT
NULL
FROM
exclude_dates
WHERE
no_work = trunc(to_date('&2') + rnum - 1)
);
I have a table called
TICKETS
that contains columns named
ID, UPDATED_AT
I want to create a view that uses the above script to return
ID, AGE
where age is the output of the script above.

You code has a few weaknesses.
There is no need for CAST(current_timestamp AS DATE).
If you need the current DATE then simply use TRUNC(SYSDATE)
You don't need to select from all_objects. Better use hierarchical query
SELECT LEVEL as rnum FROM dual CONNECT BY LEVEL <= ...
Using to_date('&2') without a format is usually bad. Either your input value is a string, then you should include the format, e.g. to_date('&2', 'YYYY-MM-DD') or your input value is a DATE, then simply use &2 - never use TO_DATE() on a value which is already a DATE!
Final query could be this one - assuming input parameter is a DATE value:
WITH t AS (
SELECT LEVEL as d
FROM dual
CONNECT BY LEVEL <= TRUNC(SYSDATE) - the_day)
SELECT COUNT(*) AS buisiness_days
FROM t
WHERE TO_CHAR(the_day + d - 1, 'DY', 'NLS_DATE_LANGUAGE = american') NOT IN ('SAT', 'SUN')
AND NOT EXISTS (
SELECT 'x'
FROM exclude_dates
WHERE no_work = TRUNC(the_day + d - 1)
)
However, for me it is not clear how you want to provide this as a view! You would need to create a separate view for each input date, or at least create a new view every day.
I would suggest to create a function:
CREATE OR REPLACE FUNCTION buisiness_days(the_date IN DATE) RETURN INTEGER AS
ret INTEGER;
BEGIN
WITH t AS (
SELECT LEVEL as d
FROM dual
CONNECT BY LEVEL <= TRUNC(SYSDATE) - the_date)
SELECT COUNT(*) AS buisiness_days
INTO ret
FROM t
WHERE TO_CHAR(the_date + d - 1, 'DY', 'NLS_DATE_LANGUAGE = american') NOT IN ('SAT', 'SUN')
AND NOT EXISTS (
SELECT 'x'
FROM exclude_dates
WHERE no_work = TRUNC(the_date + d - 1)
);
RETURN ret;
END;

The function will return a list of dates between the date range you provide so the dates don't have to be stored in a table.
CREATE OR REPLACE TYPE nt_date IS TABLE OF DATE;
/
CREATE OR REPLACE FUNCTION generate_dates_pipelined(
p_from IN DATE,
p_to IN DATE
)
RETURN nt_date PIPELINED DETERMINISTIC
IS
v_start DATE := TRUNC(LEAST(p_from, p_to));
v_end DATE := TRUNC(GREATEST(p_from, p_to));
BEGIN
LOOP
PIPE ROW (v_start);
EXIT WHEN v_start >= v_end;
v_start := v_start + INTERVAL '1' DAY;
END LOOP;
RETURN;
END generate_dates_pipelined;
/
To exclude holidays you need to know what dates they fall on so there needs to be a holiday table.
create table holidays(
holiday_date DATE not null,
holiday_name VARCHAR2(20),
constraint holidays_pk primary key (holiday_date),
constraint is_midnight check ( holiday_date = trunc ( holiday_date ) )
);
INSERT into holidays (HOLIDAY_DATE,HOLIDAY_NAME)
WITH dts as (
select to_date('25-NOV-2021 00:00:00','DD-MON-YYYY HH24:MI:SS'), 'Thanksgiving 2021' from dual union all
select to_date('29-NOV-2021 00:00:00','DD-MON-YYYY HH24:MI:SS'), 'Hanukkah 2021' from dual
)
SELECT * from dts;
This query will provide the count of days between the range, number of working days and number of holidays in the range.
SELECT COUNT (*) AS total_days
, COUNT ( CASE
WHEN h.holiday_date IS NULL
AND d.column_value - TRUNC (d.column_value, 'IW') < 5
THEN 'Business Day'
END
) AS business_days
, COUNT (h.holiday_date) AS holidays
FROM generate_dates_pipelined (DATE '2021-11-01', DATE '2021-11-30') d
LEFT JOIN holidays h ON h.holiday_date = d.column_value;
This query will provide a list of dates excluding sat, sun and holidays that fall between the range.
SELECT
COLUMN_VALUE
FROM
TABLE(generate_dates_pipelined(DATE '2021-11-01',
DATE '2021-11-30')) c
where
to_char(COLUMN_VALUE, 'DY') NOT IN ('SAT', 'SUN')
AND NOT EXISTS (
SELECT 1
FROM holidays h
WHERE c.COLUMN_VALUE = h.holiday_date
);

You don't need a function or to use a row generator function and can calculate the number of business days:
CREATE VIEW business_day_ages (ID, AGE) AS
SELECT id,
( TRUNC( SYSDATE, 'IW' ) - TRUNC( updated_at, 'IW' ) ) * 5 / 7
-- Number of full weeks.
+ LEAST( SYSDATE - TRUNC( SYSDATE, 'IW' ), 5 )
-- Add part weeks at the end.
- LEAST( updated_at - TRUNC( updated_at, 'IW' ), 5 )
-- Subtract part weeks at the start.
- COALESCE(
( SELECT SUM(
LEAST(no_work + INTERVAL '1' DAY, SYSDATE)
- GREATEST(no_work, updated_at)
)
FROM exclude_dates
WHERE no_work BETWEEN TRUNC(updated_at) AND SYSDATE
),
0
)
-- Subtract the holiday days.
FROM tickets;
Or, if you are not calculating using part days then you can simplify it to:
CREATE OR REPLACE VIEW business_day_ages (ID, AGE) AS
SELECT id,
( TRUNC( SYSDATE, 'IW' ) - TRUNC( updated_at, 'IW' ) ) * 5 / 7
-- Number of full weeks.
+ LEAST( TRUNC(SYSDATE) - TRUNC( SYSDATE, 'IW' ), 5 )
-- Add part weeks at the end.
- LEAST( updated_at - TRUNC( updated_at, 'IW' ), 5 )
-- Subtract part weeks at the start.
- COALESCE(
( SELECT 1
FROM exclude_dates
WHERE no_work BETWEEN TRUNC(updated_at) AND TRUNC(SYSDATE)
),
0
)
-- Subtract the holiday days.
FROM tickets;
db<>fiddle here

Related

Oracle PLSQL generating absences

have some PLSQL code that generates a list of dates from a range, which seems to be working fine.
As part of a larger project I want to generate a procedure that will create a list of absences for each employee.
My first step is to use the MINUS command to remove all the holidays, which fall into the range of dates. Is there an easy way of doing this instead of comparing each holiday one at a time (there maybe several in the table) against the GENERATED dates.
If possible, I would prefer breaking all these tasks into small procedures or functions for easy debugging and legibility.
If there is an easier way to do this I am open to all suggestions. Thanks in advance for your help, expertise and patience.
ALTER SESSION SET
NLS_DATE_FORMAT = 'MMDDYYYY HH24:MI:SS';
create table holidays(
holiday_date DATE,
holiday_name VARCHAR2(20)
);
INSERT into holidays
(holiday_date,
holiday_name)
VALUES
(
TO_DATE('2021/07/21 00:00:00', 'yyyy/mm/dd hh24:mi:ss'), 'July 21 2021');
CREATE OR REPLACE PROCEDURE generate_dates
(
p_start_date IN DATE,
p_end_date IN DATE
)
AS
l_day DATE := p_start_date;
BEGIN
WHILE l_day <= p_end_date
LOOP
DBMS_OUTPUT.PUT_LINE(l_day);
l_day := l_day + 1;
END LOOP;
END generate_dates;
EXEC generate_dates(TRUNC(SYSDATE),TRUNC(SYSDATE+10));
Create table employees(
employee_id NUMBER(6),
first_name VARCHAR2(20),
last_name VARCHAR2(20),
card_num VARCHAR2(10),
work_days VARCHAR2(7)
);
ALTER TABLE employees
ADD ( CONSTRAINT employees_pk
PRIMARY KEY (employee_id));
INSERT INTO employees
(
EMPLOYEE_ID,
first_name,
last_name,
card_num,
work_days
)
WITH names AS (
SELECT 1, 'Jane', 'Doe', 'F123456', 'NYYYYYN' FROM dual UNION ALL
SELECT 2, 'Madison', 'Smith', 'R33432','NYYYYYN'
FROM dual UNION ALL
SELECT 3, 'Justin', 'Case', 'C765341','NYYYYYN'
FROM dual UNION ALL
SELECT 4, 'Mike', 'Jones', 'D564311','NYYYYYN' FROM dual
) SELECT * FROM names
-- check to see if working for that day. Byte=Y for Yes
SELECT SUBSTR( work_days, to_char(TRUNC(SYSDATE), 'D'),1) Work_Day
FROM employees
create table timeoff(
seq_num integer GENERATED BY DEFAULT AS IDENTITY (START WITH 1) NOT NULL,
employee_id NUMBER(6),
timeoff_date DATE,
timeoff_type VARCHAR2(1),
constraint timeoff_chk check (timeoff_date=trunc(timeoff_date, 'dd')),
constraint timeoff_pk primary key (employee_id, timeoff_date)
);
INSERT INTO timeoff (EMPLOYEE_ID,TIMEOFF_DATE,TIMEOFF_TYPE
)
WITH dts AS (
SELECT 1, to_date('20210726 00:00:00','YYYYMMDD HH24:MI:SS'),'V' FROM dual UNION ALL
SELECT 2, to_date('20210726 00:00:00','YYYYMMDD HH24:MI:SS'),'V' FROM dual UNION ALL
SELECT 2, to_date('20210727 00:00:00','YYYYMMDD HH24:MI:SS'),'V' FROM dual )
SELECT * FROM dts
CREATE TABLE emp_attendance(
seq_num integer GENERATED BY DEFAULT AS IDENTITY (START WITH 1) NOT NULL,
employee_id NUMBER(6),
start_date DATE,
end_date DATE,
week_number NUMBER(2),
create_date DATE DEFAULT SYSDATE
);
create table absences(
seq_num integer GENERATED BY DEFAULT AS IDENTITY (START WITH 1) NOT NULL,
employee_id NUMBER(6),
absent_date DATE,
constraint absence_chk check (absent_date=trunc(absent_date, 'dd')),
constraint absence_pk primary key (employee_id, absent_date)
);
INSERT INTO emp_attendance ( EMPLOYEE_ID, START_DATE,END_DATE,WEEK_NUMBER)
WITH dts AS (
SELECT 1, to_date('20210728 13:10:00','YYYYMMDD HH24:MI:SS'),
to_date('20210728 23:15:00','YYYYMMDD HH24:MI:SS'), 30 FROM dual UNION ALL
SELECT 2, to_date('20210728 12:10:10','YYYYMMDD HH24:MI:SS'),
to_date('20210728 20:15:01','YYYYMMDD HH24:MI:SS'), 30 FROM dual)
SELECT * FROM dts
CREATE OR REPLACE TYPE obj_date IS OBJECT (
date_val DATE
);
CREATE OR REPLACE TYPE nt_date IS TABLE OF obj_date;
CREATE OR REPLACE FUNCTION generate_dates(
p_from IN DATE
,p_to IN DATE)
RETURN nt_date PIPELINED
IS
-- normalize inputs to be as-of midnight
v_from DATE :=
TRUNC(NVL(p_from, SYSDATE));
v_to DATE := TRUNC(NVL(p_to, SYSDATE));
BEGIN
LOOP
EXIT WHEN v_from > v_to;
PIPE ROW (obj_date(v_from));
v_from := v_from + 1; -- next. calendar day
END LOOP;
RETURN;
END generate_dates;
INSERT INTO absences
(employee_id, absent_date)
SELECT e.employee_id,
c.date_val
FROM employees e
INNER JOIN table(generate_dates(date '2021-07-20', DATE '2021-07-31')) c
PARTITION BY ( e.employee_id )
ON (SUBSTR(e.work_days,
TRUNC(c.date_val) -
TRUNC(c.date_val, 'IW') + 1, 1) = 'Y')
WHERE NOT EXISTS (
SELECT 1
FROM holidays h
WHERE c.date_val = h.holiday_date
)
AND NOT EXISTS(
SELECT 1
FROM timeoff t
WHERE e.employee_id = t.employee_id
AND t.timeoff_date = c.date_val
)
AND NOT EXISTS(
SELECT 1
FROM emp_attendance ea
WHERE e.employee_id = ea.employee_id
AND TRUNC(ea.start_date) = c.date_val
)
ORDER BY
e.employee_id,
c.date_val
;
Don't use lots of procedures and/or a functions; just use a single query:
SELECT e.employee_id,
c.day
FROM employees e
INNER JOIN (
WITH calendar ( start_date, end_date ) AS (
SELECT DATE '2021-07-01', DATE '2021-07-30' FROM DUAL
UNION ALL
SELECT start_date + 1, end_date
FROM calendar
WHERE start_date + 1 <= end_date
)
SELECT start_date AS day
FROM calendar
) c
PARTITION BY ( e.employee_id )
ON (SUBSTR(e.work_days, TRUNC(c.day) - TRUNC(c.day, 'IW') + 1, 1) = 'Y')
WHERE NOT EXISTS (
SELECT 1
FROM holidays h
WHERE c.day = h.holiday_date
)
AND NOT EXISTS(
SELECT 1
FROM timeoff t
WHERE e.employee_id = t.employee_id
AND t.timeoff_date = c.day
)
ORDER BY
e.employee_id,
c.day
Notes:
This assumes that your work_days column aligns with the ISO week; if it does not then you will need to adjust the substring.
Do not use TO_CHAR(date_value, 'D') as users will get different results depending on their NLS_TERRITORY session setting.
db<>fiddle here

count Sunday in plsql between two dates :date1 and :date2

I know there is lot of work for this on forums but i tried many things but get error please I have two parameters in oracle reports :date1 and :date2 I want to check sunday and then return gives me how many Sundays in these two dates
function SUNDAY_CFormula return NUMBER is
start_date DATE := :DATE1;
end_date DATE := :DATE2;
A NUMBER;
begin
SELECT Count(*)
FROM (SELECT To_char(start_date + ( LEVEL - 1 ), 'fmday')INTO A
FROM DUAL;
CONNECT BY LEVEL <= end_date - start_date + 1)
WHERE A IN ( 'sunday' );
RETURN A;
end;
You could rewrite your function like below. It is safer to add 'nls_date_language = english' clause in your to_char function in order to make your function independent from your default environnment settings.
create or replace
function SUNDAY_CFormula (DATE1 date, DATE2 date) return NUMBER is
start_date DATE := DATE1;
end_date DATE := DATE2;
A NUMBER;
begin
SELECT Count(*) INTO A
FROM (
SELECT To_char(start_date + ( LEVEL - 1 ), 'fmday', 'nls_date_language = english') A
FROM DUAL
CONNECT BY LEVEL <= end_date - start_date + 1
) t
WHERE t.A IN ( 'sunday' );
RETURN A;
end;
/
You could even use below version to make your function more flexible about the two dates it takes as parameters, no matter if date1 is greater or less than date2.
create or replace
function SUNDAY_CFormula (DATE1 date, DATE2 date) return NUMBER is
start_date DATE := DATE1;
end_date DATE := DATE2;
A NUMBER;
begin
SELECT Count(*) INTO A
FROM (SELECT To_char(start_date + ( LEVEL - 1 ), 'fmday', 'nls_date_language = english') A
FROM DUAL
CONNECT BY LEVEL <= greatest(end_date, start_date) - least(end_date, start_date) + 1
) t
WHERE t.A IN ( 'sunday' );
RETURN A;
end;
/
As an alternative. I always try creating formulas for date range process rather than "iterating", I just do not like generating data to just throw it away. Yes, it is sometimes necessary, but not in this case. The following will accomplish what you want:
create or replace
function sunday_calc ( date1_in date
, date2_in date
, sun_in varchar2 default 'sun'
)
return number
is
sun_count integer;
begin
with date_range( start_date, end_date) as
( select trunc(least(date1_in,date2_in))
, trunc(greatest(date1_in,date2_in))
from dual
)
select floor((trunc(end_date) - trunc(next_day(start_date-1,sun_in))/7)) + 1
into sun_count
from date_range;
return sun_count;
end sunday_calc;
Note: Unfortunately the next_day function does not accept a NLS_DATE_LANGUAGE parameter, so I created a substitute. Sun_in parameter: include the name in target language corresponding to English day 'Sunday'
Curious about this compared to the function by #MDO I ran some tests on each. And they produced the same result; Except in some instances where the start date was greater that the end date there was a difference of 1. Comparing to actual calendar the formula was correct (see fiddle). But why, MDO's logic seems completely sound. At that point I just had to know why. Took awhile but there is a slight bug in her/his code. Turns out when the start date is greater then the end date their routine actually began looking at dates for the greatest date and moved forward. Thus changing the period look at the the greater of the dates to that date plus number of days. This is corrected by applying the least function to the "Select to_char(start_date...", results in:
create or replace
function sunday_cformula_r (date1 date, date2 date) return number is
start_date date := date1;
end_date date := date2;
a number;
begin
select count(*) into a
from (select to_char(least(end_date, start_date) + ( level - 1 ), 'fmday', 'nls_date_language = english') a
from dual
connect by level <= greatest(end_date, start_date) - least(end_date, start_date) + 1
) t
where t.a in ( 'sunday' );
return a;
end;

Coalesce statement to handle multiple values and NULLS?

I am trying to figure out how to create an SQL query that will check for (:FROM_DATE) and (:TO_DATE) parameters and if NULL to put the past month dates in for the two values, and if not NULL to accept whatever values are entered in the parameters.
For example:
if the user enters (01-JAN-17) as FROM_DATE, and (31-JAN-17) as TO_DATE, I want the query to not automatically pass any values for the TO_DATE and FROM_DATE.
if the user does not enter any values for TO_DATE and FROM_DATE or there are NULL values passed in, I want the query to automatically enter the the past months values (i.e., if query is run July 1st 2017, the FROM_DATE would be 01-JUN-17 and the TO_DATE would be 30-JUN-17).
I was hinted to use a coalesce statement to handle multiple values and NULLS (i.e., AND ( (coalesce(null, :P_ORG) is null) or (ORG.ORGANIZATION_ID in :P_ORG)))???
Any help would be greatly appreciated.
Something like:
SELECT *
FROM your_table
WHERE your_date_column BETWEEN TO_DATE( :from_date, 'DD-MON-YYYY' )
AND TO_DATE( :to_date, 'DD-MON-YYYY' )
OR ( ( :from_date IS NULL OR :to_date IS NULL )
AND your_date_column BETWEEN ADD_MONTHS( TRUNC( SYSDATE, 'MM' ), -1 )
AND TRUNC( SYSDATE, 'MM' ) - 1
);
If either (or both) :from_date or :to_date is NULL then the dates will be compared to the previous month.
If your table has dates where the time component is not always set to midnight then you will need to use:
SELECT *
FROM your_table
WHERE your_date_column BETWEEN TO_DATE( :from_date, 'DD-MON-YYYY' )
AND TO_DATE( :to_date, 'DD-MON-YYYY' )
OR ( ( :from_date IS NULL OR :to_date IS NULL )
AND your_date_column >= ADD_MONTHS( TRUNC( SYSDATE, 'MM' ), -1 )
AND your_date_column < TRUNC( SYSDATE, 'MM' )
);
Proof of concept: consider the following query, where we have dates and values, and we want to sum the values for the dates that fall between :from_date and :to_date. If either of them is null, the query will use the first day of the prior month for from_date and the last day of the prior month for to_date. Note that this will cause problems if one date is given an actual value and the other is left null - you didn't explain how you would want that handled. But that's a different issue.
I use SQL developer, and in it I don't know how to pass in dates; I show passing in strings, and converting them to dates.
with
test_data ( dt, val ) as (
select date '2017-05-29', 200 from dual union all
select date '2017-06-13', 150 from dual union all
select date '2017-06-18', 500 from dual
)
select sum(val) as sum_val
from test_data
where dt between coalesce(to_date(:from_date, 'yyyy-mm-dd'),
add_months(trunc(sysdate, 'mm'), -1))
and coalesce(to_date(:to_date , 'yyyy-mm-dd'), trunc(sysdate, 'mm') - 1)
;
Yes, you can use COALESCE (or Oracle's NVL). When a parameter is null, replace it with the default date.
select *
from mytable
where mydate >= coalesce(:from_date, trunc(sysdate - interval '1' month), 'month')
and mydate <= coalesce(:to_date, last_day(sysdate - interval '1' month));

Oracle: Days between two date and Exclude weekdays how to handle negative numbers

I have two date columns and trying to measure days between the two dates excluding weekends. I'm getting a negative number and need help solving.
Table
CalendarDate DayNumber FirstAssgn FirstCnt DayNumber2 Id BusinessDays
5/21/2017 Sunday 5/21/17 5/21/17 Sunday 1 -1
Query:
TRUNC(TO_DATE(A.FIRST_CONTACT_DT, 'DD/MM/YYYY')) - TRUNC(TO_DATE(A.FIRST_ASSGN_DT, 'DD/MM/YYYY'))
- ((((TRUNC(A.FIRST_CONTACT_DT,'D'))-(TRUNC(A.FIRST_ASSGN_DT,'D')))/7)*2)
- (CASE WHEN TO_CHAR(A.FIRST_ASSGN_DT,'DY','nls_date_language=english') ='SUN' THEN 1 ELSE 0 END)
- (CASE WHEN TO_CHAR(A.FIRST_CONTACT_DT,'DY','nls_date_language=english')='SAT' THEN 1 ELSE 0 END)
- (SELECT COUNT(1) FROM HUM.CALENDAR CAL
WHERE 1=1
AND CAL.CALENDAR_DATE >= A.FIRST_ASSGN_DT
AND CAL.CALENDAR_DATE < A.FIRST_CONTACT_DT
--BETWEEN A.FIRST_ASSGN_DT AND A.FIRST_CONTACT_DT
AND CAL.GRH_HOLIDAY_IND = 'Y'
) AS Business_Days
Looks like below piece needs editing...
- (CASE WHEN TO_CHAR(A.FIRST_ASSGN_DT,'DY','nls_date_language=english')='SUN' THEN 1 ELSE 0 END)
Adapted from my answer here:
Get the number of days between the Mondays of both weeks (using TRUNC( datevalue, 'IW' ) as an NLS_LANGUAGE independent method of finding the Monday of the week) then add the day of the week (Monday = 1, Tuesday = 2, etc., to a maximum of 5 to ignore weekends) for the end date and subtract the day of the week for the start date. Like this:
SELECT ( TRUNC( end_date, 'IW' ) - TRUNC( start_date, 'IW' ) ) * 5 / 7
+ LEAST( end_date - TRUNC( end_date, 'IW' ) + 1, 5 )
- LEAST( start_date - TRUNC( start_date, 'IW' ) + 1, 5 )
AS WeekDaysDifference
FROM your_table
With RANGE_TEMP as (
SELECT
STARTPERIOD start_date,
ENDPERIOD end_date
FROM
TABLE_DATA -- YOUR TABLE WITH ALL DATA DATE
), DATE_TEMP AS (
SELECT
(start_date + LEVEL) DATE_ALL
FROM
RANGE_TEMP
CONNECT BY LEVEL <= (end_date - start_date)
), WORK_TMP as (
SELECT
COUNT(DATE_ALL) WORK_DATE
FROM
DATE_TEMP
WHERE
TO_CHAR(DATE_ALL,'D', 'NLS_DATE_LANGUAGE=ENGLISH') NOT IN ('1','7')
), BUSINESS_TMP as (
SELECT
COUNT(DATE_ALL) BUSINESS_DATE
FROM
DATE_TEMP
WHERE
TO_CHAR(DATE_ALL,'D', 'NLS_DATE_LANGUAGE=ENGLISH') IN ('1','7')
)
SELECT
L.WORK_DATE,
H.BUSINESS_DATE
FROM
BUSINESS_TMP H,
WORK_TMP L
;

How to exclude holidays between two dates?

I have two dates and I have to find out the number of Sundays and holidays fall between those two dates. Can I do this using BETWEEN? If so, how?
SELECT date1, date2, trunc(deposit_date - transaction_date) TOTAL
FROM Table_Name FULL OUTER JOIN Holidays ON date2 = hdate
WHERE hdate IN (date1, date2)
Using this I can definitely check whether there is a holiday on either of the two days, i.e. date1 or date2 but what I am not able to find out that whether there lies a holiday or a Sunday between these two dates. Help!
The solution you've posted is horribly inefficient; you can do all of this in a single SQL statement:
Firstly generate all possible dates between the two you have:
select trunc(:min_date) + level - 1
from dual
connect by level <= trunc(:min_date) - trunc(:max_date)
Then use your HOLIDAY table to restrict to what you want:
with all_dates as (
select trunc(:min_date) + level - 1 as the_date
from dual
connect by level <= trunc(:min_date) - trunc(:max_date)
)
select count(*)
from all_dates a
left outer join holiday b
on a.the_date = b.hdate
where b.hdate is null
and to_char(a.the_date, 'DY') <> 'SUN'
If you want to check if hdate is between the two dates you can query using
where hdate between date1 and date2
If you want to check if hdate is on the same day as date1 or date two you can query like this
where trunc(hdate) in (trunc(date1) ,trunc(date2))
The trunc function removed the time.
You should create a table with the holidays and maintain it on your own.
CREATE TABLE holidays
(
holiday VARCHAR2(100)
, d_date DATE
);
INSERT INTO holidays VALUES ('National Developer Day', DATE'2013-06-01');
SELECT *
FROM holidays;
-- National Developer Day 2013-06-01 00:00:00
The rest is just a matter of a SQL statment
Scenario 1: EXISTS
SELECT COUNT
(
CASE
WHEN TRIM(TO_CHAR(d.start_date_level, 'DAY')) = 'SUNDAY'
OR CASE
WHEN EXISTS (SELECT 1 FROM holidays h WHERE d.start_date_level = h.d_date)
THEN 1
ELSE NULL
END = 1
THEN 1
ELSE NULL
END
) AS holiday_check
FROM
(
SELECT start_date + (LEVEL - 1) AS start_date_level
FROM
(
SELECT start_date, end_date, end_date - start_date AS diff_date
FROM
(
SELECT TRUNC(ADD_MONTHS(SYSDATE, -2)) AS start_date
, TRUNC(SYSDATE) AS end_date
FROM DUAL
)
)
CONNECT BY
LEVEL <= (diff_date + 1)
) d
Scenario 2: LEFT JOIN
SELECT COUNT
(
CASE
WHEN TRIM(TO_CHAR(d.start_date_level, 'DAY')) = 'SUNDAY'
OR h.d_date IS NOT NULL
THEN 1
ELSE NULL
END
) AS holiday_check
FROM
(
SELECT start_date + (LEVEL - 1) AS start_date_level
FROM
(
SELECT start_date, end_date, end_date - start_date AS diff_date
FROM
(
SELECT TRUNC(ADD_MONTHS(SYSDATE, -2)) AS start_date
, TRUNC(SYSDATE) AS end_date
FROM DUAL
)
)
CONNECT BY
LEVEL <= (diff_date + 1)
) d
LEFT JOIN holidays h
ON d.start_date_level = h.d_date
9 Sundays + 1 "National Developer Day" = 10
CREATE OR REPLACE FUNCTION workdays (dt1 DATE, dt2 DATE) RETURN NUMBER IS
weekday_count NUMBER := 0;
date1 DATE := dt1;
date2 DATE := dt2;
cur_dt date;
holiday_count number;
begin
if date1 = date2 then
return 0;
end if;
cur_dt := transaction_date;
while cur_dt <= date2 loop
if cur_dt = date2 then
null;
else
SELECT count(*) INTO holiday_count
FROM holiday
WHERE hdate = cur_dt;
IF holiday_count = 0 THEN
IF to_char(cur_dt,'DY') NOT IN ('SUN') THEN
weekday_count := weekday_count + 1;
END IF;
END IF;
END IF;
cur_dt := cur_dt +1;
END LOOP;
RETURN weekday_count;
END;
And then I queried my database and got the right results. Do post if you have an optimal solution for this.
Here is an even better and efficient solution to the problem,
SELECT A.ID,
COUNT(A.ID) AS COUNTED
FROM tableA A
LEFT JOIN TableB B
ON A.tableB_id=B.id
LEFT JOIN holiday C
ON TRUNC(C.hdate) BETWEEN (TRUNC(a.date1) +1) AND TRUNC(B.date2)
WHERE c.hdate IS NOT NULL
GROUP BY A.ID;
where TableA contains date1 and tableB contains date2. Holiday contains the list of holidays and Sundays. And this query excludes 'date1' from the count.
RESULT LOGIC
trunc(date2) - trunc(date1) = x
x - result of the query
Make a table T$HOLIDAYS with your holidays (HDATE column). These dates will be excluded from calculation of working days within given period (sdate is start date and edate end date of period). Here is the function that calculates working days within given period excluding holidays, saturdays and sundays:
CREATE OR REPLACE FUNCTION WorkingDays(sdate IN DATE,edate IN DATE) RETURN NUMBER IS
days NUMBER;
BEGIN
WITH dates AS (SELECT sdate+LEVEL-1 AS d FROM DUAL CONNECT BY LEVEL<=edate-sdate+1)
SELECT COUNT(*) INTO days
FROM dates
WHERE d NOT IN (SELECT hdate FROM t$holidays) --exclude holidays
AND TO_CHAR(d,'D') NOT IN (6,7); --exclude saturdays + sundays
RETURN days;
END WorkingDays;
/
select sum(qq) from (
select case when to_number(to_char((trunc(sysdate-10) + level - 1),'D'))<=5 then 1 else 0 end as qq
from dual
connect by level <= trunc(sysdate) - trunc(sysdate-10))

Resources