The BUSDAYS function calculates the number of business days from a start date (inclusive) to an end date (exclusive). Saturdays and Sundays are not considered to be business days, and there is a holiday parameter passed into the function which contains the other non-business days to be used in the calculation. One of the ways that we use this function is to calculate the accrued interest for Brazilian government bonds, where interest is only accrued on Brazilian business days.

There is no exact equivalent for the BUSDAYS function in EXCEL. There is a NETWORKDAYS function (which is also in XLeratorDB), but the NETWORKDAYS function includes both the start date and the end date in the calculation of the number of days. For example, NETWORKDAYS calculates the number of days from 2012-05-29 to 2012-05-30 as 2. For interest accrual calculations, we would expect the number of days to be 1. We looked at using the NETWORKDAYS function for the interest calculations, but the nuances of knowing whether or not the start date and/or the end date were non-business days made it impractical.

Given the complexity of trying to use the existing NETWORKDAYS function, we created the new function BUSDAYS to calculate the number of business days between two dates, taking holidays into account. The function has 3 parameters: a start date, an end date, and a comma separated string containing the non-business dates in the format YYYYMMDD. We also created a new aggregate, NBD, which will take the non-business-day dates and put them into the string format used by the function. The NBD function automatically eliminate duplicates.

In our environment we have a table called HOLIDAYS containing the non-business-day dates for each country. We would calculate the number of business days from 2012-05-01 to 2012-06-01 with the following statement.

SELECT wct.BUSDAYS(

'2012-05-01' --Start Date

,'2012-06-01' --End Date

,wct.NBD(holiday) --Holidays

) as BUSDAYS

FROM HOLIDAYS

WHERE COUNTRY = 'US'

This produces the following result.

BUSDAYS

-----------

22

How do we know that number is correct?

We can use the XLeratorDB SeriesDate function to create all the dates from 2012-05-01 to 2012-06-01, excluding the Saturdays and Sundays as well as the non-business days. We can then just count the number of rows where the date is greater than or equal to the start date and less than the end date.

DECLARE @sd as datetime = '2012-05-01'

DECLARE @ed as datetime = '2012-06-01'

SELECT COUNT(*) as BUSDAYS

FROM (

SELECT SERIESVALUE

FROM wctMath.wct.SERIESDATE(@sd,@ed,NULL,NULL,NULL)

WHERE DATEPART(dw,seriesvalue) <> 1 --Sunday

AND DATEPART(dw,seriesvalue) <> 7 --Saturday

EXCEPT

SELECT HOLIDAY

FROM HOLIDAYS

WHERE HOLIDAY BETWEEN @sd AND @ed

AND COUNTRY = 'US'

) m(d)

WHERE d >= @sd and d < @ed

This produces the following result.

BUSDAYS

-----------

22

We considered a variety of techniques for calculating the number of business days. We used a very simple test that the solution should be able to produce the following resultant table, showing the number of business days between all the dates in May of 2012, where 2012-05-28 is a holiday.

DECLARE @sd as datetime = '2012-05-01'

DECLARE @ed as datetime = '2012-05-31'

DECLARE @hol as varchar(max) =(SELECT wct.NBD(holiday) FROM HOLIDAYS WHERE COUNTRY = 'US')

SELECT sd

,[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]

FROM(

SELECT DAY(k.SeriesValue) as sd

,DAY(l.SeriesValue) as ed

,wct.BUSDAYS(k.SeriesValue, l.SeriesValue, @hol) as bd

FROM wctMath.wct.SeriesDate(@sd,@ed,NULL,NULL,NULL) k

CROSS APPLY wctMath.wct.SeriesDate(@sd,@ed,NULL,NULL,NULL) l

) n

PIVOT(SUM(bd) for ed in(

[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])) as P

This produces ths following result.

In other words, the calculation has to work backwards and forwards, and has to work for when the start date is equal to the end date. Additionally it had to be flexible enough to allow for different non-business days for each row in the resultant table.

