Create a realistic start_date and ebd_date - oracle

I have a function below, which works fine and generates a random date.
I'm trying to pair a start_date with and end_date for the same row. Is there a mechanism I can use in SQL where I can add random intervals hours, minutes, seconds to the start_date to ensure the end_date is at least 1 hr greater and less than 10 hours than the start_date?
For example, say I want to populate the table below. I am open to using a function, package type if need be.
Thanks in advance to all who respond.
ALTER SESSION SET NLS_DATE_FORMAT = 'DD-MON-YYYY HH24:MI:SS';
CREATE OR REPLACE FUNCTION random_date(
p_from IN DATE,
p_to IN DATE
) RETURN DATE
IS
BEGIN
RETURN p_from + DBMS_RANDOM.VALUE() * (p_to - p_from + 1 );
END random_date;
/
CREATE TABLE t1 (
seq_num NUMBER GENERATED BY DEFAULT AS IDENTITY (START WITH 1) NOT NULL,
start_date DATE,
end_date DATE
);
/* here I need help with end_date */
INSERT INTO t1 (start_date, end_date)
SELECT
random_date(DATE '2022-04-01', DATE '2022-04-30'),
FROM dual CONNECT BY level <= 50;

The way I see it, one query won't do because you have to check whether difference of randomly fetched start and end date match criteria. Therefore, a PL/SQL block which - in a loop until counter hits the limit of 50 rows (taken from your connect by clause) - validates dates and inserts a row only if difference is between 1 and 10 hours:
SQL> DECLARE
2 l_cnt NUMBER := 0;
3 l_start_date DATE;
4 l_end_date DATE;
5 BEGIN
6 WHILE l_cnt < 50
7 LOOP
8 SELECT random_date (DATE '2022-04-01', DATE '2022-04-30') start_date,
9 random_date (DATE '2022-04-01', DATE '2022-04-30') end_date
10 INTO l_start_date, l_end_date
11 FROM DUAL;
12
13 IF l_end_date - l_start_date BETWEEN 1 / 24 AND 10 / 24
14 THEN
15 INSERT INTO test (start_date, end_date)
16 VALUES (l_start_date, l_end_date);
17
18 l_cnt := l_cnt + SQL%ROWCOUNT;
19 END IF;
20 END LOOP;
21 END;
22 /
PL/SQL procedure successfully completed.
SQL>
Result:
SQL> SELECT seq_num,
2 start_date,
3 end_date,
4 ROUND ((end_date - start_date) * 24, 1) diff_hours
5 FROM test;
SEQ_NUM START_DATE END_DATE DIFF_HOURS
---------- ---------------- ---------------- ----------
1 08.04.2022 11:14 08.04.2022 18:49 7,6
2 14.04.2022 19:23 14.04.2022 23:06 3,7
3 13.04.2022 12:12 13.04.2022 16:06 3,9
4 24.04.2022 13:04 24.04.2022 16:03 3
5 01.04.2022 12:52 01.04.2022 19:29 6,6
6 08.04.2022 08:24 08.04.2022 12:56 4,5
7 29.04.2022 22:05 29.04.2022 23:22 1,3
8 20.04.2022 05:35 20.04.2022 10:31 4,9
9 23.04.2022 00:52 23.04.2022 08:46 7,9
<snip>
46 05.04.2022 20:06 06.04.2022 01:52 5,8
47 11.04.2022 20:03 12.04.2022 05:11 9,1
48 12.04.2022 17:07 12.04.2022 23:13 6,1
49 30.04.2022 09:32 30.04.2022 13:42 4,2
50 12.04.2022 00:24 12.04.2022 10:19 9,9
50 rows selected.
SQL>

