Skip to Main Content
  • Questions
  • adding day to dates but not counting sat and sun

Breadcrumb

Question and Answer

Tom Kyte

Thanks for the question, john.

Asked: February 13, 2001 - 7:49 pm UTC

Last updated: November 11, 2013 - 9:12 am UTC

Version: 8.1.2

Viewed 10K+ times! This question is

You Asked

I am trying to add days to a scheduling task, but i cannot seem to add days to a day and disregard saturday and sunday as an example:

tuesday feb 13 + 5 days would yield sunday feb 18
what i would really like to be able to do is say that if i add
5 to tuesday feb 13 my answer would be tuesday feb 20. I wnat to just be able to add days to the work week

Any help is greatly appreciated

Thanks

JOHN LOCONTE

and Tom said...

Thanks to Mike Clegg a Consultant specializing in Oracle, we have a better answer to this then the first one I gave. He came up with a procedure that is about 2/3 times faster then my version and makes interesting use of ROWNUM. It is as follows:



ops$tkyte@DEV816> create table
2 holidays
3 ( holiday date primary key )
4 organization index;

Table created.

ops$tkyte@DEV816> -- insert into exclude_dates values ( '14-FEB-2001' );

CREATE OR REPLACE FUNCTION ADD_BUS_DAYS(
P_DATE IN DATE
,P_ADD_NUM IN INTEGER
) RETURN DATE AS
--
V_CNT NUMBER;
V_BUS_DAY DATE := TRUNC(P_DATE);
--
BEGIN
--
SELECT MAX(RNUM)
INTO V_CNT
FROM (SELECT ROWNUM RNUM
FROM ALL_OBJECTS)
WHERE ROWNUM <= P_ADD_NUM
AND TO_CHAR(V_BUS_DAY + RNUM, 'DY' ) NOT IN ('SAT', 'SUN')
AND NOT EXISTS
( SELECT 1
FROM HOLIDAYS
WHERE HOLIDAY = V_BUS_DAY + RNUM );
V_BUS_DAY := V_BUS_DAY + V_CNT;
--
RETURN V_BUS_DAY;
--
END ADD_BUS_DAYS;
/



His query can do determinisically what mine "guessed" at. He uses the same concept but does it a tad more efficiently.

We start by creating a large set of numbers 1, 2, 3, .... That is what the

(SELECT ROWNUM RNUM FROM ALL_OBJECTS)

inline view does for us (all_objects is a good pick, it generally has thousands of rows in it). So, now we just have a big set of numbers. Upon that set we start counting the number of days that are not sat and sun and not a holiday. Each time we get a day that is not one of those days -- ROWNUM in the outer query gets incremented by 1. As soon as ROWNUM in the outer query gets to be the number of business days we wanted to add -- this query terminates. We simply take the biggest RNUM (from the inline view) and add it to the original date -- we are done.

Thanks Mike.

Rating

  (28 ratings)

Is this answer out of date? If it is, please let us know via a Comment

Comments

Love your dedication and insight

Hersel Ahdout, June 22, 2002 - 3:16 pm UTC

The user's question was the first step in how to account for weekends and that could have been just to add 7 days instead of each 5 days or (7*FLOOR(ndays/5)+ MOD(ndays,5).

You answered a bigger question not only to exclude weekends but holidays as well.

The code thought me of things I would never have suspected to use.
Thanks.

Need more explanation

John, February 06, 2003 - 11:08 pm UTC

Tom: This is exactly what I was looking for. But could you elaborate a little more on the select statement of the procedure?

I ran this query to add 6 business days to 01/17/2003. I expect to get 8 days (because there are 2 weekend days in between). But I got nothing.

select max(rownum)
from (select rownum from all_objects)
where rownum <= 6
and to_char(to_date('01/17/2003', 'mm/dd/yyyy') + rownum, 'dy') not in ('sat', 'sun');

MAX(ROWNUM)
-----------


BTW, do you know which Oracle document list the complete built-in functions (eg max, min, length, etc.) for most recent release?




Tom Kyte
February 07, 2003 - 7:42 am UTC

the problem with your query is all about rownum.  You have TWO different rownums in there.  


select max(rownum)   <<<<=== rownum1
  from (select rownum   <<==== rownum2
          from all_objects)
 where rownum <= 6  <<<==== rownum1
   and to_char(to_date('01/17/2003', 'mm/dd/yyyy') + rownum, 'dy') <<<=== rownum1
    not in ('sat', 'sun');

