65.9K
CodeProject is changing. Read more.
Home

Parsomatic

starIconstarIconstarIconstarIconemptyStarIcon

4.00/5 (4 votes)

Dec 8, 2008

CPOL

3 min read

viewsIcon

27755

downloadIcon

97

A Dictionary of Parse methods for several datatypes

Introduction

This class contains a Dictionary of methods that allow parsing string values. Methods other than those provided by the datatype may be used. Once configured, all that is required to perform a parse are the value and the FullName of the type you want.

Background

I have a particular class that gets its configuration from an XML file. An entry might be:

<StartTime DataType="System.DateTime" >2008-12-06 12:34:56</StartTime>

Obviously I want to be able to parse the value into a DateTime, but all I have to go by is the name of the type; I need to parse the name as well!

Getting a type by its name isn't difficult:

System.Type t = System.Type.GetType ( "System.DateTime" ) ;

But then getting the Parse method requires Reflection:

System.Reflection.MethodInfo m = t.GetMethod
(
    "Parse"
,
    System.Reflection.BindingFlags.Public
    |
    System.Reflection.BindingFlags.Static
    |
    System.Reflection.BindingFlags.Instance
,
    null
,
    new System.Type[] { typeof(System.String) }
,
    null
) ;

Reflection is great, but it's costly, so caching the retrieved information is generally worthwhile.

