I have a function below, which shows the difference between 2 dates in days, hours, minutes and seconds, which is working fine.
I want to expand the function to include the number of years but I'm unsure how to DECLARE the variable in the function. I know it has to be larger than day(3) to second(0). Is there something like a year(3) to second(0)?
As you can see I get the following error when the DATE spans years. Any help would be appreciated.
ALTER SESSION SET NLS_DATE_FORMAT = 'DD-MON-YYYY HH24:MI:SS';
CREATE OR REPLACE FUNCTION datediff (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 datediff;
/
SELECT
datediff( TO_DATE('2022-04-01 10:11:13','YYYY-MM-DD HH24:MI:SS'),
TO_DATE('2022-04-03 17:48:09','YYYY-MM-DD HH24:MI:SS')) as diff FROM DUAL;
DIFF
2 Days 7 Hours 36 Minutes 56 Seconds
SELECT
datediff( TO_DATE('1981-04-01 10:11:13','YYYY-MM-DD HH24:MI:SS'),
TO_DATE('2022-04-03 17:48:09','YYYY-MM-DD HH24:MI:SS')) as diff FROM DUAL;
ORA-01873: the leading precision of the interval is too small
Is there something like a year(3) to second(0)?
It does not make sense to have a generic INTERVAL data type that contains years, days, hours, minutes and seconds.
If you have an interval of 365 days then if your interval starts on 2022-01-01 than that equals 1 year but if the interval starts on 2020-01-01 then is less than one year as there are 366 days in 2020 due to it being a leap year. Therefore, the number of days in a year depends on which year you are talking about and you cannot have a generic interval that combines days with either months or years as it does not make sense as you need a specific start date.
If the date spans years then you could edit your function and increase the leading precision of the INTERVAL to interval day(5) to second(0) which will let you store about 273 years and then your function will work for your sample data.
SELECT datediff( TIMESTAMP '1981-04-01 10:11:13', TIMESTAMP '2022-04-03 17:48:09')
as diff
FROM DUAL;
Outputs:
DIFF
14977 Days 7 Hours 36 Minutes 56 Seconds
However, since you have known start- and end-dates then you can find the number of full years between the two bounds and then use an INTERVAL DAY TO SECOND data type calculate the number of days, hours, minutes and seconds on the part-year remainder:
CREATE OR REPLACE FUNCTION datediff (p_from date, p_to date)
return varchar2 is
l_years PLS_INTEGER;
l_from DATE;
l_interval interval day(3) to second(0);
begin
l_years := TRUNC(MONTHS_BETWEEN(p_to, p_from)/12);
l_from := ADD_MONTHS(p_from, l_years * 12);
l_interval := (p_to - l_from) DAY(3) TO SECOND(0);
return l_years || ' Years '
|| 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 datediff;
/
Then:
SELECT datediff( TIMESTAMP '2022-04-01 10:11:13', TIMESTAMP '2022-04-03 17:48:09')
as diff
FROM DUAL;
Outputs:
DIFF
0 Years 2 Days 7 Hours 36 Minutes 56 Seconds
and:
SELECT datediff( TIMESTAMP '1981-04-01 10:11:13', TIMESTAMP '2022-04-03 17:48:09')
as diff
FROM DUAL;
Outputs:
DIFF
41 Years 2 Days 7 Hours 36 Minutes 56 Seconds
db<>fiddle here
Related
I have a table named MEAKEL with two columns:
ID NUMBER(3,0)
DAY VARCHAR2(10 CHAR)
A row will look like this:
111, sunday
I got a procedure to get the key and compare it to current day
CREATE OR REPLACE PROCEDURE check_key_day(p_key IN NUMBER) IS
v_day VARCHAR2(10);
v_today VARCHAR2(10);
BEGIN
SELECT DAY INTO v_day
FROM MEAKEL
WHERE ID = p_key;
v_today := LOWER(TO_CHAR(SYSDATE, 'day'));
DBMS_OUTPUT.PUT_LINE('v_day = ' || v_day);
DBMS_OUTPUT.PUT_LINE('Today = ' || v_today);
IF v_day != v_today THEN
RAISE_APPLICATION_ERROR(-20001, 'The day stored in the table does not match today''s day');
END IF;
END;
set serveroutput on
BEGIN
check_key_day(111);
END;
Actually the two output.put_line return "sunday":
v_day = sunday
Today = sunday
I have no idea why RAISE_APPLICATION_ERROR is raised because the condition is when NOT EQUAL, ...
What am I doing wrong?
It is date format model you used that bothers you. 'day' returns day name, but it is right-padded with spaces up to length of the longest day name, which means that all days names have the same length (see day_name_length column in example that follows; all values are 9). It means that your code would work on wednesdays.
But, if you used 'fmday', then you'd get a different result and comparison of current day name and value stored in table would work every day.
SQL> select to_char(sysdate, 'day') || '#' day_1,
2 to_char(sysdate, 'fmday') || '#' day_2
3 from dual;
DAY_1 DAY_2
------------------------------------- -------------------------------------
sunday # sunday#
---
'day': see spaces here? 'fmday': no spaces
SQL> with temp (datum) as
2 (select sysdate + level - 1
3 from dual
4 connect by level <= 7
5 )
6 select to_char(datum, 'dd.mm.yyyy') date_1,
7 to_char(datum, 'day') day_name,
8 length(to_char(datum, 'day')) day_name_length,
9 --
10 to_char(datum, 'fmday') day_name_2,
11 length(to_char(datum, 'fmday')) day_name_length_2
12 from temp
13 order by datum;
DATE_1 DAY_NAME DAY_NAME_LENGTH DAY_NAME_2 DAY_NAME_LENGTH_2
---------- ------------ --------------- ------------ -----------------
12.02.2023 sunday 9 sunday 6
13.02.2023 monday 9 monday 6
14.02.2023 tuesday 9 tuesday 7
15.02.2023 wednesday 9 wednesday 9
16.02.2023 thursday 9 thursday 8
17.02.2023 friday 9 friday 6
18.02.2023 saturday 9 saturday 8
^ ^
7 rows selected. | |
'day' 'fmday'
SQL>
Ive done the following eventually instead of trying to compare sunday with sunday Im comparing 1 with 1, and it seems to be working fine.
CREATE OR REPLACE PROCEDURE check_key_day(p_key IN NUMBER) IS
v_day number(1);
v_today number(1);
BEGIN
SELECT DAY INTO v_day
FROM MEAKEL
WHERE ID = p_key;
v_today := TO_CHAR(SYSDATE, 'D');
IF v_day != v_today THEN
RAISE_APPLICATION_ERROR(-20001, 'The day stored in the table does not match today''s day');
END IF;
END;
set serveroutput on
BEGIN
check_key_day(102);
END;
CREATE TABLE MEAKEL (
ID NUMBER(3) NOT NULL,
DAY NUMBER(1) NOT NULL,
CONSTRAINT MEAKEL_PK PRIMARY KEY (ID)
);
I have a function that calculates the difference between 2 dates or timestamps which is working fine.
Is there a way to modify the function to display the fractional part of the TIMESTAMP in the difference as part of the result. I want both cases to be handled in the same function if possible.
CREATE OR REPLACE FUNCTION datediff (p_from date, p_to date)
return varchar2 is
l_years PLS_INTEGER;
l_from DATE;
l_interval interval day(3) to second(0);
begin
l_years := TRUNC(MONTHS_BETWEEN(p_to, p_from)/12);
l_from := ADD_MONTHS(p_from, l_years * 12);
l_interval := (p_to - l_from) DAY(3) TO SECOND(0);
return l_years || ' Years '
|| 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 datediff;
/
SELECT
datediff( TO_DATE('1981-04-01 10:11:13','YYYY-MM-DD HH24:MI:SS'),
TO_DATE('2022-04-03 17:48:09','YYYY-MM-DD HH24:MI:SS')) as diff FROM DUAL;
DIFF
41 Years 2 Days 7 Hours 36 Minutes 56 Seconds
SELECT
datediff (TO_TIMESTAMP('1981-04-01 10:11:13.551000000', 'YYYY-MM-DD HH24:MI:SS.FF'),
TO_TIMESTAMP('2022-04-03 17:48:09.878700000', 'YYYY-MM-DD HH24:MI:SS.FF')) as diff FROM DUAL;
/* want to show fractional difference here */
DIFF
41 Years 2 Days 7 Hours 36 Minutes 56 Seconds
You want to pass TIMESTAMP arguments to the function and when you add years you also need to ensure that you propagate the fractional component of the timestamp (since ADD_MONTHS returns a DATE data-type without fractional seconds):
CREATE OR REPLACE FUNCTION datediff (p_from timestamp, p_to timestamp)
return varchar2 is
l_years PLS_INTEGER;
l_from TIMESTAMP;
l_interval interval day(3) to second(6);
begin
l_years := TRUNC(MONTHS_BETWEEN(p_to, p_from)/12);
l_from := CAST(TRUNC(ADD_MONTHS(p_from, l_years * 12), 'MI') AS TIMESTAMP)
+ NUMTODSINTERVAL( EXTRACT(SECOND FROM p_from), 'SECOND' );
l_interval := (p_to - l_from) DAY(3) TO SECOND(6);
return l_years || ' Years '
|| 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 datediff;
/
Then:
SELECT datediff(
TIMESTAMP '1981-04-01 10:11:13.551000000',
TIMESTAMP '2022-04-03 17:48:09.878700000'
) as diff
FROM DUAL;
Outputs:
DIFF
41 Years 2 Days 7 Hours 36 Minutes 56.3277 Seconds
fiddle
I've been trying to get the duration between two dates in my question's particular format. It would be easy for duration in days as we can subtract or if it's in months, we can use the months_between function..
i would need it to convert in weeks and days.. the other one would be months and days.. is there a function that can do this in oracle? i've been searching online if anyone is doing something similar like i need to achieve to, but cant seem to find a close one.
Could anyone help me on this?
im using oracle 11g.
examples : range between 2014/12/16 and 2014/12/01 is 2weeks 1day
Thanks.
Converting a raw number of days into a number of weeks and days is pretty simple. Just divide the total by 7 to get the weeks and then calculate the remaining days
SQL> ed
Wrote file afiedt.buf
1 declare
2 total_days integer;
3 total_weeks integer;
4 remaining_days integer;
5 begin
6 total_days := date '2014-12-16' - date '2014-12-01';
7 total_weeks := total_days/ 7;
8 remaining_days := total_days - total_weeks * 7;
9 dbms_output.put_line( total_weeks || ' weeks, ' || remaining_days || ' days.' );
10* end;
SQL> /
2 weeks, 1 days.
PL/SQL procedure successfully completed.
Months and days is a bit more complicated because that's not very well defined-- you'll have to define a number of rules. For example, months_between returns 1 for the number of months between November 30 and December 30. It also returns 1 for the number of months between November 30 and December 31. Is that the result that you would want? Or do you always want your displayed difference to increase if the number of days between the two dates increases?
In SQL, you could achieve this using TRUNC and ROUND with the logic that difference in days needs to be divided by 7 to get the number of weeks.
SQL> WITH DATA AS
2 ( SELECT DATE '2014-12-16' start_dt, DATE '2014-12-01' end_dt FROM dual
3 )
4 SELECT TRUNC((start_dt -end_dt)/7)
5 ||' weeks '
6 || ROUND(((start_dt -end_dt)/7 - TRUNC((start_dt -end_dt)/7))*7)
7 || ' days' week_days
8 FROM DATA
9 /
WEEK_DAYS
--------------
2 weeks 1 days
SQL>
You can try this one it may help you, you can rearrange the output as per your need, as its display is not much clear to me with reference to your question,generally we consider month as of 30 days so i used divide by 30.
create or replace function timediff(fdate date,tdate date)
return varchar2
as
tot_duration varchar2(25);
begin
select floor(days/30)||' Month '||floor(mod(days,30)/7)||' Week '
||mod(mod(days,30),7)||' Day ' into tot_duration
from
(
select (tdate-fdate) days from dual
);
return tot_duration;
end;/
Call the above function:-
select timediff('10.nov.2014','30.dec.2014') duration from dual;
Output result:-
Duration
1 Month 2 Week 6 Day
This query give you months/weeks/days of a range of dates:
SELECT trunc(MONTHS_BETWEEN (end_date, start_date)) months,
floor((end_date - add_months( start_date, trunc(MONTHS_BETWEEN (end_date, start_date)) ))/7) weeks,
mod(end_date - add_months( start_date, trunc(MONTHS_BETWEEN (end_date, start_date)) ),7) days
FROM (SELECT TO_DATE ('2014/12/16', 'yyyy/mm/dd') end_date,
TO_DATE ('2014/12/01', 'yyyy/mm/dd') start_date
FROM DUAL);
You can also format it as you want. For example:
SELECT trunc(MONTHS_BETWEEN (end_date, start_date)) || 'months ' ||
floor((end_date - add_months( start_date, trunc(MONTHS_BETWEEN (end_date, start_date)) ))/7) ||'weeks '||
mod(end_date - add_months( start_date, trunc(MONTHS_BETWEEN (end_date, start_date)) ),7) ||'days' date_values
FROM (SELECT TO_DATE ('2014/12/16', 'yyyy/mm/dd') end_date ,
TO_DATE ('2014/12/01', 'yyyy/mm/dd') start_date
FROM DUAL);
Returns:
DATE_VALUES
--------------------
0months 2weeks 1days.
I hope this helps you. Sorry for late answer.
CREATE FUNCTION time_spell(p_days VARCHAR2) RETURN VARCHAR2 IS
v_y NUMBER;
v_yd NUMBER;
v_m NUMBER;
v_md NUMBER;
v_d NUMBER;
v_w NUMBER;
v_wd NUMBER;
v_days NUMBER;
v_yi NUMBER;
v_mi NUMBER;
v_wi NUMBER;
BEGIN
SELECT p_days/365 INTO v_y FROM dual;
SELECT trunc(v_y) INTO v_yi FROM dual;
select substr(v_y,(select instr(v_y,'.') from dual)) INTO v_yd from dual;
SELECT v_yd * 12 INTO v_m FROM dual;
SELECT trunc(v_m) INTO v_mi FROM dual;
select substr(v_m,(select instr(v_m,'.') from dual)) INTO v_md from dual;
SELECT v_md * 30 INTO v_d FROM dual;
SELECT v_d / 7 INTO v_w FROM dual;
SELECT trunc(v_w) INTO v_wi FROM dual;
select substr(v_w,(select instr(v_w,'.') from dual)) INTO v_wd from dual;
SELECT trunc(v_wd * 7) INTO v_days FROM dual;
RETURN v_yi||' Year(s) '||v_mi||' Month(s) '||v_wi||' Week(s) '||v_days||' Day(s) ';
END;
select time_spell(sysdate- to_date('27-Jul-2014','DD-Mon-YYYY')) from dual;
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;
Can you please help in converting minutes to the format of ('HH24:MI').
I am getting the result in integer which is the minutes. So, how to convert them?
Thanks
Assuming, you want to convert 492 minutes:
select to_char(trunc(sysdate) + 492/24/60, 'hh24:mi') from dual;
If you want a function:
create or replace
function tq84_convert_minutes_hh24mm (p_minutes in number)
return varchar2 is
begin
return to_char (trunc(sysdate) + p_minutes / 24/60, 'hh24:mi');
end tq84_convert_minutes_hh24mm;
/
Later ...
begin
dbms_output.put_line (tq84_convert_minutes_hh24mm(492));
end;
/
Another way:
WITH c AS
(SELECT 492 AS MINUTES FROM DUAL)
SELECT TRIM(TO_CHAR(TRUNC(MINUTES / 60), '09')) || ':' ||
TRIM(TO_CHAR(TRUNC(MOD(ABS(MINUTES), 60)), '09')) AS HHMM
FROM C
This will have issues if the time exceeds 1440 minutes (24 hours) but it gives you something to start with.
Share and enjoy.
This can save you, but I had problems with time greater than 1440 minutes.
select to_char(trunc(sysdate) + [MINUTES]/24/60, 'hh24:mi') from dual;
So I did a function that verifies if minute is greater or equal 1440:
IF (QT_MINUTES_P IS NOT NULL) THEN
IF (QT_MINUTES_P >= 1440) THEN
SELECT ROUND(QT_MINUTES_P/ 60) ||':'|| MOD(QT_MINUTES_P, 60)
INTO DS_RESULT
FROM DUAL;
ELSE
SELECT TO_CHAR(TRUNC(SYSDATE) + (QT_MINUTES_P)/24/60, 'hh24:mi')
INTO DS_RESULT
FROM DUAL;
END IF;
END IF;