and you didn't alias the inner one so you could use it in the outer one.

Use the supplied query "as is" -- with rnum.  the answer is 10, not 8

ops$tkyte@ORA920> select max(rnum)
  2  from (select rownum rnum from all_objects)
  3  where rownum <= 6
  4  and to_char(to_date('01/17/2003', 'mm/dd/yyyy') + rnum, 'dy') not in ('sat',
  5  'sun');

 MAX(RNUM)
----------
        10

 

Thank you. You are absolutely right!

John, February 07, 2003 - 9:51 am UTC


Is there a way to go backwards

Srimal, February 07, 2003 - 10:47 am UTC

Hello Mr Tom,

The code that you have here goes forward from the date. I have a situation where I have to go backwards.

example: 7 business days prior to today 02/07/2003 was 29th January. Is there a way to calculate on the fly the same using something similar to what you have for going forward.

Thanks in advance

Tom Kyte
February 07, 2003 - 12:33 pm UTC

try substraction?

ops$tkyte@ORA920> !cal 1 2003
    January 2003
Su Mo Tu We Th Fr Sa
          1  2  3  4
 5  6  7  8  9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31


ops$tkyte@ORA920> !cal
    February 2003
Su Mo Tu We Th Fr Sa
                   1
 2  3  4  5  6  7  8
 9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28


ops$tkyte@ORA920> select max(rnum), sysdate, sysdate-max(rnum) six_business_days_AGO
  2    from (select rownum rnum from all_objects)
  3   where rownum <= 6
  4     and to_char(to_date('01/17/2003', 'mm/dd/yyyy') - rnum, 'dy') not in ('sat','sun')
  5  /

 MAX(RNUM) SYSDATE   SIX_BUSIN
---------- --------- ---------
         8 07-FEB-03 30-JAN-03

ops$tkyte@ORA920> select max(rnum), sysdate, sysdate+max(rnum) six_business_days_from_now
  2    from (select rownum rnum from all_objects)
  3   where rownum <= 6
  4     and to_char(to_date('01/17/2003', 'mm/dd/yyyy') + rnum, 'dy') not in ('sat','sun')
  5  /

 MAX(RNUM) SYSDATE   SIX_BUSIN
---------- --------- ---------
        10 07-FEB-03 17-FEB-03

 

Bussiness days to go backwards

Manish Upadhyay, February 07, 2003 - 11:28 am UTC

select max(rnum)
from (select rownum rnum from all_objects)
where rownum <= 7
and to_char(sysdate - rnum, 'dy') not in ('sat', 'sun');


The following query will give you date backwards

Manish Upadhyay, February 07, 2003 - 11:32 am UTC

SQL> select (sysdate-max(rnum))BACKDATE
   from (select rownum rnum from all_objects)
   where rownum <= 7
    and to_char(sysdate - rnum, 'dy') not in ('sat', 'sun');

BACKDATE
---------------
29-JAN-03
 

Thank You

A reader, February 07, 2003 - 3:44 pm UTC

Thanks a lot all of you for a quick response


great function but result change depends on base table

Bobby, July 05, 2007 - 4:31 pm UTC

Hi Tom,

I love your function and everything was going great until I made the change to my production environment. The function was returning weird result. After some investigation, I noticed that the function return different result

In our development server (Oracle 9.2), if the table "user_objects" is chosen the result is 545. If the table "all_objects" is chosen, the result is 7 which is correct. If this was in the production environment, it would be the reverse. Do you know why this would cause a different result?

I tested with today date (7/5/2007) and adding 5 business day.

Thanks,
Bobby


----------
CREATE FUNCTION EMS_ORACLE.ADD_BUS_DAYS(
P_DATE IN DATE
,P_ADD_NUM IN INTEGER
) RETURN DATE AS

V_CNT NUMBER;
V_BUS_DAY DATE := TRUNC(P_DATE);

BEGIN

SELECT MAX(RNUM)
INTO V_CNT
FROM (SELECT ROWNUM RNUM
FROM ALL_OBJECTS)
WHERE ROWNUM <= P_ADD_NUM
AND TO_CHAR(V_BUS_DAY + RNUM, 'DY' ) NOT IN ('SAT', 'SUN')
AND NOT EXISTS
( SELECT 1
FROM CXN_HOLIDAYS
WHERE HOLIDAY_OBSERVED = V_BUS_DAY + RNUM );
V_BUS_DAY := V_BUS_DAY + V_CNT;

RETURN V_BUS_DAY;

