calculate hours based on business hours in Oracle SQL - sql

I am looking to calculate hours between a start and end of time of a task based on business hours. I have the following sample data:
TASK | START_TIME | END_TIME
A | 16-JAN-17 10:00 | 23-JAN-17 11:35
B | 18-JAN-17 17:53 | 19-JAN-17 08:00
C | 13-JAN-17 13:00 | 17-JAN-17 14:52
D | 21-JAN-17 10:00 | 30-JAN-17 08:52
and I need to work out the difference between the two but based on the following business hours:
Mon - Sat 08:00 - 18:00
I know how to write the calculation but not sure what I do to add the business hours into the calculation.
Any advice would be appreciated.

You can directly calculate the difference in hours:
SELECT task,
start_time,
end_time,
ROUND(
(
-- Calculate the full weeks difference from the start of ISO weeks.
( TRUNC( end_time, 'IW' ) - TRUNC( start_time, 'IW' ) ) * (10/24) * (6/7)
-- Add the full days for the final week.
+ LEAST( TRUNC( end_time ) - TRUNC( end_time, 'IW' ), 6 ) * (10/24)
-- Subtract the full days from the days of the week before the start date.
- LEAST( TRUNC( start_time ) - TRUNC( start_time, 'IW' ), 6 ) * (10/24)
-- Add the hours of the final day
+ LEAST( GREATEST( end_time - TRUNC( end_time ) - 8/24, 0 ), 10/24 )
-- Subtract the hours of the day before the range starts.
- LEAST( GREATEST( start_time - TRUNC( start_time ) - 8/24, 0 ), 10/24 )
)
-- Multiply to give minutes rather than fractions of full days.
* 24,
15 -- Number of decimal places
) AS work_day_hours_diff
FROM your_table;
Which, for your sample data:
CREATE TABLE your_table ( TASK, START_TIME, END_TIME ) AS
SELECT 'A', DATE '2017-01-16' + INTERVAL '10:00' HOUR TO MINUTE, DATE '2017-01-23' + INTERVAL '11:35' HOUR TO MINUTE FROM DUAL UNION ALL
SELECT 'B', DATE '2017-01-18' + INTERVAL '17:53' HOUR TO MINUTE, DATE '2017-01-19' + INTERVAL '08:00' HOUR TO MINUTE FROM DUAL UNION ALL
SELECT 'C', DATE '2017-01-13' + INTERVAL '13:00' HOUR TO MINUTE, DATE '2017-01-17' + INTERVAL '14:52' HOUR TO MINUTE FROM DUAL UNION ALL
SELECT 'D', DATE '2017-01-21' + INTERVAL '10:00' HOUR TO MINUTE, DATE '2017-01-30' + INTERVAL '08:52' HOUR TO MINUTE FROM DUAL;
Outputs (with the date format YYYY-MM-DD HH24:MI:SS (DY)):
TASK | START_TIME | END_TIME | WORK_DAY_HOURS_DIFF
:--- | :------------------------ | :------------------------ | ------------------:
A | 2017-01-16 10:00:00 (MON) | 2017-01-23 11:35:00 (MON) | 61.583333333333333
B | 2017-01-18 17:53:00 (WED) | 2017-01-19 08:00:00 (THU) | .116666666666667
C | 2017-01-13 13:00:00 (FRI) | 2017-01-17 14:52:00 (TUE) | 31.866666666666667
D | 2017-01-21 10:00:00 (SAT) | 2017-01-30 08:52:00 (MON) | 68.866666666666667
db<>fiddle here
Previous solution:
You can use a correlated hierarchical query to generate one row for each work day and then sum the hours for each day:
SELECT task,
COALESCE( SUM( end_time - start_time ), 0 ) * 24 AS total_hours
FROM (
SELECT task,
GREATEST( t.start_time, d.column_value + INTERVAL '8' HOUR ) AS start_time,
LEAST( t.end_time, d.column_value + INTERVAL '18' HOUR ) AS end_time
FROM your_table t
LEFT OUTER JOIN
TABLE(
CAST(
MULTISET(
SELECT TRUNC( t.start_time + LEVEL - 1 )
FROM DUAL
WHERE TRUNC( t.start_time + LEVEL - 1 ) - TRUNC( t.start_time + LEVEL - 1, 'iw' ) < 6
CONNECT BY TRUNC( t.start_time + LEVEL - 1 ) < t.end_time
) AS SYS.ODCIDATELIST
)
) d
ON ( t.end_time > d.column_value + INTERVAL '8' HOUR
AND t.start_time < d.column_value + INTERVAL '18' HOUR )
)
GROUP BY task;

