Click here to Skip to main content
15,885,278 members
Articles / Programming Languages / C#
Article

Parsomatic

Rate me:
Please Sign up or sign in to vote.
4.00/5 (4 votes)
8 Dec 2008CPOL3 min read 27.2K   97   15   6
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:

XML
<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:

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

But then getting the Parse method requires Reflection:

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

C#
public static class Parsomatic
{
}

Fields

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

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

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

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

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

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

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

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

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

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

C#
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).

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

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

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

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

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

License

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


Written By
Software Developer (Senior)
United States United States
BSCS 1992 Wentworth Institute of Technology

Originally from the Boston (MA) area. Lived in SoCal for a while. Now in the Phoenix (AZ) area.

OpenVMS enthusiast, ISO 8601 evangelist, photographer, opinionated SOB, acknowledged pedant and contrarian

---------------

"I would be looking for better tekkies, too. Yours are broken." -- Paul Pedant

"Using fewer technologies is better than using more." -- Rico Mariani

"Good code is its own best documentation. As you’re about to add a comment, ask yourself, ‘How can I improve the code so that this comment isn’t needed?’" -- Steve McConnell

"Every time you write a comment, you should grimace and feel the failure of your ability of expression." -- Unknown

"If you need help knowing what to think, let me know and I'll tell you." -- Jeffrey Snover [MSFT]

"Typing is no substitute for thinking." -- R.W. Hamming

"I find it appalling that you can become a programmer with less training than it takes to become a plumber." -- Bjarne Stroustrup

ZagNut’s Law: Arrogance is inversely proportional to ability.

"Well blow me sideways with a plastic marionette. I've just learned something new - and if I could award you a 100 for that post I would. Way to go you keyboard lovegod you." -- Pete O'Hanlon

"linq'ish" sounds like "inept" in German -- Andreas Gieriet

"Things would be different if I ran the zoo." -- Dr. Seuss

"Wrong is evil, and it must be defeated." –- Jeff Ello

"A good designer must rely on experience, on precise, logical thinking, and on pedantic exactness." -- Nigel Shaw

“It’s always easier to do it the hard way.” -- Blackhart

“If Unix wasn’t so bad that you can’t give it away, Bill Gates would never have succeeded in selling Windows.” -- Blackhart

"Use vertical and horizontal whitespace generously. Generally, all binary operators except '.' and '->' should be separated from their operands by blanks."

"Omit needless local variables." -- Strunk... had he taught programming

Comments and Discussions

 
GeneralTypeConverter Pin
tonyt8-Dec-08 20:41
tonyt8-Dec-08 20:41 
GeneralRe: TypeConverter Pin
PIEBALDconsult9-Dec-08 4:12
mvePIEBALDconsult9-Dec-08 4:12 
I haven't used them, but wouldn't they be usable with the Parsomatic too?
If they provide a method that takes a string and returns the parsed value you should be able to add it to the Parsomatic.

Otherwise, you'd have to know what type to expect in order to choose the correct converter, yes?
And if you know that, you don't need this.
GeneralRe: TypeConverter Pin
tonyt9-Dec-08 5:10
tonyt9-Dec-08 5:10 
GeneralRe: TypeConverter Pin
PIEBALDconsult9-Dec-08 6:46
mvePIEBALDconsult9-Dec-08 6:46 
GeneralRe: TypeConverter Pin
tonyt9-Dec-08 11:58
tonyt9-Dec-08 11:58 
GeneralRe: TypeConverter Pin
PIEBALDconsult9-Dec-08 17:39
mvePIEBALDconsult9-Dec-08 17:39 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.