Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / database / SQL-Server

Telephone Numbers in SQL Server 2005: Part 2 – Formatting

4.03/5 (9 votes)
13 Mar 2008CPOL3 min read 1   1  
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:

SQL
-- =============================================
-- 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:

C#
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 FORMATPHONENUMBER

7450 ms

5500 ms

C# function using String.Format

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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)