In Oracle, I need to calculate a 1-based value of the number of years that have elapsed since a given date, accounting for leap years .
The original calculation did not account for leap years. It was
CEIL ((SYSDATE- prog_start_dt) / 365))
Then I decided to use MONTHS_BETWEEN
, but there is a bug in my new calculation:
CEIL(MONTHS_BETWEEN(SYSDATE, prog_start_dt) / 12)
The bug is that if I'm exactly 1 year after prog_start_date
regardless of time, the value is 1 less. Ex.:
sysdate = 2020-07-01 15:30:00
prog_start_dt = 2019-07-01 00:00:00
--> 1. CEIL ((SYSDATE- prog_start_dt) / 365)) --> Result: 2
--> 2. CEIL(MONTHS_BETWEEN(SYSDATE, prog_start_dt) / 12) --> Result 1
The correct result is 2
. So I need the behavior of the top formula, but accounting for leap years, which obviously won't be 365
. Is there a good solution?
This is a documented behavior :
If
date1
anddate2
are either the same days of the month or both last days of months, then the result is always an integer.
If you really want to work around this, then one option is to add some logic to handle this specific case: when dates being compared belong to the same month and day, then check the time portion to see if the year is yet elapsed:
ceil(months_between(sysdate, prog_start_dt) / 12)
+ case when to_char(sysdate, 'mm-dd') = to_char(prog_start_dt, 'mm-dd')
and to_char(sysdate, 'hh24:mi') > to_char(prog_start_dt, 'hh24:mi')
then 1
else 0
end
with t as (
select
to_date('2020-07-01 15:30:00', 'yyyy-mm-dd hh24:mi:ss') date1,
to_date('2019-07-01 00:00:00' , 'yyyy-mm-dd hh24:mi:ss') date2
from dual
)
select
ceil(months_between(date1, date2) / 12)
+ case when to_char(date1, 'mm-dd') = to_char(date2, 'mm-dd')
and to_char(date1, 'hh24:mi') > to_char(date2, 'hh24:mi')
then 1
else 0
end year_diff
from t
| YEAR_DIFF | | --------: | | 2 |
From the documentation on MONTHS_BETWEEN
:
MONTHS_BETWEEN
returns number of months between datesdate1
anddate2
. Ifdate1
is later thandate2
, then the result is positive. Ifdate1
is earlier thandate2
, then the result is negative. Ifdate1
anddate2
are either the same days of the month or both last days of months, then the result is always an integer. Otherwise Oracle Database calculates the fractional portion of the result based on a 31-day month and considers the difference in time componentsdate1
anddate2
.
You can "fix" MONTHS_BETWEEN
so it does not always use integer values when the day-of-the-month is the same (or if they are both last days of the month) using:
MONTHS_BETWEEN( end_date, start_date )
+
CASE
WHEN EXTRACT( DAY FROM start_date ) = EXTRACT( DAY FROM end_date )
OR ( start_date = LAST_DAY( start_date ) AND end_date = LAST_DAY( end_date ) )
THEN ( end_date
- ADD_MONTHS( start_date, MONTHS_BETWEEN( end_date, start_date ) )
) / 31
ELSE 0
END
So, your query would be:
SELECT start_date,
end_date,
CEIL(
(
MONTHS_BETWEEN( end_date, start_date )
+
CASE
WHEN EXTRACT( DAY FROM start_date ) = EXTRACT( DAY FROM end_date )
OR ( start_date = LAST_DAY( start_date ) AND end_date = LAST_DAY( end_date ) )
THEN ( end_date
- ADD_MONTHS( start_date, MONTHS_BETWEEN( end_date, start_date ) )
) / 31
ELSE 0
END
)
/ 12
) AS years
FROM test_data;
And for some test data:
CREATE TABLE test_data ( start_date, end_date ) AS
SELECT DATE '2019-07-01',
DATE '2020-07-01' + INTERVAL '15:30:00' HOUR TO SECOND
FROM DUAL
UNION ALL
SELECT DATE '2019-07-31',
DATE '2020-02-29' + INTERVAL '15:30:00' HOUR TO SECOND
FROM DUAL
UNION ALL
SELECT DATE '2019-02-28',
DATE '2020-02-29' + INTERVAL '15:30:00' HOUR TO SECOND
FROM DUAL;
This outputs:
START_DATE | END_DATE | YEARS:------------------ |:------------------ | ----: 2019-07-01 00:00:00 | 2020-07-01 15:30:00 | 2 2019-07-31 00:00:00 | 2020-02-29 15:30:00 | 1 2019-02-28 00:00:00 | 2020-02-29 15:30:00 | 2
db<>fiddle here
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.