My favorite of this problem is to use the build-in SCHEDULER SCHEDULE.
You have to create a function using DBMS_SCHEDULER.EVALUATE_CALENDAR_STRING
First, create some schedules for exceptions like public holidays, if required.
Here an example for US bank days:
BEGIN
DBMS_SCHEDULER.CREATE_SCHEDULE('NEW_YEARS_DAY', repeat_interval => 'FREQ=YEARLY;INTERVAL=1;BYDATE=0101');
DBMS_SCHEDULER.CREATE_SCHEDULE('MARTIN_LUTHER_KING_DAY', repeat_interval => 'FREQ=MONTHLY;BYMONTH=JAN;BYDAY=3 MON', comments => 'Third Monday of January');
DBMS_SCHEDULER.CREATE_SCHEDULE('WASHINGTONS_BIRTHDAY', repeat_interval => 'FREQ=MONTHLY;BYMONTH=FEB;BYDAY=3 MON', comments => 'Third Monday of February');
DBMS_SCHEDULER.CREATE_SCHEDULE('MEMORIAL_DAY', repeat_interval => 'FREQ=MONTHLY;BYMONTH=MAY;BYDAY=-1 MON', comments => 'Last Monday of May');
DBMS_SCHEDULER.CREATE_SCHEDULE('INDEPENDENCE_DAY', repeat_interval => 'FREQ=YEARLY;INTERVAL=1;BYDATE=0704');
DBMS_SCHEDULER.CREATE_SCHEDULE('CHRISTMAS_DAY', repeat_interval => 'FREQ=YEARLY;INTERVAL=1;BYDATE=1225');
DBMS_SCHEDULER.CREATE_SCHEDULE('SPRING_BREAK', repeat_interval => 'FREQ=YEARLY;BYDATE=0301+SPAN:7D');
END;
or another example for German bank days:
BEGIN
DBMS_SCHEDULER.CREATE_SCHEDULE('New_Year', repeat_interval => 'FREQ=YEARLY;BYDATE=0101');
DBMS_SCHEDULER.CREATE_SCHEDULE('Easter_Sunday', repeat_interval => 'FREQ=YEARLY;BYDATE=20150405, 20160327, 20170416, 20170416, 20180401, 20190421, 20200412', comments => 'Hard coded till 2020');
DBMS_SCHEDULER.CREATE_SCHEDULE('Good_Friday', repeat_interval => 'FREQ=YEARLY;BYDATE=20150405-2D, 20160327-2D, 20170416-2D, 20170416-2D, 20180401-2D, 20190421-2D, 20200412-2D', comments => '2 Days before Easter');
DBMS_SCHEDULER.CREATE_SCHEDULE('Easter_Monday', repeat_interval => 'FREQ=YEARLY;BYDATE=20150405+1D, 20160327+1D, 20170416+1D, 20170416+1D, 20180401+1D, 20190421+1D, 20200412+1D', comments => '1 Day after Easter');
DBMS_SCHEDULER.CREATE_SCHEDULE('Ascension_Day', repeat_interval => 'FREQ=YEARLY;BYDATE=20150405+39D,20160327+39D,20170416+39D,20170416+39D,20180401+39D,20190421+39D,20200412+39D', comments => '39 Days after Easter');
DBMS_SCHEDULER.CREATE_SCHEDULE('Pentecost_Monday', repeat_interval => 'FREQ=YEARLY;BYDATE=20150405+50D,20160327+50D,20170416+50D,20170416+50D,20180401+50D,20190421+50D,20200412+50D', comments => '50 Days after easter');
DBMS_SCHEDULER.CREATE_SCHEDULE('Repentance_and_Prayer', repeat_interval => 'FREQ=DAILY;BYDATE=1122-SPAN:7D;BYDAY=WED',
comments => 'Wednesday before November 23th, Buss- und Bettag');
-- alternative solution:
--DBMS_SCHEDULER.CREATE_SCHEDULE('Repentance_and_Prayer', repeat_interval => 'FREQ=MONTHLY;BYMONTH=NOV;BYDAY=3 WED',
-- comments => '3rd Wednesday in November');
DBMS_SCHEDULER.CREATE_SCHEDULE('Labor_Day', repeat_interval => 'FREQ=YEARLY;BYDATE=0501');
DBMS_SCHEDULER.CREATE_SCHEDULE('German_Unity_Day', repeat_interval => 'FREQ=YEARLY;BYDATE=1003');
DBMS_SCHEDULER.CREATE_SCHEDULE('Christmas', repeat_interval => 'FREQ=YEARLY;BYDATE=1225+SPAN:2D');
DBMS_SCHEDULER.CREATE_SCHEDULE('Christian_Celebration_Days', repeat_interval => 'FREQ=DAILY;INTERSECT=Easter_Sunday,Good_Friday,Easter_Monday,Ascension_Day,Pentecost_Monday,Repentance_and_Prayer,Christmas');
-- alternative solution:
-- DBMS_SCHEDULER.CREATE_SCHEDULE('Christian_Celebration_Days', repeat_interval => 'FREQ=Good_Friday;BYDAY=1 MON, 6 THU,8 MON');
DBMS_SCHEDULER.CREATE_SCHEDULE('Political_Holidays', repeat_interval => 'FREQ=DAILY;INTERSECT=New_Year,Labor_Day,German_Unity_Day');
END;
/
See syntax for calendar here: Calendaring Syntax
Then create a function similar to this:
CREATE OR REPLACE FUNCTION GetBusinessHours(start_time IN TIMESTAMP, end_time IN TIMESTAMP) RETURN INTERVAL DAY TO SECOND AS
next_run_date TIMESTAMP := start_time;
duration INTERVAL DAY(3) TO SECOND(0) := INTERVAL '0' HOUR;
BEGIN
LOOP
DBMS_SCHEDULER.EVALUATE_CALENDAR_STRING('FREQ=HOURLY;INTERVAL=1;BYHOUR=8,9,10,11,13,14,15,16,17;BYDAY=MON,TUE,WED,THU,FRI,SAT; EXCLUDE=NEW_YEARS_DAY,MARTIN_LUTHER_KING_DAY,WASHINGTONS_BIRTHDAY,MEMORIAL_DAY,INDEPENDENCE_DAY,CHRISTMAS_DAY,SPRING_BREAK', NULL, next_run_date, next_run_date);
duration := duration + INTERVAL '1' HOUR;
EXIT WHEN next_run_date >= end_time;
END LOOP;
RETURN duration;
END;
CREATE OR REPLACE FUNCTION GetBusinessStart(start_time IN TIMESTAMP, end_time IN TIMESTAMP) RETURN TIMESTAMP AS
next_run_date TIMESTAMP := start_time;
BEGIN
DBMS_SCHEDULER.EVALUATE_CALENDAR_STRING('FREQ=HOURLY;INTERVAL=1;BYHOUR=8,9,10,11,13,14,15,16,17;BYDAY=MON,TUE,WED,THU,FRI,SAT; EXCLUDE=NEW_YEARS_DAY,MARTIN_LUTHER_KING_DAY,WASHINGTONS_BIRTHDAY,MEMORIAL_DAY,INDEPENDENCE_DAY,CHRISTMAS_DAY,SPRING_BREAK', NULL, next_run_date, next_run_date);
RETURN next_run_date;
END;
CREATE OR REPLACE FUNCTION GetBusinessEnd(start_time IN TIMESTAMP, end_time IN TIMESTAMP) RETURN TIMESTAMP AS
next_run_date TIMESTAMP := start_time;
BEGIN
LOOP
DBMS_SCHEDULER.EVALUATE_CALENDAR_STRING('FREQ=HOURLY;INTERVAL=1;BYHOUR=8,9,10,11,13,14,15,16,17;BYDAY=MON,TUE,WED,THU,FRI,SAT; EXCLUDE=NEW_YEARS_DAY,MARTIN_LUTHER_KING_DAY,WASHINGTONS_BIRTHDAY,MEMORIAL_DAY,INDEPENDENCE_DAY,CHRISTMAS_DAY,SPRING_BREAK', NULL, next_run_date, next_run_date);
EXIT WHEN next_run_date >= end_time;
END LOOP;
RETURN next_run_date;
END;
If you don't have to consider pubic holidays, just skip EXCLUDE=... part.
Then you can use the function in your query:
SELECT TASK,
GetBusinessStart(START_TIME, END_TIME),
GetBusinessEnd(START_TIME, END_TIME),
GetBusinessHours(START_TIME, END_TIME)
FROM ...;
Note, the function would need some fine-tuning in case START_TIME and END_TIME fall both into the same non-working day.

