I need to return the time between two dates in Oracle except the time during the weekends, I could return the minute. But when I set a weekend date, I receive a null result instead of the remaining time in workweek.
First, we need to create a function:
CREATE OR REPLACE FUNCTION get_bus_minutes_between (start_dt DATE, end_dt DATE)
RETURN NUMBER
IS
v_return NUMBER;
BEGIN
select sum(greatest(end_dt - start_dt,0)) * 24 * 60 work_minutes
into v_return
from dual
where trunc(start_dt) - trunc(start_dt,'iw') < 5; -- exclude weekends
RETURN v_return;
END;
Case 1 - Return the minutes in the workweek - Ok
Starting and ending in the workweek.
SELECT
"GET_BUS_MINUTES_BETWEEN"(TO_DATE('14-09-2020 06:00:00', 'dd-mm-yyyy hh24:mi:ss'),
TO_DATE('14-09-2020 10:00:00', 'dd-mm-yyyy hh24:mi:ss')) "WORK_MINUTES"
FROM
"SYS"."DUAL";
Case 2 - Return the remaining minutes in the workweek - Fail
Starting at the weekend and ending in workweek.
SELECT
"GET_BUS_MINUTES_BETWEEN"(TO_DATE('13-09-2020 06:00:00', 'dd-mm-yyyy hh24:mi:ss'),
TO_DATE('14-09-2020 10:00:00', 'dd-mm-yyyy hh24:mi:ss')) "WORK_MINUTES"
FROM
"SYS"."DUAL";
13-09-2020 is Sunday, therefore I was expected the return as 600 minutes related the Monday.
In these possibilities, we can start at the workweek and end at weekend.
You don't need to use SQL or a row generator and can do it with a simple calculation using only PL/SQL. Adapted from my answers here and here.
CREATE OR REPLACE FUNCTION get_bus_minutes_between (start_dt DATE, end_dt DATE)
RETURN NUMBER
IS
p_start_date DATE;
p_end_date DATE;
p_working_days NUMBER;
BEGIN
IF start_dt IS NULL OR end_dt IS NULL THEN
RETURN NUll;
END IF;
-- Enforce that the values are earliest start date to latest end date.
p_start_date := LEAST( start_dt, end_dt );
p_end_date := GREATEST( start_dt, end_dt );
-- Calculate the number of days from the beginning of the ISO week containing
-- the start date and the beginning of the ISO week containing the end date
-- and then multiply this by 5/7 to get the number of full business days.
--
-- Then add on the extra days from the beginining of the ISO week containing
-- the end date and the end date and subtract the extra days from the
-- beginning of the ISO week containing the start date to the start date.
p_working_days := ( TRUNC( p_end_date, 'IW' ) - TRUNC( p_start_date, 'IW' ) ) * 5 / 7
+ LEAST( p_end_date - TRUNC( p_end_date, 'IW' ), 5 )
- LEAST( p_start_date - TRUNC( p_start_date, 'IW' ), 5 );
-- If the start date and end date are reversed then return a negative value.
IF start_dt > end_dt THEN
RETURN -ROUND( p_working_days * 24 * 60, 3 );
ELSE
RETURN +ROUND( p_working_days * 24 * 60, 3 );
END IF;
END;
/
Then:
SELECT GET_BUS_MINUTES_BETWEEN(
DATE '2020-09-14' + INTERVAL '6' HOUR,
DATE '2020-09-14' + INTERVAL '10' HOUR
) AS minutes_between
FROM DUAL;
Outputs:
| MINUTES_BETWEEN |
| --------------: |
| 240 |
and:
SELECT GET_BUS_MINUTES_BETWEEN(
DATE '2020-09-13' + INTERVAL '6' HOUR,
DATE '2020-09-14' + INTERVAL '10' HOUR
) AS minutes_between
FROM DUAL;
outputs:
| MINUTES_BETWEEN |
| --------------: |
| 600 |
db<>fiddle here
If the intervals are not too big, one method uses a brute force approach to generate all minutes in the range, then exclude week-ends:
with cte(dt, end_dt) as (
select start_dt, end_dt from dual
union all
select dt + 1 / 24 / 60, end_dt from cte where dt < end_dt
)
select count(*) work_minutes
from cte
where trunc(dt) - trunc(dt,'iw') < 5
If the intervals are not too big, one method uses a brute force approach to generate all minutes in the range, then exclude week-ends:
with cte(dt, end_dt) as (
select start_dt, end_dt from dual
union all
select dt + 1 / 24 / 60, end_dt from cte where dt < end_dt
)
select count(*) work_minutes
from cte
where to_char(dt, 'IW') <= 5
If you have large intervals, we can reduce the number of iterations by pre-generating minutes / hours series:
with
params (start_dt, end_dt) as (
select start_dt, end_dt from dual
)
minutes (mi) as (
select 0 from dual
union all select mi + 1 from minutes where mi < 59
),
hours (hr) as (
select 0 from dual
union all select hr + 1 from hours where hr < 23
)
select count(*) work_minutes
from params p
cross join minutes m
cross join hours h
where
p.start_dt + h.hr / 24 + m.mi / 24 / 60 <= end_dt
and trunc(p.start_dt + h.hr / 24 + m.mi / 24 / 60) - trunc(p.start_dt + h.hr / 24 + m.mi / 24 / 60,'iw') < 5
Related
Sorry for the title ... it's best to describe the problem with an example ...
I have a list of events and two dates for each event and I need to "break" or "distribute" those dates within their respective months.
Example 1:
Event: Event A
Start Date: 12/15/2017 - MM/DD/YYYY
End Date: 01/17/2018 - MM/DD/YYYY
If I do a search on my table for this event, I get a result row with that data.
But I need two results, as shown below:
Result 1: Event A > 15 to 31
Result 2: Event A > 01 to 17
Example 2:
Event: Event B
Start Date: 02/07/2018 - MM/DD/YYYY
End Date: 04/22/2018 - MM/DD/YYYY
Result 1: Event B > 07 to 28
Result 2: Event B > 01 to 31
Result 3: Event B > 01 to 22
What is the most efficient way to do this?
On Oracle 12c cross apply clause can be used:
create table e_vents(
name varchar2(10),
startdate date,
enddate date
);
insert all
into e_vents values( 'A', date '2017-12-15', date '2018-01-17' )
into e_vents values( 'B', date '2017-12-15', date '2017-12-22' )
into e_vents values( 'C', date '2017-12-15', date '2018-05-22' )
select null from dual;
commit;
select e.name,
case when e.startdate > x.s_date then e.startdate else x.s_date end as start_date,
case when e.enddate < x.e_date then e.enddate else x.e_date end as end_date
from e_vents e
cross apply (
select
trunc( e.startdate, 'mm') + (level-1) * interval '1' month as s_date,
trunc( e.startdate + (level) * interval '1' month, 'mm') -1 as e_date
from dual
connect by level <= months_between( trunc( e.enddate, 'mm'),trunc( e.startdate, 'mm')) + 1
) x
NAME START_DATE END_DATE
---------- ---------- ----------
A 2017-12-15 2017-12-31
A 2018-01-01 2018-01-17
B 2017-12-15 2017-12-22
C 2017-12-15 2017-12-31
C 2018-01-01 2018-01-31
C 2018-02-01 2018-02-28
C 2018-03-01 2018-03-31
C 2018-04-01 2018-04-30
C 2018-05-01 2018-05-22
9 rows selected.
I don't have a full solution for you (if you create a workable SQLFiddle testbench for it, I can probably work it out), but I think it's something requiring a CONNECT BY clause and it would be very close to this solution from Ask Tom.
It goes basically something like this (example from Ask Tom):
variable sdate varchar2(30);
variable edate varchar2(30);
exec :sdate := '01-mar-2011'; :edate := '31-dec-2011';
select level r,
greatest( add_months(trunc(sdate,'mm'),level-1), sdate ),
least( last_day( add_months(sdate,level-1) ), edate )
from (select to_date( :sdate, 'dd-mon-yyyy' ) sdate,
to_date( :edate, 'dd-mon-yyyy' ) edate
from dual)
connect by level <= months_between( trunc( edate,'mm'), trunc(sdate,'mm') ) + 1;
R GREATEST( LEAST(LAS
------ --------- ---------
1 01-MAR-11 31-MAR-11
2 01-APR-11 30-APR-11
3 01-MAY-11 31-MAY-11
4 01-JUN-11 30-JUN-11
5 01-JUL-11 31-JUL-11
6 01-AUG-11 31-AUG-11
7 01-SEP-11 30-SEP-11
8 01-OCT-11 31-OCT-11
9 01-NOV-11 30-NOV-11
10 01-DEC-11 31-DEC-11
10 rows selected.
two solutions are available for this question
In Oracle 12C you can use the below query
SELECT DISTINCT e.name,
CASE
WHEN e.startdate > x.sdate
THEN e.startdate
ELSE x.sdate
END AS startdate,
CASE
WHEN e.enddate < x.edate
THEN e.enddate
ELSE x.edate
END AS enddate
FROM e_vents e CROSS apply
(SELECT TRUNC( e.startdate, 'mm') + (level-1) * interval '1' MONTH AS sdate,
TRUNC( e.startdate + (level) * interval '1' MONTH, 'mm') -1 AS edate
FROM e_vents
CONNECT BY level <= months_between( TRUNC( e.enddate, 'mm'),TRUNC( e.startdate, 'mm')) + 1
) x
ORDER BY 1 ASC;
in older versions of oracle use the below query
SELECT e.name,
greatest(e.startdate,x.sdate) AS startdate,
least(e.enddate,x.edate) AS enddate
FROM e_vents e,
(SELECT TRUNC( e.min_startdate, 'mm') + (level-1) * interval '1' MONTH AS sdate,
TRUNC( e.min_startdate + (level) * interval '1' MONTH, 'mm') -1 AS edate
FROM
(SELECT MIN(startdate) min_startdate,MAX(enddate) max_enddate FROM e_vents
) e
CONNECT BY level<= months_between( TRUNC( e.max_enddate, 'mm'),TRUNC( e.min_startdate, 'mm')) + 1
) x
WHERE e.startdate BETWEEN x.sdate AND x.edate
OR e.enddate BETWEEN x.sdate AND x.edate
OR x.sdate BETWEEN e.startdate AND e.enddate
OR x.edate BETWEEN e.startdate AND e.enddate
ORDER BY 1 ASC ;
I have a date column and I need to return each day from the start date to the end date. For example if I have a date column that starts at 01-01-2020 till 22/03/2022. Then I want to return data such as:
DateColumn
01-JAN-20
02-JAN-20
03-JAN-20
and so on......
until 22-MAR-22
From Oracle 12:
SELECT t.id, d.day
FROM table_name t
CROSS JOIN LATERAL (
SELECT t.start_date + LEVEL - 1 AS day
FROM DUAL
CONNECT BY t.start_date + LEVEL - 1 <= t.end_date
) d
WHERE t.start_date <= t.end_date
Before Oracle 12, you can use:
SELECT t.id, d.COLUMN_VALUE AS day
FROM table_name t
CROSS JOIN TABLE(
CAST(
MULTISET(
SELECT t.start_date + LEVEL - 1
FROM DUAL
CONNECT BY t.start_date + LEVEL - 1 <= t.end_date
)
AS SYS.ODCIDATELIST
)
) d
WHERE t.start_date <= t.end_date
Or:
WITH dates (id, day, end_date) AS (
SELECT id, start_date, end_date
FROM table_name
WHERE start_date <= end_date
UNION ALL
SELECT id, day + 1, end_date
FROM dates
WHERE day + 1 <= end_date
)
SEARCH DEPTH FIRST BY id, day SET ord
SELECT id, day
FROM dates;
Which, for the sample data:
CREATE TABLE table_name (id, start_date, end_date) AS
SELECT 1, DATE '2021-01-01', DATE '2022-03-22' FROM DUAL UNION ALL
SELECT 2, DATE '2022-01-01', DATE '2022-01-10' FROM DUAL;
All output:
ID
DAY
1
01-JAN-21
1
02-JAN-21
...
...
1
21-MAR-22
1
22-MAR-22
2
01-JAN-22
2
02-JAN-22
...
...
2
09-JAN-22
2
10-JAN-22
db<>fiddle here
One option might also be
SQL> WITH
2 test (id, start_date, end_date)
3 AS
4 (SELECT 1, DATE '2020-01-01', DATE '2022-03-22' FROM DUAL)
5 SELECT start_date + LEVEL - 1 datecolumn
6 FROM test
7 CONNECT BY LEVEL <= end_date - start_date + 1
8 ORDER BY datecolumn;
DATECOLUMN
----------
01.01.2020
02.01.2020
03.01.2020
04.01.2020
<snip>
14.03.2022
15.03.2022
16.03.2022
17.03.2022
18.03.2022
19.03.2022
20.03.2022
21.03.2022
22.03.2022
812 rows selected.
SQL>
If there - as MT0 commented - is more than a single row in that table that contains start/end dates, then
WITH
test (id, start_date, end_date)
AS
(SELECT 1, DATE '2020-01-01', DATE '2022-03-22' FROM DUAL
UNION ALL
SELECT 2, DATE '2022-03-15', DATE '2022-03-23' FROM DUAL)
SELECT id, start_date + COLUMN_VALUE - 1 datecolumn
FROM test
CROSS JOIN
TABLE (
CAST (
MULTISET ( SELECT LEVEL
FROM DUAL
CONNECT BY LEVEL <= end_date - start_date + 1)
AS SYS.odcinumberlist))
ORDER BY id, datecolumn;
You can create a PIPELINED function, where you can just plug in the dates you want
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;
/
SELECT
c.COLUMN_VALUE
FROM
TABLE(generate_dates_pipelined(DATE '2022-01-01',
DATE '2022-01-30')) c
I have recursive CTE, which is working fine. It's designed to generate a numbers of rows based on the count(*) of the locations table, which in this test CASE is 15 or stop before crossing midnight.
My goal is to populate the schedule table. The schedule_id can be hard coded to 1 for now as I plan to wrap this code in a procedure to pass in values.
First, instead of creating rows for a single date I would prefer to use the function generate_dates_pipelined, which creates rows for a range of dates. For each date add the number of seconds ie 83760, which = a time 23:16:00 to create a start_time.
Second, associating a location_id with a date range row being generated.
Note: a unique location_id must be associated with every row. Secondly, though in my test CASE the location_id are consequently ordered that may not be the case in production. Third, there are only 3 rows instead of 15 because the next row would have crossed midnight.
Thanks in advance for your expertise and to all that answer.
Current output:
START_DATE END_DATE
08192021 23:30:00 08192021 23:35:00
08192021 23:40:00 08192021 23:45:00
08192021 23:50:00 08192021 23:55:00
Desired output:
SCHEDULE_ID LOCATION_ID START_DATE END_DATE
1 1 08192021 23:30:00 08192021 23:35:00
1 2 08192021 23:40:00 08192021 23:45:00
1 3 08192021 23:50:00 08192021 23:55:00
ALTER SESSION SET NLS_DATE_FORMAT = 'MMDDYYYY HH24:MI:SS';
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;
/
create table schedule(
schedule_id NUMBER(4),
location_id number(4),
start_date DATE,
end_date DATE,
CONSTRAINT start_min check (start_date=trunc(start_date,'MI')),
CONSTRAINT end_min check (end_date=trunc(end_date,'MI')),
CONSTRAINT end_gt_start CHECK (end_date >= start_date),
CONSTRAINT same_day CHECK (TRUNC(end_date) = TRUNC(start_date))
);
CREATE TABLE locations AS
SELECT level AS location_id,
'Door ' || level AS location_name,
CASE round(dbms_random.value(1,3))
WHEN 1 THEN 'A'
WHEN 2 THEN 'T'
WHEN 3 THEN 'T'
END AS location_type
FROM dual
CONNECT BY level <= 15;
ALTER TABLE locations
ADD ( CONSTRAINT locations_pk
PRIMARY KEY (location_id));
WITH input (start_time) AS (
SELECT TRUNC(SYSDATE) + INTERVAL '23:30' HOUR TO MINUTE FROM DUAL
)
SELECT start_time + (LEVEL-1) * INTERVAL '10' MINUTE
AS start_date,
start_time + (LEVEL-1) * INTERVAL '10' MINUTE + INTERVAL '5' MINUTE
AS end_date
FROM input
CONNECT BY (LEVEL-1) * INTERVAL '10' MINUTE < INTERVAL '1' DAY
AND LEVEL <= (SELECT COUNT(*) FROM locations)
AND start_time + (LEVEL-1) * INTERVAL '10' MINUTE < TRUNC(start_time) + INTERVAL '1' DAY;
One statement to generate the data:
-- The date range
WITH args (start_date, end_date) AS (
SELECT current_date, current_date + 2 FROM dual
)
-- generate entire date range
, dates (adate) AS (
SELECT trunc(start_date) FROM args UNION ALL
SELECT adate + INTERVAL '1' DAY FROM dates, args
WHERE adate < end_date - 1
)
-- generate initial date/times
, init_datetimes (adatetime) AS (
SELECT adate + INTERVAL '83760' SECOND FROM dates
)
-- generate the rest of the times per date until midnight
, init_schedules (adatetime, end_time, n) AS (
SELECT adatetime, adatetime + INTERVAL '5' MINUTE, 1 FROM init_datetimes UNION ALL
SELECT adatetime + INTERVAL '10' MINUTE, adatetime + INTERVAL '10' MINUTE + INTERVAL '5' MINUTE, n+1
FROM init_schedules
WHERE trunc(adatetime) = trunc(adatetime + 2*INTERVAL '10' MINUTE) -- stop before midnight
-- AND n < 10 -- Just for protection
)
-- generate some test locations
, locations (alocation) AS (
SELECT 1 FROM dual UNION
SELECT 3 FROM dual UNION
SELECT 6 FROM dual UNION
SELECT 7 FROM dual UNION
SELECT 9 FROM dual UNION
SELECT 10 FROM dual
)
-- add row_number to location list
, location_list (alocation, n) AS (
SELECT alocation, ROW_NUMBER() OVER (ORDER BY alocation) FROM locations
)
-- Now apply locations to the schedule for each date/time
, location_dates (alocation, adatetime, end_time, n) AS (
SELECT alocation, adatetime, end_time, dat.n
FROM location_list loc
JOIN init_schedules dat
ON loc.n = dat.n -- one location per row per date
)
SELECT *
FROM location_dates
ORDER BY adatetime, alocation
;
The result:
loc date/time end_time n
1 08192021 23:16:00 08192021 23:21:00 1
3 08192021 23:26:00 08192021 23:31:00 2
6 08192021 23:36:00 08192021 23:41:00 3
7 08192021 23:46:00 08192021 23:51:00 4
1 08202021 23:16:00 08202021 23:21:00 1
3 08202021 23:26:00 08202021 23:31:00 2
6 08202021 23:36:00 08202021 23:41:00 3
7 08202021 23:46:00 08202021 23:51:00 4
1 08212021 23:16:00 08212021 23:21:00 1
3 08212021 23:26:00 08212021 23:31:00 2
6 08212021 23:36:00 08212021 23:41:00 3
7 08212021 23:46:00 08212021 23:51:00 4
Nice and compact. I do cross midnight now as another process was fixed that could not handle that situation. Thanks to all who answered.
WITH params AS
(
SELECT 1 AS schedule_id,
TO_DATE ( '2021-08-21 00:00:00'
, 'YYYY-MM-DD HH24:MI:SS'
) AS base_date
, INTERVAL '83760' SECOND AS offset
, INTERVAL '10' MINUTE AS incr
, INTERVAL '5' MINUTE AS duration
FROM dual
)
SELECT p.schedule_id
, l.location_id
, p.base_date
, p.base_date + offset
+ (incr * (ROWNUM - 1)) AS start_date
, p.base_date + offset
+ (incr * (ROWNUM - 1))
+ p.duration AS end_date
FROM locations l
CROSS JOIN params p
ORDER BY start_date
;
I am trying to achieve a query that returns the time difference between two dates excluding weekends(Saturday and Sunday) and excluding time (6 pm-9 am).
For now, I have a function that is excluding the weekends, But I am unable to exclude time from the query. Can anyone help with this?
The article from which I take help is this
CREATE OR REPLACE FUNCTION get_bus_minutes_between(
p_start_date DATE,
p_end_date DATE
)
RETURN NUMBER
DETERMINISTIC -- ***** Can't hurt
IS
days_diff NUMBER := 0;
end_date DATE := p_end_date;
minutes_diff NUMBER;
start_date DATE := p_start_date;
weeks_diff NUMBER;
BEGIN
IF start_date <= end_date
THEN
-- Move start_date and end_date away from weekends
IF start_date > TRUNC (start_date, 'IW') + 5
THEN -- Use next Monday for start_date
start_date := TRUNC (start_date, 'IW') + 7;
END IF;
IF end_date > TRUNC (end_date, 'IW') + 5
THEN -- Use Friday quitting time
end_date := TRUNC (end_date, 'IW') + 4 + (16.5 / 24);
END IF;
-- Move start_date into the same weeek as end_date
-- (Remember how many weeks we had to move it)
weeks_diff := ( TRUNC (end_date, 'IW')
- TRUNC (start_date, 'IW')
) / 7;
IF weeks_diff > 0
THEN
start_date := start_date + (7 * weeks_diff);
END IF;
-- Make start_date the same day as end_date
-- (Remember how many days we had to move it)
days_diff := TRUNC (end_date) - TRUNC (start_date);
IF days_diff > 0
THEN
start_date := start_date + days_diff;
END IF;
-- Move start_date up to starting time
start_date := GREATEST ( start_date
, TRUNC (start_date) + (8.75 / 24)
);
-- Move end_date back to quitting time
end_date := LEAST ( end_date
, TRUNC (end_date) + ( CASE
WHEN TO_CHAR ( end_date
, 'DY'
, 'NLS_DATE_LANGUAGE=ENGLISH'
) = 'FRI'
THEN 16.5
ELSE 17
END
/ 24
)
);
minutes_diff := ( GREATEST ( 0
, end_date - start_date
)
* 24 * 60
)
+ (days_diff * 495) -- 495 minutes per full day (Mon.-Thu.)
+ (weeks_diff * 2445); -- 2445 minutes per full week
ELSIF start_date > end_date
THEN
minutes_diff := -get_bus_minutes_between (end_date, start_date);
ELSE -- One of the arguments was NULL
minutes_diff := NULL;
END IF;
RETURN ROUND(minutes_diff);
END get_bus_minutes_between;
You can directly calculate the difference in days (adapted from my answer here):
SELECT start_date,
end_date,
ROUND(
(
-- Calculate the full weeks difference from the start of ISO weeks.
( TRUNC( end_date, 'IW' ) - TRUNC( start_date, 'IW' ) ) * (9/24) * (5/7)
-- Add the full days for the final week.
+ LEAST( TRUNC( end_date ) - TRUNC( end_date, 'IW' ), 5 ) * (9/24)
-- Subtract the full days from the days of the week before the start date.
- LEAST( TRUNC( start_date ) - TRUNC( start_date, 'IW' ), 5 ) * (9/24)
-- Add the hours of the final day
+ LEAST( GREATEST( end_date - TRUNC( end_date ) - 9/24, 0 ), 9/24 )
-- Subtract the hours of the day before the range starts.
- LEAST( GREATEST( start_date - TRUNC( start_date ) - 9/24, 0 ), 9/24 )
)
-- Multiply to give minutes rather than fractions of full days.
* 24 * 60
) AS work_day_mins_diff
FROM table_name;
Which, for the sample data:
CREATE TABLE table_name ( start_date, end_date ) AS
SELECT DATE '2020-12-30' + INTERVAL '00' HOUR, DATE '2020-12-30' + INTERVAL '12' HOUR FROM DUAL UNION ALL
SELECT DATE '2020-12-30' + INTERVAL '18' HOUR, DATE '2020-12-30' + INTERVAL '20' HOUR FROM DUAL UNION ALL
SELECT DATE '2020-12-30' + INTERVAL '17:30' HOUR TO MINUTE, DATE '2020-12-30' + INTERVAL '21:30' HOUR TO MINUTE FROM DUAL UNION ALL
SELECT DATE '2021-01-01' + INTERVAL '00' HOUR, DATE '2021-01-04' + INTERVAL '00' HOUR FROM DUAL UNION ALL
SELECT DATE '2021-01-02' + INTERVAL '00' HOUR, DATE '2021-01-04' + INTERVAL '00' HOUR FROM DUAL UNION ALL
SELECT DATE '2020-12-28' + INTERVAL '00' HOUR, DATE '2021-01-04' + INTERVAL '00' HOUR FROM DUAL UNION ALL
SELECT DATE '2020-12-28' + INTERVAL '00' HOUR, DATE '2020-12-29' + INTERVAL '00' HOUR FROM DUAL;
Outputs:
(Using ALTER SESSION SET NLS_DATE_FORMAT = 'YYYY-MM-DD HH24:MI:SS (DY)';)
START_DATE | END_DATE | WORK_DAY_MINS_DIFF
:------------------------ | :------------------------ | -----------------:
2020-12-30 00:00:00 (WED) | 2020-12-30 12:00:00 (WED) | 180
2020-12-30 18:00:00 (WED) | 2020-12-30 20:00:00 (WED) | 0
2020-12-30 17:30:00 (WED) | 2020-12-30 21:30:00 (WED) | 30
2021-01-01 00:00:00 (FRI) | 2021-01-04 00:00:00 (MON) | 540
2021-01-02 00:00:00 (SAT) | 2021-01-04 00:00:00 (MON) | 0
2020-12-28 00:00:00 (MON) | 2021-01-04 00:00:00 (MON) | 2700
2020-12-28 00:00:00 (MON) | 2020-12-29 00:00:00 (TUE) | 540
db<>fiddle here
We are using Oracle 11.
In our CASE WHEN statement, I need to check if the number of days between the 2 dates are > 3 business days (so excluding weekends and holidays).
CASE WHEN end_date - start_date > 3 THEN 0 --> this includes weekend
and holidays
WHEN CODE = 1 THEN 1
WHEN CODE =2 THEN 2
ELSE 3
END AS MyColumn
Say I have a holiday calendar table that has column HolidayDates that contains all the holidays, for ex: 12/25/2018, 12/31/2018, etc.
HolidayDates
12/25/2018
12/31/2018
So, if
Date1 = 1/2/19 (Wednesday)
Date2 = 12/27/18 (Thursday)
The number of business days in between Date1 and Date2 is 3 days (12/27, 12/28 and 12/31).
The below query will get the number of business days excluding weekends.
How do I also exclude holidays in this query ?
SELECT TO_CHAR( start_date, 'YYYY-MM-DD "("DY")"') AS start_date,
( TRUNC( end_date, 'IW' ) - TRUNC( start_date, 'IW' ) ) * 5 / 7
+ LEAST( TRUNC( end_date ) - TRUNC( end_date, 'IW' ) + 1, 5 )
- LEAST( TRUNC( start_date ) - TRUNC( start_date, 'IW' ), 5 )
AS Num_Week_Days
FROM table_name;
Thank you.
Taking the code in this previous answer and converting it from a function to a query gives:
Oracle Setup:
CREATE TABLE Holidays ( HolidayDates ) AS
SELECT DATE '2018-12-25' FROM DUAL UNION ALL
SELECT DATE '2018-12-31' FROM DUAL;
CREATE TABLE table_name ( start_date, end_date ) AS
SELECT DATE '2018-12-21', DATE '2018-12-26' FROM DUAL UNION ALL
SELECT DATE '2018-12-28', DATE '2019-01-01' FROM DUAL;
Query:
SELECT t.*,
( TRUNC( end_date, 'IW' ) - TRUNC( start_date, 'IW' ) ) * 5 / 7
+ LEAST( TRUNC( end_date ) - TRUNC( end_date, 'IW' ) + 1, 5 )
- LEAST( TRUNC( start_date ) - TRUNC( start_date, 'IW' ), 5 )
- ( SELECT COUNT(1)
FROM holidays
WHERE HolidayDates BETWEEN t.start_date AND t.end_date
-- Exclude any weekend holidays so we don't double count.
AND TRUNC( HolidayDates ) - TRUNC( HolidayDates, 'IW' ) <= 5
)
AS Num_Week_Days
FROM table_name t;
Output:
START_DATE | END_DATE | NUM_WEEK_DAYS
:--------- | :-------- | ------------:
21-DEC-18 | 26-DEC-18 | 3
28-DEC-18 | 01-JAN-19 | 2
01-JAN-19 | 07-JAN-19 | 5
db<>fiddle here