Automatically populate a "time_hour" table - sql

I have a requirement to store 24 hours in a day in a table "time_hours". Here is the structure of my table:
Create table time_hours
(HOURS number,
HOUR_RANGE VARCHAR (20),
HOUR_MIN VARCHAR (20),
HOUR_MAX VARCHAR(20));
Here is the script I have so far:
Insert into time_hours(HOURS,
HOUR_RANGE,
HOUR_MIN,
HOUR_MAX)
Select
to_number(to_char(t,'HH24'),'00'),
to_char(t,'HH24:MI:SS'),
to_char(t,'HH24:MI:SS'),
to_char(t,'HH24:MI:SS')
FROM
(
Select trunc(sysdate) + (level-1)/24 as t
FROM dual
Connect by level <=24
);
The results is:
Hours | HOUR_RANGE | HOUR_MIN | HOUR_MAX
0 00:00:00 00:00:00 00:00:00
1 01:00:00 01:00:00 01:00:00
..
23 23:00:00 23:00:00 23:00:00
However I need this output:
Hours | HOUR_RANGE | HOUR_MIN | HOUR_MAX
0 00h-00h59 00:00:00 00:59:59
1 01h00-01h59 01:00:00 01:59:59
2 02h00-02h59 02:00:00 02:59:59
.. .. .. ..
23 23h00-23h59 23:00:00 23.59.59
My Question:
How can I format the "HOUR_RANGE" and "HOUR_MAX" columns to give me the expected output?
Thank you all in advance for your help

You could use:
Select
to_number(to_char(t,'HH24'),'00') AS Hours,
to_char(t,'HH24"h"MI"-"HH24"h59"') AS HOUR_RANGE,
to_char(t,'HH24:MI:SS') AS HOUR_MIN,
to_char(t+1/24-1/(24*3600),'HH24:MI:SS') AS HOUR_MAX
FROM (Select trunc(sysdate) + (level-1)/24 as t
FROM dual
Connect by level <=24 );
Rextester Demo

After all, it's just strings containing the hour plus some other characters. No date conversion needed at all.
insert into time_hours(hours, hour_range, hour_min, hour_max)
select
level-1 as hours,
to_char(level-1, '00') || 'h00-' || to_char(level-1, '00') || 'h59' as hour_range,
to_char(level-1, '00') || ':00:00'as hour_min,
to_char(level-1, '00') || ':59:59'as hour_max
from dual connect by level <= 24;

I think you can use code below.
You can use pipelines to concatenate characters.
You can use some arithmetic operations to get max minutes and seconds of given hour; 1/24*60 = 1/1440 to add a minute to a date value. 1/24*60*60 = 1/86400 to add a second to a date value.
INSERT INTO time_hours (HOURS, HOUR_RANGE, HOUR_MIN, HOUR_MAX)
SELECT
to_number(to_char(t, 'HH24'), '00'),
to_char(t, 'HH24') || 'h' || to_char(t, 'MI') || '-' || to_char(t, 'HH24') || 'h59',
to_char(t, 'HH24:MI:SS'),
to_char(t + 1/86400*59 + 1/1440*59, 'HH24:MI:SS')
FROM (SELECT trunc(SYSDATE) + (LEVEL - 1) / 24 AS t
FROM dual
CONNECT BY LEVEL <= 24);

Related

convert into date format oracle

