Oracle splitting date range into day and custom time intervals - sql

I am trying to split work shift date range into different date and time interval. Already found some answers, but still trying to split by time interval. Thanks in advance for any ideas or tips.
Each day need to be split out separately
Day Shift is 0600-22:00
Night Shift is 2200-0600
Range1:
2022-02-03 08:40 to 2022-02-04 10:07
Split Rows:
2022-02-03 08:40 to 2022-02-03 22:00 DAY
2022-02-03 22:00 to 2022-02-04 06:00 NIGHT
2022-02-04 06:00 to 2022-02-04 10:07 DAY
Range2:
2022-02-03 08:40 to 2022-02-04 02:07
Split Rows:
2022-02-03 08:40 to 2022-02-03 22:00 DAY
2022-02-03 22:00 to 2022-02-04 02:07 NIGHT
Range3:
2022-02-03 04:40 to 2022-02-04 02:07
Split Rows:
2022-02-03 04:40 to 2022-02-03 06:00 NIGHT
2022-02-03 08:40 to 2022-02-03 22:00 DAY
2022-02-03 22:00 to 2022-02-04 02:07 NIGHT
Sample data (Using lateral query is not working yet. I will update, if i figure it out) Also trying to see whether i can split them per hour and sum up later as in here splitting time into hour intervals
WITH SAMPLE AS (
SELECT
1 AS ID,
TO_DATE('2022-02-03 08:40', 'YYYY-MM-DD HH24:MI') AS STARTDATE,
TO_DATE('2022-02-04 10:07', 'YYYY-MM-DD HH24:MI') AS ENDDATE
FROM
DUAL
UNION ALL
SELECT
2 AS ID,
TO_DATE('2022-02-03 08:40', 'YYYY-MM-DD HH24:MI') AS STARTDATE,
TO_DATE('2022-02-04 02:07', 'YYYY-MM-DD HH24:MI') AS ENDDATE
FROM
DUAL
UNION ALL
SELECT
3 AS ID,
TO_DATE('2022-02-03 04:40', 'YYYY-MM-DD HH24:MI') AS STARTDATE,
TO_DATE('2022-02-04 02:07', 'YYYY-MM-DD HH24:MI') AS ENDDATE
FROM
DUAL
)
SELECT
ID,
L.STARTDATE,
L.ENDDATE
FROM
SAMPLE,
LATERAL (
SELECT
CASE LEVEL
WHEN 1 THEN STARTDATE
ELSE TRUNC(STARTDATE) + LEVEL - 1
END STARTDATE,
LEAST(TRUNC(STARTDATE) + LEVEL - 1 / 24 / 60, ENDDATE) ENDDATE
FROM
DUAL
CONNECT BY
TRUNC(STARTDATE) + LEVEL - 1 <= ENDDATE
) L;

You can use:
WITH SAMPLE (ID, startdate, enddate ) AS (
SELECT 1,
TO_DATE('2022-02-03 08:40', 'YYYY-MM-DD HH24:MI'),
TO_DATE('2022-02-04 10:07', 'YYYY-MM-DD HH24:MI')
FROM DUAL
UNION ALL
SELECT 2,
TO_DATE('2022-02-03 08:40', 'YYYY-MM-DD HH24:MI'),
TO_DATE('2022-02-04 02:07', 'YYYY-MM-DD HH24:MI')
FROM DUAL
UNION ALL
SELECT 3,
TO_DATE('2022-02-03 04:40', 'YYYY-MM-DD HH24:MI'),
TO_DATE('2022-02-04 02:07', 'YYYY-MM-DD HH24:MI')
FROM DUAL
)
SELECT ID,
o.type,
GREATEST(L.start_date + o.start_offset, s.startdate) AS startdate,
LEAST(L.start_date + o.end_offset, s.enddate) AS enddate
FROM SAMPLE s
CROSS JOIN LATERAL (
SELECT TRUNC(startdate - INTERVAL '6' HOUR)
+ INTERVAL '6' HOUR
+ LEVEL - 1 AS start_date
FROM DUAL
CONNECT BY
TRUNC(startdate - INTERVAL '6' HOUR)
+ INTERVAL '6' HOUR
+ LEVEL - 1
< ENDDATE
) L
CROSS JOIN (
SELECT 'DAY' AS type,
INTERVAL '0' HOUR AS start_offset,
INTERVAL '16' HOUR AS end_offset
FROM DUAL
UNION ALL
SELECT 'NIGHT' AS type,
INTERVAL '16' HOUR AS start_offset,
INTERVAL '24' HOUR AS end_offset
FROM DUAL
) o
WHERE L.start_date + o.start_offset < s.enddate
AND L.start_date + o.end_offset > s.startdate;
Which outputs:
ID
TYPE
STARTDATE
ENDDATE
1
DAY
2022-02-03 08:40:00
2022-02-03 22:00:00
1
NIGHT
2022-02-03 22:00:00
2022-02-04 06:00:00
1
DAY
2022-02-04 06:00:00
2022-02-04 10:07:00
2
DAY
2022-02-03 08:40:00
2022-02-03 22:00:00
2
NIGHT
2022-02-03 22:00:00
2022-02-04 02:07:00
3
NIGHT
2022-02-03 04:40:00
2022-02-03 06:00:00
3
DAY
2022-02-03 06:00:00
2022-02-03 22:00:00
3
NIGHT
2022-02-03 22:00:00
2022-02-04 02:07:00
db<>fiddle here