You could add a function to generate a random interval within the 1 and 10 hour limits, and add that to the start date - which would need an inline view or CTE as you need to refer to the random start date twice to do the calculation:
CREATE OR REPLACE FUNCTION random_interval(
p_min_hours IN NUMBER,
p_max_hours IN NUMBER
) RETURN INTERVAL DAY TO SECOND
IS
BEGIN
RETURN floor(dbms_random.value(p_min_hours, p_max_hours)) * interval '1' hour
+ floor(dbms_random.value(0, 60)) * interval '1' minute
+ floor(dbms_random.value(0, 60)) * interval '1' second;
END random_interval;
/
and then
INSERT INTO t1 (start_date, end_date)
WITH cte (start_date, duration) AS (
SELECT
random_date(DATE '2022-04-01', DATE '2022-04-30'),
random_interval(1, 10)
FROM dual CONNECT BY level <= 50
)
SELECT start_date, start_date + duration
FROM cte;
db<>fiddle
In recent versions of Oracle you don't need either permanent function, if this is all they will be used for; you can define them within the WITH clause:
INSERT /*+ WITH_PLSQL */ INTO t1 (start_date, end_date)
WITH
FUNCTION random_date(
p_from IN DATE,
p_to IN DATE
) RETURN DATE
IS
BEGIN
RETURN p_from + DBMS_RANDOM.VALUE() * (p_to - p_from + 1 );
END random_date;
FUNCTION random_interval(
p_min_hours IN NUMBER,
p_max_hours IN NUMBER
) RETURN INTERVAL DAY TO SECOND
IS
BEGIN
RETURN floor(dbms_random.value(p_min_hours, p_max_hours)) * interval '1' hour
+ floor(dbms_random.value(0, 60)) * interval '1' minute
+ floor(dbms_random.value(0, 60)) * interval '1' second;
END random_interval;
cte (start_date, duration) AS (
SELECT
random_date(DATE '2022-04-01', DATE '2022-04-30'),
random_interval(1, 10)
FROM dual CONNECT BY level <= 50
)
SELECT start_date, start_date + duration
FROM cte;
db<>fiddle
My overall goal is to write a function that will RETURN a string denoting of NdaysNHoursNMinutesNSeconds which represents the difference from end_date-start_date
That isn't really anything to do with what you asked, but it's fairly simple if you treat the dates as timestamps, and extract the elements from the interval value you get from subtracting those:
create function diff_as_string (p_from date, p_to date)
return varchar2 is
l_interval interval day(3) to second(0);
begin
l_interval := cast(p_to as timestamp) - cast(p_from as timestamp);
return extract (day from l_interval) || 'Days'
|| extract (hour from l_interval) || 'Hours'
|| extract (minute from l_interval) || 'Minutes'
|| extract (second from l_interval) || 'Seconds';
end diff_as_string;
/
At least as long as the elapsed time is less than 1000 days, which would exceed the range allowed by that interval type.
And again you could define that function in a WITH clause instead; or it could be done just as a query, maybe with a normal CTE or inline view to avoid repeatedly converting the data:
select seq_num, start_date, end_date,
extract (day from diff_interval) || 'Days'
|| extract (hour from diff_interval) || 'Hours'
|| extract (minute from diff_interval) || 'Minutes'
|| extract (second from diff_interval) || 'Seconds' as diff
from (
select seq_num, start_date, end_date,
cast(end_date as timestamp) - cast(start_date as timestamp) as diff_interval
from t1
)
order by seq_num;
SEQ_NUM
START_DATE
END_DATE
DIFF
1
20-APR-2022 03:46:04
20-APR-2022 07:44:30
0Days3Hours58Minutes26Seconds
2
12-APR-2022 01:37:07
12-APR-2022 10:54:40
0Days9Hours17Minutes33Seconds
3
12-APR-2022 16:20:44
12-APR-2022 20:36:13
0Days4Hours15Minutes29Seconds
4
03-APR-2022 01:24:53
03-APR-2022 07:57:52
0Days6Hours32Minutes59Seconds
5
01-APR-2022 21:33:20
01-APR-2022 23:50:13
0Days2Hours16Minutes53Seconds
...
50
22-APR-2022 22:46:46
23-APR-2022 08:03:38
0Days9Hours16Minutes52Seconds
9999
21-NOV-2019 00:00:01
17-AUG-2022 00:00:00
999Days23Hours59Minutes59Seconds
db<>fiddle with and without a function.

