Split date range in weeks in oracle SQL - sql

I need to split the following data in Oracle SQL:
WITH sample_data AS
(SELECT DATE '2020-12-16' Start_Date, DATE '2021-01-07' End_Date FROM DUAL)
in week ranges for every working week(from Monday to Friday) of this given period. The final result it should look like this:
NEW_STARTDATE NEW_END_DATE
2020-12-16 2020-12-18
2020-12-21 2020-12-25
2020-12-28 2021-01-01
2021-01-04 2021-01-07
So in this example, the first row starts with the initial start date (2020-12-16) which is on Wednesday and continues with the new end date(2020-12-18) which is the next Friday, and so on with ranges of working weeks until the actual end date of this period.

You can use:
WITH sample_data ( start_date, end_date ) AS (
SELECT DATE '2020-12-16', DATE '2021-01-07' FROM DUAL
),
weeks ( start_date, week_start, week_end, end_date ) AS (
SELECT start_date,
TRUNC( start_date, 'IW' ),
LEAST( TRUNC( start_date, 'IW' ) + INTERVAL '4' DAY, end_date ),
end_date
FROM sample_data
UNION ALL
SELECT start_date,
week_start + INTERVAL '7' DAY,
LEAST( week_end + INTERVAL '7' DAY, end_date ),
end_date
FROM weeks
WHERE week_start + INTERVAL '7' DAY <= end_date
)
SELECT GREATEST( week_start, start_date ) AS new_start_date,
week_end AS new_end_date
FROM weeks
WHERE GREATEST( week_start, start_date ) <= week_end;
Which outputs (where the NLS_DATE_FORMAT is YYYY-MM-DD (DY)):
NEW_START_DATE
NEW_END_DATE
2020-12-16 (WED)
2020-12-18 (FRI)
2020-12-21 (MON)
2020-12-25 (FRI)
2020-12-28 (MON)
2021-01-01 (FRI)
2021-01-04 (MON)
2021-01-07 (THU)
db<>fiddle here

Here is one way - compute the Monday dates for the ISO week of the input dates (start_date and end_date), while also keeping track of which is the first and which is the last such Monday in the same hierarchical (connect by) query. Then produce the requested output; for the first week, check that the start_date is not a Saturday or a Sunday (if it is, that "first week" should not produce a row in the output); that is done in the where clause. This is illustrated in the sample dates I used for testing - the input "start date" is a Saturday, so the first "new start date" is the following Monday.
with
sample_data (start_date, end_date) as (
select date '2020-12-12', date '2021-01-02' from dual
)
, mondays (dt, rn, mx) as (
select trunc(start_date, 'iw') + 7 * (level - 1), level, max(level) over ()
from sample_data
connect by level <= 1 + (trunc(end_date, 'iw') - trunc(start_date, 'iw'))/7
)
select case rn when 1 then greatest(start_date, dt)
else dt end as new_start_date,
case rn when mx then least(dt + 4, end_date)
else dt + 4 end as new_end_date
from sample_data cross join mondays
where rn >= 2 or start_date <= dt + 4
;
NEW_START_DATE NEW_END_DATE
--------------- ---------------
MON 14-DEC-2020 FRI 18-DEC-2020
MON 21-DEC-2020 FRI 25-DEC-2020
MON 28-DEC-2020 FRI 01-JAN-2021

Related

Oracle SQL counting the days of the week of the same month

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

Oracle query to Exclude weekends, and 6PM to 9PM