Step 1.
First of all you need to generate all possible intervals. You can do it using simple lateral. To make it easier and more agile, I'll save day shifts in the INTERVALS CTE:
DBFiddle
WITH SAMPLE AS (
SELECT
1 AS ID,
TO_DATE('2022-02-03 08:40', 'YYYY-MM-DD HH24:MI') AS STARTDATE,
TO_DATE('2022-02-04 10:07', 'YYYY-MM-DD HH24:MI') AS ENDDATE
FROM
DUAL
UNION ALL
SELECT
2 AS ID,
TO_DATE('2022-02-03 08:40', 'YYYY-MM-DD HH24:MI') AS STARTDATE,
TO_DATE('2022-02-04 02:07', 'YYYY-MM-DD HH24:MI') AS ENDDATE
FROM
DUAL
UNION ALL
SELECT
3 AS ID,
TO_DATE('2022-02-03 04:40', 'YYYY-MM-DD HH24:MI') AS STARTDATE,
TO_DATE('2022-02-04 02:07', 'YYYY-MM-DD HH24:MI') AS ENDDATE
FROM
DUAL
)
,intervals(i_name,i_start,i_end) as (
select 'Day Shift' ,'0600', '2159' from dual union all
select 'Night Shift','2200', '0559' from dual
)
SELECT
s.*
,days.*
,ints.*
FROM
SAMPLE s
,lateral(
select trunc(startdate) + n as n_day
from xmltable(
'-1 to xs:integer(.)'
passing trunc(trunc(enddate) - trunc(startdate))
columns n int path '.'
)
) days
,lateral(
select
i.*
,to_date(to_char(n_day,'yyyy-mm-dd ')||i_start, 'yyyy-mm-dd hh24mi')
as dtm_start
,to_date(to_char(n_day,'yyyy-mm-dd ')||i_end , 'yyyy-mm-dd hh24mi')
+ case when i_end < i_start then 1 else 0 end -- +1 if it ends on next day
as dtm_end
from intervals i
) ints
order by id,startdate,n_day,dtm_start;
Results:
ID STARTDATE ENDDATE N_DAY I_NAME I_ST I_EN DTM_START DTM_END
--- ------------------- ------------------- ---------- ----------- ---- ---- ------------------- -------------------
1 2022-02-03 08:40:00 2022-02-04 10:07:00 2022-02-02 Day Shift 0600 2159 2022-02-02 06:00:00 2022-02-02 21:59:00
1 2022-02-03 08:40:00 2022-02-04 10:07:00 2022-02-02 Night Shift 2200 0559 2022-02-02 22:00:00 2022-02-03 05:59:00
1 2022-02-03 08:40:00 2022-02-04 10:07:00 2022-02-03 Day Shift 0600 2159 2022-02-03 06:00:00 2022-02-03 21:59:00
1 2022-02-03 08:40:00 2022-02-04 10:07:00 2022-02-03 Night Shift 2200 0559 2022-02-03 22:00:00 2022-02-04 05:59:00
1 2022-02-03 08:40:00 2022-02-04 10:07:00 2022-02-04 Day Shift 0600 2159 2022-02-04 06:00:00 2022-02-04 21:59:00
1 2022-02-03 08:40:00 2022-02-04 10:07:00 2022-02-04 Night Shift 2200 0559 2022-02-04 22:00:00 2022-02-05 05:59:00
2 2022-02-03 08:40:00 2022-02-04 02:07:00 2022-02-02 Day Shift 0600 2159 2022-02-02 06:00:00 2022-02-02 21:59:00
2 2022-02-03 08:40:00 2022-02-04 02:07:00 2022-02-02 Night Shift 2200 0559 2022-02-02 22:00:00 2022-02-03 05:59:00
2 2022-02-03 08:40:00 2022-02-04 02:07:00 2022-02-03 Day Shift 0600 2159 2022-02-03 06:00:00 2022-02-03 21:59:00
2 2022-02-03 08:40:00 2022-02-04 02:07:00 2022-02-03 Night Shift 2200 0559 2022-02-03 22:00:00 2022-02-04 05:59:00
2 2022-02-03 08:40:00 2022-02-04 02:07:00 2022-02-04 Day Shift 0600 2159 2022-02-04 06:00:00 2022-02-04 21:59:00
2 2022-02-03 08:40:00 2022-02-04 02:07:00 2022-02-04 Night Shift 2200 0559 2022-02-04 22:00:00 2022-02-05 05:59:00
3 2022-02-03 04:40:00 2022-02-04 02:07:00 2022-02-02 Day Shift 0600 2159 2022-02-02 06:00:00 2022-02-02 21:59:00
3 2022-02-03 04:40:00 2022-02-04 02:07:00 2022-02-02 Night Shift 2200 0559 2022-02-02 22:00:00 2022-02-03 05:59:00
3 2022-02-03 04:40:00 2022-02-04 02:07:00 2022-02-03 Day Shift 0600 2159 2022-02-03 06:00:00 2022-02-03 21:59:00
3 2022-02-03 04:40:00 2022-02-04 02:07:00 2022-02-03 Night Shift 2200 0559 2022-02-03 22:00:00 2022-02-04 05:59:00
3 2022-02-03 04:40:00 2022-02-04 02:07:00 2022-02-04 Day Shift 0600 2159 2022-02-04 06:00:00 2022-02-04 21:59:00
3 2022-02-03 04:40:00 2022-02-04 02:07:00 2022-02-04 Night Shift 2200 0559 2022-02-04 22:00:00 2022-02-05 05:59:00
Note, that since I have specified time intervals in hhmi (ie hh24mi in oracle datetime format models), we need to ignore seconds.
As you can see lateral(...) days generates all dates between one day before startdate (to cover the end of night shift) and enddate.
Then ints generates day and night shifts for all those days.
Step 2.
So the only thing you need now is to filter them and correct start time and end time of partial intervals.
These 2 predicates filters them:
and ints.dtm_end >= s.startdate
and ints.dtm_start <= s.enddate
and these 2 lines return correct start and end time:
greatest(s.startdate, ints.dtm_start) as startdate,
least (s.enddate , ints.dtm_end ) as enddate,
So full solution: DBFiddle
WITH SAMPLE AS (
SELECT
1 AS ID,
TO_DATE('2022-02-03 08:40', 'YYYY-MM-DD HH24:MI') AS STARTDATE,
TO_DATE('2022-02-04 10:07', 'YYYY-MM-DD HH24:MI') AS ENDDATE
FROM
DUAL
UNION ALL
SELECT
2 AS ID,
TO_DATE('2022-02-03 08:40', 'YYYY-MM-DD HH24:MI') AS STARTDATE,
TO_DATE('2022-02-04 02:07', 'YYYY-MM-DD HH24:MI') AS ENDDATE
FROM
DUAL
UNION ALL
SELECT
3 AS ID,
TO_DATE('2022-02-03 04:40', 'YYYY-MM-DD HH24:MI') AS STARTDATE,
TO_DATE('2022-02-04 02:07', 'YYYY-MM-DD HH24:MI') AS ENDDATE
FROM
DUAL
)
,intervals(i_name,i_start,i_end) as (
select 'Day Shift' ,'0600', '2159' from dual union all
select 'Night Shift','2200', '0559' from dual
)
SELECT
s.id,
greatest(s.startdate, ints.dtm_start) as startdate,
least (s.enddate , ints.dtm_end ) as enddate,
i_name,
i_start,
i_end
FROM
SAMPLE s
,lateral(
select trunc(startdate) + n as n_day
from xmltable(
'-1 to xs:integer(.)'
passing trunc(trunc(enddate) - trunc(startdate))
columns n int path '.'
)
) days
,lateral(
select
i.*
,to_date(to_char(n_day,'yyyy-mm-dd ')||i_start, 'yyyy-mm-dd hh24mi')
as dtm_start
,to_date(to_char(n_day,'yyyy-mm-dd ')||i_end , 'yyyy-mm-dd hh24mi')
+ case when i_end < i_start then 1 else 0 end -- +1 if it ends on next day
as dtm_end
from intervals i
) ints
where 1=1
-- filter `ints`:
and ints.dtm_end >= s.startdate
and ints.dtm_start <= s.enddate
order by 1,2,3;
Results:
ID STARTDATE ENDDATE I_NAME I_ST I_EN
---------- ---------------- ---------------- ----------- ---- ----
1 2022-02-03 08:40 2022-02-03 21:59 Day Shift 0600 2159
1 2022-02-03 22:00 2022-02-04 05:59 Night Shift 2200 0559
1 2022-02-04 06:00 2022-02-04 10:07 Day Shift 0600 2159
2 2022-02-03 08:40 2022-02-03 21:59 Day Shift 0600 2159
2 2022-02-03 22:00 2022-02-04 02:07 Night Shift 2200 0559
3 2022-02-03 04:40 2022-02-03 05:59 Night Shift 2200 0559
3 2022-02-03 06:00 2022-02-03 21:59 Day Shift 0600 2159
3 2022-02-03 22:00 2022-02-04 02:07 Night Shift 2200 0559
8 rows selected.
Obviously, you can remove i_start and i_end columns from the output. I showed them just to highlight day/night shift intervals.