There are also some subtle little twists to consider when calculating the number of business days. For example, when you calculate the number of business days (assuming that there are no holidays) from Monday to Friday, the result is 4. If you change the end date to Saturday, the number is 5, which is also the same result that you would get if the end date were Sunday or even the following Monday.

This behavior is subtly different than what happens in the BUSINESSDATE function (see below). When we add 5 business days to Monday, it returns the following Monday, as it is the first *business* date that is 5 business day after the start date.

We tested the BUSDAYS function by randomly creating 1,000,000 start date and end date combinations and then programmitically counting the number of days between the start date and the end date.

We briefly considered making the BUSDAYS function a multi-input aggregate, but since one of the main ways that we were going to use the function was as part of an interest calculation, we were concerned that an aggregate would make the SQL too complex. We were also concerned about the fact that aggregate functions cannot be used inside other aggregate functions, like the SUM function. We opted to keep the function a scalar and keep things simple.

BUSINESSDATE

The BUSINESSDATE function calculates business dates forward or backward from a specified start date. You can also use the function to calculate a date that is weeks, months, or years from the specified start date and have the calculated date adjusted to a business date based upon a date roll rule parameter supplied to the function. The BUSINESSDATEWE function works just like the BUSINESSDATE function, but it lets you specify a weekend consisting of other than a Saturday and a Sunday.

There are many business situations where you need to calculate the next business date. For example, many securities trade on a t+1 basis. In other words, the securities settle on the next business day after the trade date. Some securities trade on t+3 basis. Many foreign exchange markets operate on a t+2 basis, but you need to consider the holiday calendar from 2 or possibly 3 countries. The BUSINESSDATE function is designed to perform these calculations in SQL Server.

Here’s a simple example. You buy a security on 2012-05-30 which regularly settles on a t+3 basis. You need calculate the settlement date incorporating the US holidays calendar. The following SQL statement will do the calculation for you.

SELECT wct.BUSINESSDATE(

'2012-05-30' --Start Date

,'D' --Date Part

,3 --Number

,NULL --Date Roll Rule

,wct.NBD(holiday) --Holidays

) as [t+3]

FROM HOLIDAYS

WHERE COUNTRY = 'US'

This produces the following result.

t+3

-----------------------

2012-06-04 00:00:00.000

If regular way settlement were 1 day, we would simply chage the number parameter to 1.

SELECT wct.BUSINESSDATE(

'2012-05-30' --Start Date

,'D' --Date Part

,1 --Number

,NULL --Date Roll Rule

,wct.NBD(holiday) --Holidays

) as [t+1]

FROM HOLIDAYS

WHERE COUNTRY = 'US'

This produces the following result.

t+1

-----------------------

2012-05-31 00:00:00.000

In this example we calculate the settlement date for USD/GBP spot FX transactions on 2012-05-31.

SELECT wct.BUSINESSDATE(

'2012-05-31' --Start Date

,'D' --Date Part

,2 --Number

,NULL --Date Roll Rule

,wct.NBD(holiday) --Holidays

) as [t+2]

FROM HOLIDAYS

WHERE COUNTRY IN('US','GB')

This produces the following result, reflecting the Spring Bank Holiday and the Diamond Jubilee Holidays in Great Britain.

t+2

-----------------------

2012-06-06 00:00:00.000

We can also use the BUSINESSDATE function in much the same way that we would use the DATEADD function, except that we can adjust the result to either the preceding or succeeding business date. Here’s a simple example where we will calculate the next business date (also known as the tom/next business date), the spot business date (t+2), and the 1 through 11 month dates (which are from the spot date) and the 1,2,5,10,20,and 30 year dates.

DECLARE @tod as datetime = '2012-05-29'

DECLARE @hol as varchar(max) =(SELECT wct.NBD(holiday) FROM HOLIDAYS WHERE COUNTRY = 'US')

SELECT n

,dp

,wct.BUSINESSDATE(wct.BUSINESSDATE(@tod,'D',2,NULL,@hol),dp,n,'M',@hol) as [date]

