C#idiomatic C#
how-to-define-and-use-custom-numeric-format-providersstandard/base-types

Core library

How to: Define and Use Custom Numeric Format Providers

Learn more about: How to: Define and Use Custom Numeric Format Providers

  • page standard/base-types/how-to-define-and-use-custom-numeric-format-providers
  • updated 03/30/2017

.NET gives you extensive control over the string representation of numeric values. It supports the following features for customizing the format of numeric values:

In some cases (such as when an application must display a formatted account number, an identification number, or a postal code) these three techniques are inappropriate. .NET also enables you to define a formatting object that is neither a System.Globalization.CultureInfo nor a System.Globalization.NumberFormatInfo object to determine how a numeric value is formatted. This topic provides the step-by-step instructions for implementing such an object, and provides an example that formats telephone numbers.

Define a custom format provider

  1. Define a class that implements the System.IFormatProvider and System.ICustomFormatter interfaces.

  2. Implement the System.IFormatProvider.GetFormat method. System.IFormatProvider.GetFormat is a callback method that the formatting method (such as the System.String.Format%28System.IFormatProvider%2CSystem.String%2CSystem.Object%5B%5D%29 method) invokes to retrieve the object that is actually responsible for performing custom formatting. A typical implementation of System.IFormatProvider.GetFormat does the following:

    1. Determines whether the System.Type object passed as a method parameter represents an System.ICustomFormatter interface.

    2. If the parameter does represent the System.ICustomFormatter interface, System.IFormatProvider.GetFormat returns an object that implements the System.ICustomFormatter interface that is responsible for providing custom formatting. Typically, the custom formatting object returns itself.

    3. If the parameter does not represent the System.ICustomFormatter interface, System.IFormatProvider.GetFormat returns null.

  3. Implement the System.ICustomFormatter.Format method. This method is called by the System.String.Format%28System.IFormatProvider%2CSystem.String%2CSystem.Object%5B%5D%29 method and is responsible for returning the string representation of a number. Implementing the method typically involves the following:

    1. Optionally, make sure that the method is legitimately intended to provide formatting services by examining the provider parameter. For formatting objects that implement both System.IFormatProvider and System.ICustomFormatter, this involves testing the provider parameter for equality with the current formatting object.

    2. Determine whether the formatting object should support custom format specifiers. (For example, an "N" format specifier might indicate that a U.S. telephone number should be output in NANP format, and an "I" might indicate output in ITU-T Recommendation E.123 format.) If format specifiers are used, the method should handle the specific format specifier. It is passed to the method in the format parameter. If no specifier is present, the value of the format parameter is System.String.Empty.

    3. Retrieve the numeric value passed to the method as the arg parameter. Perform whatever manipulations are required to convert it to its string representation.

    4. Return the string representation of the arg parameter.

Use a custom numeric formatting object

  1. Create a new instance of the custom formatting class.

  2. Call the System.String.Format%28System.IFormatProvider%2CSystem.String%2CSystem.Object%5B%5D%29 formatting method, passing it the custom formatting object, the formatting specifier (or System.String.Empty, if one is not used), and the numeric value to be formatted.

Example

The following example defines a custom numeric format provider named TelephoneFormatter that converts a number that represents a U.S. telephone number to its NANP or E.123 format. The method handles two format specifiers, "N" (which outputs the NANP format) and "I" (which outputs the international E.123 format).

sample · csharpsamples/snippets/csharp/VS_Snippets_CLR/Formatting.HowTo.NumericValue/cs/Telephone1.cs1

csharp
// <Snippet1>
using System;
using System.Globalization;

public class TelephoneFormatter : IFormatProvider, ICustomFormatter
{
   public object GetFormat(Type formatType)
   {
      if (formatType == typeof(ICustomFormatter))
         return this;
      else
         return null;
   }