END ADD_BUS_DAYS;
Tom Kyte
July 05, 2007 - 4:34 pm UTC

you need a table with enough rows.

user objects likely did not.



use:

select max(...)
into ...
from (select level rnum from dual where level <= p_add_num)
where to_char(.....)
and ....

in 9ir2 and above to avoid the need to have a real table with the right number of rows.

A small fix

Michel Cadot, July 06, 2007 - 12:39 am UTC


Tom, in your last answer you used "where" instead of "connect by" in the inner query:
select level rnum from dual connect by level <= p_add_num

Regards
Michel

Tom Kyte
July 06, 2007 - 12:31 pm UTC

indeed, thanks!

still not quite the expected result

Bobby, July 06, 2007 - 9:54 am UTC

Hi Tom,

FYI: all_objects has 47391 records and user_objects has 666 records.

I am still getting weird result.

-----
SELECT RNUM
FROM (select level rnum from dual connect by level <= 365)
WHERE TO_CHAR(to_date('7/06/2007','mm/dd/yyyy') + RNUM, 'DY' ) NOT IN ('SAT', 'SUN')
AND NOT EXISTS
( SELECT 1
FROM CXN_HOLIDAYS
WHERE HOLIDAY_OBSERVED = to_date('7/06/2007','mm/dd/yyyy') + RNUM )
and rownum <= 5
----------

Output:

RNUM
-----
123
235
339
4
38


-----
SELECT RNUM
FROM (select level rnum from dual connect by level <= 365)
WHERE TO_CHAR(to_date('7/06/2007','mm/dd/yyyy') + RNUM, 'DY' ) NOT IN ('SAT', 'SUN')
AND NOT EXISTS
( SELECT 1
FROM CXN_HOLIDAYS
WHERE HOLIDAY_OBSERVED = to_date('7/06/2007','mm/dd/yyyy') + RNUM )
order by 1
----------

Output:

RNUM
----
3
4
5
6
7
10
11
12
13
.
.

The correct answer should be 7.

Thanks again Tom and Michel for syntax correction.

Bobby
Tom Kyte
July 06, 2007 - 1:04 pm UTC

what do you mean by the answer should be 7?

you are asking for all business days for 365 days. Answer is way more than 7????

both answers above look "correct" to me, please explain your thoughts.

Clarification

Bobby, July 06, 2007 - 5:14 pm UTC

The function is to add 5 business days from today. We disregard Saturday, Sunday and holiday. If today is "7/6/2007", the fifth business day should be "7/13/2007". I am expecting MAX(RNUM) to return a value of 7.

"7/6/2007" + 7 = "7/13/2007"

Note: For my purpose I only need a minimum sequence of up to 30 days (365 was arbitrary) since I will only add 5 business days.


Tom Kyte
July 07, 2007 - 10:48 am UTC

so, give us the full function, from soup to nuts. show us what you run and what is output.

cause you haven't used max yet as far as I can see.

[RE] to Bobby

Marcio Portes, July 08, 2007 - 10:47 am UTC

Bobby, it worked out for me, (9208). I didn't use holiday's table since this is test, but looks like you have a bug in your code.

ops$marcio:LX92> select * from v$version;

BANNER
----------------------------------------------------------------
Oracle9i Enterprise Edition Release 9.2.0.8.0 - Production
PL/SQL Release 9.2.0.8.0 - Production
CORE    9.2.0.8.0       Production
TNS for Linux: Version 9.2.0.8.0 - Production
NLSRTL Version 9.2.0.8.0 - Production

5 rows selected.
ops$marcio:LX92> alter session set nls_date_format= 'dd/Mon/yyyy';

Session altered.

ops$marcio:LX92> var n number
ops$marcio:LX92> var dt varchar2(20)
ops$marcio:LX92>
ops$marcio:LX92> exec :n := 5

PL/SQL procedure successfully completed.


            N
-------------
            5

ops$marcio:LX92> exec :dt := '07/06/2007'

PL/SQL procedure successfully completed.


DT
--------------------------------
07/06/2007

ops$marcio:LX92>
ops$marcio:LX92>
ops$marcio:LX92> with loopin as
  2    ( select level rn from dual connect by level <= 365 )
  3  select to_date(:dt,'mm/dd/yyyy') + max(rn), max(rn)
  4    from loopin
  5   where rownum <= :n
  6     and to_char(to_date(:dt,'mm/dd/yyyy')+rn,'d') not in (1,7)
  7  /

