How to get monthly periods between a date range in Oracle PL/SQL - oracle

I have say, start and end date as "6/11/1996" and "3/1/2002" in "mm/dd/yyyy" format respectively.
I need to get all the monthly periods as given below.
Start Date End Date
6/11/1996 - 6/30/1996
7/01/1996 - 7/31/1996
8/01/1996 - 8/31/1996
.
.
.
Till
2/01/2002 - 2/28/2002
Any help would be highly appreciated.

Assuming the interval is given by way of two bind variables, :from_dt and :to_dt (strings in the indicated format):
with
inputs ( f_dt, t_dt ) as (
select to_date(:from_dt, 'mm/dd/yyyy'), to_date(:to_dt, 'mm/dd/yyyy') from dual
),
ld ( l_day, lvl ) as (
select add_months(last_day(f_dt), level - 1), level
from inputs
connect by level <= months_between(last_day(t_dt), last_day(f_dt)) + 1
)
select case when ld.lvl = 1 then i.f_dt else add_months(ld.l_day, -1) + 1 end
as start_date,
least(i.t_dt, ld.l_day) as end_date
from inputs i cross join ld
;
This assumes that in the original post you did, in fact, mean to have one more interval, from 3/1/2002 to 3/1/2002; and the query deals correctly with the case when the from-date and the to-date are in the same month: if the inputs are 6/11/1996 to 6/21/1996, then the output is exactly that interval.
Added: creating column aliases in the declaration of factored subqueries (in the WITH clause), as I have done, requires Oracle 11.2 or above. For earlier versions, it is necessary to write it a little differently, like so:
with
inputs as (
select to_date(:from_dt, 'mm/dd/yyyy') as f_dt,
to_date(:to_dt , 'mm/dd/yyyy') as t_dt
from dual
),
ld as (
select add_months(last_day(f_dt), level - 1) as l_day, level as lvl
from inputs ...............

You could use connect by query with functions add_months and months_between:
with p as ( select date '1996-06-11' d1, date '2002-03-01' d2 from dual )
select greatest(trunc(add_months(d1, level - 1), 'month'), d1) as d1,
trunc(add_months(d1, level), 'month') - 1 as d2
from p connect by level <= months_between(trunc(d2, 'month'), trunc(d1, 'month'))
The output is exactly as requested.

DECLARE
L_min_date DATE := '11-Jun-1996';
L_max_date DATE := '28-Feb-2002';
L_number_of_months NUMBER := 0;
L_new_date DATE := NULL;
BEGIN
L_number_of_months := MONTHS_BETWEEN(L_max_date, L_min_date);
DBMS_OUTPUT.PUT_LINE('Start_Date End_Date');
FOR i IN 1..L_number_of_months
LOOP
SELECT (ADD_MONTHS(L_min_date, 1) - 1) INTO L_new_date FROM dual;
DBMS_OUTPUT.PUT_LINE(L_min_date || ' - ' || L_new_date);
L_min_date := L_new_date + 1;
END LOOP;
END;
/
Something like above can help.

Related

Generuc function to generate date intervals

I gave the following code to several QA teams, which works fine.
with dt (dt, interv) as (
select TIMESTAMP '2022-11-01 02:37:11', numtodsinterval(5,'MINUTE') from dual
union all
select dt.dt + interv, interv from dt
where dt.dt + interv <
TIMESTAMP '2022-11-01 05:00:00')
select dt from dt;
The problem is VALUES are hardcoded and way too often when a person changes to put different values they make a mistake editing, which causes the code to fail.
Can this code be modified to a pipeline function or a procedure with an out parameter or a macro to prevent such problems.
I would need to pass in 2 dates(order of dates passed should not screw things up. Perhaps use least(), greatest()) an INTERVAL 'N' and a unit S=second M=MINUTE H=hour or D=Day.
Thanks to all who answer and your expertise.
As a macro:
CREATE OR REPLACE FUNCTION gen_dt(i_from_dat IN TIMESTAMP, i_to_dat IN TIMESTAMP, i_interval IN NUMBER, i_interval_type IN VARCHAR2)
RETURN VARCHAR2
SQL_MACRO
IS
BEGIN
RETURN q'~SELECT LEAST(i_from_dat,i_to_dat) + NUMTODSINTERVAL( (LEVEL-1)*i_interval, i_interval_type ) AS dt
FROM DUAL
CONNECT BY LEAST(i_from_dat,i_to_dat) + NUMTODSINTERVAL( (LEVEL-1)*i_interval, i_interval_type) < GREATEST(i_from_dat, i_to_dat)~';
END ;
(Note that you can't use WITH clause because there is a bug preventing substitution of variables inside the WITH() part)
SELECT * FROM gen_dt(SYSTIMESTAMP, SYSTIMESTAMP+1, 4, 'HOUR') ;
03/11/22 13:48:23,072872000
03/11/22 17:48:23,072872000
03/11/22 21:48:23,072872000
04/11/22 01:48:23,072872000
04/11/22 05:48:23,072872000
04/11/22 09:48:23,072872000

Create a PL/SQL procedure that calculate total monthly income. Total must be printed by month

Write a procedure that calculates and displays total income from all sources of all hotels. Totals must be printed by month, and for each month by event and service type. Include discounts.( 10% discount if the reservation date is 2 month before reservation start date).
The tables are:
Hotel Table has:
Hotel_id, hotel_name, Hotel_city, Hotel_state, Hotel_zip,Hotel_phone
Reservation Table has:
Reservation_id, Hotel_id, Room_num, Service_id, Guest_name, Reservation_date, Reservation_start_date, Reservation_end_date, cancelation_date, Num_of_guest, event_type
Room Table has:
Room_num, Hotel_id, Room_type, Room_capacity, Room_cost
service table has:
service_id, Service_type, Service_cost
This is what I tried, but I want to write it in a procedure form; how do I do that? Please help. Thanks
select month (Reservation_end_date) as , event_type,
sum(case when days>= 2 then cost- (cost/100)* 10
else cost) as total_cost)
((select distinct reservation.hotel_id,reservation_date, reservation_start_date,
reservation_end_date, event_type, room.room_type as R_type ,room_cost as R_cost,
months_between(reservation_start_date,reservation_date)as months
from reservation, room
where reservation.hotel_id = room.hotel_id;)
union
(select hotel_name, reservation_date, reservation_start_date,
reservation_end_date, event_type, services_type, services_cost as cost,
months_between(reservation_start_date,reservation_date)as month
from reservation,service, hotel
where reservation.services_id = service.services_id
and reservation.hotel_id = hotel.hotel_id;))
group by month(reservation_end_date),event_type;
The first step is to get the base query right.
To consolidate a set of dates into their common month use trunc(date_col, 'mm'). Presumably room costs and service costs should be calculated on a per night basis.
To calculate the number of nights simply subtract the start date from the end date.
This query should produce the correct result (your stated business rules are incomplete so it's hard to be certain). Like your posted code it has subqueries to calculate the cost of each room reservation and each service reservation. These are aggregated in the outer query:
select to_char(r_month, 'YYYY-MON') as rpt_month
, event_type
, service_type
, sum ( (r_cost - r_discount ) * no_of_nights ) as tot_cost
from (
select trunc(r.reservation_end_date , 'mm') as r_month
, r.event_type
, cast(null as varchar2(10)) as service_type
, rm.room_cost as r_cost
, case when months_between (r.reservation_start_date, r.reservation_date) >= 2
then rm.room_cost * 0.1
else 0 end as r_discount
, (r.reservation_end_date - r.reservation_start_date ) as no_of_nights
from reservation r
join room rm
on ( r.room_num = rm.room_num
and r.hotel_id = rm.hotel_id )
union all
select trunc(r.reservation_end_date , 'mon') as r_month
, r.event_type
, sv.service_type
, sv.service_cost as r_cost
, case when months_between (r.reservation_start_date, r.reservation_date) >= 2
then sv.service_cost * 0.1
else 0 end as r_discount
, (r.reservation_end_date - r.reservation_start_date ) as no_of_nights
from reservation r
join service sv
on ( r.service_id = sv.service_id )
)
group by r_month
, event_type
, service_type
order by r_month
, event_type
, service_type
;
The second step is put this into a procedure. Again your requirements are fuzzy: should the procedure take any parameters? what format should the output be in? As the business domain (hotel bookings) and the format of the question ("write a procedure that ...") this appears to be a homework assignment so here is the simplest interpretation of "display". It uses dbms_output routines to print to the screen, and rpad() and lpad() to give a nice layout (obviously the spacings may be wonky, because you haven't provide the datatypes of the various columns) .
create or replace procedure display_monthly_reservations as
begin
<< heading >>
dbms_output.put(rpad('MONTH', 8));
dbms_output.put( rpad('EVENT_TYPE' , 20 ) || ' ');
dbms_output.put( rpad('SERVICE_TYPE', 20 ) || ' ');
dbms_output.put_line('TOTAL_COST');
<< per_line >>
for r in (
<< insert the query here >>
) loop
dbms_output.put(r.rpt_month || ' ');
dbms_output.put( rpad(r.event_type , 20 ) || ' ');
dbms_output.put( rpad(r.service_type , 20 ) || ' ');
dbms_output.put_line( lpad(to_char(r.tot_cost , '9999999.99'), 10 ) );
end loop per_line;
end display_monthly_reservations;
/

Loop through date range

How to loop Oracle query through the date? I have to put variable in 4 place. My query start with WITH AS, so I can't use Oracle SQL Loop through Date Range solution.
I also can't create temporary table.
Here is my attempt:
WITH d
AS (
SELECT DATE'2015-06-22' + LEVEL - 1 AS current_d
FROM dual
CONNECT BY DATE'2015-06-22' + LEVEL - 1 < DATE'2015-10-04'
),
OrderReserve
AS (
SELECT cvwarehouseid
,lproductid
,SUM(lqty) lqty
FROM ABBICS.iOrdPrdQtyDate
GROUP BY cvwarehouseid
,lproductid
)
SELECT
...
WHERE IORDREFILL.DNCONFIRMEDDELDATE < CAST(TO_CHAR(d.current_d , 'YYYYMMDD') AS NUMBER(38))
...
If I understand you correctly, you assume that you can only use 1 inline table per query. That is not true, you can use multiple inline tables and expand the existing WITH clause with another to loop through dates:
with OrderReserve as (
SELECT cvwarehouseid
,lproductid
,SUM(lqty) lqty
FROM ABBICS.iOrdPrdQtyDate
GROUP BY cvwarehouseid
,lproductid
), date_range as (
select sysdate+level
from dual
connect by level <= 30
)
select *
from OrderReserve, date_range
... -- expand with date_range as you see fit
;

modifying query resultset to include all dates in a range

I have a query which I run on a table TXN_DEC(id, resourceid, usersid, date, eventdesc) which return distinct count of users for a given date-range and resourceid, group by date and eventdesc (each resource can have 4 to 5 eventdesc)
if there is no value of distinct users count on a date in the range, for an eventdesc, then it skips that date row in the resultset.
I need to have all date rows in my resultset or collection such that if there is no value of count for a date,eventdesc combination, then its value is set to 0 but that date still exists in the collection..
How do I go about getting such a collection
I know getting the final dataset entirely from the query result would be too complicated,
but I can use collections in groovy to modify and populate my map/list to get the data in the required format
something similar to following: if
input date range = 5th Feb to 3 March 2011
DataMap = [dateval: '02/05/2011' eventdesc: 'Read' dist_ucnt: 23,
dateval: '02/06/2011' eventdesc: 'Read' dist_ucnt: 23,
dateval: '02/07/2011' eventdesc: 'Read' dist_ucnt: 0, -> this row was not present in query resultset, but row exists in the map with value 0
....and so on till 3 march 2011 and then whole range repeated for each eventdesc
]
If you want all dates (including those with no entries in your TXN_DEC table) for a given range, you could use Oracle to generate your date range and then use an outer join to your existing query. Then you would just need to fill in null values. Something like:
select
d.dateInRange as dateval,
'Read' as eventdesc,
nvl(td.dist_ucnt, 0) as dist_ucnt
from (
select
to_date('02-FEB-2011','dd-mon-yyyy') + rownum - 1 as dateInRange
from all_objects
where rownum <= to_date('03-MAR-2011','dd-mon-yyyy') - to_date('02-FEB-2011','dd-mon-yyyy') + 1
) d
left join (
select
date,
count(distinct usersid) as dist_ucnt
from
txn_dec
where eventDesc = 'Read'
group by date
) td on td.date = d.dateInRange
That's my purely Oracle solution since I'm not a Groovy guy (well, actually, I am a pretty groovy guy...)
EDIT: Here's the same version wrapped in a stored procedure. It should be easy to call if you know the API....
create or replace procedure getDateRange (
p_begin_date IN DATE,
p_end_date IN DATE,
p_event IN txn_dec.eventDesc%TYPE,
p_recordset OUT SYS_REFCURSOR)
AS
BEGIN
OPEN p_recordset FOR
select
d.dateInRange as dateval,
p_event as eventdesc,
nvl(td.dist_ucnt, 0) as dist_ucnt
from (
select
p_begin_date + rownum - 1 as dateInRange
from all_objects
where rownum <= p_end_date - p_begin_date + 1
) d
left join (
select
date,
count(distinct usersid) as dist_ucnt
from
txn_dec
where eventDesc = p_event
group by date
) td on td.date = d.dateInRange;
END getDateRange;

Create View with 365 days

How to Create a View with all days in year. view should fill with dates from JAN-01 to Dec-31. How can I do this in Oracle ?
If current year have 365 days,view should have 365 rows with dates. if current year have 366 days,view should have 366 rows with dates. I want the view to have a single column of type DATE.
This simple view will do it:
create or replace view year_days as
select trunc(sysdate, 'YYYY') + (level-1) as the_day
from dual
connect by level <= to_number(to_char(last_day(add_months(trunc(sysdate, 'YYYY'),11)), 'DDD'))
/
Like this:
SQL> select * from year_days;
THE_DAY
---------
01-JAN-11
02-JAN-11
03-JAN-11
04-JAN-11
05-JAN-11
06-JAN-11
07-JAN-11
08-JAN-11
09-JAN-11
10-JAN-11
11-JAN-11
...
20-DEC-11
21-DEC-11
22-DEC-11
23-DEC-11
24-DEC-11
25-DEC-11
26-DEC-11
27-DEC-11
28-DEC-11
29-DEC-11
30-DEC-11
31-DEC-11
365 rows selected.
SQL>
The date is generated by applying several Oracle date functions:
trunc(sysdate, 'yyyy') gives us the first of January for the current year
add_months(x, 11) gives us the first of December
last_day(x) gives us the thirty-first of December
to_char(x, 'DDD') gives us the number of the thirty-first of December, 365 this year and 366 next.
This last figure provides the upper bound for the row generator CONNECT BY LEVEL <= X
you can use piplined table, it should be something like this:
create or replace type year_date_typ as object (v_day date);
create or replace type year_date_tab as table of year_date_typ;
CREATE OR REPLACE FUNCTION get_dates(year IN VARCHAR2) RETURN year_date_tab PIPELINED IS
v_start_date date := to_date('0101' || year, 'ddmmyyyy');
res year_date_typ := year_date_typ(null);
v_days_in_year integer := 365;
BEGIN
if to_char(last_day(to_date('0102'||year, 'ddmmyyyy')), 'dd') = '29' then
v_days_in_year := 366;
end if;
FOR i in 0 .. v_days_in_year integer-1 LOOP
res.v_day := v_start_date + i;
pipe row(res);
END LOOP;
return;
END get_dates;
and you can use it:
select * from table(get_dates('2011'));
This works well in MS SQL
SELECT TOP (DATEDIFF(day, DATEADD(yy, DATEDIFF(yy,0,getdate()), 0), DATEADD(yy, DATEDIFF(yy,0,getdate()) + 1, -1))) n = ROW_NUMBER() OVER (ORDER BY [object_id]),
dateadd(day, ROW_NUMBER() OVER (ORDER BY [object_id]) - 1, DATEADD(yy, DATEDIFF(yy,0,getdate()), 0)) AS AsOfDate FROM sys.all_objects

Resources