Random TIMESTAMP out of range - oracle

I'm trying to create a function, which returns a random TIMESTAMP between a range of timestamps.
It doesn't appear to be working ALL the time as sometimes I get a value back before the starting range and sometimes I get a value back after the ending range.
Below is my test CASE and example of a TIMESTAMP out of range.In this example the TIMESTAMP is after the ending range of TIMESTAMP '2023-01-25 12:00:00'
Can someone please explain what the problem is and show me how to fix it as I can't seem to figure this out.
ALTER SESSION SET NLS_TIMESTAMP_FORMAT = 'DD-MON-YYYY HH24:MI:SS.FF';
CREATE OR REPLACE FUNCTION random_timestamp(
p_from IN TIMESTAMP,
p_to IN TIMESTAMP,
p_fraction IN VARCHAR2 DEFAULT 'Y'
) RETURN TIMESTAMP
IS
return_val_y TIMESTAMP := p_from + dbms_random.value () * (p_to - p_from + INTERVAL '1' DAY);
return_val_n TIMESTAMP (0) := return_val_y;
BEGIN
RETURN CASE
WHEN UPPER (SUBSTR (p_fraction, 1, 1)) = 'Y'
THEN return_val_y
ELSE return_val_N
END;
END random_timestamp;
/
SELECT random_timestamp(
TIMESTAMP '2023-01-25 09:00:00', TIMESTAMP '2023-01-25 12:00:00') as ts from dual
TS
26-JAN-2023 03:59:06.013730

You are adding 1 day:
p_from + dbms_random.value () * (p_to - p_from + INTERVAL '1' DAY);
It falls within the range of p_from to p_to plus 1 day and is doing exactly what you told it to do.
If you don't want the range to be 1 day extra then remove + INTERVAL '1' DAY
CREATE OR REPLACE FUNCTION random_timestamp(
p_from IN TIMESTAMP,
p_to IN TIMESTAMP,
p_fraction IN VARCHAR2 DEFAULT 'Y'
) RETURN TIMESTAMP
IS
return_val_y TIMESTAMP(9) := p_from + dbms_random.value() * (p_to - p_from);
return_val_n TIMESTAMP(0) := return_val_y;
BEGIN
RETURN CASE
WHEN p_fraction LIKE 'Y%' OR p_fraction LIKE 'y%'
THEN return_val_y
ELSE return_val_n
END;
END random_timestamp;
/
Then:
SELECT MIN(ts),
MAX(ts)
FROM (
SELECT random_timestamp(
TIMESTAMP '2023-01-25 09:00:00',
TIMESTAMP '2023-01-25 12:00:00'
) AS ts
FROM DUAL
CONNECT BY LEVEL <= 1e6
);
May randomly output:
MIN(TS)
MAX(TS)
25-JAN-23 09.00.00.017186000
25-JAN-23 11.59.59.999534000
And stays within the range.
fiddle

Related

How to convert string in 'MMDD' format into a date using the current year

I have this procedure that goes PROCEDURE(monthday varchar2). It receives an varchar2 that represents the month and date concatenated always with the format of MMDD. I then want to create a DATE type variable that uses this month and day, and the year being the current year.
Like: desired_date DATE;
desired_date = ?
I'm using Oracle SQL Developer.
EXTRACT the year from SYSDATE and then combine using string concatenation with your input and use TO_DATE to convert to a date:
CREATE PROCEDURE test (
monthday IN VARCHAR2,
desired_date OUT DATE
)
IS
BEGIN
desired_date := TO_DATE( EXTRACT( YEAR FROM SYSDATE ) || monthday, 'YYYYMMDD' );
END;
/
then:
DECLARE
dt DATE;
BEGIN
test( '0101', dt );
DBMS_OUTPUT.PUT_LINE( '0101: ' || dt );
test( '1231', dt );
DBMS_OUTPUT.PUT_LINE( '1231: ' || dt );
BEGIN
test( '9876', dt );
DBMS_OUTPUT.PUT_LINE( '9876: ' || dt );
EXCEPTION
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE( '9876: ' || SQLERRM );
END;
END;
/
outputs:
0101: 2019-01-01 00:00:00
1231: 2019-12-31 00:00:00
9876: ORA-01843: not a valid month
db<>fiddle here
If you want to return NULL for invalid inputs then:
CREATE PROCEDURE test (
monthday IN VARCHAR2,
desired_date OUT DATE
)
IS
BEGIN
desired_date := TO_DATE( EXTRACT( YEAR FROM SYSDATE ) || monthday, 'YYYYMMDD' );
EXCEPTION
WHEN OTHERS THEN
-- In general don't catch OTHERS but in this case the only exceptions
-- are going to be from TO_DATE
desired_date := NULL;
END;
/
db<>fiddle here
Update:
You could simplify the code (as suggested by Aleksej and William Robertson) by not specifying the year value in TO_DATE and use the default which appears to be the current year; however this behaviour is not, obviously, documented in any Oracle documentation pages so I would also include inline documentation within the function so future developers reviewing your function know that you are deliberately using this behaviour:
CREATE PROCEDURE test (
monthday IN VARCHAR2,
desired_date OUT DATE
)
IS
BEGIN
-- Assumes that TO_DATE will, when not specified, default the year to the
-- current year and the time to midnight.
desired_date := TO_DATE( monthday, 'MMDD' );
END;
/
This could be a way
desired_date := to_date(monthday, 'MMDD');
According to this old post in AskTom, to_date should use the current year, if not given:
default year = current year
default month = current month
default day = 1
default hour = 0
default minute = 0
default second = 0
ops$tkyte%ORA10GR2> select sysdate, to_date( ' ', ' ' ) from dual;
SYSDATE TO_DATE('','')
-------------------- --------------------
17-aug-2012 13:41:06 01-aug-2012 00:00:00
Still unable to find this information in Oracle Docs