Related

Oracle sql create agenda

I have a table with interval dates and times. Can i create a full list with this data?
Table example:
Start_Date, End_Date, Start_Time, End_Time, Interval
01-jun-2021 02-jun-2021 08:00 10:00 30
03-jun-2021 04-jun-2021 10:00 12:00 15
Result:
01-jun-2021 08:00
01-jun-2021 08:30
01-jun-2021 09:00
01-jun-2021 09:30
02-jun-2021 08:00
02-jun-2021 08:30
02-jun-2021 09:00
02-jun-2021 09:30
03-jun-2021 10:00
03-jun-2021 10:15
03-jun-2021 10:30
03-jun-2021 11:00
03-jun-2021 11:15
03-jun-2021 11:30
03-jun-2021 11:45
04-jun-2021 10:00
04-jun-2021 10:15
04-jun-2021 10:30
04-jun-2021 11:00
04-jun-2021 11:15
04-jun-2021 11:30
04-jun-2021 11:45
Thanks.
This is a handy place to use a recursive CTE:
with cte (start_date, end_date, interval) as (
select to_date(start_date||start_time, 'DD-Mon-YYYYHH24:MI'), to_date(end_date||end_time, 'DD-Mon-YYYYHH24:MI'), interval
from t
union all
select cte.start_date + cte.interval * interval '1' minute, end_date, interval
from cte
where cte.start_date < end_date
)
select cast(start_date as timestamp)
from cte
order by start_date;
Here is a db<>fiddle.
You can use a recursive CTE, but the logic has to skip to the next day when you reach the end time; so this works:
with rcte (date_time, end_date, start_int, end_int, step_int) as (
select
start_date + to_dsinterval('0 ' || start_time || ':00'),
end_date,
to_dsinterval('0 ' || start_time || ':00'),
to_dsinterval('0 ' || end_time || ':00'),
interval * interval '1' minute
from your_table
union all
select
case
when date_time + step_int < trunc(date_time) + end_int
then date_time + step_int
else trunc(date_time) + interval '1' day + start_int
end,
end_date,
start_int,
end_int,
step_int
from rcte
where date_time + step_int < end_date + end_int
)
select date_time
from rcte
order by date_time
DATE_TIME
-------------------
2021-06-01 08:00:00
2021-06-01 08:30:00
2021-06-01 09:00:00
2021-06-01 09:30:00
2021-06-02 08:00:00
2021-06-02 08:30:00
2021-06-02 09:00:00
2021-06-02 09:30:00
2021-06-03 10:00:00
2021-06-03 10:15:00
2021-06-03 10:30:00
2021-06-03 10:45:00
2021-06-03 11:00:00
2021-06-03 11:15:00
2021-06-03 11:30:00
2021-06-03 11:45:00
2021-06-04 10:00:00
2021-06-04 10:15:00
2021-06-04 10:30:00
2021-06-04 10:45:00
2021-06-04 11:00:00
2021-06-04 11:15:00
2021-06-04 11:30:00
2021-06-04 11:45:00
db<>fiddle showing the anchor member including converting the times and interval to real day to second intervals types for later use; the anchor and recursive members with all the intermediate columns; and finally just this version with a single column.
You can format the resulting date value however you want, of course.