Related

Split date range in weeks in oracle SQL

I need to split the following data in Oracle SQL:
WITH sample_data AS
(SELECT DATE '2020-12-16' Start_Date, DATE '2021-01-07' End_Date FROM DUAL)
in week ranges for every working week(from Monday to Friday) of this given period. The final result it should look like this:
NEW_STARTDATE NEW_END_DATE
2020-12-16 2020-12-18
2020-12-21 2020-12-25
2020-12-28 2021-01-01
2021-01-04 2021-01-07
So in this example, the first row starts with the initial start date (2020-12-16) which is on Wednesday and continues with the new end date(2020-12-18) which is the next Friday, and so on with ranges of working weeks until the actual end date of this period.
You can use:
WITH sample_data ( start_date, end_date ) AS (
SELECT DATE '2020-12-16', DATE '2021-01-07' FROM DUAL
),
weeks ( start_date, week_start, week_end, end_date ) AS (
SELECT start_date,
TRUNC( start_date, 'IW' ),
LEAST( TRUNC( start_date, 'IW' ) + INTERVAL '4' DAY, end_date ),
end_date
FROM sample_data
UNION ALL
SELECT start_date,
week_start + INTERVAL '7' DAY,
LEAST( week_end + INTERVAL '7' DAY, end_date ),
end_date
FROM weeks
WHERE week_start + INTERVAL '7' DAY <= end_date
)
SELECT GREATEST( week_start, start_date ) AS new_start_date,
week_end AS new_end_date
FROM weeks
WHERE GREATEST( week_start, start_date ) <= week_end;
Which outputs (where the NLS_DATE_FORMAT is YYYY-MM-DD (DY)):
NEW_START_DATE
NEW_END_DATE
2020-12-16 (WED)
2020-12-18 (FRI)
2020-12-21 (MON)
2020-12-25 (FRI)
2020-12-28 (MON)
2021-01-01 (FRI)
2021-01-04 (MON)
2021-01-07 (THU)
db<>fiddle here
Here is one way - compute the Monday dates for the ISO week of the input dates (start_date and end_date), while also keeping track of which is the first and which is the last such Monday in the same hierarchical (connect by) query. Then produce the requested output; for the first week, check that the start_date is not a Saturday or a Sunday (if it is, that "first week" should not produce a row in the output); that is done in the where clause. This is illustrated in the sample dates I used for testing - the input "start date" is a Saturday, so the first "new start date" is the following Monday.
with
sample_data (start_date, end_date) as (
select date '2020-12-12', date '2021-01-02' from dual
)
, mondays (dt, rn, mx) as (
select trunc(start_date, 'iw') + 7 * (level - 1), level, max(level) over ()
from sample_data
connect by level <= 1 + (trunc(end_date, 'iw') - trunc(start_date, 'iw'))/7
)
select case rn when 1 then greatest(start_date, dt)
else dt end as new_start_date,
case rn when mx then least(dt + 4, end_date)
else dt + 4 end as new_end_date
from sample_data cross join mondays
where rn >= 2 or start_date <= dt + 4
;
NEW_START_DATE NEW_END_DATE
--------------- ---------------
MON 14-DEC-2020 FRI 18-DEC-2020
MON 21-DEC-2020 FRI 25-DEC-2020
MON 28-DEC-2020 FRI 01-JAN-2021