I am trying to achieve a query that returns the time difference between two dates excluding weekends(Saturday and Sunday) and excluding time (6 pm-9 am).
For now, I have a function that is excluding the weekends, But I am unable to exclude time from the query. Can anyone help with this?
The article from which I take help is this
CREATE OR REPLACE FUNCTION get_bus_minutes_between(
p_start_date DATE,
p_end_date DATE
)
RETURN NUMBER
DETERMINISTIC -- ***** Can't hurt
IS
days_diff NUMBER := 0;
end_date DATE := p_end_date;
minutes_diff NUMBER;
start_date DATE := p_start_date;
weeks_diff NUMBER;
BEGIN
IF start_date <= end_date
THEN
-- Move start_date and end_date away from weekends
IF start_date > TRUNC (start_date, 'IW') + 5
THEN -- Use next Monday for start_date
start_date := TRUNC (start_date, 'IW') + 7;
END IF;
IF end_date > TRUNC (end_date, 'IW') + 5
THEN -- Use Friday quitting time
end_date := TRUNC (end_date, 'IW') + 4 + (16.5 / 24);
END IF;
-- Move start_date into the same weeek as end_date
-- (Remember how many weeks we had to move it)
weeks_diff := ( TRUNC (end_date, 'IW')
- TRUNC (start_date, 'IW')
) / 7;
IF weeks_diff > 0
THEN
start_date := start_date + (7 * weeks_diff);
END IF;
-- Make start_date the same day as end_date
-- (Remember how many days we had to move it)
days_diff := TRUNC (end_date) - TRUNC (start_date);
IF days_diff > 0
THEN
start_date := start_date + days_diff;
END IF;
-- Move start_date up to starting time
start_date := GREATEST ( start_date
, TRUNC (start_date) + (8.75 / 24)
);
-- Move end_date back to quitting time
end_date := LEAST ( end_date
, TRUNC (end_date) + ( CASE
WHEN TO_CHAR ( end_date
, 'DY'
, 'NLS_DATE_LANGUAGE=ENGLISH'
) = 'FRI'
THEN 16.5
ELSE 17
END
/ 24
)
);
minutes_diff := ( GREATEST ( 0
, end_date - start_date
)
* 24 * 60
)
+ (days_diff * 495) -- 495 minutes per full day (Mon.-Thu.)
+ (weeks_diff * 2445); -- 2445 minutes per full week
ELSIF start_date > end_date
THEN
minutes_diff := -get_bus_minutes_between (end_date, start_date);
ELSE -- One of the arguments was NULL
minutes_diff := NULL;
END IF;
RETURN ROUND(minutes_diff);
END get_bus_minutes_between;
You can directly calculate the difference in days (adapted from my answer here):
SELECT start_date,
end_date,
ROUND(
(
-- Calculate the full weeks difference from the start of ISO weeks.
( TRUNC( end_date, 'IW' ) - TRUNC( start_date, 'IW' ) ) * (9/24) * (5/7)
-- Add the full days for the final week.
+ LEAST( TRUNC( end_date ) - TRUNC( end_date, 'IW' ), 5 ) * (9/24)
-- Subtract the full days from the days of the week before the start date.
- LEAST( TRUNC( start_date ) - TRUNC( start_date, 'IW' ), 5 ) * (9/24)
-- Add the hours of the final day
+ LEAST( GREATEST( end_date - TRUNC( end_date ) - 9/24, 0 ), 9/24 )
-- Subtract the hours of the day before the range starts.
- LEAST( GREATEST( start_date - TRUNC( start_date ) - 9/24, 0 ), 9/24 )
)
-- Multiply to give minutes rather than fractions of full days.
* 24 * 60
) AS work_day_mins_diff
FROM table_name;
Which, for the sample data:
CREATE TABLE table_name ( start_date, end_date ) AS
SELECT DATE '2020-12-30' + INTERVAL '00' HOUR, DATE '2020-12-30' + INTERVAL '12' HOUR FROM DUAL UNION ALL
SELECT DATE '2020-12-30' + INTERVAL '18' HOUR, DATE '2020-12-30' + INTERVAL '20' HOUR FROM DUAL UNION ALL
SELECT DATE '2020-12-30' + INTERVAL '17:30' HOUR TO MINUTE, DATE '2020-12-30' + INTERVAL '21:30' HOUR TO MINUTE FROM DUAL UNION ALL
SELECT DATE '2021-01-01' + INTERVAL '00' HOUR, DATE '2021-01-04' + INTERVAL '00' HOUR FROM DUAL UNION ALL
SELECT DATE '2021-01-02' + INTERVAL '00' HOUR, DATE '2021-01-04' + INTERVAL '00' HOUR FROM DUAL UNION ALL
SELECT DATE '2020-12-28' + INTERVAL '00' HOUR, DATE '2021-01-04' + INTERVAL '00' HOUR FROM DUAL UNION ALL
SELECT DATE '2020-12-28' + INTERVAL '00' HOUR, DATE '2020-12-29' + INTERVAL '00' HOUR FROM DUAL;
Outputs:
(Using ALTER SESSION SET NLS_DATE_FORMAT = 'YYYY-MM-DD HH24:MI:SS (DY)';)
START_DATE | END_DATE | WORK_DAY_MINS_DIFF
:------------------------ | :------------------------ | -----------------:
2020-12-30 00:00:00 (WED) | 2020-12-30 12:00:00 (WED) | 180
2020-12-30 18:00:00 (WED) | 2020-12-30 20:00:00 (WED) | 0
2020-12-30 17:30:00 (WED) | 2020-12-30 21:30:00 (WED) | 30
2021-01-01 00:00:00 (FRI) | 2021-01-04 00:00:00 (MON) | 540
2021-01-02 00:00:00 (SAT) | 2021-01-04 00:00:00 (MON) | 0
2020-12-28 00:00:00 (MON) | 2021-01-04 00:00:00 (MON) | 2700
2020-12-28 00:00:00 (MON) | 2020-12-29 00:00:00 (TUE) | 540
db<>fiddle here