FROM (VALUES

(1,'M'),(2,'M'),(3,'M'),(4,'M'),(5,'M'),(6,'M'),(7,'M'),(8,'M'),(9,'M'),(10,'M'),(11,'M')

,(1,'Y'),(2,'Y'),(5,'Y'),(10,'Y'),(20,'Y'),(30,'Y')

) x(n,dp)

UNION

SELECT 1,'D',wct.BUSINESSDATE(@tod,'D',1,NULL,@hol)

UNION

SELECT 2,'D',wct.BUSINESSDATE(@tod,'D',2,NULL,@hol)

ORDER BY 3

This produces the following result.

n dp date

----------- ---- -----------------------

1 D 2012-05-30 00:00:00.000

2 D 2012-05-31 00:00:00.000

1 M 2012-06-29 00:00:00.000

2 M 2012-07-31 00:00:00.000

3 M 2012-08-31 00:00:00.000

4 M 2012-09-28 00:00:00.000

5 M 2012-10-31 00:00:00.000

6 M 2012-11-30 00:00:00.000

7 M 2012-12-31 00:00:00.000

8 M 2013-01-31 00:00:00.000

9 M 2013-02-28 00:00:00.000

10 M 2013-03-29 00:00:00.000

11 M 2013-04-30 00:00:00.000

1 Y 2013-05-31 00:00:00.000

2 Y 2014-05-30 00:00:00.000

5 Y 2017-05-31 00:00:00.000

10 Y 2022-05-31 00:00:00.000

20 Y 2032-05-31 00:00:00.000

30 Y 2042-05-30 00:00:00.000

Notice that the Monthly and Yearly dates are calculated from the spot date and that the date roll rule is ‘M’. The M (for modified) means that if the calculated date is a non-business date, then roll the date to the next business date, unless the new date is a in a different month, in which case roll the date to the preceding business date. What would the calculation have been using the DATEADD function?

It’s harder to find that out then you would otherwise think, because the DATEADD function does not permit the datepart variable to be passed in as a variable. The on-line documentation clearly states, “User-defined variable equivalents are not valid.” That certainly makes it hard to use in any kind of a set operation, especially for many typical financial calculations. We can use a CASE statement to overcome this limitation.

DECLARE @tod as datetime = '2012-05-29'

DECLARE @hol as varchar(max) =(SELECT wct.NBD(holiday) FROM HOLIDAYS WHERE COUNTRY = 'US')

SELECT n

,dp

,wct.BUSINESSDATE(wct.BUSINESSDATE(@tod,'D',2,NULL,@hol),dp,n,'M',@hol) as [date]

,CASE dp

WHEN 'M' THEN DATEADD(month, n, wct.BUSINESSDATE(@tod,'D',2,NULL,@hol))

WHEN 'Y' THEN DATEADD(year, n, wct.BUSINESSDATE(@tod,'D',2,NULL,@hol))

END as [DATEADD]

FROM (VALUES

(1,'M'),(2,'M'),(3,'M'),(4,'M'),(5,'M'),(6,'M'),(7,'M'),(8,'M'),(9,'M'),(10,'M'),(11,'M')

,(1,'Y'),(2,'Y'),(5,'Y'),(10,'Y'),(20,'Y'),(30,'Y')

) x(n,dp)

UNION

SELECT 1,'D',wct.BUSINESSDATE(@tod,'D',1,NULL,@hol), DATEADD(day,1,@tod)

UNION

SELECT 2,'D',wct.BUSINESSDATE(@tod,'D',2,NULL,@hol), DATEADD(day,2,@tod)

ORDER BY 3

This produces the following result.

n dp date DATEADD

----------- ---- ----------------------- -----------------------

1 D 2012-05-30 00:00:00.000 2012-05-30 00:00:00.000

2 D 2012-05-31 00:00:00.000 2012-05-31 00:00:00.000

1 M 2012-06-29 00:00:00.000 2012-06-30 00:00:00.000

2 M 2012-07-31 00:00:00.000 2012-07-31 00:00:00.000

3 M 2012-08-31 00:00:00.000 2012-08-31 00:00:00.000