   public string Format(string format, object arg, IFormatProvider formatProvider)
   {
      // Check whether this is an appropriate callback
      if (!this.Equals(formatProvider))
         return null;

      // Set default format specifier
      if (string.IsNullOrEmpty(format))
         format = "N";

      string numericString = arg.ToString();

      if (format == "N")
      {
         if (numericString.Length <= 4)
            return numericString;
         else if (numericString.Length == 7)
            return numericString.Substring(0, 3) + "-" + numericString.Substring(3, 4);
         else if (numericString.Length == 10)
               return "(" + numericString.Substring(0, 3) + ") " +
                      numericString.Substring(3, 3) + "-" + numericString.Substring(6);
         else
            throw new FormatException(
                      string.Format("'{0}' cannot be used to format {1}.",
                                    format, arg.ToString()));
      }
      else if (format == "I")
      {
         if (numericString.Length < 10)
            throw new FormatException(string.Format("{0} does not have 10 digits.", arg.ToString()));
         else
            numericString = "+1 " + numericString.Substring(0, 3) + " " + numericString.Substring(3, 3) + " " + numericString.Substring(6);
      }
      else
      {
         throw new FormatException(string.Format("The {0} format specifier is invalid.", format));
      }
      return numericString;
   }
}

public class TestTelephoneFormatter
{
   public static void Main()
   {
      Console.WriteLine(String.Format(new TelephoneFormatter(), "{0}", 0));
      Console.WriteLine(String.Format(new TelephoneFormatter(), "{0}", 911));
      Console.WriteLine(String.Format(new TelephoneFormatter(), "{0}", 8490216));
      // <Snippet3>
      Console.WriteLine(String.Format(new TelephoneFormatter(), "{0}", 4257884748));
      // </Snippet3>

      Console.WriteLine(String.Format(new TelephoneFormatter(), "{0:N}", 0));
      Console.WriteLine(String.Format(new TelephoneFormatter(), "{0:N}", 911));
      Console.WriteLine(String.Format(new TelephoneFormatter(), "{0:N}", 8490216));
      // <Snippet2>
      Console.WriteLine(String.Format(new TelephoneFormatter(), "{0:N}", 4257884748));
      // </Snippet2>

      Console.WriteLine(String.Format(new TelephoneFormatter(), "{0:I}", 4257884748));
   }
}
// </Snippet1>

sample · vbsamples/snippets/visualbasic/VS_Snippets_CLR/Formatting.HowTo.NumericValue/vb/Telephone1.vb1

vb
' Visual Basic .NET Document
Option Strict On

' <Snippet1>
Public Class TelephoneFormatter : Implements IFormatProvider, ICustomFormatter
    Public Function GetFormat(formatType As Type) As Object _
                    Implements IFormatProvider.GetFormat
        If formatType Is GetType(ICustomFormatter) Then
            Return Me
        Else
            Return Nothing
        End If
    End Function

    Public Function Format(fmt As String, arg As Object, _
                           formatProvider As IFormatProvider) As String _
                    Implements ICustomFormatter.Format
        ' Check whether this is an appropriate callback             
        If Not Me.Equals(formatProvider) Then Return Nothing

        ' Set default format specifier             
        If String.IsNullOrEmpty(fmt) Then fmt = "N"

        Dim numericString As String = arg.ToString

        If fmt = "N" Then
            Select Case numericString.Length
                Case <= 4
                    Return numericString
                Case 7
                    Return Left(numericString, 3) & "-" & Mid(numericString, 4)
                Case 10
                    Return "(" & Left(numericString, 3) & ") " & _
                           Mid(numericString, 4, 3) & "-" & Mid(numericString, 7)
                Case Else
                    Throw New FormatException( _
                              String.Format("'{0}' cannot be used to format {1}.", _
                                            fmt, arg.ToString()))
            End Select
        ElseIf fmt = "I" Then
            If numericString.Length < 10 Then
                Throw New FormatException(String.Format("{0} does not have 10 digits.", arg.ToString()))
            Else
                numericString = "+1 " & Left(numericString, 3) & " " & Mid(numericString, 4, 3) & " " & Mid(numericString, 7)
            End If
        Else
            Throw New FormatException(String.Format("The {0} format specifier is invalid.", fmt))
        End If
        Return numericString
    End Function