Oracle query to get the number of business days between 2 dates, excluding holidays

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

Get current day count from current month except holidays

I am a beginner in SQL. I have to fetch current day count(Day Number) from current system month, which should not consider the weekend (Saturday and Sunday).
For example if I am executing the query today (05-Dec-2018) then my output should be 3 (current date is 05-12-2018, here 1st Dec is Saturday and 2nd dec is Sunday. I don't want to include the weekends in this calculation. So Monday (3rd Dec) will be 1, Tue (4th Dec) will be 2 and Wed (5 Dec) will be 3.
Any help for this highly appreciated.
You do not need to use a hierarchical query and can do it independent of the NLS settings using TRUNC( date_value, 'IW' ) to find the start of the ISO week, which is always a Monday.
So:
TRUNC( SYSDATE, 'IW' ) - TRUNC( TRUNC( SYSDATE, 'MM' ), 'IW' )
Will find the number of days between the start of the ISO week containing the first day of the month and the start of the current ISO week. Multiplying this by 5/7 will give the number of week days.
Then all you need to find is how many of those days occurred in the previous month and subtract them. This can be found using:
LEAST( TRUNC( SYSDATE, 'MM' ) - TRUNC( TRUNC( SYSDATE, 'MM' ), 'IW' ), 5 )
and how many days need to be added on for the current week; which is given by:
LEAST( TRUNC( SYSDATE ) - TRUNC( SYSDATE, 'IW' ) + 1, 5 )
So the total can be found using:
SELECT ( TRUNC( SYSDATE, 'IW' ) - TRUNC( TRUNC( SYSDATE, 'MM' ), 'IW' ) ) * 5 / 7
+ LEAST( TRUNC( SYSDATE ) - TRUNC( SYSDATE, 'IW' ) + 1, 5 )
- LEAST( TRUNC( SYSDATE, 'MM' ) - TRUNC( TRUNC( SYSDATE, 'MM' ), 'IW' ), 5 )
AS Num_Week_Days
FROM DUAL;
An example with multiple days:
WITH calendar ( date_value ) AS (
SELECT DATE '2018-12-01' + LEVEL - 1
FROM DUAL
CONNECT BY LEVEL <= 15
)
SELECT date_value,
TO_CHAR( date_value, 'DY' ) AS day,
( TRUNC( date_value, 'IW' ) - TRUNC( TRUNC( date_value, 'MM' ), 'IW' ) ) * 5 / 7
+ LEAST( TRUNC( date_value ) - TRUNC( date_value, 'IW' ) + 1, 5 )
- LEAST( TRUNC( date_value, 'MM' ) - TRUNC( TRUNC( date_value, 'MM' ), 'IW' ), 5 )
AS Num_Week_Days
FROM Calendar;
Output:
DATE_VALUE DAY NUM_WEEK_DAYS
---------- --- -------------
2018-12-01 SAT 0
2018-12-02 SUN 0
2018-12-03 MON 1
2018-12-04 TUE 2
2018-12-05 WED 3
2018-12-06 THU 4
2018-12-07 FRI 5
2018-12-08 SAT 5
2018-12-09 SUN 5
2018-12-10 MON 6
2018-12-11 TUE 7
2018-12-12 WED 8
2018-12-13 THU 9
2018-12-14 FRI 10
2018-12-15 SAT 10
This type of query is really easy to write if you maintain a calendar table in your database. Here is a query that generates a partial calendar of all days from the start of the month up to and including today. Then we can count the number of weekdays.
select count(*)
from (select trunc(sysdate, 'MM') - 1 + level as d
from dual
connect by level <= trunc(sysdate, 'DD') + 1 -- Today
- trunc(sysdate, 'MM') -- First day of current month
)
-- Exclude weekends
where to_char(d, 'DY', 'nls_date_language=american') not in('SAT', 'SUN')
;
You might use this sql select statement with connect by level clause :
select sum(dy) "Total Day"
from
(
select (case when to_char(sysdate-level+1,'D','nls_date_language=turkish') in (6,7)
then 0
else 1 end ) as dy
from dual
connect by level <= to_number(to_char(sysdate,'DD'))
);
Total Day
---------
3
You can try the other cases by replacing both sysdate keywords with sysdate + 3, sysdate + 4 , sysdate + 5 ... etc.
This should work for SQL Server
SET #today= '2018-12-14'
SET #firstDate = (SELECT DATEADD(month, DATEDIFF(month, 0, #today), 0) AS StartOfMonth);
WITH allDates AS
(
SELECT
#firstDate AS DT
UNION ALL
SELECT
DATEADD(dd, 1, DT)
FROM
allDates as S
WHERE
DATEADD(dd, 1, DT) <= #today
)
SELECT count(*) FROM allDates where DATENAME(dw,DT) <> 'Saturday' and DATENAME(dw,DT) <> 'Sunday' option (maxrecursion 0)

Calculate time diffrence in SQL with shifts

i have two table
the first table contains the record of a ticket with start date and end date
start_date | End_Date
21-02-2017 07:52:32 | 22-02-2017 09:56:32
21-02-2017 09:52:32 | 23-02-2017 17:52:32
the second table contains the details of the weekly shift:
shift_day | Start_Time | End_Time
MON 9:00 18:00
TUE 10:00 19:00
WED 9:00 18:00
THU 10:00 19:00
FRI 9:00 18:00
I am looking to get the time difference in the first table which will only include the time as per the second table.
Use a recursive sub-query factoring clause to generate each day within your time ranges and then correlate that with your shifts to restrict the time for each day to be within the shift hours and then aggregate to get the total:
Oracle 18 Setup:
CREATE TABLE times ( start_date, End_Date ) AS
SELECT DATE '2017-02-21' + INTERVAL '07:52:32' HOUR TO SECOND,
DATE '2017-02-22' + INTERVAL '09:56:32' HOUR TO SECOND
FROM DUAL
UNION ALL
SELECT DATE '2017-02-21' + INTERVAL '09:52:32' HOUR TO SECOND,
DATE '2017-02-23' + INTERVAL '17:52:32' HOUR TO SECOND
FROM DUAL;
CREATE TABLE weekly_shifts ( shift_day, Start_Time, End_Time ) AS
SELECT 'MON', INTERVAL '09:00' HOUR TO MINUTE, INTERVAL '18:00' HOUR TO MINUTE FROM DUAL UNION ALL
SELECT 'TUE', INTERVAL '10:00' HOUR TO MINUTE, INTERVAL '19:00' HOUR TO MINUTE FROM DUAL UNION ALL
SELECT 'WED', INTERVAL '09:00' HOUR TO MINUTE, INTERVAL '18:00' HOUR TO MINUTE FROM DUAL UNION ALL
SELECT 'THU', INTERVAL '10:00' HOUR TO MINUTE, INTERVAL '19:00' HOUR TO MINUTE FROM DUAL UNION ALL
SELECT 'FRI', INTERVAL '09:00' HOUR TO MINUTE, INTERVAL '18:00' HOUR TO MINUTE FROM DUAL;
Query 1:
WITH days ( id, start_date, day_start, day_end, end_date ) AS (
SELECT ROWNUM,
start_date,
start_date,
LEAST( TRUNC( start_date ) + INTERVAL '1' DAY, end_date ),
end_date
FROM times
UNION ALL
SELECT id,
start_date,
day_end,
LEAST( day_end + INTERVAL '1' DAY, end_date ),
end_date
FROM days
WHERE day_end < end_date
)
SELECT start_date,
end_date,
SUM( shift_end - shift_start ) AS days_worked_on_shift
FROM (
SELECT ID,
start_date,
end_date,
GREATEST( day_start, TRUNC( day_start ) + start_time ) AS shift_start,
LEAST( day_end, TRUNC( day_start ) + end_time ) AS shift_end
FROM days d
INNER JOIN
weekly_shifts w
ON ( TO_CHAR( d.day_start, 'DY' ) = w.shift_day )
)
GROUP BY id, start_date, end_date;
Result:
START_DATE END_DATE DAYS_WORKED_ON_SHIFT
------------------- ------------------- --------------------
2017-02-21 07:52:32 2017-02-22 09:56:32 0.414259259259259259
2017-02-21 09:52:32 2017-02-23 17:52:32 1.078148148148148148