Oracle query to Exclude weekends, and 6PM to 9PM

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

Return time between two dates except weekends

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

Return date value from nth week of month in SQL Developer without creating function

I am writing a select query which has to return date.
Eg: if i need 4th week of friday for current month, then i am expecting 2019-09-27.
But my challenge is i don't want to use any function.
I have tried below code
select next_day( (trunc(to_date('nov','mon'),'mm')-1), 'fri' ) + 21 from dual
Above code gives last week's date, but i am expecting any week's date.
It depends on how you determine which week is in a month.
If want the first week to be the iso-week where any day of the week is in the month:
TRUNC( SYSDATE, 'MM' ) you will get the first day of the month.
TRUNC( first_day_of_month, 'IW' ) will truncate the date to the start of the iso-week (i.e. Monday).
NEXT_DAY( monday_of_week_including_first_day_of_month, 'FRIDAY' ) will find the friday in the iso-week containing the first day of the month.
If you want the Friday of the 4th iso-week then add 3 weeks:
I.e.:
SELECT NEXT_DAY( TRUNC( TRUNC( SYSDATE, 'MM' ), 'IW' ) - INTERVAL '1' DAY, 'FRIDAY' ) + 3 * INTERVAL '7' DAY AS friday4,
NEXT_DAY( TRUNC( TRUNC( SYSDATE, 'MM' ), 'IW' ) - INTERVAL '1' DAY, 'MONDAY' ) + 0 * INTERVAL '7' DAY AS monday1,
NEXT_DAY( TRUNC( TRUNC( SYSDATE, 'MM' ), 'IW' ) - INTERVAL '1' DAY, 'SUNDAY' ) + 0 * INTERVAL '7' DAY AS sunday1
FROM DUAL;
FRIDAY4 | MONDAY1 | SUNDAY1
:-------- | :-------- | :--------
20-SEP-19 | 26-AUG-19 | 01-SEP-19
If you want to use iso-weeks but start where the first week of the month is the iso-week which contains the first Thursday of the month (as is used to calculate when a week is within an iso-year) then you need to use some additional logic to determine this:
SELECT NEXT_DAY(
TRUNC( TRUNC( SYSDATE, 'MM' ), 'IW' )
+ CASE
WHEN TRUNC( TRUNC( SYSDATE, 'MM' ), 'IW' ) + INTERVAL '3' DAY >= TRUNC( SYSDATE, 'MM' )
THEN INTERVAL '-1' DAY
ELSE INTERVAL '6' DAY
END,
'FRIDAY'
) + 3 * INTERVAL '7' DAY
AS friday4,
NEXT_DAY(
TRUNC( TRUNC( SYSDATE, 'MM' ), 'IW' )
+ CASE
WHEN TRUNC( TRUNC( SYSDATE, 'MM' ), 'IW' ) + INTERVAL '3' DAY >= TRUNC( SYSDATE, 'MM' )
THEN INTERVAL '-1' DAY
ELSE INTERVAL '6' DAY
END,
'MONDAY'
) + 0 * INTERVAL '7' DAY
AS monday4,
NEXT_DAY(
TRUNC( TRUNC( SYSDATE, 'MM' ), 'IW' )
+ CASE
WHEN TRUNC( TRUNC( SYSDATE, 'MM' ), 'IW' ) + INTERVAL '3' DAY >= TRUNC( SYSDATE, 'MM' )
THEN INTERVAL '-1' DAY
ELSE INTERVAL '6' DAY
END,
'SUNDAY'
) + 0 * INTERVAL '7' DAY
AS sunday4
FROM DUAL;
FRIDAY4 | MONDAY1 | SUNDAY1
:-------- | :-------- | :--------
27-SEP-19 | 02-SEP-19 | 08-SEP-19
If you want start the counting from the first day of the month (regardless of which day of the week it is):
SELECT NEXT_DAY( TRUNC( SYSDATE, 'MM' ) - INTERVAL '1' DAY, 'FRIDAY' ) + 3 * INTERVAL '7' DAY AS friday4,
NEXT_DAY( TRUNC( SYSDATE, 'MM' ) - INTERVAL '1' DAY, 'MONDAY' ) + 0 * INTERVAL '7' DAY AS monday1,
NEXT_DAY( TRUNC( SYSDATE, 'MM' ) - INTERVAL '1' DAY, 'SUNDAY' ) + 0 * INTERVAL '7' DAY AS sunday1
FROM DUAL;
FRIDAY4 | MONDAY1 | SUNDAY1
:-------- | :-------- | :--------
27-SEP-19 | 02-SEP-19 | 01-SEP-19
db<>fiddle here
Here's one option; calendar CTE I created refers to this September.
SQL> set ver off
SQL>
SQL> with calendar as
2 (select trunc(sysdate, 'mm') + level - 1 datum
3 from dual
4 connect by level <= 30
5 ),
6 inter as
7 (select datum,
8 to_char(datum, 'w') week_in_month,
9 to_char(datum, 'dy', 'nls_date_language=english') day
10 from calendar
11 )
12 select datum
13 from inter
14 where day = '&day'
15 and week_in_month = &week;
Enter value for day: tue
Enter value for week: 2
DATUM
----------
10.09.2019
SQL> /
Enter value for day: fri
Enter value for week: 3
DATUM
----------
20.09.2019
SQL>

