6
\$\begingroup\$

This class has the responsibility of parsing a string to one of the primitive types: int, double, float, decimal...

While creating it, I have noticed that it'll be terribly ugly.

It basically should perform a TryParse of each primitive type and return the first that returns true, along with the parsed value.

public static class PrimitiveParser
{
    public static bool TryParse(Type targetType, string sourceValue, out object result)
    {
        result = null;

        if (targetType == typeof(int))
        {
            int intResult;
            var parseResult = int.TryParse(sourceValue, out intResult);
            {
                result = intResult;
                return parseResult;
            }
        }
        if (targetType == typeof(double))
        {
            double doubleResult;
            var parseResult = double.TryParse(sourceValue, out doubleResult);
            {
                result = doubleResult;
                return parseResult;
            }
        }
        if (targetType == typeof(float))
        {
            float floatResult;
            var parseResult = float.TryParse(sourceValue, out floatResult);
            {
                result = floatResult;
                return parseResult;
            }
        }

        /// ...

        return false;
    }
}
\$\endgroup\$
  • 3
    \$\begingroup\$ As I am reading this it does not perform a TryParse of each primitive type and return the first that returns true. \$\endgroup\$ – paparazzo Nov 30 '16 at 21:50
11
\$\begingroup\$

I'd prefer to use a dictionary for this.

private delegate bool TryParseDelegate<T>(string text, out T value);

private static readonly Dictionary<Type, object> TryParsers = new Dictionary<Type, object>
{
    { typeof(int), (TryParseDelegate<int>)int.TryParse },
    { typeof(float), (TryParseDelegate<float>)float.TryParse },
    { typeof(double), (TryParseDelegate<double>)double.TryParse }
};

The TryParse method:

public static bool TryParse<T>(string value, out T returnValue)
{
    // Check if we have a parse method for T in the dictionary.
    object parserObj;
    if (TryParsers.TryGetValue(typeof(T), out parserObj))
    {
        TryParseDelegate<T> parser = (TryParseDelegate<T>)parserObj;
        return parser(value, out returnValue);
    }
    // If no, fallback to IConvertible.
    if (typeof(IConvertible).IsAssignableFrom(typeof(T)))
    {
        try
        {
            returnValue = (T)Convert.ChangeType(value, typeof(T));
            return true;
        }
        catch { }
    }
    // No luck.
    returnValue = default(T);
    return false;
}

This approach support exensibility.
For example, imagine we have a class A:

public class A
{
    public static readonly A Zero = new A("Zero");
    public static readonly A One = new A("One");

    public readonly string Text;

    private A(string text)
    {
        Text = text;
    }

    public static bool TryParse(string text, out A value)
    {
        switch (text.ToLower())
        {
            case "zero":
                value = Zero;
                break;
            case "one":
                value = One;
                break;
            default:
                value = null;
                return false;
        }
        return true;
    }

    public override string ToString()
    {
        return Text;
    }
}

Then the only thing you have to do is to append an item to the dictionary:

{ typeof(A), (TryParseDelegate<A>)A.TryParse }

Let's test it:

int i;
double d;
A a;

if (TryParse("123", out i))
    Console.WriteLine($"i={i}"); // Prints i=123
if (TryParse("123.456", out d))
    Console.WriteLine($"d={d}"); // Prints d=123.456
if (TryParse("ONE", out a))
    Console.WriteLine($"a={a}"); // Prints a=One
\$\endgroup\$
10
\$\begingroup\$

You may be over thinking this, all the types have a common IConvertible implementation and you can use Convert.ChangeType to parse from a string.

public static bool TryParse<T>(string value, out T returnValue)
{
    returnValue = default(T);
    try
    {
        returnValue = (T)Convert.ChangeType(value, typeof(T));
        return true;
    }
    catch { }

    return false;
}

Which you can call like this:

float output = 0.0f;
var val = TryParse("19.2", out output);
//val == true, output = 19.2
val = TryParse("puppies", out output);
//val == false, output = 0.0f (default(T))

As an added bonus, this works with any type that supports IConvertible. See Convert.ChangeType for more information.

The only thing I don't like about this approach is that it uses a try/catch to determine if the conversion is possible or not. This is very performant when the types are convertible but does suffer a performance penalty for the exception when the type is not convertible.

\$\endgroup\$
  • 1
    \$\begingroup\$ The interface definitely lacks a CanConvert method. \$\endgroup\$ – t3chb0t Nov 30 '16 at 16:54
  • 1
    \$\begingroup\$ @t3chb0t I agree, although the designers probably recognized that CanConvert would have to do pretty much the same as above (attempt the conversion and fail) since it wouldn't be any less time-complex. I think a nicer addition would be the TryConvert which returns true/false and the value like all the TryParse methods on the primitive types. It may be possible to use dynamic in this situation and just call TryParse directly if the type is primitive (and not a pointer), but that may cost extra and I'm not sure how you'd call that without reflection (I don't think you can (dynamic)T. \$\endgroup\$ – Ron Beyer Nov 30 '16 at 16:57

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.