Oracle get last weekday Mon-Fri

I would like to obtain the last weekday.
If it's Tues to Sat, it will be the previous day. If it's Sunday or Monday, it will be Friday.
So far, I've tried this, but I'm struggling to get the desired output.
SELECT
level AS dow,
trunc(sysdate, 'D') + level day,
to_char(trunc(sysdate, 'D') + level, 'Day') AS day_week,
CASE
WHEN to_char(trunc(sysdate, 'D') + level, 'Day') IN (
'Sunday',
'Monday'
) THEN
trunc(sysdate - 2, 'IW') + 4
ELSE
sysdate - 1
END calculation
FROM
dual
CONNECT BY
level <= 7;
This solution works independent of language and territory:
SELECT date_value,
date_value - CASE TRUNC(date_value) - TRUNC(date_value, 'IW')
WHEN 0 THEN 3 -- Monday
WHEN 6 THEN 2 -- Sunday
ELSE 1 -- Tuesday to Saturday
END AS previous_weekday
FROM table_name;
Which, for the sample data:
CREATE TABLE table_name (date_value) AS
SELECT TRUNC(sysdate - LEVEL + 1)
FROM DUAL
CONNECT BY LEVEL <= 7;
Outputs (with the date format YYYY-MM-DD HH24:MI:SS (DY)):
DATE_VALUE
PREVIOUS_WEEKDAY
2021-07-20 00:00:00 (TUE)
2021-07-19 00:00:00 (MON)
2021-07-19 00:00:00 (MON)
2021-07-16 00:00:00 (FRI)
2021-07-18 00:00:00 (SUN)
2021-07-16 00:00:00 (FRI)
2021-07-17 00:00:00 (SAT)
2021-07-16 00:00:00 (FRI)
2021-07-16 00:00:00 (FRI)
2021-07-15 00:00:00 (THU)
2021-07-15 00:00:00 (THU)
2021-07-14 00:00:00 (WED)
2021-07-14 00:00:00 (WED)
2021-07-13 00:00:00 (TUE)
db<>fiddle here