4 M 2012-09-28 00:00:00.000 2012-09-30 00:00:00.000

5 M 2012-10-31 00:00:00.000 2012-10-31 00:00:00.000

6 M 2012-11-30 00:00:00.000 2012-11-30 00:00:00.000

7 M 2012-12-31 00:00:00.000 2012-12-31 00:00:00.000

8 M 2013-01-31 00:00:00.000 2013-01-31 00:00:00.000

9 M 2013-02-28 00:00:00.000 2013-02-28 00:00:00.000

10 M 2013-03-29 00:00:00.000 2013-03-31 00:00:00.000

11 M 2013-04-30 00:00:00.000 2013-04-30 00:00:00.000

1 Y 2013-05-31 00:00:00.000 2013-05-31 00:00:00.000

2 Y 2014-05-30 00:00:00.000 2014-05-31 00:00:00.000

5 Y 2017-05-31 00:00:00.000 2017-05-31 00:00:00.000

10 Y 2022-05-31 00:00:00.000 2022-05-31 00:00:00.000

20 Y 2032-05-31 00:00:00.000 2032-05-31 00:00:00.000

30 Y 2042-05-30 00:00:00.000 2042-05-31 00:00:00.000

I have highlighted the differences.

There are some interesting subtleties with the BUSINESSDATE function. For example, what if you want to go 1 business day from a Saturday?

SELECT wct.BUSINESSDATE(

'2012-06-02' --Start Date

,'D' --Date Part

,1 --Number

,NULL --Date Roll Rule

,wct.NBD(holiday) --Holidays

)

FROM HOLIDAYS

WHERE COUNTRY = 'US'

This produces the following result.

-----------------------

2012-06-04 00:00:00.000

But, if we calculate the number of business days from 2012-06-02 to 2012-06-04, we will get zero. We could also use the following statement to get the next business day.

SELECT wct.BUSINESSDATE(

'2012-06-02' --Start Date

,'M' --Date Part

,0 --Number

,'F' --Date Roll Rule

,wct.NBD(holiday) --Holidays

)

FROM HOLIDAYS

WHERE COUNTRY = 'US'

This calculates zero months from the start and uses the F (Following) date roll rule, which advances the calculated date to the next business date.

The DAYS360 functions calculates the number of days between a start date and an end date using any one of three 30/360 date functions. The 30/360 date conventions assumes that a year contains 12 30-day months. 30/360 date conventions are quite common in fixed income calculations but may also be relevant for other types of financial calculations. As in all the date functions that we are discussing, the number of days calculated includes the start date and not the end date in the calculation. In other words, the number of days from 29-May-2012 to 30-May-2012 is calculated as one day, not two.

The XLeratorDB DAYS360 function is slightly different then the EXCEL function of the same name, as the EXCEL function only supports two *methods* and produces some results that are hard to reconcile. For example, if you enter the following information in a spreadsheet:

You get the following result:

which is hard to see as anything other than nonsensical. In XLeratorDB, we can enter

SELECT wct.DAYS360('2012-02-29','2012-02-29', 0) as DAYS360

producing the following result.

DAYS360

-----------

0

EXCEL also supports a

One interesting aspect of the DAYS360 calculations, in both XLeratorDB and EXCEL, is that the calculations do not produce the same results going from start date to end date and from end date to start date when using the US method.

SELECT *

FROM (

SELECT s as [Start]

,e as [End]

,wct.DAYS360(s,e,0) as [fwd]

,-wct.DAYS360(e,s,0) as [bkwd]

FROM (VALUES

('2007-01-15','2007-01-30'),

('2007-01-15','2007-02-15'),

('2007-01-15','2007-07-15'),

('2007-09-30','2008-03-31'),

('2007-09-30','2007-10-31'),

('2007-09-30','2008-09-30'),

('2007-01-15','2007-01-31'),

('2007-01-31','2007-02-28'),

('2007-02-28','2007-03-31'),

('2006-08-31','2007-02-28'),

('2007-02-28','2007-08-31'),

('2007-02-14','2007-02-28'),

('2007-02-26','2008-02-29'),

('2008-02-29','2009-02-28'),

('2008-02-29','2008-03-30'),

('2008-02-29','2008-03-31'),

('2007-02-28','2007-03-05'),

('2007-10-31','2007-11-28'),

('2007-08-31','2008-02-29'),

('2008-02-29','2008-08-31'),

('2008-08-31','2009-02-28'),

('2009-02-28','2009-08-31')

) n(s,e)

) m