CREATE OR REPLACE FUNCTION gen_dates(i_from_date IN DATE, i_end_date IN DATE, i_min_delta IN NUMBER, i_max_delta IN NUMBER, i_num_rows IN NUMBER)
RETURN VARCHAR2
SQL_MACRO
IS
BEGIN
RETURN q'{
SELECT start_date, end_date,
EXTRACT(DAY FROM (CAST(end_date AS TIMESTAMP) - CAST(start_date AS TIMESTAMP))) || 'Days' ||
EXTRACT(HOUR FROM (CAST(end_date AS TIMESTAMP) - CAST(start_date AS TIMESTAMP))) || 'Hours' ||
EXTRACT(MINUTE FROM (CAST(end_date AS TIMESTAMP) - CAST(start_date AS TIMESTAMP))) || 'Minutes' ||
EXTRACT(SECOND FROM (CAST(end_date AS TIMESTAMP) - CAST(start_date AS TIMESTAMP))) || 'Seconds' as formated_delta FROM (
SELECT pivot_date AS start_date, pivot_date + NUMTODSINTERVAL( i_min_delta + (i_max_delta-i_min_delta) * DBMS_RANDOM.VALUE(), 'hour') AS end_date
FROM (
SELECT pivot_date + DBMS_RANDOM.VALUE() AS pivot_date
FROM (
SELECT rownum AS rn, pivot_date AS pivot_date FROM (
SELECT TRUNC(i_from_date)+level-1 AS pivot_date FROM DUAL
CONNECT BY TRUNC(i_from_date)+level-1<=TRUNC(i_end_date)
)
)
CONNECT BY LEVEL <= i_num_rows AND PRIOR rn = rn AND PRIOR sys_guid() IS NOT NULL
)
)
}' ;
END;
/
SELECT count(*) FROM gen_dates( to_date('01-4-2022','dd-mm-yyyy'), to_date('03-4-2022','dd-mm-yyyy'), 1, 10, 100000)
;
-- 300000
SELECT count(*) FROM gen_dates( to_date('01-4-2022','dd-mm-yyyy'), to_date('03-4-2022','dd-mm-yyyy'), 1, 10, 100000)
WHERE (end_date - start_date < 1/24) OR (end_date - start_date > 10/24)
;
-- 0

If your version of ORACLE is recent enough, you can use a SQL_MACRO to avoid context switches (and any way you don't need PL/SQL at all since you can INSERT .. FROM SELECT...):
CREATE OR REPLACE FUNCTION gen_dates(i_from_date IN DATE, i_min_delta IN NUMBER, i_max_delta IN NUMBER, i_delta_type IN VARCHAR2, i_num_rows IN NUMBER)
RETURN VARCHAR2
SQL_MACRO
IS
BEGIN
RETURN q'{SELECT start_date, end_date, EXTRACT(DAY FROM (CAST(end_date AS TIMESTAMP) - CAST(start_date AS TIMESTAMP))) || 'Days' ||
EXTRACT(HOUR FROM (CAST(end_date AS TIMESTAMP) - CAST(start_date AS TIMESTAMP))) || 'Hours' ||
EXTRACT(MINUTE FROM (CAST(end_date AS TIMESTAMP) - CAST(start_date AS TIMESTAMP))) || 'Minutes' ||
EXTRACT(SECOND FROM (CAST(end_date AS TIMESTAMP) - CAST(start_date AS TIMESTAMP))) || 'Seconds' as formated_delta FROM (
SELECT i_from_date AS start_date, i_from_date + NUMTODSINTERVAL( i_min_delta + (i_max_delta-i_min_delta) * DBMS_RANDOM.VALUE(), i_delta_type) AS end_date FROM DUAL
CONNECT BY LEVEL <= i_num_rows)}' ;
END gen_dates;
/
SELECT * FROM gen_dates( TO_DATE('2022-04-01','YYYY-MM-DD'), 1, 10, 'hour', 200 ) ;
01-APR-2022 00:00:00 01-APR-2022 08:54:05 0Days8Hours54Minutes5Seconds
01-APR-2022 00:00:00 01-APR-2022 08:32:28 0Days8Hours32Minutes28Seconds
01-APR-2022 00:00:00 01-APR-2022 02:17:42 0Days2Hours17Minutes42Seconds
01-APR-2022 00:00:00 01-APR-2022 03:24:33 0Days3Hours24Minutes33Seconds
01-APR-2022 00:00:00 01-APR-2022 03:28:17 0Days3Hours28Minutes17Seconds
01-APR-2022 00:00:00 01-APR-2022 09:31:37 0Days9Hours31Minutes37Seconds
01-APR-2022 00:00:00 01-APR-2022 05:04:53 0Days5Hours4Minutes53Seconds
01-APR-2022 00:00:00 01-APR-2022 04:15:49 0Days4Hours15Minutes49Seconds
01-APR-2022 00:00:00 01-APR-2022 02:16:10 0Days2Hours16Minutes10Seconds
01-APR-2022 00:00:00 01-APR-2022 04:49:56 0Days4Hours49Minutes56Seconds
01-APR-2022 00:00:00 01-APR-2022 06:46:32 0Days6Hours46Minutes32Seconds
01-APR-2022 00:00:00 01-APR-2022 02:18:45 0Days2Hours18Minutes45Seconds
01-APR-2022 00:00:00 01-APR-2022 05:38:22 0Days5Hours38Minutes22Seconds
01-APR-2022 00:00:00 01-APR-2022 08:57:54 0Days8Hours57Minutes54Seconds
01-APR-2022 00:00:00 01-APR-2022 03:17:17 0Days3Hours17Minutes17Seconds
01-APR-2022 00:00:00 01-APR-2022 05:59:49 0Days5Hours59Minutes49Seconds
01-APR-2022 00:00:00 01-APR-2022 04:03:42 0Days4Hours3Minutes42Seconds
01-APR-2022 00:00:00 01-APR-2022 08:40:43 0Days8Hours40Minutes43Seconds
...
(and if you have an even more recent version of ORACLE you can make of the formatting expression a SQL_MACRO(SCALAR))