End Class

Public Module TestTelephoneFormatter
    Public Sub Main
        Console.WriteLine(String.Format(New TelephoneFormatter, "{0}", 0))
        Console.WriteLine(String.Format(New TelephoneFormatter, "{0}", 911))
        Console.WriteLine(String.Format(New TelephoneFormatter, "{0}", 8490216))
        ' <Snippet3>
        Console.WriteLine(String.Format(New TelephoneFormatter, "{0}", 4257884748))
        ' </Snippet3>

        Console.WriteLine(String.Format(New TelephoneFormatter, "{0:N}", 0))
        Console.WriteLine(String.Format(New TelephoneFormatter, "{0:N}", 911))
        Console.WriteLine(String.Format(New TelephoneFormatter, "{0:N}", 8490216))
        ' <Snippet2>
        Console.WriteLine(String.Format(New TelephoneFormatter, "{0:N}", 4257884748))
        ' </Snippet2>

        Console.WriteLine(String.Format(New TelephoneFormatter, "{0:I}", 4257884748))
    End Sub
End Module
' </Snippet1>

The custom numeric format provider can be used only with the System.String.Format%28System.IFormatProvider%2CSystem.String%2CSystem.Object%5B%5D%29 method. The other overloads of numeric formatting methods (such as ToString) that have a parameter of type System.IFormatProvider all pass the System.IFormatProvider.GetFormat implementation a System.Type object that represents the System.Globalization.NumberFormatInfo type. In return, they expect the method to return a System.Globalization.NumberFormatInfo object. If it does not, the custom numeric format provider is ignored, and the System.Globalization.NumberFormatInfo object for the current culture is used in its place. In the example, the TelephoneFormatter.GetFormat method handles the possibility that it may be inappropriately passed to a numeric formatting method by examining the method parameter and returning null if it represents a type other than System.ICustomFormatter.

If a custom numeric format provider supports a set of format specifiers, make sure you provide a default behavior if no format specifier is supplied in the format item used in the System.String.Format%28System.IFormatProvider%2CSystem.String%2CSystem.Object%5B%5D%29 method call. In the example, "N" is the default format specifier. This allows for a number to be converted to a formatted telephone number by providing an explicit format specifier. The following example illustrates such a method call.

sample · csharpsamples/snippets/csharp/VS_Snippets_CLR/Formatting.HowTo.NumericValue/cs/Telephone1.cs2

csharp
// <Snippet1>
using System;
using System.Globalization;

public class TelephoneFormatter : IFormatProvider, ICustomFormatter
{
   public object GetFormat(Type formatType)
   {
      if (formatType == typeof(ICustomFormatter))
         return this;
      else
         return null;
   }

   public string Format(string format, object arg, IFormatProvider formatProvider)
   {
      // Check whether this is an appropriate callback
      if (!this.Equals(formatProvider))
         return null;

      // Set default format specifier
      if (string.IsNullOrEmpty(format))
         format = "N";

      string numericString = arg.ToString();

      if (format == "N")
      {
         if (numericString.Length <= 4)
            return numericString;
         else if (numericString.Length == 7)
            return numericString.Substring(0, 3) + "-" + numericString.Substring(3, 4);
         else if (numericString.Length == 10)
               return "(" + numericString.Substring(0, 3) + ") " +
                      numericString.Substring(3, 3) + "-" + numericString.Substring(6);
         else
            throw new FormatException(
                      string.Format("'{0}' cannot be used to format {1}.",
                                    format, arg.ToString()));
      }
      else if (format == "I")
      {
         if (numericString.Length < 10)
            throw new FormatException(string.Format("{0} does not have 10 digits.", arg.ToString()));
         else
            numericString = "+1 " + numericString.Substring(0, 3) + " " + numericString.Substring(3, 3) + " " + numericString.Substring(6);
      }
      else
      {
         throw new FormatException(string.Format("The {0} format specifier is invalid.", format));
      }
      return numericString;
   }
}