Oracle PLSQL Recurrence Pattern RFC 2445

I have a requisite on which I need to convert a RFC 2445 Recurrence Pattern to Dates using PLSQL.
Example:
RRULE = FREQ=DAILY;INTERVAL=5;COUNT=10
From that rule, I need to write a table with the next 10 occurrences of that pattern. Something like the image bellow, considering start date as 1/1/2019 12:00:00 AM:
Does Oracle provides any PLSQL Package that allows me to do this? If doesn't, does anybody knows any PLSQL project initiative for this?
Ps: this is the same exactly pattern that Oracle uses on Job Schedules.
DBMS_SCHEDULER.EVALUATE_CALENDAR_STRING might be able to do this.
The syntax supported by the package seems similar to RFC 2445, but not identical. The below PL/SQL block prints out the dates based on a calendar string. There are some complications, such as parsing out the COUNT=10 to determine how many times to repeat the calculation.
declare
--Test different calendar strings and start dates.
--p_calendar_string varchar2(4000) := 'FREQ=DAILY;INTERVAL=5;';
p_calendar_string varchar2(4000) := 'FREQ=DAILY;INTERVAL=5;COUNT=10';
p_start_date date := timestamp '2019-01-01 00:00:00';
v_next_run_date date;
v_count number;
--Find the COUNT and remove it rom the calendar string, if it exists.
procedure get_and_remove_count(p_calendar_string in out varchar2, p_count out number) is
begin
if lower(p_calendar_string) like '%count%' then
p_count := to_number(regexp_substr(p_calendar_string, 'COUNT=([0-9]+)', 1, 1, null, 1));
p_calendar_string := regexp_replace(p_calendar_string, 'COUNT=[0-9]+;?');
else
p_count := 1;
end if;
end;
begin
get_and_remove_count(p_calendar_string, v_count);
--TEST
--dbms_output.put_line('String: '||p_calendar_string||', count: '||v_count);
--Start with the original date.
v_next_run_date := p_start_date-1/24/60/60;
--Loop through the COUNT and display all dates.
for i in 1 .. v_count loop
dbms_scheduler.evaluate_calendar_string
(
calendar_string => p_calendar_string,
start_date => p_start_date,
return_date_after => v_next_run_date,
next_run_date => v_next_run_date
);
dbms_output.put_line(to_char(v_next_run_date, 'mm/dd/yyyy hh:mi:ss am'));
end loop;
end;
/
Output:
01/01/2019 12:00:00 am
01/06/2019 12:00:00 am
01/11/2019 12:00:00 am
01/16/2019 12:00:00 am
01/21/2019 12:00:00 am
01/26/2019 12:00:00 am
01/31/2019 12:00:00 am
02/05/2019 12:00:00 am
02/10/2019 12:00:00 am
02/15/2019 12:00:00 am
You can write a PL/SQL function to parse the string and output a pipelined collection of dates:
Oracle Setup:
CREATE FUNCTION parseRRule(
rrule IN VARCHAR2,
start_date IN DATE
) RETURN SYS.ODCIDATELIST PIPELINED
IS
freq VARCHAR2(10) := UPPER( REGEXP_SUBSTR( rrule, '(^|;)FREQ=(MONTHLY|WEEKLY|DAILY|HOURLY)(;|$)', 1, 1, 'i', 2 ) );
inter NUMBER(4,0) := TO_NUMBER( REGEXP_SUBSTR( rrule, '(^|;)INTERVAL=(\d+)(;|$)', 1, 1, 'i', 2 ) );
cnt NUMBER(4,0) := TO_NUMBER( REGEXP_SUBSTR( rrule, '(^|;)COUNT=(\d+)(;|$)', 1, 1, 'i', 2 ) );
dt DATE := start_date;
step_ds INTERVAL DAY TO SECOND;
step_m NUMBER(3,0);
BEGIN
IF freq IS NULL OR inter IS NULL OR cnt IS NULL OR dt IS NULL THEN
RETURN;
END IF;
IF freq = 'MONTHLY' THEN
step_ds := INTERVAL '0' DAY;
step_m := inter;
ELSIF freq = 'WEEKLY' THEN
step_ds := inter * INTERVAL '7' DAY;
step_m := 0;
ELSIF freq = 'DAILY' THEN
step_ds := inter * INTERVAL '1' DAY;
step_m := 0;
ELSIF freq = 'HOURLY' THEN
step_ds := inter * INTERVAL '1' HOUR;
step_m := 0;
ELSE
NULL;
-- raise exception
END IF;
PIPE ROW ( dt );
FOR i IN 1 .. cnt - 1 LOOP
dt := ADD_MONTHS( dt + step_ds, step_m );
PIPE ROW ( dt );
END LOOP;
END;
/
Query:
SELECT *
FROM TABLE(
parseRRule(
rrule => 'FREQ=DAILY;INTERVAL=5;COUNT=10',
start_date => DATE '2019-01-01'
)
)
Output:
| COLUMN_VALUE |
| :----------- |
| 2019-01-01 |
| 2019-01-06 |
| 2019-01-11 |
| 2019-01-16 |
| 2019-01-21 |
| 2019-01-26 |
| 2019-01-31 |
| 2019-02-05 |
| 2019-02-10 |
| 2019-02-15 |
db<>fiddle here
You can achieve this using connect by query but you need to find out the way of getting frequency and count (use regexp) and use them in below query:
Select date '2019-01-01' + (level-1) * 5 as dates
From dual
Connect by level <= 10;
Cheers!!

