How to calculate number of leap years between two dates in Oracle? - sql

I want to calculate the number of leap years between the hire date of the employee and the current date (on hr.employees table in Oracle SQL Developer). How to do this?

A leap year consists of 366 days. I assume a "leap year" between two dates consists of all the days from Jan 1 to Dec 31 of a year with Feb 29th.
Based on this understanding, there is a pretty simple solution.
Count the number of days between the Jan 1 of the year following the hire date and Jan 1 of the year of the end date.
Count the number of years between those two dates.
Subtract the difference between the days and the number of years * 365
Happily, built-in functions do most of the work.
This results in:
(trunc(sysdate, 'YYYY') -
trunc(hiredate + interval '1' year, 'YYYY') -
365 * (extract(year from sysdate) - extract(year from hiredate) - 1)
) as num_years
It is a little trickier to count leap days but that is not what the question is asking.

You can reuse the code Oracle has already written: just check if creating a leap day raises an exception:
SELECT TO_DATE('2016-02-29','YYYY-MM-DD') FROM DUAL;
29.02.2016
but
SELECT TO_DATE('2018-02-29','YYYY-MM-DD') FROM DUAL;
ORA-01839: date not valid for month specified
So you just have to count the exceptions:
CREATE OR REPLACE FUNCTION count_leap_years (p_from DATE, p_to DATE) RETURN NUMBER
IS
number_of_leap_days NUMBER := 0;
date_not_valid EXCEPTION;
PRAGMA EXCEPTION_INIT(date_not_valid, -1839);
BEGIN
FOR y IN EXTRACT(YEAR FROM p_from) .. EXTRACT(YEAR FROM p_to) LOOP
DECLARE
d DATE;
BEGIN
d := TO_DATE(to_char(y,'fm0000')||'-02-29', 'YYYY-MM-DD');
IF p_from < d AND d < p_to THEN
number_of_leap_days := number_of_leap_days + 1;
END IF;
EXCEPTION WHEN date_not_valid THEN
NULL;
END;
END LOOP;
RETURN number_of_leap_days;
END count_leap_years;
/

Something like this will allow you to select numbers, in this case, from 1 to 2999.
Select Rownum year
From dual
Connect By Rownum <= 2999
Something like this will allow you to check if a specific year is a leap year or not
CASE WHEN ((MOD(YEAR, 4) = 0 AND (MOD(YEAR, 100) <> 0)) OR MOD(year, 400) = 0) THEN 1 ELSE 0 END as isLeapYear
Now you just need to add the filtering and the sum of the leap years.
Select sum(isLeapYear)
from (
Select year, CASE WHEN ((MOD(YEAR, 4) = 0 AND (MOD(YEAR, 100) <> 0)) OR MOD(year, 400) = 0) THEN 1 ELSE 0 END as isLeapYear
FROM (
Select Rownum year
From dual
Connect By Rownum <= 2999
) a
Where a.year >= EXTRACT(YEAR FROM DATE %DateStart%) and a.year <= EXTRACT(YEAR FROM DATE %DateEnd%)
) b
This can be really improved in terms of performance, but like this I think its easier do understand what each step is doing and then you can decompose it into what you really want and expect.

try this:
CREATE OR REPLACE FUNCTION LEAP_YEARS(EMP_ID IN NUMBER)
RETURN NUMBER
IS
HIRE_YEAR NUMBER:=0;
SYS_YEAR NUMBER:=0;
NUMBER_LEAP_YEARS NUMBER:=0;
BEGIN
SELECT EXTRACT(YEAR FROM HIRE_DATE) INTO HIRE_YEAR
FROM EMPLOYEE
WHERE EMPLOYEE_ID = EMP_ID;
SELECT EXTRACT(YEAR FROM SYSDATE()) INTO SYS_YEAR
FROM DUAL;
FOR IDX IN HIRE_YEAR..SYS_YEAR LOOP
IF MOD(IDX,4)=0 THEN
NUMBER_LEAP_YEARS := NUMBER_LEAP_YEARS + 1;
END IF;
END LOOP;
RETURN NUMBER_LEAP_YEARS;
END;
I tested it on a toy example and it works. Maybe you need to improve it (hint: maybe it needs some exceptions and it assume that hire_date < sysdate).
Then you can use it as:
SELECT LEAP_YEARS(E.EMPLOYEE_ID) FROM EMPLOYEE E;

Related

Looping with Dates in SQL