public class TestTelephoneFormatter
{
   public static void Main()
   {
      Console.WriteLine(String.Format(new TelephoneFormatter(), "{0}", 0));
      Console.WriteLine(String.Format(new TelephoneFormatter(), "{0}", 911));
      Console.WriteLine(String.Format(new TelephoneFormatter(), "{0}", 8490216));
      // <Snippet3>
      Console.WriteLine(String.Format(new TelephoneFormatter(), "{0}", 4257884748));
      // </Snippet3>

      Console.WriteLine(String.Format(new TelephoneFormatter(), "{0:N}", 0));
      Console.WriteLine(String.Format(new TelephoneFormatter(), "{0:N}", 911));
      Console.WriteLine(String.Format(new TelephoneFormatter(), "{0:N}", 8490216));
      // <Snippet2>
      Console.WriteLine(String.Format(new TelephoneFormatter(), "{0:N}", 4257884748));
      // </Snippet2>

      Console.WriteLine(String.Format(new TelephoneFormatter(), "{0:I}", 4257884748));
   }
}
// </Snippet1>

sample · vbsamples/snippets/visualbasic/VS_Snippets_CLR/Formatting.HowTo.NumericValue/vb/Telephone1.vb2

vb
' Visual Basic .NET Document
Option Strict On

' <Snippet1>
Public Class TelephoneFormatter : Implements IFormatProvider, ICustomFormatter
    Public Function GetFormat(formatType As Type) As Object _
                    Implements IFormatProvider.GetFormat
        If formatType Is GetType(ICustomFormatter) Then
            Return Me
        Else
            Return Nothing
        End If
    End Function

    Public Function Format(fmt As String, arg As Object, _
                           formatProvider As IFormatProvider) As String _
                    Implements ICustomFormatter.Format
        ' Check whether this is an appropriate callback             
        If Not Me.Equals(formatProvider) Then Return Nothing

        ' Set default format specifier             
        If String.IsNullOrEmpty(fmt) Then fmt = "N"

        Dim numericString As String = arg.ToString

        If fmt = "N" Then
            Select Case numericString.Length
                Case <= 4
                    Return numericString
                Case 7
                    Return Left(numericString, 3) & "-" & Mid(numericString, 4)
                Case 10
                    Return "(" & Left(numericString, 3) & ") " & _
                           Mid(numericString, 4, 3) & "-" & Mid(numericString, 7)
                Case Else
                    Throw New FormatException( _
                              String.Format("'{0}' cannot be used to format {1}.", _
                                            fmt, arg.ToString()))
            End Select
        ElseIf fmt = "I" Then
            If numericString.Length < 10 Then
                Throw New FormatException(String.Format("{0} does not have 10 digits.", arg.ToString()))
            Else
                numericString = "+1 " & Left(numericString, 3) & " " & Mid(numericString, 4, 3) & " " & Mid(numericString, 7)
            End If
        Else
            Throw New FormatException(String.Format("The {0} format specifier is invalid.", fmt))
        End If
        Return numericString
    End Function
End Class

Public Module TestTelephoneFormatter
    Public Sub Main
        Console.WriteLine(String.Format(New TelephoneFormatter, "{0}", 0))
        Console.WriteLine(String.Format(New TelephoneFormatter, "{0}", 911))
        Console.WriteLine(String.Format(New TelephoneFormatter, "{0}", 8490216))
        ' <Snippet3>
        Console.WriteLine(String.Format(New TelephoneFormatter, "{0}", 4257884748))
        ' </Snippet3>

        Console.WriteLine(String.Format(New TelephoneFormatter, "{0:N}", 0))
        Console.WriteLine(String.Format(New TelephoneFormatter, "{0:N}", 911))
        Console.WriteLine(String.Format(New TelephoneFormatter, "{0:N}", 8490216))
        ' <Snippet2>
        Console.WriteLine(String.Format(New TelephoneFormatter, "{0:N}", 4257884748))
        ' </Snippet2>

        Console.WriteLine(String.Format(New TelephoneFormatter, "{0:I}", 4257884748))
    End Sub
End Module
' </Snippet1>

