N S

Novick Software Management • Design • Programming • Training • Consulting

   

 

 

SQL Server T-SQL User-Defined Function of the Week

Volume 1 Number 31     June 17, 2003

The importance of Testing!

Sign up for this newsletter at:
http://www.novicksoftware.com/UdfOfWeek/UDFofWeekSignup.htm

Check out the UDF Frequently Asked Questions at:
http://www.novicksoftware.com/UdfOfWeek/UDF_FAQ.htm

To ask a UDF related question or to contribute a UDF,
send e-mail to udf@NovickSoftware.com

Testing! Got to do it. That's what this issue is about. And what goes
with testing? Bugs, of course. 

You should have received a message this morning with a revised 
version of udf_Txt_CharIndexRev. It corrects an error in that 
function.

Next week I'll publish a couple of stored procedures that allow
convenient regression testing of that function. Had I written
them earlier, the bug might not have been published. 

The stored procedures are not ready for publication but this week's 
issue is about a different bug that I found in a different piece of code.

I found a useful UDF on planet-source-code a few weeks ago. 
It used a very good idea about how to round a date down to the 
beginning of a part of a datetime. You use the name of the 
datepart from the DATEPART built-in function to tell it how to
round.

So if your date was '2003-07-01 14:03:43.231' then this table 
shows how the function should work 

Datepart        Result
--------------  --------------------------------------
 year           2003-01-01 00:00:00.000
 quarter        2003-07-01 00:00:00.000
 month          2003-07-01 00:00:00.000
 week           2003-06-29 00:00:00.000
 day            2003-07-01 00:00:00.000
 hour           2003-07-01 14:00:00.000
 minute         2003-07-01 14:03.00.000
 second         2003-07-01 14:03.43.000

The problem with the original function was that it was incorrect.
It produced the wrong result for week and second dateparts. Not
that these are the most important dateparts to round, but a
function shouldn't be shipped with bugs.

To manage the testing of UDFs I've been putting at least a few
tests into the header comment of each scalar UDF.  These are 
rarely a comprehensive test but at least they give a quick 
indication if the UDF works.  This is particularly useful after
making a minor change.  The header tests can show you if the 
UDF is messed up by a change.

Lets look at the UDF and I'll have more to say about testing
once we put the UDF to use. Here's the CREATE FUNCTION script:

/------- Start copying below this line -------------------------\
SET QUOTED_IDENTIFIER ON 
SET ANSI_NULLS ON 
GO

CREATE  FUNCTION dbo.udf_DT_Round2Part (

    @InDate   datetime     -- The datetime to round
  , @DatePart nvarchar(20) -- Datepart to round to Uses the same
                           -- values as built-in DATEPART
)   RETURNS datetime       -- @InDate rounded to the @DatePart
    WITH SCHEMABINDING
/* 
* Returns a date rounded to a named date part.  For most 
* date parts this is done by getting the quantity of the unit 
* since the zero date.  Then this number is added to a date of
* zero.  This gives a datetime that is rounded to the datepart
* requested.
* Returns NULL for a bad DATEPART. Language sensitive.
*
* Attribution: based on similar UDF by James Travis on Planet
* Source Code. This version corrects errors in wk and ss
*
* Example:
select dbo.udf_DT_Round2Part (getdate(), 'mm') -- month start
*
* Test:
PRINT 'Test 1    ' + CASE WHEN '2003-04-01'=
         dbo.udf_DT_Round2Part('2003-04-04 10:19:32.233', 'mm')
                     THEN 'Worked' ELSE 'ERROR' END
PRINT 'Test 2    ' + CASE WHEN '2003-04-04 10:00:00'=
         dbo.udf_DT_Round2Part('2003-04-04 10:19:32.233', 'hour')
                     THEN 'Worked' ELSE 'ERROR' END
PRINT 'Test 3    ' + CASE WHEN '2003-04-04 10:19:00'=
         dbo.udf_DT_Round2Part('2003-04-04 10:19:32.233', 'mi')
                     THEN 'Worked' ELSE 'ERROR' END
PRINT 'Test 4    ' + CASE WHEN '2003-04-04 10:19:32'=
         dbo.udf_DT_Round2Part('2003-04-04 10:19:32.233', 'ss')
                     THEN 'Worked' ELSE 'ERROR' END
PRINT 'Test 5    ' + CASE WHEN '2003-03-30 00:00:00'=
         dbo.udf_DT_Round2Part('2003-04-04 10:19:32.233', 'wk')
                     THEN 'Worked' ELSE 'ERROR' END
PRINT 'Test 6    ' + CASE WHEN '2003-03-30 00:00:00'=
         dbo.udf_DT_Round2Part('2003-04-04 10:19:32.233', 'wk')
                     THEN 'Worked' ELSE 'ERROR' END
PRINT 'Test 7    ' + CASE WHEN '2003-03-30 00:00:00'=
         dbo.udf_DT_Round2Part('2003-03-30 00:00:00.00', 'wk')
                     THEN 'Worked' ELSE 'ERROR' END
PRINT 'Test 8    ' + CASE WHEN 
dbo.udf_DT_Round2Part('2003-04-04 10:19:00', 'foobar') IS NULL
                     THEN 'Worked' ELSE 'ERROR' END
PRINT 'Test 9    ' + CASE WHEN 
dbo.udf_DT_Round2Part('2003-04-04 10:19:00', NULL) IS NULL
                     THEN 'Worked' ELSE 'ERROR' END
*
* History:
* When        Who     Description
* ----------- --- ----------------------------------------------
* 2003-05-04  ASN Initial Coding, see attribution
*
* © Copyright 2003 Andrew Novick http://www.NovickSoftware.com
* You may use this function in any of your SQL Server databases
* including databases that you sell, so long as they contain 
* other unrelated database objects. You may not publish this 
* UDF either in print or electronically.
* Look for an article about this UDF in the UDF of the Week
http://www.NovickSoftware.com/UDFofWeek/UDFofWeek.htm
****************************************************************/
AS BEGIN