I'm working in SQL Developer and calculating the average amount for all active cases at the end of a given month. The way I’ve written it, if I want results for every month in the past year, I have to re-run the code 12 times:
-- DEFINE month_end = '28/02/21';
-- DEFINE month_end = '31/03/21';
DEFINE month_end = '30/04/21';
with active_at_month_end as (
SELECT amount
FROM table
WHERE start_date <= '&month_end'
AND end_date > '&month_end'
)
SELECT extract(year from to_date('&month_end','DD/MM/YY')) as year,
extract(month from to_date('&month_end','DD/MM/YY')) as month,
avg(amount) as avg_amount
FROM active_at_month_end
Is there a way I could rewrite it (maybe using a for loop?) so I only have to run it once and get results like this?
Year
Month
avg_amt
2021
2
###
2021
3
###
2021
4
###
If you're using Oracle, you may use something like below -
DECLARE
month_end DATE := '31-DEC-2020'; -- assuming you want to display output from Jan-21
no_Of_Months NUMBER := 12; -- if you want to display 12 months
BEGIN
FOR i IN 1 .. no_Of_Months
LOOP
month_end := ADD_MONTHS(month_end, 1); -- output will start from Jan-2021
Select year, month, avg(amount) as avg_amount
from
(SELECT extract(year from month_end) as year,
extract(month from month_end) as month,
amount
FROM table
WHERE start_date <= month_end
AND end_date > month_end)
) temp
group by year, month;
END LOOP;
END;

SQL: Getting the values of all weekdays of a certain calendar week

I am trying to get the value of bought items of all weekdays of a certain calendar week.
select to_char(angelegt_am, 'Day') angelegt_am,
sum(menge) menge
from fadorders_out
group by to_char(angelegt_am, 'Day');
this Query is giving me all values of the year but i don't know how to change it so i get the data from a certain single week.
Solutions like
where to_char(angelegt_am,'IW') = 44
group by to_char(angelegt_am, 'Day')
have a problem, they return grouped values from all years, not only the current year.
One solution could be this one:
select to_char(angelegt_am, 'Day') angelegt_am,
sum(menge) menge
from fadorders_out
where to_char(angelegt_am, 'IYYY-"W"IW') = '2020-W44'
group by to_char(angelegt_am, 'Day')
Getting the date from a Week (according to ISO-8601) is not trivial, for example 2021-01-04 is Week 53 of year 2020. The year from ISO-Week can be different to the actual year.
For proper conversion I use these functions:
CREATE OR REPLACE FUNCTION ISOWeek2Date(YEAR INTEGER, WEEK INTEGER) RETURN DATE DETERMINISTIC IS
res DATE;
BEGIN
IF WEEK > 53 OR WEEK < 1 THEN
RAISE VALUE_ERROR;
END IF;
res := NEXT_DAY(TO_DATE( YEAR || '0104', 'YYYYMMDD' ) - 7, 'MONDAY') + ( WEEK - 1 ) * 7;
IF TO_CHAR(res, 'fmIYYY') = YEAR THEN
RETURN res;
ELSE
RAISE VALUE_ERROR;
END IF;
END ISOWeek2Date;
CREATE OR REPLACE FUNCTION ISOWeek2Date(WEEK VARCHAR2) RETURN DATE DETERMINISTIC IS
BEGIN
IF NOT REGEXP_LIKE(WEEK, '^\d{4}-?W\d{2}$') THEN
RAISE VALUE_ERROR;
END IF;
RETURN ISOWeek2Date(REGEXP_SUBSTR(WEEK, '^\d{4}'), REGEXP_SUBSTR(WEEK, '\d{2}$'));
END ISOWeek2Date;
Actually you can extract week number from date and then filter using it:
select to_char(angelegt_am, 'Day') angelegt_am,
sum(menge) menge
from fadorders_out
where to_char(angelegt_am, 'iw') = 2 -- specify your week number here
group by to_char(angelegt_am, 'Day')
But please check that you and your DB have the same view on weeks start and end dates
You can further read https://docs.oracle.com/cd/B19306_01/server.102/b14200/sql_elements004.htm#i34510
Here you can find all other format arguments accepted by to_char function under the "Datetime Format Models" topic
Filter the data in your where clause, e.g.:
select to_char(angelegt_am, 'Day') angelegt_am,
sum(menge) menge
from fadorders_out
where angelegt_am >= to_date ( :week_start_date, 'yyyy-mm-dd' ) -- change format as appropriate
and angelegt_am < to_date ( :week_start_date, 'yyyy-mm-dd' ) + 7
group by to_char(angelegt_am, 'Day')

How do I convert a Week Number to From date of the week in oracle