I have a field c_days in the table my_table that accepts numeric values ​​from 1 to 31.
In this field, numbers from 1 to 9 are single digits.
I am writing a condition, if c_day is greater than today, then you need to display c_day in the to_date date format, if less, then display c_day in the date format, only the next month.
For example, c_day is 14, and today we have the 8th number, so you need to display the date 14.02.2023, If c_day is equal to 5, then you need to display the date of the next month 05.03.2023
I did something like this:
SELECT
C_DAY,
CASE
WHEN C_DAY >= TO_CHAR(SYSDATE, 'DD') THEN
TO_DATE(C_DAY || '.' || TO_CHAR(SYSDATE, 'MM') || '.' || TO_CHAR(SYSDATE, 'YYYY'), 'dd.mm.yyyy')
WHEN C_DAY < TO_CHAR(SYSDATE, 'DD') THEN
TO_DATE(C_DAY || '.' || TO_CHAR(SYSDATE, 'MM') || '.' || TO_CHAR(SYSDATE, 'YYYY'), 'dd.mm.yyyy')
WHEN C_DAY IS NULL THEN
null
END AS new_field
FROM my_table
The problem is that the end result is not converted to the date format, I thought it's cause of that the dates can be displayed as 1.03.2023, 7.03.2023, so i tried convert it into
TO_CHAR(C_DAY, 'fm00')
and did this:
SELECT
C_DAY,
CASE
WHEN TO_CHAR(C_DAY, 'fm00') >= TO_CHAR(SYSDATE, 'DD') THEN
TO_DATE(TO_CHAR(C_DAY, 'fm00') || '.' || TO_CHAR(SYSDATE, 'MM') || '.' || TO_CHAR(SYSDATE, 'YYYY'), 'dd.mm.yyyy')
WHEN TO_CHAR(C_DAY, 'fm00') < TO_CHAR(SYSDATE, 'DD') THEN
TO_DATE(TO_CHAR(C_DAY, 'fm00') || '.' || TO_CHAR(SYSDATE, 'MM') || '.' || TO_CHAR(SYSDATE, 'YYYY'), 'dd.mm.yyyy')
WHEN C_DAY IS NULL THEN
null
END AS new_field
FROM my_table
But its not even working, it shows ora error
You can do it without any string-to-date (or vice-versa) conversions using:
SELECT C_DAY,
LEAST(
ADD_MONTHS(
TRUNC(SYSDATE, 'MM'),
CASE WHEN c_day <= EXTRACT(DAY FROM SYSDATE) THEN 0 ELSE 1 END
) + c_day - 1,
LAST_DAY(
ADD_MONTHS(
TRUNC(SYSDATE, 'MM'),
CASE WHEN c_day <= EXTRACT(DAY FROM SYSDATE) THEN 0 ELSE 1 END
)
)
) AS new_field
FROM my_table
Which, for the sample data:
CREATE TABLE my_table(c_day) AS
SELECT LEVEL FROM DUAL CONNECT BY LEVEL <= 31;
Outputs:
C_DAY
NEW_FIELD
1
2023-02-01 00:00:00
2
2023-02-02 00:00:00
3
2023-02-03 00:00:00
4
2023-02-04 00:00:00
5
2023-02-05 00:00:00
6
2023-02-06 00:00:00
7
2023-02-07 00:00:00
8
2023-02-08 00:00:00
9
2023-03-09 00:00:00
10
2023-03-10 00:00:00
11
2023-03-11 00:00:00
12
2023-03-12 00:00:00
13
2023-03-13 00:00:00
14
2023-03-14 00:00:00
15
2023-03-15 00:00:00
16
2023-03-16 00:00:00
17
2023-03-17 00:00:00
18
2023-03-18 00:00:00
19
2023-03-19 00:00:00
20
2023-03-20 00:00:00
21
2023-03-21 00:00:00
22
2023-03-22 00:00:00
23
2023-03-23 00:00:00
24
2023-03-24 00:00:00
25
2023-03-25 00:00:00
26
2023-03-26 00:00:00
27
2023-03-27 00:00:00
28
2023-03-28 00:00:00
29
2023-03-29 00:00:00
30
2023-03-30 00:00:00
31
2023-03-31 00:00:00
fiddle
Here's one option:
SQL> alter session set nls_date_format = 'dd.mm.yyyy';
Session altered.
SQL> WITH
2 my_table (c_day)
3 AS
4 (SELECT 14 FROM DUAL
5 UNION ALL
6 SELECT 5 FROM DUAL
7 UNION ALL
8 SELECT 30 FROM DUAL)
9 SELECT c_day,
10 TO_DATE (
11 LPAD (LEAST (c_day, TO_CHAR (LAST_DAY (SYSDATE), 'dd')), 2, '0')
12 || '.'
13 || TO_CHAR (
14 CASE
15 WHEN c_day < TO_CHAR (SYSDATE, 'dd')
16 THEN
17 ADD_MONTHS (SYSDATE, 1)
18 ELSE
19 SYSDATE
20 END,
21 'mm.yyyy'),
22 'dd.mm.yyyy') AS result
23 FROM my_table;
C_DAY RESULT
---------- ----------
14 14.02.2023
5 05.03.2023
30 28.02.2023
SQL>
As far as I understand your question, one solution could be this one
SELECT
C_DAY,
CASE
WHEN C_DAY >= TO_CHAR(SYSDATE, 'DD') THEN
ADD_MONTHS(TO_DATE(TO_CHAR(SYSDATE, 'YYYY-MM-')||C_DAY , 'YYYY-MM-DD'), 1)
WHEN C_DAY < TO_CHAR(SYSDATE, 'DD') THEN
TO_DATE(TO_CHAR(SYSDATE, 'YYYY-MM-')||C_DAY , 'YYYY-MM-DD')
END AS new_field
FROM my_table
The ADD_MONTHS function works as this:
If date is the last day of the month or if the resulting month has fewer days than the day component of date, then the result is the last day of the resulting month. Otherwise, the result has the same day component as date.

find number of hours gap when no one is working