Another problem we run into is that not every type I want to support has a public Parse method that takes a string as its only parameter:

  • System.Guid has a constructor that can be used instead (and what's up with that anyway?)
  • Enumerations use the System.Enum.Parse method (which requires the type as well as the value)
  • System.String has a Copy method we can use instead (String is included so as not to have exceptional cases)

Concept

So I want a Dictionary of datatype names and their associated MethodBase instances (MethodInfo and ConstructorInfo both derive from MethodBase). I decided I also want a Dictionary that contains the default values for the types so I can return a reasonable value when a TryParse fails.

Other points:

  • I want to pre-populate the dictionaries with some common datatypes.
  • I want to allow methods that come from "helper" datatypes.
  • I want to allow the user to add other datatypes with their methods and defaults.
  • I want to allow the user to override existing methods and defaults.
  • I want to implement Parse and TryParse for datatypes that don't have their own.

Parsomatic

I chose to make this a static class because I don't expect to need more than one:

public static class Parsomatic
{
}

Fields

The class contains only the two dictionaries and an array that we can use when we call GetMethod:

private static readonly System.Type[] types ;
private static readonly System.Collections.Generic.Dictionary
			<string,System.Reflection.MethodBase> parsers ;
private static readonly System.Collections.Generic.Dictionary<string,object> defaults ;

Constructor

The constructor instantiates and pre-populates the fields:

types = new System.Type[] { typeof(System.String) } ;

parsers = new System.Collections.Generic.Dictionary<string,System.Reflection.MethodBase>
    ( System.StringComparer.CurrentCultureIgnoreCase ) ;

defaults = new System.Collections.Generic.Dictionary<string,object>
    ( System.StringComparer.CurrentCultureIgnoreCase ) ;

(I'll show the pre-population later.)

AddType ( object )

This overload of AddType is the simplest, it will determine the type to use automatically:

public static void
AddType
(
    object Default
)
{
    if ( Default == null )
    {
        throw ( new System.ArgumentNullException 
		( "Default" , "The Default must not be null" ) ) ;
    }

    if ( Default.GetType().IsEnum )
    {
        AddType
        (
            Default
        ,
            typeof(EnumParser<>).MakeGenericType 
		( new System.Type[] { Default.GetType() } )
        ) ;
    }
    else
    {
        AddType
        (
            Default
        ,
            Default.GetType()
        ) ;
    }

    return ;
}

(I'll describe EnumParser<> later.)

This overload is used for pre-populating the following types:

AddType ( default(System.Byte)     ) ;
AddType ( default(System.SByte)    ) ;
AddType ( default(System.Int16)    ) ;
AddType ( default(System.UInt16)   ) ;
AddType ( default(System.Int32)    ) ;
AddType ( default(System.UInt32)   ) ;
AddType ( default(System.Int64)    ) ;
AddType ( default(System.UInt64)   ) ;
AddType ( default(System.Single)   ) ;
AddType ( default(System.Double)   ) ;
AddType ( default(System.Decimal)  ) ;
AddType ( default(System.DateTime) ) ;
AddType ( default(System.Boolean)  ) ;

AddType ( object , Type )

This overload calls GetMethod on the provided type to find a public Parse method that takes a string as its only parameter:

public static void
AddType
(
    object      Default
,
    System.Type Type
)
{
    if ( Default == null )
    {
        throw ( new System.ArgumentNullException 
		( "Default" , "The Default must not be null" ) ) ;
    }

    if ( Type == null )
    {
        throw ( new System.ArgumentNullException 
		( "Type" , "The Type must not be null" ) ) ;
    }

    AddType
    (
        Default
    ,
        Type.GetMethod
        (
            "Parse"
        ,
            System.Reflection.BindingFlags.Public
            |
            System.Reflection.BindingFlags.Static
            |
            System.Reflection.BindingFlags.Instance
        ,
            null
        ,
            types
        ,
            null
        )
    ) ;

    return ;
}

AddType ( object , MethodBase )

This overload ensures that the provided method meets certain criteria and, if it does, updates the dictionaries:

public static void
AddType
(
    object                       Default
,
    System.Reflection.MethodBase Method
)
{
    if ( Default == null )
    {
        throw ( new System.ArgumentNullException 
		( "Default" , "The Default must not be null" ) ) ;
    }

    if ( Method == null )
    {
        throw ( new System.ArgumentNullException 
		( "Method" , "The Method must not be null" ) ) ;
    }

    if
    (
        ( Method.GetParameters().Length != 1 )
    ||
        ( Method.GetParameters() [ 0 ].ParameterType != typeof(System.String) )
    )
    {
        throw ( new System.ArgumentException 
		( "The Method must take one string parameter" , "Method" ) ) ;
    }

    if
    (
        ( Method is System.Reflection.MethodInfo )
    &&
        ( ((System.Reflection.MethodInfo) Method).ReturnType != Default.GetType() )
    )
    {
        throw ( new System.ArgumentException 
	( "The Method must return the same type as the Default" , "Method" ) ) ;
    }

    if
    (
        ( Method is System.Reflection.ConstructorInfo )
    &&
        ( ((System.Reflection.ConstructorInfo) Method).ReflectedType 
						!= Default.GetType() )
    )
    {
        throw ( new System.ArgumentException 
	( "The Method must return the same type as the Default" , "Method" ) ) ;
    }

    defaults [ Default.GetType().FullName ] = Default ;

    parsers [ Default.GetType().FullName ] = Method ;

    return ;
}

This overload is used for pre-populating the following types:

AddType
(
    default(System.Guid)
,
    typeof(System.Guid).GetConstructor
    (
        System.Reflection.BindingFlags.Public
        |
        System.Reflection.BindingFlags.Instance
    ,
        null
    ,
        types
    ,
        null
    )
) ;

AddType
(
    ""
,
    typeof(System.String).GetMethod
    (
        "Copy"
    ,
        System.Reflection.BindingFlags.Public
        |
        System.Reflection.BindingFlags.Static
    ,
        null
    ,
        types
    ,
        null
    )
) ;

Note that the default value for strings is an empty string rather than a null.

Contains ( string )

public static bool
Contains
(
    string Type
)
{
    if ( Type == null )
    {
        throw ( new System.ArgumentNullException 
		( "Type" , "The Type must not be null" ) ) ;
    }

    return ( parsers.ContainsKey ( Type ) ) ;
}

Contains ( Type )

public static bool
Contains
(
    System.Type Type
)
{
    if ( Type == null )
    {
        throw ( new System.ArgumentNullException 
		( "Type" , "The Type must not be null" ) ) ;
    }

    return ( parsers.ContainsKey ( Type.FullName ) ) ;
}

Types

public static System.Type[]
Types
{
    get
    {
        return ( (System.Type[]) types.Clone() ) ;
    }
}

Parse ( string , string )

This overload of Parse is called by the others. Note that it works whether the provided parser is a constructor, static method, or instance method (though I don't know why an instance method would be used).

public static object
Parse
(
    string Type
,
    string Value
)
{
    if ( Type == null )
    {
        throw ( new System.ArgumentNullException 
		( "Type" , "The Type must not be null" ) ) ;
    }

    if ( Value == null )
    {
        throw ( new System.ArgumentNullException 
		( "Value" , "The Value must not be null" ) ) ;
    }

    if ( !parsers.ContainsKey ( Type ) )
    {
        throw ( new System.ArgumentException ( "Unsupported type" , "Type" ) ) ;
    }

    try
    {
        object temp = defaults [ Type ] ;

        return ( parsers [ Type ].Invoke ( temp , new object[] { Value } ) ?? temp ) ;
    }
    catch ( System.Exception err )
    {
        throw ( new System.ArgumentException ( "Could not parse" , "Value" , err ) ) ;
    }
}

Parse ( type , string )

public static object
Parse
(
    System.Type Type
,
    string      Value
)
{
    if ( Type == null )
    {
        throw ( new System.ArgumentNullException 
		( "Type" , "The Type must not be null" ) ) ;
    }

    return ( Parse ( Type.FullName , Value ) ) ;
}

TryParse ( string , string , out object )

public static bool
TryParse
(
    string     Type
,
    string     Value
,
    out object Result
)
{
    bool result = false ;

    Result = null ;

    if
    (
        ( Type != null )
    &&
        ( Value != null )
    &&
        ( parsers.ContainsKey ( Type ) )
    )
    {
        try
        {
            Result = Parse ( Type , Value ) ;

            result = true ;
        }
        catch
        {
            Result = defaults [ Type ] ;
        }
    }

    return ( result ) ;
}

TryParse ( Type , string , out object )

public static bool
TryParse
(
    System.Type Type
,
    string      Value
,
    out object  Result
)
{
    bool result = false ;

    Result = null ;

    if
    (
        ( Type != null )
    &&
        ( Value != null )
    &&
        ( parsers.ContainsKey ( Type.FullName ) )
    )
    {
        try
        {
            Result = Parse ( Type.FullName , Value ) ;

            result = true ;
        }
        catch
        {
            Result = defaults [ Type.FullName ] ;
        }
    }

    return ( result ) ;
}

EnumParser<>

EnumParser is an example of how a class may be written to provide a custom parser for another type. In this case, EnumParser.Parse wraps System.Enum.Parse, it is used by AddType ( object ).

NumberParser<>

NumberParser is included as another example of how a class may be written to provide a custom parser for another type. In this case, NumberParser.Parse will parse the standard numeric types and provide hexadecimal support. The class was written merely as a simple demonstration and is not intended for actual use.

Using the Code

The zip file also contains ParsomaticTest.cs which contains a few simple examples of how the Parsomatic may be used:

// The following line sets the current date as the default value for DateTime
PIEBALD.Types.Parsomatic.AddType ( System.DateTime.Today ) ;

// The following line will add a parser for StringSplitOptions
PIEBALD.Types.Parsomatic.AddType ( System.StringSplitOptions.None ) ;

// The following line will use that entry to parse a value
PIEBALD.Types.Parsomatic.Parse ( "System.StringSplitOptions" , "RemoveEmptyEntries" ) ;

// The following line overrides the method to be use 
// for Int32 with the one from NumberParser
PIEBALD.Types.Parsomatic.AddType 
	( 0 , typeof(PIEBALD.Types.Parsomatic.NumberParser<System.Int32>) ) ;

// NumberParser.Parse will handle the hexadecimal value
PIEBALD.Types.Parsomatic.Parse ( "System.Int32" , "0xFF" ) ;

// My Rational class is an example of a class whose parser isn't named Parse
PIEBALD.Types.Parsomatic.AddType
(
    PIEBALD.Types.Rational.Zero
,
    typeof(PIEBALD.Types.Rational).GetMethod
    (
        "ParseInfix"
    ,
        System.Reflection.BindingFlags.Public
        |
        System.Reflection.BindingFlags.Static
    ,
        null
    ,
        PIEBALD.Types.Parsomatic.Types
    ,
        null
    )
) ;

PIEBALD.Types.Parsomatic.Parse ( "PIEBALD.Types.Rational" , "1/3" ) ;

History

  • 2008-12-06 First submitted