Telephone Numbers in SQL Server 2005: Part 2 – Formatting






4.03/5 (9 votes)
Formatting telephone numbers in SQL Server.
Introduction
In my first article, Telephone Numbers in SQL Server 2005: Part 1 – The Data Type, I discussed various ways of persisting a simple telephone number in SQL Server. Simply displaying the raw data stored in SQL Server would not be suitable for human consumption, so some form of data formatting must be done. Data formatting is usually performed in the user interface layer, for example, by the ASPX page or the WinForms application. Often, it is convenient to have SQL Server format the data in a view to be passed into another document, like a report that might have a difficult time formatting the number correctly. In this article, I will discuss a couple methods that can be used to format a phone number for presentation to the user on SQL Server.
The UDF
SQL Server 2000 introduced the ability to create User Defined Functions. Using a custom formatting function for telephone numbers is a perfect example of where to use UDFs. The code below can be used to create a telephone number formatting function in TSQL:
-- =============================================
-- Author: Bryan Kowalchuk
-- Create date: Sept 14, 2007
-- Description: Formats a telephone number
-- to North American Numbering Plan standard
-- =============================================
CREATE FUNCTION [dbo].[FORMATPHONENUMBER]
(
@Number money
)
RETURNS varchar(25)
AS
BEGIN
-- Declare the return variable here
DECLARE @Formatted varchar(25) -- Formatted number to return
DECLARE @CharNum varchar(18) -- Character type of phone number
DECLARE @Extension int -- Phone extesion
DECLARE @Numerator bigint -- Working number variable
IF @Number IS NULL
BEGIN
--Just return NULL if input string is NULL
RETURN NULL
END
-- Just enough room, since max phone number
-- digits is 14 + 4 for extension is 18
-- Get rid of the decimal
SET @Numerator = CAST(@Number * 10000 AS bigint)
-- Cast to int to strip off leading zeros
SET @Extension = CAST(RIGHT(@Numerator, 4) AS int)
-- Strip off the extension
SET @CharNum = CAST(LEFT(@Numerator , LEN(@Numerator) - 4)
AS varchar(18))
IF LEN(@CharNum) = 10 -- Full phone number, return (905) 555-1212
BEGIN
SET @Formatted = '(' + LEFT(@CharNum, 3) + ') ' +
SUBSTRING(@CharNum,4,3) + '-' + RIGHT(@CharNum, 4)
IF @Extension > 0 -- Add Extension
BEGIN
SET @Formatted = @Formatted + ' ext '+
CAST(@Extension AS varchar(4))
END
RETURN @Formatted
END
IF LEN(@CharNum) = 7 -- No Area Code, return 555-1212
BEGIN
SET @Formatted = LEFT(@CharNum, 3) + '-' + RIGHT(@CharNum, 4)
IF @Extension > 0 -- Add Extension
BEGIN
SET @Formatted = @Formatted + ' ext '+
CAST(@Extension AS varchar(6))
END
RETURN @Formatted
END
IF LEN(@CharNum) = 11
-- Full phone number with access code,
-- return 1 (905) 555-1212 (19055551212)
BEGIN
SET @Formatted = LEFT(@CharNum, 1) + ' (' + SUBSTRING(@CharNum, 2, 3) + ') ' +
SUBSTRING(@CharNum,4,3) + '-' + RIGHT(@CharNum, 4)
IF @Extension > 0 -- Add Extension
BEGIN
SET @Formatted = @Formatted + ' ext '+ CAST(@Extension AS varchar(4))
END
RETURN @Formatted
END
-- Last case, just return the number unformatted (unhandled format)
SET @Formatted = @CharNum
IF @Extension > 0 -- Just the Extension
BEGIN
SET @Formatted = @Formatted + ' ext '+ CAST(@Extension AS varchar(4))
RETURN 'ext '+ CAST(@Extension AS varchar(4))
END
RETURN @Formatted
END
The CLR Version
SQL Server 2005 added the ability to write your own DLLs in the .NET programming language of your choice, so I also wrote the equivalent function in C#. To do this in Visual Studio 2005, start a new SQL Server project, and add a new User-Defined Function. Paste this code into it:
using System;
using System.Data;
using System.Data.SqlClient;
using System.Data.SqlTypes;
using Microsoft.SqlServer.Server;
using System.Text.RegularExpressions;
public partial class UserDefinedFunctions
{
[Microsoft.SqlServer.Server.SqlFunction(
IsDeterministic=true, IsPrecise=true)]
public static SqlString FormatNAPhoneNumber(SqlMoney Number)
{
// Return Null if number is null
if (Number.IsNull)
return SqlString.Null;
string phoneNumber = Number.ToString();
string [] phone = phoneNumber.Split(new Char [] {'.'});
string charnum = phoneNumber; //Default to unformatted number
int extension = int.Parse(phone[1]);
switch (phone[0].Length)
{
case 10:
//Format standard North American phone number
//'(416) 555-1212' or '(415) 555-1212 ext1234'
{
if (extension > 0)
{
charnum = string.Format("({0}) {1}-{2} ext{3}",
phone[0].Substring(0, 3),
phone[0].Substring(3, 3),
phone[0].Substring(6),
extension.ToString());
// charnum = Regex.Replace(phone[0], @"(\d{3})(\d{3})(\d{4})",
// "($1) $2-$3"); //Test Regex performance
// charnum = "(" + phone[0].Substring(0, 3) + ") " +
// phone[0].Substring(3, 3) + //Test standard string concat
// phone[0].Substring(6) + " ext" + extension.ToString();
return new SqlString(charnum);
}
else
{
charnum = string.Format("({0}) {1}-{2}",
phone[0].Substring(0, 3),
phone[0].Substring(3, 3),
phone[0].Substring(6));
// charnum = Regex.Replace(phone[0], @"(\d{3})(\d{3})(\d{4})",
// "($1) $2-$3");
// charnum = "(" + phone[0].Substring(0, 3) + ") " +
// phone[0].Substring(3, 3) + phone[0].Substring(6);
return new SqlString(charnum);
}
break;
}
case 7: // Format without the area code '555-1212' or '555-1212 ext1234'
{
if (extension > 0)
{
charnum = string.Format("{0}-{1} ext{2}",
phone[0].Substring(0, 3),
phone[0].Substring(3),
extension.ToString());
return new SqlString(charnum);
break;
}
else
{
charnum = string.Format("{0}-{1}",
phone[0].Substring(0, 3),
phone[0].Substring(3));
return new SqlString(charnum);
break;
}
}
case 11:
// Format with dialing prefix '1 (416) 555-1212' or '1 (416) 555-1212 ext1234'
{
// return new SqlString(phone[0] + " and " + phone[1]);
if (extension > 0)
{
charnum = string.Format("{0} ({1}) {2}-{3} ext{4}",
phone[0].Substring(0, 1),
phone[0].Substring(1, 3),
phone[0].Substring(4, 3),
phone[0].Substring(7),
extension.ToString());
return new SqlString(charnum);
}
else
{
charnum = string.Format("{0} ({1}) {2}-{3}",
phone[0].Substring(0, 1),
phone[0].Substring(1, 3),
phone[0].Substring(4, 3),
phone[0].Substring(7));
return new SqlString(charnum);
}
}
default: //Just return the number as a string, no formatting
{
return new SqlString(charnum);
break;
}
}
}
}
I did some quick benchmarking, to see how it affected performance, against a simple table containing the phone number data type. The table contains a small set of 580 rows, and the SQL SELECT
command was executed 1,000 times, which generated 580,000 calls to the function. The SELECT
command was executed without bringing the dataset back to the client so that the network time was minimized. Execution times are in milliseconds. Your numbers may vary, but use these numbers as a relative comparison of the different methods.
Method |
Time |
Function Overhead |
No phone number formatting |
1950 ms |
|
TSQL |
7450 ms |
5500 ms |
C# function using |
4750 ms |
2800 ms |
C# function using RegEx |
7187 ms |
5237 ms |
The Suprise!
To my surprise, the .NET function call ran twice as fast as the native TSQL function! My first guess would be that the native TSQL functions would certainly run faster by eliminating the overhead of the .NET engine and interface. It seems Microsoft has done their homework on .NET integration. This is another good example of where testing shows results that are often counter to what you would expect.
The C# version also has various lines commented out if you choose to use the RegEx library or the simple string concatenate operator for formatting the telephone number. One way to handle international phone numbers would be to store the RegEx pattern and evaluator strings in a table keyed by country. This table could then be linked to the phone number and the appropriate formatting codes passed to the function. I didn't go this far with the design, but I did try using the RegEx method of formatting to test the performance over using the String.Format
method. As you can see, it performed about the same as the TSQL version, so the overhead of the RegEx object was significant.
Conclusion
Using TSQL to write user defined functions can be awkward if it requires a fair amount of string manipulation or complicated logic. Using the .NET CLR integration allows you to easily use the full power of the .NET library and probably get better performance than with TSQL.
In my first article, it was stated that the small details make all the difference. By paying attention to proper design and doing a little bit of testing, we have cut the data footprint of storing a phone number in half as well as cut the CPU requirements for formatting the number in half. Small changes like these can make all the difference when it comes to how well your applications can scale.