i have a table view of table
taking scenario of a hotel that works 24/7 non stop, i want to calculate total number of hours in a month when no one is present .
in image you can see we have shift hours in 24hh format, shifts are 8 hours long and different employees may have different week offs , we have date range for which this shift would be valid , i.e. a typical month and we have planned leaves when employee is completely off.
can you suggest oracle SQL to find total number of hours when no employee is working in a month.
You can generate the shifts and then find the shifts where no-one worked:
WITH shifts (shift_start) AS (
SELECT DATE '2022-07-01' + INTERVAL '6' HOUR + INTERVAL '8' HOUR * (LEVEL - 1)
FROM DUAL
CONNECT BY
DATE '2022-07-01' + INTERVAL '6' HOUR + INTERVAL '8' HOUR * (LEVEL - 1)
< DATE '2022-08-01'
)
SELECT s.shift_start
FROM shifts s
WHERE NOT EXISTS(
SELECT 1
FROM work_master w
WHERE w.shift_date_frm <= s.shift_start
AND s.shift_start < w.shift_date_to + INTERVAL '1' DAY
AND ':' || w.week_off_day || ':' NOT LIKE '%:' || TO_CHAR(s.shift_start, 'Dy') || ':%'
AND EXTRACT(HOUR FROM CAST(s.shift_start AS TIMESTAMP)) = w.shift_start
AND (
(
w.vac_date_frm IS NULL
AND w.vac_date_to IS NULL
)
OR NOT (
w.vac_date_frm <= s.shift_start
AND s.shift_start < w.vac_date_to + INTERVAL '1' DAY
)
)
)
Which, for the sample data:
CREATE TABLE work_master (
employee_name,
shift_start,
shift_end,
shift_date_frm,
shift_date_to,
vac_date_frm,
vac_date_to,
week_off_day
) AS
SELECT 'emp1', 22, 6, DATE '2022-07-01', DATE '2022-07-31', NULL, NULL, 'Sat:Sun' FROM DUAL UNION ALL
SELECT 'emp2', 14, 22, DATE '2022-07-01', DATE '2022-07-31', NULL, NULL, 'Sat:Sun' FROM DUAL UNION ALL
SELECT 'emp3', 6, 14, DATE '2022-07-01', DATE '2022-07-31', DATE '2022-07-27', DATE '2022-07-27', 'Sat:Sun' FROM DUAL UNION ALL
SELECT 'emp4', 14, 22, DATE '2022-07-01', DATE '2022-07-31', NULL, NULL, 'Fri:Sat' FROM DUAL UNION ALL
SELECT 'emp5', 22, 6, DATE '2022-07-01', DATE '2022-07-31', NULL, NULL, 'Wed:Thu' FROM DUAL;
Outputs:
SHIFT_START
2022-07-02 06:00:00 (Sat)
2022-07-02 14:00:00 (Sat)
2022-07-03 06:00:00 (Sun)
2022-07-09 06:00:00 (Sat)
2022-07-09 14:00:00 (Sat)
2022-07-10 06:00:00 (Sun)
2022-07-16 06:00:00 (Sat)
2022-07-16 14:00:00 (Sat)
2022-07-17 06:00:00 (Sun)
2022-07-23 06:00:00 (Sat)
2022-07-23 14:00:00 (Sat)
2022-07-24 06:00:00 (Sun)
2022-07-27 06:00:00 (Wed)
2022-07-30 06:00:00 (Sat)
2022-07-30 14:00:00 (Sat)
2022-07-31 06:00:00 (Sun)
If you just want the total hours then:
WITH shifts (shift_start) AS (
SELECT DATE '2022-07-01' + INTERVAL '6' HOUR + INTERVAL '8' HOUR * (LEVEL - 1)
FROM DUAL
CONNECT BY
DATE '2022-07-01' + INTERVAL '6' HOUR + INTERVAL '8' HOUR * (LEVEL - 1)
< DATE '2022-08-01'
)
SELECT COUNT(*) * 8 AS hours_not_worked
FROM shifts s
WHERE NOT EXISTS(
SELECT 1
FROM work_master w
WHERE w.shift_date_frm <= s.shift_start
AND s.shift_start < w.shift_date_to + INTERVAL '1' DAY
AND ':' || w.week_off_day || ':' NOT LIKE '%:' || TO_CHAR(s.shift_start, 'Dy') || ':%'
AND EXTRACT(HOUR FROM CAST(s.shift_start AS TIMESTAMP)) = w.shift_start
AND (
(
w.vac_date_frm IS NULL
AND w.vac_date_to IS NULL
)
OR NOT (
w.vac_date_frm <= s.shift_start
AND s.shift_start < w.vac_date_to + INTERVAL '1' DAY
)
)
)
Which outputs:
HOURS_NOT_WORKED
128
db<>fiddle here

Counting difference of two dates in a PL SQL FUNCTION, should be returing number of days