Generate Appointment Time Slots

I have branch timing view as follows. Date will change based on sysdate
TIME_FROM TIME_TO
09/08/2020 07:00:00 AM 09/08/2020 02:00:00 PM
09/08/2020 04:00:00 PM 09/08/2020 06:00:00 PM
I want to generate appointment slots with 60 minutes duration like the following. 60 minutes is variable and i will pass it as parameter. I want to get the query result like this
7.00 AM
8.00 AM
9.00 AM
10.00 AM
11.00 AM
12.00 PM
1.00 PM
4.00 PM
5.00 PM
Exclude shift ending times( 2.00 PM and 06:00 PM) as no point in including them
Here is a recursive CTE approach:
with cte (time_from, time_to, lev) as (
select time_from, time_to, 1 as lev
from t
union all
select time_from + interval '1' hour, time_to, lev + 1
from cte
where time_from < time_to - interval '1' hour
)
select time_from
from cte;
And a db<>fiddle.
Another, non-recursive CTE approach, might be
SQL> with test (time_from, time_to) as
2 (select to_date('09.08.2020 07:00', 'dd.mm.yyyy hh24:mi'),
3 to_date('09.08.2020 14:00', 'dd.mm.yyyy hh24:mi')
4 from dual union all
5 select to_date('09.08.2020 16:00', 'dd.mm.yyyy hh24:mi'),
6 to_date('09.08.2020 18:00', 'dd.mm.yyyy hh24:mi')
7 from dual
8 )
9 select time_from + ((column_value - 1) * 60) / (24 * 60) time
10 from test cross join
11 table(cast(multiset(select level from dual
12 connect by level <= (time_to - time_from) * 24
13 ) as sys.odcinumberlist));
TIME
----------------
09.08.2020 07:00
09.08.2020 08:00
09.08.2020 09:00
09.08.2020 10:00
09.08.2020 11:00
09.08.2020 12:00
09.08.2020 13:00
09.08.2020 16:00
09.08.2020 17:00
9 rows selected.
SQL>
These are dates with times - you'd apply TO_CHAR with desired format mask to display it as you want, e.g.
select to_char(time_from + ((column_value - 1) * 60) / (24 * 60), 'hh:mi am') time
which results in
TIME
--------
07:00 AM
08:00 AM
09:00 AM
10:00 AM
11:00 AM
12:00 PM
01:00 PM
04:00 PM
05:00 PM
9 rows selected.
If you want to use "number of minutes" as parameter, then modify lines #9 and #12:
SQL> with test (time_from, time_to) as
2 (select to_date('09.08.2020 07:00', 'dd.mm.yyyy hh24:mi'),
3 to_date('09.08.2020 14:00', 'dd.mm.yyyy hh24:mi')
4 from dual union all
5 select to_date('09.08.2020 16:00', 'dd.mm.yyyy hh24:mi'),
6 to_date('09.08.2020 18:00', 'dd.mm.yyyy hh24:mi')
7 from dual
8 )
9 select to_char(time_from + ((column_value - 1) * &&par_minutes) / (24 * 60), 'hh:mi am') time
10 from test cross join
11 table(cast(multiset(select level from dual
12 connect by level <= (time_to - time_from) * 24 * (60 / &&par_minutes)
13 ) as sys.odcinumberlist));
Enter value for par_minutes: 20
old 9: select to_char(time_from + ((column_value - 1) * &&par_minutes) / (24 * 60), 'hh:mi am') time
new 9: select to_char(time_from + ((column_value - 1) * 20) / (24 * 60), 'hh:mi am') time
old 12: connect by level <= (time_to - time_from) * 24 * (60 / &&par_minutes)
new 12: connect by level <= (time_to - time_from) * 24 * (60 / 20)
TIME
--------
07:00 AM
07:20 AM
07:40 AM
08:00 AM
08:20 AM
08:40 AM
09:00 AM
09:20 AM
09:40 AM
10:00 AM
10:20 AM
10:40 AM
11:00 AM
11:20 AM
11:40 AM
12:00 PM
12:20 PM
12:40 PM
01:00 PM
01:20 PM
01:40 PM
04:00 PM
04:20 PM
04:40 PM
05:00 PM
05:20 PM
05:40 PM
27 rows selected.
SQL>

Return the end of the interval in Returning Functions?

In this case, 9.24. "Set Returning Functions" of the PostgreSQL 9.5 manual, only the initial dates and time are returned. Is it possible to return the date and time of the end of each interval?
SELECT * FROM generate_series('2008-03-01 00:00'::timestamp,
'2008-03-04 12:00', '10 hours');
generate_series
---------------------
2008-03-01 00:00:00
2008-03-01 10:00:00
2008-03-01 20:00:00
2008-03-02 06:00:00
2008-03-02 16:00:00
2008-03-03 02:00:00
2008-03-03 12:00:00
2008-03-03 22:00:00
2008-03-04 08:00:00
(9 rows)
Is this what you want?
SELECT gs.dte, LEAD(gs.dte) OVER (ORDER BY gs.dte) as next_dte
FROM generate_series('2008-03-01 00:00'::timestamp,
'2008-03-04 12:00',
'10 hours'
) gs(dte);
Or, if you don't want NULL for the last interval, explicitly do the calculation:
SELECT gs.dte, (gs.dte + interval '10 hours') as end_date
FROM generate_series('2008-03-01 00:00'::timestamp,
'2008-03-04 12:00',
'10 hours'
) gs(dte);