WHERE fwd <> bkwd

This produces the following result.

Start End fwd bkwd

---------- ---------- ----------- -----------

2007-01-15 2007-01-31 16 15

2007-01-31 2007-02-28 28 30

2007-02-28 2007-03-31 30 32

2006-08-31 2007-02-28 178 180

2007-02-28 2007-08-31 180 182

2007-02-14 2007-02-28 14 16

2007-02-26 2008-02-29 363 364

2008-02-29 2008-03-30 30 31

2008-02-29 2008-03-31 30 31

2007-02-28 2007-03-05 5 7

2007-10-31 2007-11-28 28 27

2007-08-31 2008-02-29 179 180

2008-02-29 2008-08-31 180 181

2008-08-31 2009-02-28 178 180

2009-02-28 2009-08-31 180 182

The DAYS360 function is most often used in the calculation of accrued interest. Generally the accrued interest calculation looks like:

F * R * DAYS360(start, end, method) / 360

Where F is the face amount and R is the rate of interest.

Like the DAYS360 function, the DAYSNL function calculates the number of days between a start date (inclusive) and an end date (exclusive), but does not count any occurrence of Feb 29th. Effectively, the DAYSNL function makes every year exactly 365 days (the NL in the function name standing for No Leap-years). As with the DAYS360 frunction, the DAYSNL function is used in some fixed income calculations. Here’s an example of the calculation:

SELECT wct.DAYSNL('2011-02-28','2013-02-28') as DAYSNL

This produces the following result.

DAYSNL

-----------

730

While the function ignores instances of Feb-29th, it does not prohibit you from passing Feb-29th into the function as either the start date or the end date. In the following example, we show 3 different ways of breaking up the 2-year period in the previous example, and the results returned by the function.

SELECT *

,wct.DAYSNL(d1,d3) as [Total]

,wct.DAYSNL(d1,d2) as [Year 1]

,wct.DAYSNL(d2,d3) as [Year 2]

FROM (VALUES

('2011-02-28','2012-02-28','2013-02-28'),

('2011-02-28','2012-02-29','2013-02-28'),

('2011-02-28','2012-03-01','2013-02-28')

) as n(d1,d2,d3)

This produces the following result.

d1 d2 d3 Total Year 1 Year 2

---------- ---------- ---------- ----------- ----------- -----------

2011-02-28 2012-02-28 2013-02-28 730 365 365

2011-02-28 2012-02-29 2013-02-28 730 365 364

2011-02-28 2012-03-01 2013-02-28 730 366 364

As you can see in the second row, Year 1 was calculated as 365 days and year 2 was caclulated as 364 days, which is not equal to the total of 730 days for the 2-year period. This happens because the Year 2 calculation is using 2012-02-29 as the start date and the function excludes all instances of the Feb 29th. This ends up being the same as passing 2012-03-01 as the start date, leaving a 1-day gap between 2012-03-01 and 2012-02-28.

All 5 of these date functions are part of XLeratorDB / finance and XLeratorDB / finance 2008 release 1.09. We think that these function can simplify a great many of your financial calculations, allowing you to do very sophisticated date calculations directly on the database, which is generally where you will want the results persisted. This means no network traffic and potentially big improvements in perfromance for large datasets and sophisticated calculations. Let us know what you think.

Archive

Monthly

Go

| |||||||||

Sun | Mon | Tue | Wed | Thu | Fri | Sat | |||
---|---|---|---|---|---|---|---|---|---|

30 | 31 | 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 | 1 | 2 | |||

3 | 4 | 5 | 6 | 7 | 8 | 9 |

Go