The function number_of_days(start_date, end_date) should calculate how manny work days are in between start_date and end_date. It wroks for whatever start_date as long it is not sunday.
Calculation has 3 steps:
take number of days before whole week (whole week can be 0 for small range dates like 21th – 24th)
then it adds whole weeks
takes all days that are leftover after whole week and add them up.
i need 2nd step to to produce whole number like 0, 5, 10, instead i get 0,7142857142857142857142857142857142857 on this example, because start_date parameter is sunday.
I could use ROUND() to solve it, but perhaps there is a better way?
CREATE OR REPLACE FUNCTION number_of_days(start_date IN DATE, end_date IN DATE)
RETURN NUMBER
IS v_number_of_days NUMBER;
first_week_day DATE := TO_DATE('31-12-2017', 'DD-MM-YYYY');
BEGIN
--step 1
SELECT ( CASE WHEN MOD(start_date - first_week_day, 7) BETWEEN 2 AND 5
THEN 6 - MOD(start_date - first_week_day, 7)
ELSE 0 END )
+
--step 2
(( CASE WHEN MOD(end_date - first_week_day, 7) < 7
THEN end_date - MOD(end_date - first_week_day, 7)
ELSE end_date END )
-
( CASE WHEN MOD(start_date - first_week_day, 7) > 1
THEN start_date + 8 - MOD(start_date - first_week_day, 7)
ELSE start_date END ) + 1
) / 7 * 5
+
--step 3
( CASE WHEN MOD(end_date - first_week_day,7) BETWEEN 1 AND 6
THEN CASE WHEN MOD(end_date - first_week_day, 7) = 6
THEN MOD(end_date - first_week_day, 7) - 1
ELSE MOD(end_date - first_week_day, 7) END
ELSE 0 END )
INTO v_number_of_days
FROM DUAL;
RETURN v_number_of_days;
END;
--test
SELECT number_of_days(TO_DATE('21-11-2021', 'DD-MM-YYYY'), TO_DATE('24-11-2021', 'DD-MM-YYYY'))
FROM DUAL;
Advertisement
Answer
You’re just making this too complicated… First of all, it’s better to work with in ISO weeks: it starts from Monday.
- https://en.wikipedia.org/wiki/ISO_week_date
- https://docs.oracle.com/cd/B19306_01/server.102/b14200/functions230.htm
- https://docs.oracle.com/en/database/oracle/oracle-database/18/sqlrf/TRUNC-date.html#GUID-BC82227A-2698-4EC8-8C1A-ABECC64B0E79
So you can easily get first day of ISO week using trunc(dt, 'IW'). For example, 19th of November is Friday and we can easily get the first day of this week using trunc(date'2021-11-19','IW'):
SQL> select trunc(date'2021-11-19','IW') xx from dual; XX ------------------- 2021-11-15 00:00:00
So 2021-11-15 is Monday.
Now let’s imagine we got some dates:
Mo Tu We Th Fr Sa Su
-- -- -- -- -- -- --
X X X X
X X X X X X X
X X X X X X X
X X X X X X X
X X X X X X X
X X X X X X X
X X X X X X X
X X
We can easily get number of working days in full weeks (count_full_weeks * 5/7), so let’s exclude them:
Mo Tu We Th Fr Sa Su
-- -- -- -- -- -- --
X X X X
X X
Then, let’s transform it graphically, since we know that the number of remaining days will always be less than 7 days (less than a week) and the first day will be one of the first 7 days:
Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa Su
-- -- -- -- -- -- -- -- -- -- -- -- -- --
X X X X X X
Let’s replace Mo,Tu,We… with their numbers:
0 1 2 3 4 5 6 0 1 2 3 4 5 6
-- -- -- -- -- -- -- -- -- -- -- -- -- --
X X X X X X
Since we know that number of days in this partial week can’t be 7, we know that the latest end will be:
0 1 2 3 4 5 6 0 1 2 3 4 5 6
-- -- -- -- -- -- -- -- -- -- -- -- -- --
X X X X X X
Now let’s replace numbers of the second week to 7-13:
0 1 2 3 4 5 6 7 8 9 10 11 12 13
-- -- -- -- -- -- -- -- -- -- -- -- -- --
X X X X X X
Now we can understand that we need to check just for 5th and 6th days, ie to check if our period contains 5 and 6.
So it will be like this (I’ve detailed as much as possible):
CREATE OR REPLACE FUNCTION number_of_days(start_date IN DATE, end_date IN DATE)
RETURN NUMBER
as
days_between int;
full_weeks int;
remaining_days int;
remaining_workdays int;
v_first_week_day date;
v_start int;
v_end int;
result int;
begin
days_between := end_date - start_date;
full_weeks := trunc(days_between/7);
remaining_days:= mod(days_between,7); -- or days_between-full_weeks*7
v_first_week_day := trunc(start_date,'IW');
v_start := start_date - v_first_week_day;
v_end := v_start + remaining_days - 1;
remaining_workdays := remaining_days
- case when 5 between v_start and v_end then 1 else 0 end
- case when 6 between v_start and v_end then 1 else 0 end
;
result := full_weeks * 5 + remaining_workdays;
RETURN result;
end;
/
I did it with a lot of sub-steps and intermediate calculations and variables, just to make it more clear. Obviously, you can make it much shorter, for example:
CREATE OR REPLACE FUNCTION number_of_days(start_date IN DATE, end_date IN DATE)
RETURN NUMBER
as
begin
return 5*trunc((end_date-start_date)/7) + mod((end_date-start_date),7)
- case when 5-(start_date - trunc(start_date,'IW')) between 0 and mod((end_date-start_date),7)-1 then 1 else 0 end
- case when 6-(start_date - trunc(start_date,'IW')) between 0 and mod((end_date-start_date),7)-1 then 1 else 0 end
;
end;
/