Suppose I enter WeekNo: 14 the query should return the From Date: April 4th 2016, since the week 14 starts from 4th April to 10th April
select to_date('14','iw') FROM dual;
something like this ? (it work for current year) there discard data from another years
with dates as (select to_char(
to_date('1.01.'||extract(year from sysdate),'dd.mm.yyyy' ) + level -1
,'IW') we,
to_date('1.01.'||extract(year from sysdate),'dd.mm.yyyy' ) + level -1 da
from dual
connect by level <= 365 + 10 )
select * from (
select case
when -- we = null if number of week in jan > 1,2,3,4....
((to_number(we) > 40 )
and extract(year from sysdate) = extract(year from da)
and extract(month from da) = '01') or
-- we = null when current year < year of da
(extract(year from sysdate) != extract(year from da))
then
null
else we
end we,
da
from dates
)
where we = 14
and rownum = 1
Dealing with ISO-Weeks is not trivial, for example January, 1st 2016 is week 53 of 2015, see
select to_char(date '2016-01-01', 'iyyy-"W"iw') from dual;
So, providing only the week number without the (ISO-) year is ambiguous - although it is obvious as along as you are not around new-years date.
Some time ago I wrote this function to get the date from ISO-Week.
FUNCTION ISOWeekDate(week INTEGER, YEAR INTEGER) RETURN DATE DETERMINISTIC IS
res DATE;
BEGIN
IF week > 53 OR week < 1 THEN
RAISE VALUE_ERROR;
END IF;
res := NEXT_DAY(TO_DATE( YEAR || '0104', 'YYYYMMDD' ) - 7, 'MONDAY') + ( week - 1 ) * 7;
IF TO_CHAR(res, 'fmIYYY') = YEAR THEN
RETURN res;
ELSE
RAISE VALUE_ERROR;
END IF;
END ISOWeekDate;
Of course you can just select NEXT_DAY(TO_DATE( YEAR || '0104', 'YYYYMMDD' ) - 7, 'MONDAY') + ( week - 1 ) * 7;, however this would not be error-safe if somebody uses the wrong year.
If it is not an issue to append the year to the week you are looking for, you can also use this :
SELECT (TRUNC ( TO_DATE (SUBSTR ('201627', 1, 4) || '0131', 'YYYY'|| 'MMDD'), 'IYYY')
+ ( 7 * ( TO_NUMBER (SUBSTR ('201627', 5)) - 1)) ) AS iw_Monday
FROM dual
With in this example 201627 being the YYYYIW you are looking for.
It will return the date of the MONDAY of that week.
Found it on Oracle forums, there are a couple of other solutions there. I found this one to be the most elegant.
The advantage is that you do everything from the SELECT, and you don't need either a function or PL/SQL or a WHERE clause.
Disadvantages : you must append the year and specify your search week 2 times, unless you use a variable
Here is a simple and direct computation, taking advantage of various Oracle date functions. Since it compares to what Oracle already counts as ISO week etc., it shouldn't be subject to any of the difficulties other solutions correctly point to and address with additional code.
The "magic number" 14 in the formula should instead be a bind variable, perhaps :iw, or some other mechanism of inputting the ISO week number into the query.
select trunc(sysdate, 'iw') - 7 * (to_number(to_char(trunc(sysdate), 'iw')) - 14) as dt
from dual;
DT
----------
2016-04-04
1 row selected.

How do I select dates between two given dates in an Oracle query?