I have the following output:
from_date
until_date
17.03.2020
18.05.2020
18.05.2020
08.06.2020
21.12.2020
01.03.2021
01.03.2021
11.03.2021
19.10.2021
22.10.2021
10.01.2022
14.01.2022
14.01.2022
NULL
I need to count the days between these two dates, second date inclusively, with this logic:
(18.05.2020 - 17.03.2020)+1 = 63
The next row begins at the same day as it ends in the first row,
then the 18.05.2020 must not be counted in the days difference, so:
08.06.2020 - 18.05.2020
if the until_date is null then it will be:
sysdate-from_date
but what im struggling to do is to get next element and previous element values in a loop so I can compare them
You can use the LAG analytic function to find the previous until_date:
SELECT from_date,
until_date,
COALESCE(until_date, TRUNC(SYSDATE)) - from_date
+ CASE
WHEN LAG(until_date) OVER (ORDER BY FROM_DATE) = from_date
THEN 0
ELSE 1
END
AS difference
FROM table_name;
Which, for the sample data:
CREATE TABLE table_name (from_date, until_date) AS
SELECT DATE '2020-03-17', DATE '2020-05-18' FROM DUAL UNION ALL
SELECT DATE '2020-05-18', DATE '2020-06-08' FROM DUAL UNION ALL
SELECT DATE '2020-12-21', DATE '2021-03-01' FROM DUAL UNION ALL
SELECT DATE '2021-03-01', DATE '2021-03-11' FROM DUAL UNION ALL
SELECT DATE '2021-10-19', DATE '2021-10-22' FROM DUAL UNION ALL
SELECT DATE '2022-01-10', DATE '2022-01-14' FROM DUAL UNION ALL
SELECT DATE '2022-01-14', NULL FROM DUAL;
Outputs:
FROM_DATE
UNTIL_DATE
DIFFERENCE
2020-03-17 00:00:00
2020-05-18 00:00:00
63
2020-05-18 00:00:00
2020-06-08 00:00:00
21
2020-12-21 00:00:00
2021-03-01 00:00:00
71
2021-03-01 00:00:00
2021-03-11 00:00:00
10
2021-10-19 00:00:00
2021-10-22 00:00:00
4
2022-01-10 00:00:00
2022-01-14 00:00:00
5
2022-01-14 00:00:00
null
154
If you want it in PL/SQL then wrap the query in a cursor and loop through the cursor in PL/SQL.
BEGIN
FOR r IN (
SELECT from_date,
until_date,
COALESCE(until_date, TRUNC(SYSDATE)) - from_date
+ CASE
WHEN LAG(until_date) OVER (ORDER BY FROM_DATE) = from_date
THEN 0
ELSE 1
END
AS difference
FROM table_name
) LOOP
DBMS_OUTPUT.PUT_LINE( r.from_date || ', ' || r.until_date || ', ' || r.difference );
END LOOP;
END;
/
Or you can do exactly the same thing by storing the previous until_date in a PL/SQL variable:
DECLARE
v_until_date DATE;
v_diff NUMBER;
BEGIN
FOR r IN (
SELECT from_date,
until_date
FROM table_name
ORDER BY from_date
) LOOP
v_diff := COALESCE(r.until_date, TRUNC(SYSDATE)) - r.from_date
+ CASE WHEN v_until_date = r.from_date THEN 0 ELSE 1 END;
DBMS_OUTPUT.PUT_LINE(
r.from_date
|| ', ' || r.until_date
|| ', ' || v_diff
);
v_until_date := r.until_date;
END LOOP;
END;
/
db<>fiddle here

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

Edit the answer that is returned by my select

I have a selection to find the difference between two dates
select SUBSTR(to_timestamp('19.09.2019 11:26:00', 'dd.mm.yyyy hh24:mi:ss' ) - to_timestamp('01.01.2019 00:00:00' , 'dd.mm.yyyy hh24:mi:ss' ),8, 12) diff
from dual
The answer I teach is correct
261 11:26:00
Is it possible to somehow get the answer of this format
261 day 11 hour 26 min 00 sec
Use EXTRACT and string concatenation:
SELECT EXTRACT( DAY FROM diff ) || ' days'
|| ' ' || LPAD( EXTRACT( HOUR FROM diff ), 2, '0' ) || ' hours'
|| ' ' || LPAD( EXTRACT( MINUTE FROM diff ), 2, '0' ) || ' minutes'
|| ' ' || LPAD( EXTRACT( SECOND FROM diff ), 2, '0' ) || ' seconds'
AS diff
FROM (
SELECT TIMESTAMP '2019-09-19 11:26:00' - TIMESTAMP '2019-01-01 00:00:00'
AS diff
FROM DUAL
)
Outputs:
| DIFF |
| :-------------------------------------- |
| 261 days 11 hours 26 minutes 00 seconds |
db<>fiddle here