Related

Oracle INSERT and CONNECT by

I'm trying to load some test data into table t1 by calling 2 functions but I can't seem to get the INSERT and CONNECT by to work.
Any help would be greatly appreciated.
ALTER SESSION SET NLS_TIMESTAMP_FORMAT = 'DD-MON-YYYY HH24:MI:SS.FF';
ALTER SESSION SET NLS_DATE_FORMAT = 'DD-MON-YYYY HH24:MI:SS';
CREATE OR REPLACE FUNCTION random_date(
p_from IN DATE,
p_to IN DATE
) RETURN DATE
IS
BEGIN
RETURN p_from + DBMS_RANDOM.VALUE() * (p_to -p_from);
END random_date;
/
CREATE OR REPLACE FUNCTION random_timestamp(
p_from IN TIMESTAMP,
p_to IN TIMESTAMP
) RETURN TIMESTAMP
IS
BEGIN
RETURN p_from + DBMS_RANDOM.VALUE() * (p_to -p_from);
END random_timestamp;
/
CREATE TABLE t1 (
seq_num NUMBER GENERATED BY DEFAULT AS IDENTITY (START WITH 1) NOT NULL,
dt DATE,
ts TIMESTAMP
);
INSERT into t1 (dt, ts) VALUES
random_date (DATE'2022-04-01',DATE '2022-04-30'),
random_timestamp (DATE'2022-04-01',DATE '2022-04-30')
CONNECT BY LEVEL<=1000;
Not values, but select from dual (didn't insert 1000 rows; 10 will suffice; also, SEQ_NUM values don't start from 1 as I ran that code several times, but that's irrelevant):
SQL> INSERT INTO t1 (
2 dt,
3 ts
4 )
5 SELECT
6 random_date(DATE '2022-04-01', DATE '2022-04-30'),
7 random_timestamp(DATE '2022-04-01', DATE '2022-04-30')
8 FROM
9 dual
10 CONNECT BY
11 level <= 10;
10 rows created.
Result:
SQL> select * From t1;
SEQ_NUM DT TS
---------- -------------------- ------------------------------
41 03-APR-2022 00:36:33 05-APR-2022 03:38:39.215073
42 22-APR-2022 15:29:50 26-APR-2022 23:44:20.687417
43 27-APR-2022 23:42:49 29-APR-2022 23:54:17.692053
44 21-APR-2022 19:24:10 22-APR-2022 23:07:20.602254
45 13-APR-2022 20:45:39 04-APR-2022 04:05:08.815214
46 07-APR-2022 09:35:37 07-APR-2022 21:32:28.443624
47 23-APR-2022 11:48:18 18-APR-2022 06:40:39.608578
48 22-APR-2022 23:53:04 02-APR-2022 13:13:54.285010
49 14-APR-2022 21:35:57 10-APR-2022 12:26:08.419025
50 11-APR-2022 21:49:32 10-APR-2022 17:20:45.033907
SQL>

PL SQL iterate loop through date range

I have a date range, I am trying to take one date every week through a loop
DECLARE
start_date DATE := TO_DATE('06.01.2021', 'dd.MM.yyyy');
end_date DATE := TO_DATE('26.05.2021', 'dd.mm.yyyy');
active_date DATE;
start_number NUMBER;
end_number NUMBER;
BEGIN
start_number := TO_NUMBER(TO_CHAR(start_date, 'j'));
end_number := TO_NUMBER(TO_CHAR(end_date, 'j'));
active_date := start_date;
FOR cur_r IN start_number..end_number
LOOP
INSERT INTO test_tbl
SELECT snap_date FROM s_act
WHERE
snap_date = active_date;
active_date := TRUNC(active_date) + 7;
COMMIT;
END LOOP;
END;
When I execute this script, only one date 06.01.2021 is written to the table through all iterations.
Where am I making a mistake? How can this be fixed?
You do not need PL/SQL for this and can just use a recursive sub-query:
INSERT INTO test_tbl
WITH date_range ( start_date, end_date ) AS (
SELECT DATE '2021-01-06', DATE '2021-05-26' FROM DUAL
UNION ALL
SELECT start_date + INTERVAL '7' DAY,
end_date
FROM date_range
WHERE start_date + INTERVAL '7' DAY <= end_date
)
SELECT snap_date
FROM s_act s
WHERE EXISTS(
SELECT 1
FROM date_range r
WHERE r.start_date = s.snap_date
);
or a hierarchical query:
INSERT INTO test_tbl
SELECT snap_date
FROM s_act s
WHERE EXISTS(
WITH date_range ( start_date, end_date ) AS (
SELECT DATE '2021-01-06', DATE '2021-05-26' FROM DUAL
)
SELECT 1
FROM date_range r
WHERE r.start_date + ( LEVEL - 1 ) * INTERVAL '7' DAY = s.snap_date
CONNECT BY r.start_date + ( LEVEL - 1 ) * INTERVAL '7' DAY <= r.end_date
);
If you really want to use PL/SQL then you can make it much simpler and iterate by weeks rather than days (however, this will be much less efficient as you will have one INSERT per week and the associated context switch from PL/SQL to SQL compared to the SQL solution which is only a single INSERT for the entire operation and no context switches):
DECLARE
start_date DATE := DATE '2021-01-06';
end_date DATE := DATE '2021-05-26';
active_date DATE := start_date;
BEGIN
LOOP
EXIT WHEN active_date > end_date;
INSERT INTO test_tbl
SELECT snap_date FROM s_act
WHERE snap_date = active_date;
active_date := active_date + INTERVAL '7' DAY;
END LOOP;
END;
/
db<>fiddle here
To me, it looks as if everything is, actually, OK with code you wrote, because active_date gets its new value:
SQL> set serveroutput on;
SQL> declare
2 start_date date := to_date('06.01.2021', 'dd.MM.yyyy');
3 end_date date := to_date('26.05.2021', 'dd.mm.yyyy');
4 active_date date;
5 start_number number;
6 end_number number;
7 begin
8 start_number := to_number(to_char(start_date, 'j'));
9 end_number := to_number(to_char(end_date, 'j'));
10 active_date := start_date;
11
12 for cur_r in start_number..end_number
13 loop
14 dbms_output.put_line('Active_date = ' || to_char(active_date, 'dd.mm.yyyy'));
15 /* Commented, as I don't have your tables nor data
16 INSERT INTO test_tbl
17 SELECT snap_date
18 FROM s_act
19 WHERE snap_date = active_date;
20 */
21 active_date := trunc(active_date) + 7;
22 end loop;
23 -- move COMMIT out of the loop!
24 commit;
25 end;
26 /
Active_date = 06.01.2021
Active_date = 13.01.2021
Active_date = 20.01.2021
<snip>
Active_date = 06.09.2023
Active_date = 13.09.2023
PL/SQL procedure successfully completed.
SQL>
You said
When I execute this script, only one date 06.01.2021 is written to the table through all iterations.
This is piece of code responsible for that:
INSERT INTO test_tbl
SELECT snap_date
FROM s_act
WHERE snap_date = active_date;
I interpret it as:
s_act table contains rows only with snap_date equal to 06.01.2021, or
if it contains rows with other dates, maybe they contain a time component (hours, minutes, seconds) and where condition prevents them to be inserted. If that's so, try with
where trunc(snap_date) = active_date
and see what happens.

How to get the first and last date of previous month without using to_date, trunc and predefined add_months function

I wrote the below code but I did not get the expected output:
SQL> select TO_CHAR(TO_CHAR(sysdate,'MM')-1,'DD-MM-YYYY') AS PREV_MON_FIRST,
TO_CHAR(TO_CHAR(sysdate,'MM'),'DD-MM-YYYY')-1 AS PREV_MON_LAST from dual;
select TO_CHAR(TO_CHAR(sysdate,'MM')-1,'DD-MM-YYYY') AS PREV_MON from dual
*
ERROR at line 1:
ORA-01481: invalid number format model
It's correct that 'DD' and 'YYYY' values are missing but when I tried to retrieve only month also it shows the same error
You can use the following solution:
SELECT
TRUNC(LAST_DAY(ADD_MONTHS(sysdate, -2))) + 1 AS first_date,
TRUNC(LAST_DAY(ADD_MONTHS(sysdate, -1))) AS last_date
FROM dual
Selecting the first day of the previous month using TO_CHAR only is easy, if YYYYMMDD format suits your needs:
SQL> select (to_char(sysdate, 'yyyymm') - 1) || '01' first_day
2 from dual;
FIRST_DAY
------------------------------------------
20180701
SQL>
But, for the last day - I have no idea.
Usual way to do it works, but involves other functions:
SQL> select trunc(add_months(sysdate, -1), 'mm') first_day,
2 trunc(sysdate, 'mm') - 1 last_day
3 from dual;
FIRST_DAY LAST_DAY
---------- ----------
2018-07-01 2018-07-31
SQL>
For LAST_DAY here is the query with hard_code of values of month without using add_months,trunc,to_date
select to_char(sysdate,'yyyy')||
to_char(sysdate,'mm')-1||
case when to_char(sysdate,'mm')-1 in (1,3,5,7,8,10,12) THEN 31
when to_char(sysdate,'mm')-1 in (4,6,9,11) THEN 30
when mod(to_char(sysdate,'yyyy'),4)=0 and to_char(sysdate,'mm')-1=2 THEN 29
when mod(to_char(sysdate,'yyyy'),4)!=0 and to_char(sysdate,'mm')-1=2 THEN 28
end
from dual
If I take your question literally one solution is
CREATE OR REPLACE FUNCTION MY_LAST_DAY RETURN TIMESTAMP AS
next_run_date TIMESTAMP;
return_date_after TIMESTAMP := SYSTIMESTAMP - INTERVAL '100' DAY;
BEGIN
LOOP
DBMS_SCHEDULER.EVALUATE_CALENDAR_STRING('FREQ=DAILY;INTERVAL=1;BYMONTHDAY=-1', NULL, return_date_after, next_run_date);
EXIT WHEN EXTRACT(MONTH FROM next_run_date) = EXTRACT(MONTH FROM SYSTIMESTAMP);
return_date_after := next_run_date;
END LOOP;
RETURN return_date_after;
END;
/
CREATE OR REPLACE FUNCTION MY_FIRST_DAY RETURN TIMESTAMP AS
next_run_date TIMESTAMP;
return_date_after TIMESTAMP := SYSTIMESTAMP - INTERVAL '100' DAY;
BEGIN
LOOP
DBMS_SCHEDULER.EVALUATE_CALENDAR_STRING('FREQ=DAILY;INTERVAL=1;BYMONTHDAY=1', NULL, return_date_after, next_run_date);
EXIT WHEN EXTRACT(MONTH FROM next_run_date) = EXTRACT(MONTH FROM SYSTIMESTAMP);
return_date_after := next_run_date;
END LOOP;
RETURN return_date_after;
END;
/
It returns the first and last date of previous month without using TO_DATE (or TO_TIMESTAMP), TRUNC or ADD_MONTHS
But I hope you agree that your question is rather stupid.
For LAST_DAY here is the query with hard_code of values of month without using add_months,trunc,to_date
select to_char(sysdate,'yyyy')||
to_char(sysdate,'mm')-1||
case when to_char(sysdate,'mm')-1 in (1,3,5,7,8,10,12) THEN 31
when to_char(sysdate,'mm')-1 in (4,6,9,11) THEN 30
when mod(to_char(sysdate,'yyyy'),4)=0 and to_char(sysdate,'mm')-1=2 THEN 29
when mod(to_char(sysdate,'yyyy'),4)!=0 and to_char(sysdate,'mm')-1=2 THEN 28
end
from dual
New Query to handle Janurary and Leap year :
select decode(to_char(sysdate,'mm'),1,to_char(sysdate,'yyyy')-1 ,to_char(sysdate,'yyyy'))||
decode(to_char(sysdate,'mm'),1,12, to_char(sysdate,'mm')-1)||
CASE WHEN to_char(sysdate,'mm')-1 in (1,3,5,7,8,10,0) THEN 31
WHEN to_char(sysdate,'mm')-1 in (4,6,9,11) THEN 30
WHEN to_char(sysdate,'mm')-1=2 AND mod(to_char(sysdate,'yyyy'),4)!=0 AND mod(to_char(sysdate,'yyyy'),400)!=0 THEN 29
ELSE 28
END
FROM dual

Oracle Function -- I am passing a date in an Oracle function. Before I use that date in Sql queries

In a function I want to pass a date. beDate is what i am passing.
I want to update beDate as last day of beDate month if beDate is Last_business_day otherwise last_day of previous month.
Can someone lease provide the syntactically correct answer for the IF part in the below code.
CREATE OR REPLACE Function AnyFunction ( beDate IN varchar2 )
RETURN date
IS
IF beDate = [ Last_Business_Day ] then beDate = last_day(beDate) ;
ELSIF beDate > [ Last_Business_Day ] then beDate = last_day(beDate) ;
ELSE beDate = last_day(add_months(last_day(beDate),-1) ) ;
END IF ;
BEGIN
END;
Correct IF-ELSE statement:
CREATE OR REPLACE Function AnyFunction ( beDate IN date )
RETURN date
IS
r_date DATE;
BEGIN
IF beDate >= [ Last_Business_Day ] then rDate = last_day(beDate);
ELSE rDate = last_day(add_months(last_day(beDate),-1) ) ;
END IF ;
RETURN r_date;
END;
Date should be passed as DATE type. Using VARCHAR2 would create conversion problems.
In my opinion there is no need for function or IF at all. CASE expression could do the same.
SELECT t.*
CASE WHEN beDate >= [ Last_Business_Day ] THEN last_day(beDate)
ELSE last_day(add_months(last_day(beDate),-1) )
END AS last_date
FROM tab_name t;
You could try this:
CREATE OR REPLACE Function AnyFunction ( beDate date )
RETURN date
IS
r_date DATE;
BEGIN
IF beDate >= next_day(last_day(beDate)-7,'FRIDAY') then
r_Date = last_day(beDate);
ELSE
r_Date = last_day(add_months(last_day(beDate),-1) ) ;
END IF;
RETURN r_date;
END;
Here's one option.
SQL> create or replace function f_test (beDate in date)
2 return date
3 is
4 l_last_business_day date;
5 retval date;
6 begin
7 -- Calculate last working day in the beDate's month. In Croatian calendar,
8 -- ordinal numbers of Saturday and Sunday are 6 and 7 - change it if necessary.
9 -- I'm intentionally not using "Saturday" (nor Sunday) as our language word for
10 -- it is "subota", Germans call it "Samstag", etc. - it is probably simpler to use
11 -- numbers than words.
12 -- Inline view is here to create the whole month; in it, I'm searching for the
13 -- MAX date which doesn't belong to weekend (i.e. days 6 and 7)
14 select max(datum)
15 into l_last_business_day
16 from (select trunc(beDate, 'mm') + level - 1 datum
17 from dual
18 connect by level <= to_number(to_char(last_day(beDate), 'dd'))
19 )
20 where to_char(datum, 'd') not in (6, 7);
21
22 -- What to return?
23 if beDate >= l_last_business_day then
24 retval := last_day(beDate);
25 else
26 retval := last_day(add_months(beDate, -1));
27 end if;
28
29 -- TRUNC so that the time component (hours, minutes, seconds) aren't returned
30 return trunc(retval);
31 end;
32 /
Function created.
SQL> select
2 f_test(date '2018-04-30') lbd_april,
3 f_test(date '2018-04-15') not_lbd_april
4 from dual;
LBD_APRIL NOT_LBD_APRIL
------------------- -------------------
30.04.2018 00:00:00 31.03.2018 00:00:00
SQL>
[EDIT: Include example for DEC 2017]
29.12.2017 was Friday, and it was the last working day in December. Therefore, function should return 31.12.2017 as a result and - guess what? - it does:
SQL> SELECT f_test (DATE '2017-12-29') lbd_dec
2 FROM DUAL;
LBD_DEC
-------------------
31.12.2017 00:00:00
SQL>
Or, with your SELECT statement:
SQL> select f_test (to_date('29-DEC-2017','DD-MON-YYYY')) from dual;
F_TEST(TO_DATE('29-
-------------------
31.12.2017 00:00:00

How could i get the date by the day of week in oracle 11g PL/SQL

How can i find what date will be in the next MONDAY if i have a current date(sysdate) and a current day of week?
p.s please tell me how to get date by day of week NOT day of week by date.
You can use the next_day function:
select next_day(sysdate, 'MONDAY') from dual;
In case the national settings are not English we can have Oracle generate the localized translation of "Monday" and use it like this:
SQL> set serveroutput on
SQL> alter session set nls_date_format="YYYY-MM-DD";
Session altered.
SQL> alter session set nls_date_language=italian;
Session altered.
SQL> declare
2 v_monday constant varchar2(100) := to_char(to_date('2013-09-30', 'yyyy-mm-dd'), 'day');
3 begin
4 dbms_output.put_line('v_monday = ' || v_monday);
5 dbms_output.put_line('next monday will be ' || next_day(sysdate, v_monday));
6 end;
7 /
v_monday = lunedi
next monday will be 2013-09-30
PL/SQL procedure successfully completed.
SQL> alter session set nls_date_language=spanish;
Session altered.
SQL> declare
2 v_monday constant varchar2(100) := to_char(to_date('2013-09-30', 'yyyy-mm-dd'), 'day');
3 begin
4 dbms_output.put_line('v_monday = ' || v_monday);
5 dbms_output.put_line('next monday will be ' || next_day(sysdate, v_monday));
6 end;
7 /
v_monday = lunes
next monday will be 2013-09-30
PL/SQL procedure successfully completed.
You can try this if you want to force giving current day and current date. Else you can make it more simpler with only date or with only month day.
WITH DATASET
AS (SELECT
'MON' AS CURRENT_DAY,
'2013-06-26' AS CURENT_DATE
FROM
DUAL)
SELECT
CASE
WHEN CURRENT_DAY = 'MON'
THEN
TO_DATE ( CURENT_DATE,
'yyyy-mm-dd' )
+ INTERVAL '7' DAY
WHEN CURRENT_DAY = 'TUE'
THEN
TO_DATE ( CURENT_DATE,
'yyyy-mm-dd' )
+ INTERVAL '6' DAY
WHEN CURRENT_DAY = 'WED'
THEN
TO_DATE ( CURENT_DATE,
'yyyy-mm-dd' )
+ INTERVAL '5' DAY
WHEN CURRENT_DAY = 'THU'
THEN
TO_DATE ( CURENT_DATE,
'yyyy-mm-dd' )
+ INTERVAL '4' DAY
WHEN CURRENT_DAY = 'FRI'
THEN
TO_DATE ( CURENT_DATE,
'yyyy-mm-dd' )
+ INTERVAL '3' DAY
WHEN CURRENT_DAY = 'SAT'
THEN
TO_DATE ( CURENT_DATE,
'yyyy-mm-dd' )
+ INTERVAL '2' DAY
WHEN CURRENT_DAY = 'SUN'
THEN
TO_DATE ( CURENT_DATE,
'yyyy-mm-dd' )
+ INTERVAL '1' DAY
END
AS DATE_OF_NEXT_MON
FROM
DATASET;
This depends on whether sunday is the first or last day of "your" week.
if your week starts with monday then use
select trunc(sysdate, 'iw') + 7 from dual;
if your week starts with sunday then use
select trunc(sysdate, 'w') + 8 from dual;

Resources