Splitting time range based on activities in sql

I need a Oracle sql to show the following output given the sample input.
Basically, an employee is schedule for a 9 hour shift.
I need to split up the activities during the day to separate records.
Especially the general activity of Cash. I need to create new records.
Activity start time end time
Shift 2010-01-01 8:00:00 2010-01-01 17:00:00
Open 2010-01-01 8:00:00 2010-01-01 9:00:00
Cash 2010-01-01 9:00:00 2010-01-01 16:00:00
Break 2010-01-01 10:00:00 2010-01-01 10:15:00
Lunch 2010-01-01 12:00:00 2010-01-01 13:00:00
Break 2010-01-01 14:30:00 2010-01-01 14:45:00
Close 2010-01-01 16:00:00 2010-01-01 17:00:00
OUTPUT:
Activity start time end time
Open 2010-01-01 8:00:00 2010-01-01 9:00:00
Cash 2010-01-01 9:00:00 2010-01-01 10:00:00
Break 2010-01-01 10:00:00 2010-01-01 10:15:00
Cash 2010-01-01 10:15:00 2010-01-01 12:00:00
Lunch 2010-01-01 12:00:00 2010-01-01 13:00:00
Cash 2010-01-01 13:00:00 2010-01-01 14:30:00
Break 2010-01-01 14:30:00 2010-01-01 14:45:00
Cash 2010-01-01 14:45:00 2010-01-01 16:00:00
Close 2010-01-01 16:00:00 2010-01-01 17:00:00
Any help is greatly appreciated.
This is a kind of gaps-and-islands problem. Assuming there is always an 'Open' record whose start matches the 'Shift' start, and a 'Close' record whose end matches the 'Shift' end; and that the general activity is always 'Cash' and its start matches the 'Open' end and its end matches the 'Close' start; then some of those records are redundant when filling in the gaps.
You can use the lead and lag functions to generate dummy 'Cash' records that sit between all the other activities, looking both forward and behind:
select activity orig_activity, start_time orig_start, end_time orig_end,
'Cash' as activity, lag(end_time) over (order by end_time) as start_time, start_time as end_time
from table1
where activity not in ('Shift', 'Cash')
union all
select activity orig_activity, start_time orig_start, end_time orig_end,
'Cash' as activity, end_time as start_time, lead(start_time) over (order by start_time) as end_time
from table1
where activity not in ('Shift', 'Cash')
order by orig_start;
ORIG_ ORIG_START ORIG_END ACTI START_TIME END_TIME
----- ------------------- ------------------- ---- ------------------- -------------------
Open 2010-01-01 08:00:00 2010-01-01 09:00:00 Cash 2010-01-01 08:00:00
Open 2010-01-01 08:00:00 2010-01-01 09:00:00 Cash 2010-01-01 09:00:00 2010-01-01 10:00:00
Break 2010-01-01 10:00:00 2010-01-01 10:15:00 Cash 2010-01-01 09:00:00 2010-01-01 10:00:00
Break 2010-01-01 10:00:00 2010-01-01 10:15:00 Cash 2010-01-01 10:15:00 2010-01-01 12:00:00
Lunch 2010-01-01 12:00:00 2010-01-01 13:00:00 Cash 2010-01-01 10:15:00 2010-01-01 12:00:00
Lunch 2010-01-01 12:00:00 2010-01-01 13:00:00 Cash 2010-01-01 13:00:00 2010-01-01 14:30:00
Break 2010-01-01 14:30:00 2010-01-01 14:45:00 Cash 2010-01-01 14:45:00 2010-01-01 16:00:00
Break 2010-01-01 14:30:00 2010-01-01 14:45:00 Cash 2010-01-01 13:00:00 2010-01-01 14:30:00
Close 2010-01-01 16:00:00 2010-01-01 17:00:00 Cash 2010-01-01 17:00:00
Close 2010-01-01 16:00:00 2010-01-01 17:00:00 Cash 2010-01-01 14:45:00 2010-01-01 16:00:00
That has duplicates from the same gap being seen, for instance, after the break and before lunch. By ignoring the original values you can remove those with distinct, or with union instead of union all. You can also exclude any generated rows with null start or end times, and any that overlap with other records - which could happen if two other activities were contiguous:
select activity, start_time, end_time from (
select 'Cash' as activity,
lag(end_time) over (order by end_time) as start_time,
start_time as end_time
from table1
where activity not in ('Shift', 'Cash')
union
select 'Cash' as activity,
end_time as start_time,
lead(start_time) over (order by start_time) as end_time
from table1
where activity not in ('Shift', 'Cash')
) tmp
where start_time is not null
and end_time is not null
and not exists (
select null from table1 where activity not in ('Shift', 'Cash') and (start_time = tmp.start_time or end_time = tmp.end_time)
)
order by start_time;
ACTI START_TIME END_TIME
---- ------------------- -------------------
Cash 2010-01-01 09:00:00 2010-01-01 10:00:00
Cash 2010-01-01 10:15:00 2010-01-01 12:00:00
Cash 2010-01-01 13:00:00 2010-01-01 14:30:00
Cash 2010-01-01 14:45:00 2010-01-01 16:00:00
You can then union that with all the original table rows, except the 'Cash' record:
...
union all
select activity, start_time, end_time
from table1
where activity not in ('Shift', 'Cash')
order by start_time;
ACTIV START_TIME END_TIME
----- ------------------- -------------------
Open 2010-01-01 08:00:00 2010-01-01 09:00:00
Cash 2010-01-01 09:00:00 2010-01-01 10:00:00
Break 2010-01-01 10:00:00 2010-01-01 10:15:00
Cash 2010-01-01 10:15:00 2010-01-01 12:00:00
Lunch 2010-01-01 12:00:00 2010-01-01 13:00:00
Cash 2010-01-01 13:00:00 2010-01-01 14:30:00
Break 2010-01-01 14:30:00 2010-01-01 14:45:00
Cash 2010-01-01 14:45:00 2010-01-01 16:00:00
Close 2010-01-01 16:00:00 2010-01-01 17:00:00
This also assumes that activities never overlap, but non-'Cash' activities could be adjacent.
There are probably other gaps-and-islands approaches that would work too.
I'll second Alex's answer. But, just for something completely different, you could figure out the distinct seconds in the shift, figure out what the person was doing each second, then group those into ranges for your results.
I think this would be less efficient than Alex's approach, but might be more flexible: it doesn't assume as much about how the input data will look.
with shift_data ( activity, start_time, end_time ) AS
-- This is just test data that would be in your database table
(
SELECT 'Shift',to_date('2010-01-01 8:00:00','YYYY-MM-DD HH24:MI:SS'),to_date('2010-01-01 17:00:00','YYYY-MM-DD HH24:MI:SS') FROM DUAL UNION ALL
SELECT 'Open',to_date('2010-01-01 8:00:00','YYYY-MM-DD HH24:MI:SS'),to_date('2010-01-01 9:00:00','YYYY-MM-DD HH24:MI:SS') FROM DUAL UNION ALL
SELECT 'Cash',to_date('2010-01-01 9:00:00','YYYY-MM-DD HH24:MI:SS'),to_date('2010-01-01 16:00:00','YYYY-MM-DD HH24:MI:SS') FROM DUAL UNION ALL
SELECT 'Break',to_date('2010-01-01 10:00:00','YYYY-MM-DD HH24:MI:SS'),to_date('2010-01-01 10:15:00','YYYY-MM-DD HH24:MI:SS') FROM DUAL UNION ALL
SELECT 'Lunch',to_date('2010-01-01 12:00:00','YYYY-MM-DD HH24:MI:SS'),to_date('2010-01-01 13:00:00','YYYY-MM-DD HH24:MI:SS') FROM DUAL UNION ALL
SELECT 'Break',to_date('2010-01-01 14:30:00','YYYY-MM-DD HH24:MI:SS'),to_date('2010-01-01 14:45:00','YYYY-MM-DD HH24:MI:SS') FROM DUAL UNION ALL
SELECT 'Close',to_date('2010-01-01 16:00:00','YYYY-MM-DD HH24:MI:SS'),to_date('2010-01-01 17:00:00','YYYY-MM-DD HH24:MI:SS') FROM DUAL
),
seconds_in_shift as (
-- Step 1: get a list of every second that falls in the shift
SELECT start_time + (ROWNUM - 1) / 86400 second
FROM shift_data
WHERE activity = 'Shift'
CONNECT BY ROWNUM <= ( (end_time - start_time) * 86400) + 1),
activity_each_second as (
-- Step 2: figure out what the person was doing every second. If multiple
-- activities overlap, choose whichever one had the shortest duration
-- Also, mark which seconds represent a transition from one activity to
-- another ("marker" column)
SELECT second,
MAX (activity) KEEP (DENSE_RANK FIRST ORDER BY end_time - start_time) activity,
CASE WHEN MAX (activity) KEEP (DENSE_RANK FIRST ORDER BY end_time - start_time)
!= NVL(LAG(MAX (activity) KEEP (DENSE_RANK FIRST ORDER BY end_time - start_time))
OVER ( PARTITION BY NULL ORDER BY SECOND),'#NULL#') THEN 'Y' ELSE NULL END marker
FROM seconds_in_shift ss
INNER JOIN shift_data sd ON ss.second BETWEEN sd.start_time AND sd.end_time
GROUP BY second),
ranges as (
-- Step 3: count the number of marker columns from the beginning of the shift
-- to the current second. Call this "activity_number".
select aes.*,
count(marker) OVER ( PARTITION BY NULL ORDER BY second) activity_number
from activity_each_second aes )
-- Finally, show the activity, start, and end time for each activity_number
SELECT activity,
round(min(second),'MI') start_time,
round(max(second),'MI') end_time
FROM ranges
GROUP BY activity, activity_number
ORDER BY activity_number;
Results:
Open 1/1/2010 8:00:00 A 1/1/2010 9:00:00 AM
Cash 1/1/2010 9:00:00 A 1/1/2010 10:00:00 AM
Break 1/1/2010 10:00:00 1/1/2010 10:15:00 AM
Cash 1/1/2010 10:15:00 1/1/2010 12:00:00 PM
Lunch 1/1/2010 12:00:00 1/1/2010 1:00:00 PM
Cash 1/1/2010 1:00:00 P 1/1/2010 2:30:00 PM
Break 1/1/2010 2:30:00 P 1/1/2010 2:45:00 PM
Cash 1/1/2010 2:45:00 P 1/1/2010 4:00:00 PM
Close 1/1/2010 4:00:00 P 1/1/2010 5:00:00 PM
NOTE: I cheated a bit by rounding the times to the nearest minute. Without rounding, there would be overlap in the ranges. E.g., 4PM on-the-dot would either be "Cash" or "Close", it wouldn't be both.
Assumptions: All activity intervals fall within the 'Shift' interval, and two activity intervals may have at most an endpoint in common (they may be adjacent - but they can't overlap in any way).
Also, I assumed you may have more than one employee in your table (so that must be addressed), and that the computation must be done separately for each calendar day. You will see this in the input data, and handled in the query.
Here is a way to get the desired result using only the analytic lag() function. It first collects only the activities different from 'Shift' and 'Cash', then it fills the gaps with 'Cash' (including at the beginning and/or end of the 'Shift', if no specific activity, like 'Open' or 'Close', starts or ends at the beginning or the end of a 'Shift'). The 'Shift' interval in particular, as presented in the inputs, is not particularly helpful in this solution; you will see how I handle that in the CTE I called prep below.
So I don't need to enter nls_date_format everywhere, I first ran
alter session set nls_date_format = 'yyyy-mm-dd hh24:mi:ss'
Then:
with
table1 ( empno, activity, start_time, end_time ) as (
select 101, 'Shift', to_date('2010-01-01 8:00:00') , to_date('2010-01-01 17:00:00') from dual union all
select 101, 'Open' , to_date('2010-01-01 8:00:00') , to_date('2010-01-01 9:00:00') from dual union all
select 101, 'Cash' , to_date('2010-01-01 9:00:00') , to_date('2010-01-01 16:00:00') from dual union all
select 101, 'Break', to_date('2010-01-01 10:00:00'), to_date('2010-01-01 10:15:00') from dual union all
select 101, 'Lunch', to_date('2010-01-01 12:00:00'), to_date('2010-01-01 13:00:00') from dual union all
select 101, 'Break', to_date('2010-01-01 14:30:00'), to_date('2010-01-01 14:45:00') from dual union all
select 101, 'Close', to_date('2010-01-01 16:00:00'), to_date('2010-01-01 17:00:00') from dual
),
prep ( empno, activity, start_time, end_time, flag ) as (
select empno, activity, start_time, end_time, 1
from table1
where activity not in ('Shift', 'Cash')
union all select empno, 'Shift', start_time, start_time, 0
from table1
where activity = 'Shift'
union all select empno, 'Shift', end_time, end_time, 2
from table1
where activity = 'Shift'
),
with_cash_intervals ( empno, activity, start_time, end_time ) as (
select empno, activity, start_time, end_time
from prep
where activity != 'Shift'
union all
select empno, 'Cash', lag(end_time) over (partition by empno, trunc(start_time)
order by flag, start_time), start_time
from prep
)
select empno, activity, start_time, end_time
from with_cash_intervals
where start_time < end_time
order by empno, start_time -- if needed
Output:
EMPNO ACTIVITY START_TIME END_TIME
----- -------- ------------------- -------------------
101 Open 2010-01-01 08:00:00 2010-01-01 09:00:00
101 Cash 2010-01-01 09:00:00 2010-01-01 10:00:00
101 Break 2010-01-01 10:00:00 2010-01-01 10:15:00
101 Cash 2010-01-01 10:15:00 2010-01-01 12:00:00
101 Lunch 2010-01-01 12:00:00 2010-01-01 13:00:00
101 Cash 2010-01-01 13:00:00 2010-01-01 14:30:00
101 Break 2010-01-01 14:30:00 2010-01-01 14:45:00
101 Cash 2010-01-01 14:45:00 2010-01-01 16:00:00
101 Close 2010-01-01 16:00:00 2010-01-01 17:00:00
9 rows selected.