But it also allows the conversion to occur if no format specifier is present. The following example illustrates such a method call.

sample · csharpsamples/snippets/csharp/VS_Snippets_CLR/Formatting.HowTo.NumericValue/cs/Telephone1.cs3

csharp
// <Snippet1>
using System;
using System.Globalization;

public class TelephoneFormatter : IFormatProvider, ICustomFormatter
{
   public object GetFormat(Type formatType)
   {
      if (formatType == typeof(ICustomFormatter))
         return this;
      else
         return null;
   }

   public string Format(string format, object arg, IFormatProvider formatProvider)
   {
      // Check whether this is an appropriate callback
      if (!this.Equals(formatProvider))
         return null;

      // Set default format specifier
      if (string.IsNullOrEmpty(format))
         format = "N";

      string numericString = arg.ToString();

      if (format == "N")
      {
         if (numericString.Length <= 4)
            return numericString;
         else if (numericString.Length == 7)
            return numericString.Substring(0, 3) + "-" + numericString.Substring(3, 4);
         else if (numericString.Length == 10)
               return "(" + numericString.Substring(0, 3) + ") " +
                      numericString.Substring(3, 3) + "-" + numericString.Substring(6);
         else
            throw new FormatException(
                      string.Format("'{0}' cannot be used to format {1}.",
                                    format, arg.ToString()));
      }
      else if (format == "I")
      {
         if (numericString.Length < 10)
            throw new FormatException(string.Format("{0} does not have 10 digits.", arg.ToString()));
         else
            numericString = "+1 " + numericString.Substring(0, 3) + " " + numericString.Substring(3, 3) + " " + numericString.Substring(6);
      }
      else
      {
         throw new FormatException(string.Format("The {0} format specifier is invalid.", format));
      }
      return numericString;
   }
}

public class TestTelephoneFormatter
{
   public static void Main()
   {
      Console.WriteLine(String.Format(new TelephoneFormatter(), "{0}", 0));
      Console.WriteLine(String.Format(new TelephoneFormatter(), "{0}", 911));
      Console.WriteLine(String.Format(new TelephoneFormatter(), "{0}", 8490216));
      // <Snippet3>
      Console.WriteLine(String.Format(new TelephoneFormatter(), "{0}", 4257884748));
      // </Snippet3>

      Console.WriteLine(String.Format(new TelephoneFormatter(), "{0:N}", 0));
      Console.WriteLine(String.Format(new TelephoneFormatter(), "{0:N}", 911));
      Console.WriteLine(String.Format(new TelephoneFormatter(), "{0:N}", 8490216));
      // <Snippet2>
      Console.WriteLine(String.Format(new TelephoneFormatter(), "{0:N}", 4257884748));
      // </Snippet2>

      Console.WriteLine(String.Format(new TelephoneFormatter(), "{0:I}", 4257884748));
   }
}
// </Snippet1>

sample · vbsamples/snippets/visualbasic/VS_Snippets_CLR/Formatting.HowTo.NumericValue/vb/Telephone1.vb3

vb
' Visual Basic .NET Document
Option Strict On

' <Snippet1>
Public Class TelephoneFormatter : Implements IFormatProvider, ICustomFormatter
    Public Function GetFormat(formatType As Type) As Object _
                    Implements IFormatProvider.GetFormat
        If formatType Is GetType(ICustomFormatter) Then
            Return Me
        Else
            Return Nothing
        End If
    End Function

    Public Function Format(fmt As String, arg As Object, _
                           formatProvider As IFormatProvider) As String _
                    Implements ICustomFormatter.Format
        ' Check whether this is an appropriate callback             
        If Not Me.Equals(formatProvider) Then Return Nothing

        ' Set default format specifier             
        If String.IsNullOrEmpty(fmt) Then fmt = "N"

        Dim numericString As String = arg.ToString

        If fmt = "N" Then
            Select Case numericString.Length
                Case <= 4
                    Return numericString
                Case 7
                    Return Left(numericString, 3) & "-" & Mid(numericString, 4)
                Case 10
                    Return "(" & Left(numericString, 3) & ") " & _
                           Mid(numericString, 4, 3) & "-" & Mid(numericString, 7)
                Case Else
                    Throw New FormatException( _
                              String.Format("'{0}' cannot be used to format {1}.", _
                                            fmt, arg.ToString()))
            End Select
        ElseIf fmt = "I" Then
            If numericString.Length < 10 Then
                Throw New FormatException(String.Format("{0} does not have 10 digits.", arg.ToString()))
            Else
                numericString = "+1 " & Left(numericString, 3) & " " & Mid(numericString, 4, 3) & " " & Mid(numericString, 7)
            End If
        Else
            Throw New FormatException(String.Format("The {0} format specifier is invalid.", fmt))
        End If
        Return numericString
    End Function