SET @DatePart = LOWER(@DatePart) -- In case of CS collation

RETURN CASE WHEN @InDate IS NULL -- Be explicit about this case.
                THEN NULL
            WHEN @DatePart IN ('year', 'yy', 'yyyy') 
	            THEN DATEADD(yyyy,DATEDIFF(yyyy,0,@InDate),0)
            WHEN @DatePart IN ('quarter', 'qq', 'q')
	            THEN DATEADD(qq,DATEDIFF(qq,0,@InDate),0)
            WHEN @DatePart IN ('month', 'mm', 'm')
                THEN DATEADD(mm,DATEDIFF(mm,0,@InDate),0)
            WHEN @DatePart IN ('day', 'dd', 'd', 'dy', 'y',
                                'dayofyear', 'dw', 'weekday')
                THEN DATEADD(dd,DATEDIFF(dd,0,@InDate),0)
            WHEN @DatePart IN ('wk','w','week')
                THEN DATEADD(DD 
                           , -1 -- SUBTRACT ONE DAY
                           , DATEADD(WK
                                   , DATEDIFF(WK,0,@InDate)
                                   , 0
                                    )
                            )
            WHEN @DatePart IN ('hh','hour')
                THEN DATEADD(hh,DATEDIFF(hh,0,@InDate),0)
            WHEN @DatePart IN ('mi','n','minute')
                THEN DATEADD(mi,DATEDIFF(mi,0,@InDate),0)
            WHEN @DatePart IN ('ss','s','second')
                THEN DATEADD(ms -- Subtract the ms from the time.
                            ,-1*DATEPART(ms,@InDATE) 
                            ,@InDATE)
            ELSE
                NULL -- When unknown @DatePart
            END
END
GO

GRANT EXEC, REFERENCES ON dbo.udf_DT_Round2Part to [PUBLIC]
go
\-------Stop copying above this line ---------------------------/

As you can see, inside the header is a Test section. The test 
section doesn't have asterisks at the start of each line like
the rest of the header.  That's because it's set up to run in 
Query Analyzer by selecting the lines of the test and using 
the F5 key or the execute button.

Instead of printing the result, each tests compares the result
of the function to a know value.  I do that because it's much 
easier to evaluate the result of the test this way.  All I you
have to do is look at the output of the tests.  If each test
returned a datetime, you'd have to figure out what the correct
result should have been.  That's not so hard while you're 
developing the function.  It becomes more difficult months later
when you're making a minor change but want to know if the UDF 
still works.

Lets run the tests:
/------- Start copying below this line -------------------------\
PRINT 'Test 1    ' + CASE WHEN '2003-04-01'=
         dbo.udf_DT_Round2Part('2003-04-04 10:19:32.233', 'mm')
                     THEN 'Worked' ELSE 'ERROR' END
PRINT 'Test 2    ' + CASE WHEN '2003-04-04 10:00:00'=
         dbo.udf_DT_Round2Part('2003-04-04 10:19:32.233', 'hour')
                     THEN 'Worked' ELSE 'ERROR' END
