SideBar    The Logical Puzzle
DOWNLOAD THE CODE:
Download the Code 95675.zip

 See correction to this article

For the past four months, I've discussed various calculations involving datetime data. This month, I conclude the series by introducing techniques to calculate working days, age, and next birthday date. Later in the article, I also point you to additional resources where you can find more information about temporal data manipulation.

Calculating Working Days
To tackle this problem, we can write a function called fn_workingdays that calculates the number of working days in an input period. The function accepts three input parameters:

  • @from_dt: start date of period
  • @to_dt: end date of period
  • @datefirst: first working day of the week (e.g., 1 for Monday, 2 for Tuesday, 3 for Wednesday)

The output of the function is an integer representing the number of working days in the input period. Working days are weekdays 1 through 5, based on the input first working day of the week (@datefirst). For example, if @datefirst is set to 1 (Monday), Monday through Friday are considered working days, whereas Saturday and Sunday are considered nonworking days. If @datefirst is set to 7 (Sunday), Sunday through Thursday are considered working days, whereas Friday and Saturday are considered nonworking days.

Note that the recommended approach to handle such calculations is to maintain an auxiliary table of dates, with attributes signifying whether a certain date is a working day. This approach is flexible because it can also take holidays and other special cases into consideration. However, for the purpose of this discussion, we'll narrow the problem to a simpler case in which you need to take into consideration weekdays 1 through 5 (based on the given first working day of the week)—not weekdays 6 and 7. We won't handle holidays and other special cases. This problem is solvable with a single expression that doesn't involve the need to query any auxiliary tables.

You can find my suggested solution—which is, of course, only one of many options—in Web Listing 1, http://www.sqlmag.com, InstantDoc ID 95675. (You might try solving the problem on your own before looking at my solution.) The logical approach I've taken is as follows:

  • Calculate the number of whole weeks in the period and multiply by 5 (working days)
  • Add the number of remaining days in the part of the week left at the end of the period (can be 0 through 6 days)
  • Subtract 1 day if weekday 6 is within the remaining part of the week
  • Subtract 1 day if weekday 7 is within the remaining part of the week

As for the more technical description of the function's definition, I'll start with the query defining the innermost derived table D1 and go outward. The query defining D1 calculates two columns— days, which is the number of days in the input period, and from_weekday, which is the weekday number of @from_dt based on the input first working day of the week (@datefirst). The calculation of days is straightforward: using a simple DATEDIFF function that calculates the difference in terms of days between @ from_dt and @to_dt, plus one. As for the calculation of from_weekday, by now you should be comfortable with this calculation of a language-independent weekday number, which I explained in "DATETIME Calculations, Part 2," InstantDoc ID 94819.

The purpose of the query defining D2 is to calculate the to_weekday column: from_weekday plus the number of days in the remaining part of the week, minus 1. Note that to_weekday can be higher than 7, allowing for simpler calculations in the outermost query. For example, if from_weekday is 5 and there are 4 days in the remaining part of the week, to_weekday will be 8. Therefore, figuring out whether days 6 and 7 are within the remaining part of the week at the end of the period is simple.

The outermost query then performs the final calculation of the number of working days. As I described earlier, the result amounts to the number of whole weeks times 5 (days/7*5), plus the number of days in the remaining part of the week (days%7), minus 1 if 6 is within the input period (6 between from_weekday and to_weekday), minus 1 if 7 is within the input period (7 between from_weekday and to_weekday).

To verify that the calculation is correct, let's test the function. Run the code in Web Listing 1 to create it. (Make sure you're connected to the database in which you want the function to be created—for example, tempdb.) Next, run the following code:

SELECT dbo.fn_workingdays
(‘20070212', ‘20070223', 1)

This period covers 12 days (starting on a Monday and ending on a Friday), two days of which are nonworking days. (Monday is set as the first working day of the week.) You get 10 working days back, as expected. Next, run the following code:

SELECT dbo.fn_workingdays
  (‘20070212', ‘20070223', 7)

It's the same period as before, but now three days within the period are nonworking days. (Sunday is set as the first working day of the week.) You get nine working days back, as expected.

   Prev. page   [1] 2     next page
CORRECTIONS TO THIS ARTICLE:
Table 2 contains a typo. The correct date for George Leaping should be as follows: Table 2: Next birthday date of employees LastName FirstName BirthDate NextBirthDay -------------------- ---------- ----------------------- ----------------------- Leaping George 1972-02-29 00:00:00.000 2007-03-01 00:00:00.000 DianaMay- June 01, 2007




You must log on before posting a comment.

If you don't have a username & password, please register now.

Reader Comments

Interesting set of articles on date calculations - for age calculation I like to normalize the birthday to the current year and determine if that date has passed yet; seems to work for leap year as well: select bdate, datediff(year,bdate,getdate()) - case when dateadd(year,datediff(year,bdate,getdate()),bdate) > getdate() then 1 else 0 end from ...

mgress

Article Rating 5 out of 5

We're working to get a new version of the table published. (Diana)

Note from Itzik on the date error in Table 2: Regarding Table 2, it’s a typo. I just inspected the original article I sent, and this is what I had (and should be): Table 2: Next birthday date of employees LastName FirstName BirthDate NextBirthDay -------------------- ---------- ----------------------- ----------------------- Leaping George 1972-02-29 00:00:00.000 2007-03-01 00:00:00.000

DianaMay

Article Rating 5 out of 5

I found a neat trick several years ago for calculating the age in years between two dates. It takes advantage of integer truncation on division, and works for leap years as well. Working through an example: (20070601 - 19710212) / 10000 360389 / 10000 36

SELECT (ABS(CONVERT(INT,(CONVERT(CHAR(8),@i_Date2,112))) - CONVERT(INT,(CONVERT(CHAR(8),@i_Date1,112))))) / 10000

[Itzik: This is pretty cool!]

craigpessano

Article Rating 4 out of 5

good article

CBenac

Article Rating 3 out of 5

Great Article. Just a quick point about the website: why are the other articles in this series not available from the "Related Articles" listing?

ukribe1

Article Rating 4 out of 5

ukribe1, thanks for the suggestion about adding links to all the article's in Itzik's popular DATETIME series. I'll add a box with the article links. Anne Grubb, Web Lead Editor, Windows IT Pro and SQL Server Magazine

AnneG_editor

Article Rating 5 out of 5