End Class

Public Module TestTelephoneFormatter
    Public Sub Main
        Console.WriteLine(String.Format(New TelephoneFormatter, "{0}", 0))
        Console.WriteLine(String.Format(New TelephoneFormatter, "{0}", 911))
        Console.WriteLine(String.Format(New TelephoneFormatter, "{0}", 8490216))
        ' <Snippet3>
        Console.WriteLine(String.Format(New TelephoneFormatter, "{0}", 4257884748))
        ' </Snippet3>

        Console.WriteLine(String.Format(New TelephoneFormatter, "{0:N}", 0))
        Console.WriteLine(String.Format(New TelephoneFormatter, "{0:N}", 911))
        Console.WriteLine(String.Format(New TelephoneFormatter, "{0:N}", 8490216))
        ' <Snippet2>
        Console.WriteLine(String.Format(New TelephoneFormatter, "{0:N}", 4257884748))
        ' </Snippet2>

        Console.WriteLine(String.Format(New TelephoneFormatter, "{0:I}", 4257884748))
    End Sub
End Module
' </Snippet1>

If no default format specifier is defined, your implementation of the System.ICustomFormatter.Format method should include code such as the following so that .NET can provide formatting that your code does not support.

sample · csharpsamples/snippets/csharp/VS_Snippets_CLR_System/system.ICustomFormatter.Format/cs/format.cs1

csharp
using System;

public class CustomFormatter : IFormatProvider, ICustomFormatter
{
   public object GetFormat(Type formatType)
   {
      return this;
   }

   public string Format(string format, object arg, IFormatProvider formatProvider)
   {
      string s = null;

      // <Snippet1>
      if (arg is IFormattable)
         s = ((IFormattable)arg).ToString(format, formatProvider);
      else if (arg != null)
         s = arg.ToString();
      // </Snippet1>
      return s;
   }
}

sample · vbsamples/snippets/visualbasic/VS_Snippets_CLR_System/system.ICustomFormatter.Format/vb/Format.vb1

vb
' Visual Basic .NET Document
Option Strict On

Public Class CustomFormatter : Implements IFormatProvider, ICustomFormatter
    Public Function GetFormat(formatType As Type) As Object _
                    Implements IFormatProvider.GetFormat
        Return Me
    End Function

    Public Function Format(fmt As String, arg As Object, formatProvider As IFormatProvider) As String _
                    Implements ICustomFormatter.Format
        Dim s As String = String.Empty

        ' <Snippet1>
        If TypeOf (arg) Is IFormattable Then
            s = DirectCast(arg, IFormattable).ToString(fmt, formatProvider)
        ElseIf arg IsNot Nothing Then
            s = arg.ToString()
        End If
        ' </Snippet1>
        Return s
    End Function
End Class

In the case of this example, the method that implements System.ICustomFormatter.Format is intended to serve as a callback method for the System.String.Format%28System.IFormatProvider%2CSystem.String%2CSystem.Object%5B%5D%29 method. Therefore, it examines the formatProvider parameter to determine whether it contains a reference to the current TelephoneFormatter object. However, the method can also be called directly from code. In that case, you can use the formatProvider parameter to provide a System.Globalization.CultureInfo or System.Globalization.NumberFormatInfo object that supplies culture-specific formatting information.