How do I select dates between two given dates in an Oracle query?
SELECT TO_DATE('12/01/2003', 'MM/DD/YYYY') - 1 + rownum AS d
FROM all_objects
WHERE TO_DATE('12/01/2003', 'MM/DD/YYYY') - 1 + rownum <= TO_DATE('12/05/2003', 'MM/DD/YYYY')
from
http://forums.devshed.com/oracle-development-96/select-all-dates-between-two-dates-92997.html
SELECT * FROM your_table WHERE your_date_field BETWEEN DATE '2010-01-01' AND DATE '2011-01-01';
You can use the LEVEL pseudocolumn in a tricky way to generate a series, so, for example, to get the list of days between today and 20 days from now I can:
select trunc(sysdate+lvl) from
(select level lvl from dual connect by level < ((sysdate+20)-sysdate - 1) )
order by 1
Generically you can see how this would apply for any two given dates.
select trunc(early_date+lvl) from
(select level lvl from dual connect by level < (later_Date-early_date-1) )
order by 1
And you can adjust the clauses if you want to include the two end dates as well.
You could also use the below to get a list of calendar dates between a date range (similar to Michael Broughton's solution)
select (trunc(sysdate) - (trunc(sysdate) - (to_date('start_date')))) -1 + level from dual
connect by level <=
((select (trunc(sysdate) - (trunc(sysdate) - (to_date('end_date'))))-
(trunc(sysdate) - (trunc(sysdate) - (to_date('start_date'))))from dual)+1);
I do this so often for a scheduling app I work on that I created a pipelined table function. Sometimes I need days, hours or 15 minutes between times. This is not exactly my function, because my code is in a package. For example, here, I'm getting days between Jan 1 2020 and Jan 10 2020:
SELECT
days.date_time
FROM
table(between_times(TO_DATE('2020-01-01'),TO_DATE('2020-01-10'),(60*24), 'Y')) days
The pipelined function:
function between_times(i_start_time TIMESTAMP, i_end_time TIMESTAMP, i_interval_in_minutes NUMBER, include_end_time VARCHAR2 := 'N')
RETURN DateTableType PIPELINED
AS
time_counter TIMESTAMP := i_start_time;
BEGIN
IF i_start_time IS NULL OR i_end_time IS NULL or i_start_time > i_end_time OR i_interval_in_minutes IS NULL OR
i_interval_in_minutes <= 0 THEN
RETURN;
END IF;
LOOP
-- by default does not include end time
if (include_end_time = 'Y') THEN
exit when time_counter > i_end_time;
ELSE
exit when time_counter >= i_end_time;
END IF;
pipe row(DateType( time_counter ));
time_counter := time_counter + i_interval_in_minutes/(60*24);
END LOOP;
EXCEPTION WHEN NO_DATA_NEEDED THEN NULL;
END;
Use "between". In a general sense:
select * from someTable where dateCol between date1 and date2;
note that dateCol is defined as a date and date1 and date2 are also date values. If these aren't dates, then you'll convert them to dates using to_date function.
with all_days as (select trunc(to_date('12-03-2017','dd-mm-yyyy')+levl)-1 as all_dates from
(select level levl from dual connect by level < (sysdate-to_date('12-03-2017','DD-MM-YYYY')+1) )
order by 1)
select count(*) as no_of_days from all_days where ltrim(rtrim(to_char(all_dates,'DAY'))) not in ('SATURDAY','SUNDAY');

Number of fridays between two dates

How do I find the number of fridays between two dates(including both the dates) using a select statement in oracle sql?
This will do it:
select ((next_day(date2-7,'FRI')-next_day(date-1,'FRI'))/7)+1 as num_fridays
from data
Perhaps best if I break that down. The NEXT_DAY function returns the next day that is a (Friday in this case) after the date.
So to find the first Friday after d1 would be:
next_day( d1, 'FRI')
But if d1 is a Friday that would return the following Friday, so we adjust:
next_day( d1-1, 'FRI')
Similarly to find the last Friday up to and including d2 we do:
next_day( d1-7, 'FRI')
Subtracting the 2 gives a number of days: 0 if they are the same date, 7 if they a re a week apart and so on:
next_day( d1-7, 'FRI') - next_day( d1-1, 'FRI')
Convert to weeks:
(next_day( d1-7, 'FRI') - next_day( d1-1, 'FRI')) / 7
Finally, if they are the same date we get 0, but really there is 1 Friday, and so on so we add one:
((next_day( d1-7, 'FRI') - next_day( d1-1, 'FRI')) / 7) + 1
I have to throw in my two cents for using a calendar table. (It's a compulsion.)
select count(*) as num_fridays
from calendar
where day_of_week = 'Fri'
and cal_date between '2011-01-01' and '2011-02-17';
num_fridays
-----------
6
Dead simple to understand. Takes advantage of indexes.
Maybe I should start a 12-step group. Calendar Table Anonymous.
See:
Why should I consider using an auxiliary calendar table?
The article's code is specifically for SQL Server but the techniques are portable to most SQL platforms.
With a Calendar table in place your query could be as simple as
SELECT COUNT(*) AS friday_tally
FROM YourTable AS T1
INNER JOIN Calendar AS C1
ON C1.dt BETWEEN T1.start_date AND T1.end_date
WHERE C1.day_name = 'Friday'; -- could be a numeric code
select sum(case when trim(to_char(to_date('2009-01-01','YYYY-MM-DD')+rownum,'Day')) = 'Friday' then 1 else 0 end) number_of_fridays
from dual
connect by level <= to_date('&end_date','YYYY-MM-DD') - to_date('&start_date','YYYY-MM-DD')+1;
Original source - http://forums.oracle.com/forums/thread.jspa?messageID=3987357&tstart=0
Try modifying this one:
CREATE OR REPLACE FUNCTION F_WORKINGS_DAYS
(V_START_DATE IN DATE, V_END_DATE IN DATE)
RETURN NUMBER IS
DAY_COUNT NUMBER := 0;
CURR_DATE DATE;
BEGIN -- loop through and update
CURR_DATE := V_START_DATE;
WHILE CURR_DATE <= V_END_DATE
LOOP
IF TO_CHAR(CURR_DATE,'DY') NOT IN ('SAT','SUN') -- Change this bit to ignore all but Fridays
THEN DAY_COUNT := DAY_COUNT + 1;
END IF;
CURR_DATE := CURR_DATE + 1;
END LOOP;
RETURN DAY_COUNT;
END F_WORKINGS_DAYS;
/
SELECT (NEXT_DAY('31-MAY-2012','SUN')
-NEXT_DAY('04-MAR-2012','SUN'))/7 FROM DUAL
select ((DATEDIFF(dd,#a,#b)) + DATEPART(dw,(#a-6)))/7