TO_DATE(:DT       MAX(RN)
----------- -------------
13/Jul/2007             7

1 row selected.

Regards,

Bobby, July 09, 2007 - 10:15 am UTC

I notice that if I commented out the code for the holiday table it would return the correct answer.

----------------
select * from v$version;

BANNER
----------------------------------------------------------------
Oracle Database 10g Enterprise Edition Release 10.2.0.3.0 - 64bi
PL/SQL Release 10.2.0.3.0 - Production
CORE 10.2.0.3.0 Production
TNS for Solaris: Version 10.2.0.3.0 - Production
NLSRTL Version 10.2.0.3.0 - Production

SQL> select ems_oracle.ADD_BUS_DAYS(to_date('07/06/2007', 'mm/dd/yyyy'), 5)
2 from dual;

EMS_ORACL
---------
09-JUN-08

SQL>
-------------------

CREATE FUNCTION EMS_ORACLE.ADD_BUS_DAYS(
P_DATE IN DATE
,P_ADD_NUM IN INTEGER
) RETURN DATE AS

V_CNT NUMBER;
V_BUS_DAY DATE := TRUNC(P_DATE);

BEGIN

SELECT MAX(RNUM)
INTO V_CNT
FROM (select level rnum from dual connect by level <= 365 )
WHERE ROWNUM <= P_ADD_NUM
AND TO_CHAR(V_BUS_DAY + RNUM, 'DY' ) NOT IN ('SAT', 'SUN')
AND NOT EXISTS
( SELECT 1
FROM CXN_HOLIDAYS
WHERE HOLIDAY_OBSERVED = V_BUS_DAY + RNUM );
V_BUS_DAY := V_BUS_DAY + V_CNT;

RETURN V_BUS_DAY;

END ADD_BUS_DAYS;

-------


Tom Kyte
July 09, 2007 - 10:46 am UTC

I see what happened, the optimizer came up with an interesting plan - a hash anti join - for the not exists, and that screwed up the implicit ordering I was accidentally counting on :(

eg: there is a bug in my query, we need to order the data explicitly.

BEGIN
   select rnum into v_cnt
     from (
   SELECT RNUM, row_number() over (order by rnum) rn
     FROM (select level rnum from dual connect by level <= 365 order by 1 )
    WHERE TO_CHAR(v_bus_day + RNUM, 'DY' ) NOT IN ('SAT', 'SUN')
      AND NOT EXISTS ( SELECT 1 FROM  CXN_HOLIDAYS WHERE HOLIDAY_OBSERVED = v_bus_day + rnum )
          )
    where rn = p_add_num;

   RETURN V_BUS_DAY+v_cnt;

END ADD_BUS_DAYS;
/

Let's globalize while we're at it

Stew Ashton, July 09, 2007 - 1:01 pm UTC


Tom, may I suggest changing
WHERE TO_CHAR(v_bus_day + RNUM, 'DY' ) NOT IN ('SAT', 'SUN')
to
WHERE TO_CHAR(v_bus_day + RNUM, 'DY', 'NLS_DATE_LANGUAGE=AMERICAN' ) NOT IN ('SAT', 'SUN')
so it will work no matter what the NLS settings are?

Tom Kyte
July 09, 2007 - 2:21 pm UTC

sure - no problem with that from me...

NLS_TERRITORY

A reader, July 09, 2007 - 2:58 pm UTC

and this is indepedent of the
NLS_TERRITORY setting ?

I remember some of the to_date / to_char Conversions are not
and you can't specify the NLS_TERRITORY setting as a parameter
to them, that's the main cause I don't use them at all

Re: NLS_TERRITORY

Stew Ashton, July 10, 2007 - 5:15 am UTC

Reader,
NLS_DATE_LANGUAGE dictates the "Abbreviated name of day" (option 'DY'), NLS_TERRITORY dictates the first "Day of week (1-7)" (option 'D'). Therefore, the 'DY' option can be made independent of the NLS settings but the 'D' option cannot.

http://download.oracle.com/docs/cd/B10501_01/server.920/a96540/sql_elements4a.htm#62977
SQL> column "Day of week (1-7)" format A20
SQL> column "Day of week AMERICAN" format A20
SQL> column "Abbreviated name of day" format A25
SQL> column "Abbreviated AMERICAN" format A20
SQL> show parameters nls

NAME                                 TYPE        VALUE
------------------------------------ ----------- ------------------------------
...
nls_date_language                    string
...
nls_language                         string      AMERICAN
...
nls_territory                        string      AMERICA
...
SQL> with dreamliner as (select to_date('07/08/07', 'MM/DD/YY') as premiere from dual)
  2  select premiere,
  3  to_char(premiere, 'D') as "Day of week (1-7)",
  4  to_char(premiere, 'D', 'NLS_DATE_LANGUAGE=AMERICAN') as "Day of week AMERICAN",
  5  to_char(premiere, 'DY') as "Abbreviated name of day",
  6  to_char(premiere, 'DY', 'NLS_DATE_LANGUAGE=AMERICAN') as "Abbreviated AMERICAN" from dreamliner;

PREMIERE   Day of week (1-7) Day of week AMERICAN Abbreviated name of day Abbreviated AMERICAN
---------- ----------------- -------------------- ----------------------- --------------------
2007/07/08 1                 1                    SUN                     SUN

SQL> alter session set nls_date_language='FRENCH';

Session altered.

SQL> with dreamliner as (select to_date('07/08/07', 'MM/DD/YY') as premiere from dual)
  2  select premiere,
  3  to_char(premiere, 'D') as "Day of week (1-7)",
  4  to_char(premiere, 'D', 'NLS_DATE_LANGUAGE=AMERICAN') as "Day of week AMERICAN",
  5  to_char(premiere, 'DY') as "Abbreviated name of day",
  6  to_char(premiere, 'DY', 'NLS_DATE_LANGUAGE=AMERICAN') as "Abbreviated AMERICAN" from dreamliner;

PREMIERE   Day of week (1-7) Day of week AMERICAN Abbreviated name of day Abbreviated AMERICAN
---------- ----------------- -------------------- ----------------------- --------------------
2007/07/08 1                 1                    DI                      SUN

SQL> alter session set nls_territory='FRANCE';

Session altered.

SQL> with dreamliner as (select to_date('07/08/07', 'MM/DD/YY') as premiere from dual)
select premiere,
  2    3  to_char(premiere, 'D') as "Day of week (1-7)",
  4  to_char(premiere, 'D', 'NLS_DATE_LANGUAGE=AMERICAN') as "Day of week AMERICAN",
  5  to_char(premiere, 'DY') as "Abbreviated name of day",
  6  to_char(premiere, 'DY', 'NLS_DATE_LANGUAGE=AMERICAN') as "Abbreviated AMERICAN" from dreamliner;

PREMIERE Day of week (1-7) Day of week AMERICAN Abbreviated name of day Abbreviated AMERICAN
-------- ----------------- -------------------- ----------------------- --------------------
08/07/07 7                 7                    DI                      SUN

Variation on a theme

A reader, August 17, 2007 - 8:11 pm UTC

Tom,
This is just a variation on a theme....

I just thought i could use your VTABLE Pipelined function and came up with this query which could achieve the same purpose.

Any thoughts on its performance.

select DATES from
(select sysdate+column_value DATES from table(vtable) where to_char(sysdate+column_value,'DY') not in ('SAT','SUN')
and NOT EXISTS (select NULL from holidays where holiday=to_date(sysdate+column_value,'DD-MON-RR')))  where rownum<=5

Tom Kyte
August 22, 2007 - 10:02 am UTC

sure, you can use anything these days

with data
as
(select level l from dual connect by level < :n )
select * from data;


for example - don't even need a pipelined function.

Corrected

A reader, August 17, 2007 - 8:12 pm UTC

select MAX(DATES) from
(select sysdate+column_value DATES from table(vtable) where to_char(sysdate+column_value,'DY') not in ('SAT','SUN')
and NOT EXISTS (select NULL from holidays where holiday=to_date(sysdate+column_value,'DD-MON-RR'))) where rownum<=5

-- Need to include the MAX() function.

Strange behaviour

Mark, August 24, 2007 - 2:54 am UTC

Hi Tom,

I have used your query to generate a list of working days in order to select rows from a table. I am currently working on 9.2i and I would like the following:
I have a table with a date and a number (working days) in it. I want to select all rows from that table where the date is smaller or equal to a supplied date minus the number of working days.

I wrote a query to do that, but it doens't always return the data I excpected. Here is the code:
-- Create the table:
CREATE TABLE times AS
SELECT TO_DATE( '01-01-2007', 'DD-MM-YYYY' ) start_date
, 2 wait_days
FROM DUAL
UNION ALL
SELECT TO_DATE( '01-01-2001', 'DD-MM-YYYY' )
, 20
FROM DUAL
UNION ALL
SELECT TO_DATE( '01-01-2004', 'DD-MM-YYYY' )
, 2
FROM DUAL;
-- The select statement:
SELECT *
FROM times t
WHERE t.start_date <=
(
SELECT trunc( TO_DATE( '20-08-2007 12:54:57', 'DD-MM-YYYY HH24:MI:SS' ) ) - rnum
FROM (
SELECT rnum
, row_number() OVER (ORDER BY rnum) rn
FROM (
SELECT level rnum
FROM dual
CONNECT BY level <= 1500
ORDER BY 1
)
WHERE to_char(trunc(TO_DATE( '20-08-2007 12:54:57', 'DD-MM-YYYY HH24:MI:SS' )) - rnum, 'DY' ) NOT IN (mvs$constant.cn_za, mvs$constant.cn_zo)
)
WHERE rn = t.wait_days
);

The result is only the rows where the wait_days equals 2, although the row with wait_days equals 20 should also be returned. When the first row in the times table has a wait_days of 20, then all rows are returned as expected.

Can you clarify the behaviour?

Thanks! Mark
Tom Kyte
August 24, 2007 - 3:05 pm UTC

I want a full example I can just cut, paste and run. Showing what you got, and what you expected to get.

mark, August 24, 2007 - 4:27 am UTC

Hi Tom,
My previous post contained an error. The query contained two constants. The query should be:
SELECT *
FROM times t
WHERE t.start_date <=
(
SELECT trunc( TO_DATE( '20-08-2007 12:54:57', 'DD-MM-YYYY HH24:MI:SS' ) ) - rnum
FROM (
SELECT rnum
, row_number() OVER (ORDER BY rnum) rn
FROM (
SELECT level rnum
FROM dual
CONNECT BY level <= 1500
ORDER BY 1
)
WHERE to_char(trunc(TO_DATE( '20-08-2007 12:54:57', 'DD-MM-YYYY HH24:MI:SS' )) - rnum, 'DY' ) NOT IN ('SAT', 'SUN')
)
WHERE rn = t.wait_days
);

One other thing: The select qeury is actually the where clause of an update query. But I have removed that for making the problem clear.

Tom Kyte
August 24, 2007 - 3:07 pm UTC

full example.... needed....

Mark, August 27, 2007 - 4:55 am UTC

Hi Tom,

I hope this is the complete example.
So in short what I expect:
A list of dates from the table times, where the start_date is smaller or equal than the date I give ( august 20th 2007 ) minus the number of working days in from the table times.
So from the table below I expect to see all 3 rows and not 2. The row with the wait_days = 20 is missing.

My question: what is causing this behaviour and how to solve it without unsing the WITH statement?


--
-- Create the table times
--
CREATE TABLE times AS
SELECT TO_DATE( '01-01-2007', 'DD-MM-YYYY' ) start_date
, 2 wait_days
FROM DUAL
UNION ALL
SELECT TO_DATE( '01-01-2001', 'DD-MM-YYYY' )
, 20
FROM DUAL
UNION ALL
SELECT TO_DATE( '01-01-2004', 'DD-MM-YYYY' )
, 2
FROM DUAL;
-
-- See all rows in times
-- Result:
-- START_DAT WAIT_DAYS
--------- ----------
-- 01-JAN-07 2
-- 01-JAN-01 20
-- 01-JAN-04 2
--
SELECT *
FROM times
--
-- Do the select: This one return just two rows, while
-- I expect all 3 of them, because 20-08-2007 minus 20 working days is larger than
-- 01-01-2001 ( second row in the create table )
--
-- Result: START_DAT WAIT_DAYS
--------- ----------
-- 01-JAN-07 2
-- 01-JAN-04 2
--
SELECT *
FROM times t
WHERE t.start_date <=
(
SELECT trunc( TO_DATE( '20-08-2007 12:54:57', 'DD-MM-YYYY HH24:MI:SS' ) ) - rnum
FROM (
SELECT rnum
, row_number() OVER (ORDER BY rnum) rn
FROM (
SELECT level rnum
FROM dual
CONNECT BY level <= 1500
ORDER BY 1
)
WHERE to_char(trunc(TO_DATE( '20-08-2007 12:54:57', 'DD-MM-YYYY HH24:MI:SS' ))
- rnum, 'DY' ) NOT IN ('SAT', 'SUN')
)
WHERE rn = t.wait_days
);
--
-- Select to get the date for 20-08-2007 minus 20 working days. This return 23-07-2007 which is correct
--
-- Result:
-- TRUNC(TO_
---------
-- 23-JUL-07

SELECT trunc( TO_DATE( '20-08-2007 12:54:57', 'DD-MM-YYYY HH24:MI:SS' ) ) - rnum
FROM (
SELECT rnum
, row_number() OVER (ORDER BY rnum) rn
FROM (
SELECT level rnum
FROM dual
CONNECT BY level <= 1500
ORDER BY 1
)
WHERE to_char(trunc(TO_DATE( '20-08-2007 12:54:57', 'DD-MM-YYYY HH24:MI:SS' ))
- rnum, 'DY' ) NOT IN ('SAT', 'SUN')
)
WHERE rn = 20;
--
-- Same select as the first, to check whether the row is now returned.
--
-- Result:
-- START_DAT WAIT_DAYS
--------- ----------
-- 01-JAN-01 20
SELECT *
FROM times t
WHERE wait_days = 20
AND start_date <= TO_DATE( '23-07-2007', 'DD-MM-YYYY' );


Excellent

Sanjay Kumar, September 01, 2009 - 7:51 am UTC

Thanks Tom. Worked for me in first shot.Was searching for this since yesterday.



Curiosity... 5th business day using no function/table

Allen, March 23, 2010 - 11:26 am UTC

Looks funny but if you make up calendar months with the 1st starting on each day, you will see the patterns that make this possible. Add to a report you only want to return data on the 5th business day of a month:

WHERE... 'Y' = 
CASE
WHEN TRIM (TO_CHAR (SYSDATE, 'MONTH')) NOT IN
          ('JANUARY', 'JULY', 'SEPTEMBER')
      AND ( (TRIM (TO_CHAR (SYSDATE, 'DAY')) IN ('FRIDAY')
             AND TO_CHAR (SYSDATE, 'dd') IN ('05','06','07')
           OR (TRIM (TO_CHAR (SYSDATE, 'DAY')) IN
                                  ('MONDAY',
                                   'TUESDAY',
                                   'WEDNESDAY',
                                   'THURSDAY',
                                   'FRIDAY')
                AND TO_CHAR (SYSDATE, 'dd') IN ('07'))))
THEN 'Y'
WHEN TRIM (TO_CHAR (SYSDATE, 'MONTH')) IN ('JULY','SEPTEMBER')
     AND ( (TRIM (TO_CHAR (SYSDATE, 'DAY')) = 'MONDAY'
            AND TO_CHAR (SYSDATE, 'dd') IN ('08','09','10'))
                      OR (TRIM (TO_CHAR (SYSDATE, 'DAY')) IN
                                ('MONDAY',
                                 'TUESDAY',
                                 'WEDNESDAY',
                                 'THURSDAY',
                                 'FRIDAY')
                        AND TO_CHAR (SYSDATE, 'dd') IN   ('08')))
THEN 'Y'
WHEN TRIM (TO_CHAR (SYSDATE, 'MONTH')) IN ('JANUARY')
              AND ( (TRIM (TO_CHAR (SYSDATE, 'DAY')) = 'MONDAY'
              AND TO_CHAR (SYSDATE, 'dd') IN ('08', '09'))
                 OR (TRIM (TO_CHAR (SYSDATE, 'DAY')) = 'FRIDAY'
              AND TO_CHAR (SYSDATE, 'dd') IN ('07'))
                OR (TRIM (TO_CHAR (SYSDATE, 'DAY')) IN
                                ('MONDAY',
                                 'TUESDAY',
                                 'WEDNESDAY',
                                 'THURSDAY',
                                 'FRIDAY')
              AND TO_CHAR (SYSDATE, 'dd') IN ('08')))
THEN 'Y'
ELSE
'N'
END

I know... TRIM not needed...

Allen, March 23, 2010 - 11:50 am UTC

Sorry about putting those TRIM's in there. Other efficiencies can be gained/logic combined but I wanted each section per month situation to have it be clear because it's so odd. I needed this for a user program in a product (ARGOS) where I could not use looping and scheduling options were rudimentary. Of course, I could have used a Function for the business date but in an organization it is sometimes time consuming to get tables and functions put in place and this was a fun concept.

CASE
            WHEN TO_CHAR (SYSDATE, 'MONTH') NOT IN
                       ('JANUARY', 'JULY', 'SEPTEMBER')
                 AND ( (TO_CHAR (SYSDATE, 'DAY') IN ('FRIDAY')
                        AND TO_CHAR (SYSDATE, 'dd') IN ('05', '06', '07')
                        OR (TO_CHAR (SYSDATE, 'DAY') IN
                                  ('MONDAY',
                                   'TUESDAY',
                                   'WEDNESDAY',
                                   'THURSDAY',
                                   'FRIDAY')
                            AND TO_CHAR (SYSDATE, 'dd') IN ('07'))))
            THEN
               'Y'
            WHEN TO_CHAR (SYSDATE, 'MONTH') IN ('JULY', 'SEPTEMBER')
                 AND ( (TO_CHAR (SYSDATE, 'DAY') = 'MONDAY'
                        AND TO_CHAR (SYSDATE, 'dd') IN ('08', '09', '10'))
                      OR (TO_CHAR (SYSDATE, 'DAY') IN
                                ('MONDAY',
                                 'TUESDAY',
                                 'WEDNESDAY',
                                 'THURSDAY',
                                 'FRIDAY')
                          AND TO_CHAR (SYSDATE, 'dd') IN ('08')))
            THEN
               'Y'
            WHEN TO_CHAR (SYSDATE, 'MONTH') IN ('JANUARY')
                 AND ( (TO_CHAR (SYSDATE, 'DAY') = 'MONDAY'
                        AND TO_CHAR (SYSDATE, 'dd') IN ('08', '09'))
                      OR (TO_CHAR (SYSDATE, 'DAY') = 'FRIDAY'
                          AND TO_CHAR (SYSDATE, 'dd') IN ('07'))
                      OR (TO_CHAR (SYSDATE, 'DAY') IN
                                ('MONDAY',
                                 'TUESDAY',
                                 'WEDNESDAY',
                                 'THURSDAY',
                                 'FRIDAY')
                          AND TO_CHAR (SYSDATE, 'dd') IN ('08')))
            THEN
               'Y'
            ELSE
               'N'
         END

A reader, May 14, 2010 - 2:22 pm UTC

Thanks Tom!

A reader, April 07, 2011 - 2:30 am UTC

hi tom,
your answers are always great and very useful...

i have two columns.

date business days
2/1/2011 11
1/3/2011 4
2/11/2011 7
1/23/2011 13
1/15/2011 3
1/18/2011 25

i need to add the business days(excludes holidays, assume sat and sun are holidays) from date for eg:

1/3/2011 11
adding these it must give me 18 th of jan

what is the simplest way of writing this in a single query since for some purpose we dont want it in PL/SQL.

Can this be converted to hours?

Jeff, November 06, 2013 - 6:47 pm UTC

Is it possible to add hours instead of days using this query? I've tried using partial days but it always returns null:

select ADD_BUS_DAYS('16-NOV-13', .375) from dual

I'm trying this because I need to call this function multiple times and the end date of row 1 will become the start date for row 2, so the hours need to accumulate.

Think of a MS Project graph. In my case, one task ends and the next one begins. All I have is the original start date and the length of each task.
Tom Kyte
November 07, 2013 - 3:42 pm UTC

but you'd have to define what working hours are (9-5? 8-4? etc) and whether you wanted "holidays" to be part of the equation. your input should not be a string, it should be a date with a time component on it - right? because you might be adding two hours to 4pm on a friday.

then we'd generate a set of maximum days it could possibly take (so we'd have a set with two columns in it - the start time/end time for each day)....

but a better specification is required first.

Can this be converted to hours?

Jeff, November 07, 2013 - 5:11 pm UTC

No need to define working hours. I just convert 8 hours into one working day, so 26 hours = 3.25 days. I just can't get the original query to accept .375 days, for example.

I do have a holiday table as well, a vendor supplied table that is horribly designed, but I switched out my query for the table into the original function above and it works fine.

I realized I asked 2 different questions with my original post. You can just ignore the second part, unless you'd like another programming challenge. I already have a solution (albeit clumsy) for the whole "end date on the first row becomes the start date for the second row" part.

My input would be a "request ID" that refers to group of tasks with a start date and a set of records that are ordered tasks that have a duration on them.

Thanks again.
Tom Kyte
November 11, 2013 - 9:12 am UTC

because .371 isn't a "working day", it is a fraction of a day.

you *need* working hours, else how can you tell when something starts and stops????

I guess just round up, 3.25 is 4 working days then isn't it?

More to Explore

PL/SQL demos

Check out more PL/SQL tutorials on our LiveSQL tool.

PL/SQL docs

PL/SQL reference manual from the Oracle documentation library