I have a table:
table1
start end
1/jan/2012 15/jan/2012
1/feb/2013 5/april/2013
I need to find all the possible monthly, quarterly and yearly timeframes. For ex.
1)
1/jan/2012 15/jan/2012
will fall between:
1/jan/2012 31/jan/2012
1/jan/2012 31/march/2012
1/jan/2012 31/dec/2012
2)
1/feb/2013 5/april/2013
will fall between:
1/feb/2013 28/feb/2013
1/march/2013 31/march/2013
1/april/2013 30/april/2013
1/jan/2013 31/march/2013
1/april/2013 30/june/2013
1/jan/2013 31/dec/2013
Is it possible to do it through SQL query to get all the possible date combinations?
Hope it helps:
-- test data
with table1 as
(select 1 as id,
to_date('20120101', 'YYYYMMDD') as start_dt,
to_date('20120115', 'YYYYMMDD') as end_dt
from dual
union all
select 2 as id,
to_date('20130201', 'YYYYMMDD') as start_dt,
to_date('20130405', 'YYYYMMDD') as end_dt
from dual),
-- get sequences in range [0..max date interval-1]
idx_tab as
(select level - 1 as idx
from dual
connect by level < (select max(end_dt - start_dt) from table1)),
-- expand interval [start_dt; end_dt] by day
dt_tb as
(select t.id, t.start_dt, t.end_dt, t.start_dt + i.idx as dt
from table1 t, idx_tab i
where t.start_dt + idx <= t.end_dt)
select 'Month-' || to_char(dt, 'YYYY-MM'), id, start_dt, end_dt
from dt_tb
union
select 'Quarter-' || to_char(dt, 'YYYY-Q'), id, start_dt, end_dt
from dt_tb
union
select 'Year-' || to_char(dt, 'YYYY'), id, start_dt, end_dt
from dt_tb
order by 1, 2;
WITH date_range AS (
SELECT TO_DATE('2012-01-01', 'YYYY-MM-DD') AS start_date, TO_DATE('2012-01-15', 'YYYY-MM-DD') AS end_date FROM DUAL
UNION
SELECT TO_DATE('2012-02-01', 'YYYY-MM-DD') AS start_date, TO_DATE('2012-04-05', 'YYYY-MM-DD') AS end_date FROM DUAL
), monthly_range AS (
SELECT dr.start_date
, dr.end_date
, 'Monthly' AS range_type
, ADD_MONTHS(TRUNC(dr.start_date, 'MM'), LEVEL - 1) AS month_start
, ADD_MONTHS(LAST_DAY(dr.start_date), LEVEL - 1) AS month_end
FROM date_range dr
CONNECT BY LEVEL <= CEIL(MONTHS_BETWEEN(dr.end_date, dr.start_date))
), quarterly_range AS (
SELECT
dr.start_date
, dr.end_date
, 'Quarterly' AS range_type
, ADD_MONTHS(TRUNC(dr.start_date, 'MM'), (LEVEL - 1) * 3) AS range_start
, ADD_MONTHS(TRUNC(dr.start_date, 'MM'), LEVEL * 3) - 1 AS range_end
FROM date_range dr
CONNECT BY LEVEL <= CEIL(MONTHS_BETWEEN(dr.end_date, dr.start_date)/3)
), yearly_range AS (
SELECT
dr.start_date
, dr.end_date
, 'Yearly' AS range_type
, ADD_MONTHS(TRUNC(dr.start_date, 'MM'), (LEVEL - 1) * 12) AS range_start
, ADD_MONTHS(TRUNC(dr.start_date, 'MM'), LEVEL * 12) - 1 AS range_end
FROM date_range dr
CONNECT BY LEVEL <= CEIL(MONTHS_BETWEEN(dr.end_date, dr.start_date)/12)
)
SELECT mr.* FROM monthly_range mr
UNION
SELECT qr.* FROM quarterly_range qr
UNION
SELECT yr.* FROM yearly_range yr
ORDER BY 1,2,3,4;
SQL Fiddle
Query 1:
WITH dates ( date_start, date_end ) AS (
SELECT DATE '2013-02-01', DATE '2013-04-05' FROM DUAL
)
SELECT 'M' AS period,
ADD_MONTHS( TRUNC( date_start, 'MM' ), LEVEL - 1 ) AS range_start,
ADD_MONTHS( TRUNC( date_start, 'MM' ), LEVEL ) - INTERVAL '1' DAY AS range_end
FROM dates
CONNECT BY
ADD_MONTHS( TRUNC( date_start, 'MM' ), LEVEL - 1 ) <= TRUNC( date_end, 'MM' )
UNION ALL
SELECT 'Q' AS period,
ADD_MONTHS( TRUNC( date_start, 'Q' ), 3 * ( LEVEL - 1) ) AS range_start,
ADD_MONTHS( TRUNC( date_start, 'Q' ), 3 * LEVEL ) - INTERVAL '1' DAY AS range_end
FROM dates
CONNECT BY
ADD_MONTHS( TRUNC( date_start, 'Q' ), 3 * (LEVEL - 1) ) <= TRUNC( date_end, 'Q' )
UNION ALL
SELECT 'Y' AS period,
ADD_MONTHS( TRUNC( date_start, 'Y' ), 12 * ( LEVEL - 1) ) AS range_start,
ADD_MONTHS( TRUNC( date_start, 'Y' ), 12 * LEVEL ) - INTERVAL '1' DAY AS range_end
FROM dates
CONNECT BY
ADD_MONTHS( TRUNC( date_start, 'Y' ), 12 * (LEVEL - 1) ) <= TRUNC( date_end, 'Y' )
Results:
| PERIOD | RANGE_START | RANGE_END |
|--------|----------------------------|----------------------------|
| M | February, 01 2013 00:00:00 | February, 28 2013 00:00:00 |
| M | March, 01 2013 00:00:00 | March, 31 2013 00:00:00 |
| M | April, 01 2013 00:00:00 | April, 30 2013 00:00:00 |
| Q | January, 01 2013 00:00:00 | March, 31 2013 00:00:00 |
| Q | April, 01 2013 00:00:00 | June, 30 2013 00:00:00 |
| Y | January, 01 2013 00:00:00 | December, 31 2013 00:00:00 |
Related
Is there a way to calculate column names automatically in SQL like below. I need top calculate the Calendar weeks based on from and to date and distribute evenly
Material
From
To
Sales
M01
03.10.2022
31.10.2022
1000
M02
14.11.2022
28.11.2022
1000
Expected output
CW =calendar week
Material
Cw40
CW41
Cw42
CW43
CW44
CW45
CW46
CW47
M01
250
250
250
250
M02
500
500
Is there a way to calculate column names automatically in SQL like below.
No, in SQL (not just Oracle SQL) you needs a fixed, known number of column names so it is impossible to dynamically generate columns with a static SQL query.
If you want to generate the data then either:
Generate the data as rows (rather than columns) and pivot the result in whatever third-party application you are using to access the database. You can generate the output using a correlated row-generator:
SELECT t.material,
w.iso_year,
w.iso_week,
w.weekly_sales
FROM table_name t
CROSS APPLY (
SELECT TO_NUMBER(
TO_CHAR(
TRUNC(from_dt, 'IW') + INTERVAL '7' DAY * (LEVEL - 1),
'IYYY'
)
) AS iso_year,
TO_NUMBER(
TO_CHAR(
TRUNC(from_dt, 'IW') + INTERVAL '7' DAY * (LEVEL - 1),
'IW'
)
) AS iso_week,
( LEAST(
TRUNC(from_dt, 'IW') + INTERVAL '7' DAY * LEVEL,
to_dt
)
- GREATEST(
TRUNC(from_dt, 'IW') + INTERVAL '7' DAY * (LEVEL - 1),
from_dt
)
) / (to_dt - from_dt) * sales AS weekly_sales
FROM DUAL
CONNECT BY TRUNC(from_dt, 'IW') + INTERVAL '7' DAY * (LEVEL-1) < to_dt
) w
or:
WITH data (from_dt, dt, to_dt, material, sales) AS (
SELECT from_dt, from_dt, to_dt, material, sales
FROM table_name
UNION ALL
SELECT from_dt,
TRUNC(dt + INTERVAL '7' DAY, 'IW'),
to_dt,
material,
sales
FROM data
WHERE TRUNC(dt + INTERVAL '7' DAY, 'IW') < to_dt
)
SELECT material,
TO_NUMBER(TO_CHAR(dt, 'IYYY')) AS iso_year,
TO_NUMBER(TO_CHAR(dt, 'IW')) AS iso_week,
( LEAST(dt + INTERVAL '7' DAY, to_dt) - dt)
/ (to_dt - from_dt) * sales AS weekly_sales
FROM data
Which, for the sample data:
CREATE TABLE table_name (Material, From_dt, To_dt, Sales) AS
SELECT 'M01', DATE '2022-10-03', DATE '2022-10-31', 1000 FROM DUAL UNION ALL
SELECT 'M02', DATE '2022-11-14', DATE '2022-11-28', 1000 FROM DUAL;
Both output:
MATERIAL
ISO_YEAR
ISO_WEEK
WEEKLY_SALES
M01
2022
40
250
M01
2022
41
250
M01
2022
42
250
M01
2022
43
250
M02
2022
46
500
M02
2022
47
500
Or, if you did want to output the values as columns then you need to specify the columns (which would be 53 columns for all 53 potential ISO weeks) and can do that using:
SELECT *
FROM (
SELECT t.material,
w.iso_year,
w.iso_week,
w.weekly_sales
FROM table_name t
CROSS APPLY (
SELECT TO_NUMBER(
TO_CHAR(
TRUNC(from_dt, 'IW') + INTERVAL '7' DAY * (LEVEL - 1),
'IYYY'
)
) AS iso_year,
TO_NUMBER(
TO_CHAR(
TRUNC(from_dt, 'IW') + INTERVAL '7' DAY * (LEVEL - 1),
'IW'
)
) AS iso_week,
( LEAST(
TRUNC(from_dt, 'IW') + INTERVAL '7' DAY * LEVEL,
to_dt
)
- GREATEST(
TRUNC(from_dt, 'IW') + INTERVAL '7' DAY * (LEVEL - 1),
from_dt
)
) / (to_dt - from_dt) * sales AS weekly_sales
FROM DUAL
CONNECT BY TRUNC(from_dt, 'IW') + INTERVAL '7' DAY * (LEVEL-1) < to_dt
) w
)
PIVOT (
SUM(weekly_sales)
FOR iso_week IN (
1 AS cw01,
2 AS cw02,
3 AS cw03,
-- ...
40 AS cw40,
41 AS cw41,
42 AS cw42,
43 AS cw43,
44 AS cw44,
45 AS cw45,
46 AS cw46,
47 AS cw47,
48 AS cw48,
49 AS cw49,
50 AS cw50,
51 AS cw51,
52 AS cw52,
53 AS cw53
)
)
or:
WITH data (from_dt, dt, to_dt, material, sales) AS (
SELECT from_dt, from_dt, to_dt, material, sales
FROM table_name
UNION ALL
SELECT from_dt,
TRUNC(dt + INTERVAL '7' DAY, 'IW'),
to_dt,
material,
sales
FROM data
WHERE TRUNC(dt + INTERVAL '7' DAY, 'IW') < to_dt
)
SELECT *
FROM (
SELECT material,
TO_NUMBER(TO_CHAR(dt, 'IYYY')) AS iso_year,
TO_NUMBER(TO_CHAR(dt, 'IW')) AS iso_week,
( LEAST(dt + INTERVAL '7' DAY, to_dt) - dt)
/ (to_dt - from_dt) * sales AS weekly_sales
FROM data
)
PIVOT (
SUM(weekly_sales)
FOR iso_week IN (
1 AS cw01,
2 AS cw02,
3 AS cw03,
-- ...
40 AS cw40,
41 AS cw41,
42 AS cw42,
43 AS cw43,
44 AS cw44,
45 AS cw45,
46 AS cw46,
47 AS cw47,
48 AS cw48,
49 AS cw49,
50 AS cw50,
51 AS cw51,
52 AS cw52,
53 AS cw53
)
)
Which both output:
MATERIAL
ISO_YEAR
CW01
CW02
CW03
CW40
CW41
CW42
CW43
CW44
CW45
CW46
CW47
CW48
CW49
CW50
CW51
CW52
CW53
M01
2022
null
null
null
250
250
250
250
null
null
null
null
null
null
null
null
null
null
M02
2022
null
null
null
null
null
null
null
null
null
500
500
null
null
null
null
null
null
fiddle
How can I count the days of each week for a specific month that the user will give to my SQL query? For example, if the user gives April 2021, the result will be:
If the user gives May 2021 the result will be:
Here is a direct computation of the same. The input is given as a string, such as May 2021; you can use a bind variable in its place. Just keep in mind the possibility that the user may be in a non-English-speaking locale; as long as they use their local language in passing the month to the query, everything should work fine.
with
inputs (mth) as (select 'May 2021' from dual)
, first_day (dt) as (select to_date(mth, 'fmMonth yyyy') from inputs)
, mondays (dt, ord, lst) as (
select trunc(dt, 'iw') + 7 * (level - 1), level, max(level) over ()
from first_day
connect by level <= 1 + (trunc(add_months(dt, 1), 'iw') - trunc(dt, 'iw')) / 7
)
select to_number(to_char(dt, 'iw')) as week_number,
case ord when 1 then dt + 7 - trunc(dt + 7, 'mm')
when lst then last_day(dt) + 1 - dt
else 7 end as week_days
from mondays
order by week_number
;
WEEK_NUMBER WEEK_DAYS
----------- ----------
17 2
18 7
19 7
20 7
21 7
22 1
I think this is the query you are looking for:
SELECT WEEK AS WEEK_NUMBER, COUNT(*) AS WEEK_DAYS
FROM (SELECT TO_CHAR(FIRST_DAY + (LEVEL-1), 'IW') AS WEEK
FROM (SELECT supplied_date AS FIRST_DAY, LAST_DAY(supplied_date) - SUPPLIED_DATE+1 AS DAYS
FROM (SELECT TO_DATE('05/2021', 'MM/YYYY') AS SUPPLIED_DATE
FROM DUAL))
CONNECT BY LEVEL <= DAYS)
GROUP BY WEEK
ORDER BY WEEK;
Just replace the inner TO_DATE with your date.
Edit 1: Additional Column (See Comments)
SELECT MONTH_NAME, WEEK AS WEEK_NUMBER, COUNT(*) AS WEEK_DAYS
FROM (SELECT TO_CHAR(FIRST_DAY, 'MONTH') AS MONTH_NAME, TO_CHAR(FIRST_DAY + (LEVEL-1), 'IW') AS WEEK
FROM (SELECT supplied_date AS FIRST_DAY, TO_CHAR(LAST_DAY(supplied_date), 'DD') AS DAYS
FROM (SELECT TO_DATE('05/2021', 'MM/YYYY') AS SUPPLIED_DATE
FROM DUAL))
CONNECT BY LEVEL <= DAYS)
GROUP BY MONTH_NAME, WEEK
ORDER BY WEEK;
Note: I also changed the way I calculate the number of days in the month.
You can generate the weeks (without needing to use any aggregation) using:
WITH input ( month ) AS (
SELECT DATE '2021-04-01' FROM DUAL
),
weeks_of_month ( month_start, week_start, end_day ) AS (
SELECT TRUNC( month, 'MM' ),
TRUNC( month, 'IW' ),
LAST_DAY( TRUNC( month ) )
FROM input
UNION ALL
SELECT month_start,
week_start + INTERVAL '7' DAY,
end_day
FROM weeks_of_month
WHERE week_start + INTERVAL '7' DAY <= end_day
)
SELECT TO_CHAR( week_start, 'IW' ) AS iso_week,
GREATEST( month_start, week_start ) AS first_day_of_week,
LEAST( end_day, week_start + INTERVAL '6' DAY ) AS last_day_of_week,
LEAST( end_day + 1, week_start + INTERVAL '7' DAY )
- GREATEST( month_start, week_start ) AS days
FROM weeks_of_month;
Which outputs (with the NLS_DATE_FORMAT set to YYYY-MM-DD (DY)):
ISO_WEEK
FIRST_DAY_OF_WEEK
LAST_DAY_OF_WEEK
DAYS
13
2021-04-01 (THU)
2021-04-04 (SUN)
4
14
2021-04-05 (MON)
2021-04-11 (SUN)
7
15
2021-04-12 (MON)
2021-04-18 (SUN)
7
16
2021-04-19 (MON)
2021-04-25 (SUN)
7
17
2021-04-26 (MON)
2021-04-30 (FRI)
5
db<>fiddle here
We are using Oracle 11.
In our CASE WHEN statement, I need to check if the number of days between the 2 dates are > 3 business days (so excluding weekends and holidays).
CASE WHEN end_date - start_date > 3 THEN 0 --> this includes weekend
and holidays
WHEN CODE = 1 THEN 1
WHEN CODE =2 THEN 2
ELSE 3
END AS MyColumn
Say I have a holiday calendar table that has column HolidayDates that contains all the holidays, for ex: 12/25/2018, 12/31/2018, etc.
HolidayDates
12/25/2018
12/31/2018
So, if
Date1 = 1/2/19 (Wednesday)
Date2 = 12/27/18 (Thursday)
The number of business days in between Date1 and Date2 is 3 days (12/27, 12/28 and 12/31).
The below query will get the number of business days excluding weekends.
How do I also exclude holidays in this query ?
SELECT TO_CHAR( start_date, 'YYYY-MM-DD "("DY")"') AS start_date,
( TRUNC( end_date, 'IW' ) - TRUNC( start_date, 'IW' ) ) * 5 / 7
+ LEAST( TRUNC( end_date ) - TRUNC( end_date, 'IW' ) + 1, 5 )
- LEAST( TRUNC( start_date ) - TRUNC( start_date, 'IW' ), 5 )
AS Num_Week_Days
FROM table_name;
Thank you.
Taking the code in this previous answer and converting it from a function to a query gives:
Oracle Setup:
CREATE TABLE Holidays ( HolidayDates ) AS
SELECT DATE '2018-12-25' FROM DUAL UNION ALL
SELECT DATE '2018-12-31' FROM DUAL;
CREATE TABLE table_name ( start_date, end_date ) AS
SELECT DATE '2018-12-21', DATE '2018-12-26' FROM DUAL UNION ALL
SELECT DATE '2018-12-28', DATE '2019-01-01' FROM DUAL;
Query:
SELECT t.*,
( TRUNC( end_date, 'IW' ) - TRUNC( start_date, 'IW' ) ) * 5 / 7
+ LEAST( TRUNC( end_date ) - TRUNC( end_date, 'IW' ) + 1, 5 )
- LEAST( TRUNC( start_date ) - TRUNC( start_date, 'IW' ), 5 )
- ( SELECT COUNT(1)
FROM holidays
WHERE HolidayDates BETWEEN t.start_date AND t.end_date
-- Exclude any weekend holidays so we don't double count.
AND TRUNC( HolidayDates ) - TRUNC( HolidayDates, 'IW' ) <= 5
)
AS Num_Week_Days
FROM table_name t;
Output:
START_DATE | END_DATE | NUM_WEEK_DAYS
:--------- | :-------- | ------------:
21-DEC-18 | 26-DEC-18 | 3
28-DEC-18 | 01-JAN-19 | 2
01-JAN-19 | 07-JAN-19 | 5
db<>fiddle here
For example consider this two dates "2012-04-02" "2012-04-30"
I want to get output as shown below but it should not include this dates "2012-04-02" "2012-04-30"
**COLA**
2012-04-01
2012-04-03
2012-04-04
2012-04-05
2012-04-06
2012-04-07
..
..
..
2012-04-29
2012-04-31
Use a hierarchical query:
SELECT start_date + LEVEL - 1
FROM (
SELECT DATE '2012-04-02' + 1 AS start_date,
DATE '2012-04-30' - 1 AS end_date
FROM DUAL
)
CONNECT BY start_date + LEVEL - 1 <= end_date;
or, a recursive sub-query factoring clause:
WITH dates ( value ) AS (
SELECT CAST( DATE '2014-04-02' + 1 AS DATE ) FROM DUAL
UNION ALL
SELECT value + 1 FROM dates WHERE value + 1 < DATE '2014-04-30'
)
SELECT * FROM dates;
or, if you are doing it for the entire month except those dates:
WITH boundaries ( start_date, end_date ) AS (
SELECT DATE '2014-04-02', DATE '2014-04-30' FROM DUAL
)
SELECT TRUNC( start_date, 'MM' ) + LEVEL - 1
FROM boundaries
WHERE TRUNC( start_date, 'MM' ) + LEVEL - 1 NOT IN ( start_date, end_date )
CONNECT BY TRUNC( start_date, 'MM' ) + LEVEL - 1 <= LAST_DAY( end_date );
or:
WITH boundaries ( start_date, end_date ) AS (
SELECT DATE '2014-04-02', DATE '2014-04-30' FROM DUAL
),
dates ( value ) AS (
SELECT TRUNC( start_date, 'MM' ) FROM boundaries
UNION ALL
SELECT value + 1 FROM dates WHERE value + 1 <= LAST_DAY( value )
)
SELECT *
FROM dates d
WHERE NOT EXISTS ( SELECT 1 FROM boundaries WHERE d.value IN ( start_date, end_date ) );
I would like to get a table of months between two dates with a fraction of each month that the two dates cover.
For example with a start date of 15/01/2017 and end date of 01/03/2017 it would output:
01/2017 : 0.5483..
02/2017 : 1
03/2017: 0.0322..
where for January and March the calculations are 17/31 and 1/31 respectively. I currently have the query:
WITH dates_between as (SELECT ADD_MONTHS(TRUNC(TO_DATE(:givenStartDate,'dd/mm/yyyy'), 'MON'), ROWNUM - 1) date_out
FROM DUAL
CONNECT BY ADD_MONTHS(TRUNC(TO_DATE(:givenStartDate,'dd/mm/yyyy'), 'MON'), ROWNUM - 1)
<= TRUNC(TO_DATE(:givenEndDate,'dd/mm/yyyy'), 'MON')
)
select * from dates_between
This outputs each month between two dates and formats it to the start of the month. I just need another column to give me the fraction the start and end dates cover. I'm not sure of a way to do this without it getting messy.
The months_between() function "calculates the fractional portion of the result based on a 31-day month". That means that if your range starts or ends in a month that doesn't have 31 days, the fraction you get might not be quite what you expect:
select months_between(date '2017-04-02', date '2017-04-01') as calc from dual
CALC
----------
.0322580645
... which is 1/31, not 1/30. To get 0.0333... instead you'd need to calculate the number of days in each month, at least for the first and last month. This uses a recursive CTE (11gR2+) to get the months, using a couple of date ranges provided by another CTE as a demo to show the difference (you can use a hierarchical query too of course):
with ranges (id, start_date, end_date) as (
select 1, date '2017-01-15', date '2017-03-01' from dual
union all select 2, date '2017-01-31', date '2017-03-01' from dual
union all select 3, date '2017-02-28', date '2017-04-01' from dual
),
months (id, month_start, month_days, range_start, range_end) as (
select id,
trunc(start_date, 'MM'),
extract(day from last_day(start_date)),
start_date,
end_date
from ranges
union all
select id,
month_start + interval '1' month,
extract(day from last_day(month_start + interval '1' month)),
range_start,
range_end
from months
where month_start < range_end
)
select id,
to_char(month_start, 'YYYY-MM-DD') as month_start,
month_days,
case when month_start = trunc(range_start, 'MM')
then month_days - extract(day from range_start) + 1
when month_start = trunc(range_end, 'MM')
then extract(day from range_end)
else month_days end as range_days,
(case when month_start = trunc(range_start, 'MM')
then month_days - extract(day from range_start) + 1
when month_start = trunc(range_end, 'MM')
then extract(day from range_end)
else month_days end) / month_days as fraction
from months
order by id, month_start;
which gets:
ID MONTH_STAR MONTH_DAYS RANGE_DAYS FRACTION
------ ---------- ---------- ---------- --------
1 2017-01-01 31 17 0.5483
1 2017-02-01 28 28 1
1 2017-03-01 31 1 0.0322
2 2017-01-01 31 1 0.0322
2 2017-02-01 28 28 1
2 2017-03-01 31 1 0.0322
3 2017-02-01 28 1 0.0357
3 2017-03-01 31 31 1
3 2017-04-01 30 1 0.0333
The first CTE ranges is just the demo data. The second, recursive, CTE months generates the start and number of days in each month, while keeping track of the original range dates too. The final query just calculates the fractions based on the number of days in the month in the range against the number of days in that month overall.
The month_days and range_days are only shown in the output so you can see what the calculation is based on, you can obviously omit those from your actual result, and format the month start date however you want.
With your original single pair of bind variables the equivalent would be:
with months (month_start, month_days, range_start, range_end) as (
select trunc(to_date(:givenstartdate, 'DD/MM/YYYY'), 'MM'),
extract(day from last_day(to_date(:givenstartdate, 'DD/MM/YYYY'))),
to_date(:givenstartdate, 'DD/MM/YYYY'),
to_date(:givenenddate, 'DD/MM/YYYY')
from dual
union all
select month_start + interval '1' month,
extract(day from last_day(month_start + interval '1' month)),
range_start,
range_end
from months
where month_start < range_end
)
select to_char(month_start, 'MM/YYYY') as month,
(case when month_start = trunc(range_start, 'MM')
then month_days - extract(day from range_start) + 1
when month_start = trunc(range_end, 'MM')
then extract(day from range_end)
else month_days end) / month_days as fraction
from months
order by month_start;
MONTH FRACTION
------- --------
01/2017 0.5483
02/2017 1
03/2017 0.0322
Here's how I would do it (n.b. I have expanded your dates_between to work against multiple rows, purely for demonstration purposes. If you're only working with a single set of parameters, you wouldn't need to do that):
WITH params AS (SELECT 1 ID, '15/01/2017' givenstartdate, '01/03/2017' givenenddate FROM dual UNION ALL
SELECT 2 ID, '15/01/2017' givenstartdate, '23/01/2017' givenenddate FROM dual UNION ALL
SELECT 3 ID, '01/01/2017' givenstartdate, '07/04/2017' givenenddate FROM dual),
dates_between AS (SELECT ID,
to_date(givenstartdate, 'dd/mm/yyyy') givenstartdate,
to_date(givenenddate, 'dd/mm/yyyy') givenenddate,
add_months(trunc(to_date(givenstartdate, 'dd/mm/yyyy'), 'MON'), LEVEL - 1) start_of_month,
last_day(add_months(trunc(to_date(givenstartdate, 'dd/mm/yyyy'), 'MON'), LEVEL - 1)) end_of_month
FROM params
CONNECT BY add_months(trunc(to_date(givenstartdate, 'dd/mm/yyyy'), 'MON'), LEVEL - 1) <=
trunc(to_date(givenenddate, 'dd/mm/yyyy'), 'MON')
AND PRIOR ID = ID
AND PRIOR sys_guid() IS NOT NULL)
SELECT ID,
givenstartdate,
givenenddate,
start_of_month date_out,
end_of_month,
months_between(LEAST(givenenddate, end_of_month) + 1, GREATEST(start_of_month, givenstartdate))
FROM dates_between;
ID GIVENSTARTDATE GIVENENDDATE DATE_OUT END_OF_MONTH DIFF
1 15/01/2017 01/03/2017 01/01/2017 31/01/2017 0.54838709
1 15/01/2017 01/03/2017 01/02/2017 28/02/2017 1
1 15/01/2017 01/03/2017 01/03/2017 31/03/2017 0.03225806
2 15/01/2017 23/01/2017 01/01/2017 31/01/2017 0.29032258
3 01/01/2017 07/04/2017 01/01/2017 31/01/2017 1
3 01/01/2017 07/04/2017 01/02/2017 28/02/2017 1
3 01/01/2017 07/04/2017 01/03/2017 31/03/2017 1
3 01/01/2017 07/04/2017 01/04/2017 30/04/2017 0.22580645
N.B. You may need to add a case statement to decide whether you want to add 1 or not to the diff calculation, based on your requirements.
Try this
For first month, I have calculated remaining days / total days and for last month, I subtracted it by 1 to get days passed / total days.
DBFiddle Demo
WITH tbl AS
(SELECT date '2017-01-15' AS givenStartDate
,date '2017-03-01' AS givenEndDate
FROM dual
)
SELECT ADD_MONTHS(TRUNC(givenStartDate, 'MON'), ROWNUM - 1) AS date_out ,
CASE
WHEN
rownum - 1 = 0
THEN months_between(last_day(givenStartDate), givenStartDate)
WHEN ADD_MONTHS(TRUNC(givenStartDate, 'MON'), ROWNUM - 1) = TRUNC(givenEndDate, 'MON')
THEN 1 - (months_between(last_day(givenEndDate), givenEndDate))
ELSE 1
END AS perc
FROM tbl
CONNECT BY ADD_MONTHS(TRUNC(givenStartDate, 'MON'), ROWNUM - 1)
<= TRUNC(givenEndDate, 'MON');
Output
+-----------+-------------------------------------------+
| DATE_OUT | PERC |
+-----------+-------------------------------------------+
| 01-JAN-17 | .5161290322580645161290322580645161290323 |
| 01-FEB-17 | 1 |
| 01-MAR-17 | .0322580645161290322580645161290322580645 |
+-----------+-------------------------------------------+