Calculating Business Days and Business Days Between
I recently had a requirement to populate the day dimension of a data mart (I won’t put all of the code here as it’s pretty large). That’s not that big deal but part of the requirement was to set several columns: BUSINESS_DAY_FLAG, BUSINESS_DAY_NO and BUSINESS_DAYS_REMAINING_NO.
- The BUSINESS_DAY_FLAG is Y is the date is MON-FRI and N is the date is SAT or SUN.
- BUSINESS_DAY_NO is the business day of the month. There are 5 business days per week so if the month started on a monday, the second monday would be business day 6.
- BUSINESS_DAYS_REMAINING_NO is the number of business days remaining in the month. There are either 20, 21, or 22 business days in a month and those will be spread across 28, 29, 30 or 31 days.
No holidays are considered in this. For our uses, that is not needed. Weekdays and business days are really interchangeable here.
The Tasks
Now that I have my requirement, how do I solve the problem? I do it the same way that I solve any problem, I break the problem into it’s component tasks. In no particular order, I see the following tasks:
- write an insert statement to insert into DIM_DAY (not included here)
- determine the number of business days in a month
- determine the current business day number
- determine how to handle a business day number on the weekend
- set a flag for business day
Defining a Base Calendar
First up is to set a flag for a business day and determine how to handle the day number on the weekend. After discussing with the user, it is determined that: if I ask for a date that has a BUSINESS_DAY_FLAG of N then the BUSINESS_DAY_NO will be the same BUSINESS_DAY_NO as the last business day (that would be the previous Friday).
Cool, let’s figure out what a business day is. I need a base set of records to work with. You can use CONNECT BY to generate records (and if you are doing millions of records, that may be your best choice). I prefer to just use ALL_OBJECTS. For this data mart, we will only insert a 2 years at a time so ALL_OBJECTS has plenty enough records.
So now I use ALL_OBJECTS to generate a range of dates. To generate a range, we set a starting date and select that against ALL_OBJECTS for a certain number of rows (in my case I will just say 1000 to keep it simple):
SELECT TO_DATE ('31-dec-2008', 'DD-MON-YYYY') + rownum the_date
FROM all_objects
WHERE ROWNUM < 1000
This is a very simple query and I will use it as the basis for all of the computations that I have yet to make. I picked 31-DEC-2008 because it fits my needs. You can start with any date that works for you.
Notice that I add rownum to the date to advance though the dates. You could put your start date in a table and make this very dynamic. Put it in a global temporary table and you could even make it dynamic per session and compute things like this in real time.
Calculate Business Days
Now, I add the query that will determine if a day is a business day. I’m not quite ready to set the flag yet. I’ll need a number to count the days within a month. For that I will just set a 1 for business day and a 0 for weekend.
SELECT CASE TO_NUMBER (TO_CHAR (the_date, 'D'))
WHEN 1 THEN 0
WHEN 7 THEN 0
ELSE 1
END business_day,
the_date
FROM ( [days base query] )
This query gives me a date and a 1 or 0 depending on the day of week.
BUSINESS_DAY THE_DATE ---------------------- ------------------------- 1 01-JAN-09 1 02-JAN-09 0 03-JAN-09 0 04-JAN-09 1 05-JAN-09
Are we having fun yet? This query could be wrapped in a view and used to calculate business days very easily. If you were to do that, I would suggest that you make the start date and the number of rows be dynamic (either through a table or with binds). Example: If we create a view:
CREATE OR REPLACE VIEW business_days
AS
SELECT CASE TO_NUMBER (TO_CHAR (the_date, 'D'))
WHEN 1 THEN 0
WHEN 7 THEN 0
ELSE 1
END business_day,
the_date
FROM (
SELECT TO_DATE ('31-dec-2008', 'DD-MON-YYYY') + rownum the_date
FROM all_objects
WHERE ROWNUM < 1000 )
/
And then select from that view:
SELECT SUM (business_day)
FROM business_days
WHERE the_date BETWEEN TO_DATE ('31-dec-2008', 'DD-MON-YYYY')
AND TO_DATE ('31-dec-2010', 'DD-MON-YYYY')
SUM(BUSINESS_DAY)
-----------------
522
You could very easily turn this into a function so that you could call it just like TO_DATE or LAST_DAY.
Business Days in a Month
That’s a good start but I still need to figure out the current business day of the month and the number remaining in the month. This next query uses the same view and will tell me the current week day (i.e.. business day) of the month as well as the total number of weekdays in the month.
SELECT the_date,
sum(business_day) over
(partition by to_char(the_date, 'MON-YYYY'))
business_days_in_month,
sum(business_day) over
(partition by to_char(the_date, 'MON-YYYY')
order by to_char(the_date, 'DD-MON-YYYY')
rows to_char(the_date, 'DD') preceding)
weekday_of_month_no,
decode( business_day, 1, 'Y', 0, 'N') week_day_flag
FROM business_days
WHERE the_date BETWEEN TO_DATE ('31-dec-2008', 'DD-MON-YYYY')
AND TO_DATE ('31-dec-2010', 'DD-MON-YYYY')
ORDER BY the_date
The results of a select to this query looks like this:
THE_DATE BUSINESS_DAYS_IN_MONTH WEEKDAY_OF_MONTH_NO W --------- ---------------------- ------------------- - 01-JAN-09 22 1 Y 02-JAN-09 22 2 Y 03-JAN-09 22 2 N 04-JAN-09 22 2 N 05-JAN-09 22 3 Y 06-JAN-09 22 4 Y 07-JAN-09 22 5 Y
The Final Select
I could have finished up with the query by subtracting the current week day number from the total but that would have required me to duplicate some of the logic for calculating the current and totals day so I prefer to wrap that in a parent query. Here is the final query that calculates all of the fields to meet the user requirements. I am using the sub-selects instead of the view:
SELECT the_date,
business_days_in_month,
weekday_of_month_no,
week_day_flag,
business_days_in_month - weekday_of_month_no week_days_remaining
FROM (
SELECT the_date,
sum(business_day) over
(partition by to_char(the_date, 'MON-YYYY'))
business_days_in_month,
sum(business_day) over
(partition by to_char(the_date, 'MON-YYYY')
order by to_char(the_date, 'DD-MON-YYYY')
rows to_char(the_date, 'DD') preceding)
weekday_of_month_no,
decode( business_day, 1, 'Y', 0, 'N') week_day_flag
FROM (
SELECT CASE TO_NUMBER (TO_CHAR (the_date, 'D'))
WHEN 1 THEN 0
WHEN 7 THEN 0
ELSE 1
END business_day,
the_date
FROM (
SELECT TO_DATE ('31-dec-2008', 'DD-MON-YYYY') + rownum the_date
FROM all_objects
WHERE ROWNUM < 1000 )
)
)
WHERE the_date BETWEEN TO_DATE ('31-dec-2008', 'DD-MON-YYYY')
AND TO_DATE ('31-dec-2010', 'DD-MON-YYYY')
ORDER BY the_date
And the final output:
THE_DATE BUSINESS_DAYS_IN_MONTH WEEKDAY_OF_MONTH_NO W WEEK_DAYS_REMAINING --------- ---------------------- ------------------- - ------------------- 01-JAN-09 22 1 Y 21 02-JAN-09 22 2 Y 20 03-JAN-09 22 2 N 20 04-JAN-09 22 2 N 20 05-JAN-09 22 3 Y 19 06-JAN-09 22 4 Y 18 07-JAN-09 22 5 Y 17
There it is. Is this the only way to do it? Nope. I had fun doing it though.
NOTE: Use this at your own risk. I tested it and it seems to work for what I need to use it for. Your mileage may vary. Before using it in production, I strongly suggest that YOU test it in YOUR environment.
On that note, if you do find any bugs, please let me know. I will update the code (here and in my system).
LewisC
You can follow any responses to this entry through the RSS 2.0 feed. Both comments and pings are currently closed.




Comments are closed.