ORA-01027: bind variables not allowed for data definition when trying to use if elseif

I am getting 'ORA-01027: bind variables not allowed for data definition'
procedure create_dates_testing (dummy_variable varchar2 default
to_char(sysdate,'YYYYMMDD')) is
begin
DECLARE
day_of_month varchar2(255) := extract(day from sysdate);
today varchar2(255) := to_char(sysdate, 'DAY');
start_date date;
next_start_date date;
BEGIN
IF today='SUNDAY' THEN
-- Select yesterday
start_date := trunc(sysdate) - interval '1' day;
next_start_date := trunc(sysdate);
ELSE IF day_of_month=3 then
-- Select the whole of last month
start_date := runc(sysdate, 'MM') - interval '1' month;
next_start_date := runc(sysdate, 'MM') - interval '1' month
END IF;
END;
execute immediate 'drop table new_customers';
execute immediate 'create table new_customers as
select id, client_name, invoice_date
from clients table
where transactiondate >= :start_date
and transactiondate < :next_start_date;';
end;
How can I resolve this error? Where am I going wrong? I need to put this procedure in a pl/sql package.
As the error says, you can't use bind variables here, so you have to concatenate:
create or replace procedure create_dates_testing
( dummy_variable varchar2 default to_char(sysdate,'YYYYMMDD') )
as
day_of_month varchar2(255) := extract(day from sysdate);
today varchar2(255) := to_char(sysdate +1, 'fmDAY', 'nls_date_language = English');
start_date date;
next_start_date date;
begin
if today = 'SUNDAY' then
-- select yesterday
start_date := trunc(sysdate) - interval '1' day;
next_start_date := trunc(sysdate);
elsif day_of_month = 3 then
-- select the whole of last month
start_date := trunc(sysdate, 'MM') - interval '1' month;
next_start_date := trunc(sysdate, 'MM') - interval '1' month;
else
return;
end if;
execute immediate 'drop table new_customers';
execute immediate 'create table new_customers as
select id, client_name, invoice_date
from clients table
where transactiondate >= date ''' || to_char(start_date,'YYYY-MM-DD') ||
''' and transactiondate < date ''' || to_char(next_start_date,'YYYY-MM-DD') ||'''';
end create_dates_testing;
Presumably there will be some more code to handle the case where it is neither Sunday nor the third of the month, or the new_customers table does not exist.
Edit: added else condition to end processing if neither of the date conditions are met.

Error in increasing Datetime in Oracle/PLSQL