Calculate time diffrence in SQL with shifts

i have two table
the first table contains the record of a ticket with start date and end date
start_date | End_Date
21-02-2017 07:52:32 | 22-02-2017 09:56:32
21-02-2017 09:52:32 | 23-02-2017 17:52:32
the second table contains the details of the weekly shift:
shift_day | Start_Time | End_Time
MON 9:00 18:00
TUE 10:00 19:00
WED 9:00 18:00
THU 10:00 19:00
FRI 9:00 18:00
I am looking to get the time difference in the first table which will only include the time as per the second table.
Use a recursive sub-query factoring clause to generate each day within your time ranges and then correlate that with your shifts to restrict the time for each day to be within the shift hours and then aggregate to get the total:
Oracle 18 Setup:
CREATE TABLE times ( start_date, End_Date ) AS
SELECT DATE '2017-02-21' + INTERVAL '07:52:32' HOUR TO SECOND,
DATE '2017-02-22' + INTERVAL '09:56:32' HOUR TO SECOND
FROM DUAL
UNION ALL
SELECT DATE '2017-02-21' + INTERVAL '09:52:32' HOUR TO SECOND,
DATE '2017-02-23' + INTERVAL '17:52:32' HOUR TO SECOND
FROM DUAL;
CREATE TABLE weekly_shifts ( shift_day, Start_Time, End_Time ) AS
SELECT 'MON', INTERVAL '09:00' HOUR TO MINUTE, INTERVAL '18:00' HOUR TO MINUTE FROM DUAL UNION ALL
SELECT 'TUE', INTERVAL '10:00' HOUR TO MINUTE, INTERVAL '19:00' HOUR TO MINUTE FROM DUAL UNION ALL
SELECT 'WED', INTERVAL '09:00' HOUR TO MINUTE, INTERVAL '18:00' HOUR TO MINUTE FROM DUAL UNION ALL
SELECT 'THU', INTERVAL '10:00' HOUR TO MINUTE, INTERVAL '19:00' HOUR TO MINUTE FROM DUAL UNION ALL
SELECT 'FRI', INTERVAL '09:00' HOUR TO MINUTE, INTERVAL '18:00' HOUR TO MINUTE FROM DUAL;
Query 1:
WITH days ( id, start_date, day_start, day_end, end_date ) AS (
SELECT ROWNUM,
start_date,
start_date,
LEAST( TRUNC( start_date ) + INTERVAL '1' DAY, end_date ),
end_date
FROM times
UNION ALL
SELECT id,
start_date,
day_end,
LEAST( day_end + INTERVAL '1' DAY, end_date ),
end_date
FROM days
WHERE day_end < end_date
)
SELECT start_date,
end_date,
SUM( shift_end - shift_start ) AS days_worked_on_shift
FROM (
SELECT ID,
start_date,
end_date,
GREATEST( day_start, TRUNC( day_start ) + start_time ) AS shift_start,
LEAST( day_end, TRUNC( day_start ) + end_time ) AS shift_end
FROM days d
INNER JOIN
weekly_shifts w
ON ( TO_CHAR( d.day_start, 'DY' ) = w.shift_day )
)
GROUP BY id, start_date, end_date;
Result:
START_DATE END_DATE DAYS_WORKED_ON_SHIFT
------------------- ------------------- --------------------
2017-02-21 07:52:32 2017-02-22 09:56:32 0.414259259259259259
2017-02-21 09:52:32 2017-02-23 17:52:32 1.078148148148148148