PRINT 'Test 3    ' + CASE WHEN '2003-04-04 10:19:00'=
         dbo.udf_DT_Round2Part('2003-04-04 10:19:32.233', 'mi')
                     THEN 'Worked' ELSE 'ERROR' END
PRINT 'Test 4    ' + CASE WHEN '2003-04-04 10:19:32'=
         dbo.udf_DT_Round2Part('2003-04-04 10:19:32.233', 'ss')
                     THEN 'Worked' ELSE 'ERROR' END
PRINT 'Test 5    ' + CASE WHEN '2003-03-30 00:00:00'=
         dbo.udf_DT_Round2Part('2003-04-04 10:19:32.233', 'wk')
                     THEN 'Worked' ELSE 'ERROR' END
PRINT 'Test 6    ' + CASE WHEN '2003-03-30 00:00:00'=
         dbo.udf_DT_Round2Part('2003-04-04 10:19:32.233', 'wk')
                     THEN 'Worked' ELSE 'ERROR' END
PRINT 'Test 7    ' + CASE WHEN '2003-03-30 00:00:00'=
         dbo.udf_DT_Round2Part('2003-03-30 00:00:00.00', 'wk')
                     THEN 'Worked' ELSE 'ERROR' END
PRINT 'Test 8    ' + CASE WHEN 
dbo.udf_DT_Round2Part('2003-04-04 10:19:00', 'foobar') IS NULL
                     THEN 'Worked' ELSE 'ERROR' END
PRINT 'Test 9    ' + CASE WHEN 
dbo.udf_DT_Round2Part('2003-04-04 10:19:00', NULL) IS NULL
                     THEN 'Worked' ELSE 'ERROR' END
\-------Stop copying above this line ---------------------------/
(Results)
Test 1    Worked
Test 2    Worked
Test 3    Worked
Test 4    Worked
Test 5    Worked
Test 6    Worked
Test 7    Worked
Test 8    Worked
Test 9    Worked

So it's pretty easy to tell if the tests worked.  Of course,
it is just as easy to make a mistake in coding the test than
in coding the function but without the testing it's just to 
easy to leave a mistake in the code.

The next script shows the results shown in the table at the 
start of this article.
/------- Start copying below this line -------------------------\
DECLARE @TestDT datetime
SET @TestDT = '2003-07-01 14:03:43.231'
SELECT dbo.udf_DT_Round2Part (@TestDT, 'year') [To Year]
     , dbo.udf_DT_Round2Part (@TestDT, 'qq')   [To Qtr]
SELECT dbo.udf_DT_Round2Part (@TestDT, 'mm')   [To Mon]
     , dbo.udf_DT_Round2Part (@TestDT, 'wk')   [To Week]
SELECT dbo.udf_DT_Round2Part (@TestDT, 'dy')   [To Day]
     , dbo.udf_DT_Round2Part (@TestDT, 'hh')   [To Hr]
SELECT dbo.udf_DT_Round2Part (@TestDT, 'mi')   [To Min]
     , dbo.udf_DT_Round2Part (@TestDT, 'ss')   [To Sec]
GO
\-------Stop copying above this line ---------------------------/
(Results)
To Year                    To Qtr                     
-------------------------- -------------------------- 
2003-01-01 00:00:00.000    2003-07-01 00:00:00.000

To Mon                     To Week                    
-------------------------- -------------------------- 
2003-07-01 00:00:00.000    2003-06-29 00:00:00.000

To Day                     To Hr                      
-------------------------- -------------------------- 
2003-07-01 00:00:00.000    2003-07-01 14:00:00.000

To Min                     To Sec                     
-------------------------- -------------------------- 
2003-07-01 14:03:00.000    2003-07-01 14:03:43.000

Now that it's been tested this is a useful function that I'm 
sure I'll find many reasons to use.

One more thing.  I don't know where, but I'm sure that to one
degree or another that I've published code with bugs also.
Testing shouldn't be about blame. It should be about finding
and eliminating bugs in code and moving on from there.

+--------------------------------------------------------------+
Do you have a great UDF that you'd like to share?  Or maybe you
have a T-SQL problem that you think could be solved by a UDF
but you don't know how? Send them to:

UDF@NovickSoftware.com 

and they might be published in this newsletter.  I try and 
respond to every request that I get.  

Thanks,  
Andrew Novick
+--------------------------------------------------------------+


RSS as HTML

Personal Blog

 
New Tips:

Loading SQL Profiler trace (.trc) files with fn_trace_gettrace

Upcoming
Presentations:

Big Data:
Working with Terabytes
in SQL Server

July 9 '08


Full Schedule