Here I'm using a loop from StartDateTime to EndDatetime and adding 1 hour in every iteration. Everything is working in Loop.But problem is in insert query.Please Check the insert query.
declare
StartDateTime TIMESTAMP :=to_date( '2017-01-01 00:00:00','yyyy-mm-dd
hh24:mi:ss');
EndDateTime TIMESTAMP :=to_date( '2017-12-31 00:00:00','yyyy-mm-dd
hh24:mi:ss');
dateti TIMESTAMP;
dateti2 TIMESTAMP;
StartDateTime1 TIMESTAMP;
sub INTEGER;
semester Number;
begin
sub:=( CAST( EndDateTime AS DATE ) - CAST( StartDateTime AS DATE ) ) ;
FOR i IN 0 .. 1
LOOP
StartDateTime1:=StartDateTime+i;
for idx in 0..2 loop
dateti:=to_date(StartDateTime1+(idx/24.0),'yyyy-mm-dd hh24:mi:ss');
dateti2:=to_date(StartDateTime1+((idx+1)/24.0)+ interval '-1' second,'yyyy-
mm-dd hh24:mi:ss');
case
when to_number(to_char(dateti ,'Q'))>6 then semester:=to_number(2);
else semester:=to_number(1);
end case;
Problem start from here in insert query.It saying non numeric character found.DateSlotStart and DateSlotEnd datatype is Timestramp .Please see the image and advise what should I change?
insert into DimDate1(DateSlotStart,DateSlotEnd,
"Date",SlotName,MonthName,MonthNumberOfYear,Quarter,Year,Semester) values
(to_date(dateti ,'yyyy-mm-dd hh24:mi:ss') ,to_date(dateti2 ,'yyyy-mm-dd
hh24:mi:ss') ,
to_date(dateti ,'yyyy-mm-dd'),to_char(dateti ,'hh24:mi' )||' To
'||to_char(dateti2 ,'hh24:mi' ),to_char(dateti
,'Month'),to_number(to_char(dateti ,'mm')),to_number(to_char(dateti ,'Q')),
to_number(to_char(dateti ,'YYYY')) , semester);
end loop;
END LOOP;
end;
/
Please also check the this
The problem is that you are trying to convert TIMESTAMP to date where it is not required. And it is always better to use CAST when you need to convert it to DATE. Replace your insert with this. It should work.
INSERT INTO dimdate1 (
dateslotstart,
dateslotend,
"Date",
slotname,
monthname,
monthnumberofyear,
quarter,
year,
semester
) VALUES (
dateti,
dateti2,
CAST (dateti AS DATE),
TO_CHAR(dateti,'hh24:mi')
|| ' To '
|| TO_CHAR(dateti2,'hh24:mi'),
TO_CHAR(dateti,'Month'),
to_number(TO_CHAR(dateti,'mm') ),
to_number(TO_CHAR(dateti,'Q') ),
to_number(TO_CHAR(dateti,'YYYY') ),
semester
);

How to dynamically add interval to timestamp?

I need at some point to increment dynamically a timestamp plsql variable.
So, instead of doing this:
timestamp_ := timestamp_ + INTERVAL '1' DAY;
I would like to do thomething like this:
timestamp_ := timestamp_ + INTERVAL days_ DAY;
It doesn't really work.
My final goal is to dynamically create some scheduler jobs for some entities that have an variable expiration date, to avoid creating a single one which would be often executed.
It sounds like you want
timestamp_ := timestamp + numtodsinterval( days_, 'day' );
I would be somewhat cautious, however, about an architecture that involves creating thousands of scheduler jobs rather than one job that runs periodically to clear out expired rows. A single job is a heck of a lot easier to manage and oversee.
Special note:
1. INTERVAL YEAR TO MONTH and
2. INTERVAL DAY TO SECOND
are the only two valid interval datatypes;
Sample Example:
=============================
DECLARE
l_time INTERVAL YEAR TO MONTH;
l_newtime TIMESTAMP;
l_year PLS_INTEGER := 5;
l_month PLS_INTEGER := 11;
BEGIN
-- Notes :
-- 1. format is using "-" to connect year and month
-- 2. No need to mention any other keyword ; Implicit conversion takes place to set interval
l_time := l_year || '-' || l_month;
DBMS_OUTPUT.put_line ( l_time );
SELECT SYSTIMESTAMP + l_time INTO l_newtime FROM DUAL;
DBMS_OUTPUT.put_line ( 'System Timestamp :' || SYSTIMESTAMP );
DBMS_OUTPUT.put_line ( 'New Timestamp After Addition :' || l_newtime );
END;
=============================
Try this:
DECLARE
l_val NUMBER;
l_result VARCHAR2( 20 );
BEGIN
l_val := 1;
SELECT SYSDATE - INTERVAL '1' DAY * l_val INTO l_result FROM DUAL;
DBMS_OUTPUT.put_line( 'Current Date is ' || SYSDATE || ' minus ' || l_val || ' day(s) is ' || l_result );
END;
Output will be:
Current Date is 25-FEB-16 minus 1 day(s) is 24-FEB-16

Resources