Parsomatic





4.00/5 (4 votes)
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 aCopy
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
datatype
s. - I want to allow methods that come from "helper"
datatype
s. - I want to allow the user to add other
datatype
s with their methods and defaults. - I want to allow the user to override existing methods and defaults.
- I want to implement
Parse
andTryParse
fordatatype
s 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 string
s 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