views:

263

answers:

4

I am working on a control that can take a number of different datatypes (anything that implements IComparable).

I need to be able to compare these with another variable passed in.

If the main datatype is a DateTime, and I am passed a String, I need to

  • attempt to convert the String to a DateTime to perform a Date comparison.
  • if the String cannot be converted to a DateTime then do a String comparison.

So I need a general way to attempt to convert from any type to any type. Easy enough, .Net provides us with the TypeConverter class.

Now, the best I can work out to do to determine if the String can be converted to a DateTime is to use exceptions. If the ConvertFrom raises an exception, I know I cant do the conversion and have to do the string comparison.

The following is the best I got :

        string theString = "99/12/2009";
        DateTime theDate = new DateTime ( 2009, 11, 1 );

        IComparable obj1 = theString as IComparable;
        IComparable obj2 = theDate as IComparable;

        try
        {
            TypeConverter converter = TypeDescriptor.GetConverter ( obj2.GetType () );
            if ( converter.CanConvertFrom ( obj1.GetType () ) )
            {
                Console.WriteLine ( obj2.CompareTo ( converter.ConvertFrom ( obj1 ) ) );
                Console.WriteLine ( "Date comparison" );
            }
        }
        catch ( FormatException )
        {
            Console.WriteLine ( obj1.ToString ().CompareTo ( obj2.ToString () ) );
            Console.WriteLine ( "String comparison" );
        }

Part of our standards at work state that :

Exceptions should only be raised when an Exception situation - ie. an error is encountered.

But this is not an exceptional situation. I need another way around it.

Most variable types have a TryParse method which returns a boolean to allow you to determine if the conversion has succeeded or not. But there is no TryConvert method available to TypeConverter. CanConvertFrom only dermines if it is possible to convert between these types and doesnt consider the actual data to be converted. The IsValid method is also useless.

Any ideas?

EDIT

I cannot use AS and IS. I do not know either data types at compile time. So I dont know what to As and Is to!!!

EDIT

Ok nailed the bastard. Its not as tidy as Marc Gravells, but it works (I hope). Thanks for the inpiration Marc. Will work on tidying it up when I get the time, but I've got a bit stack of bugfixes that I have to get on with.

    public static class CleanConverter
    {
        /// <summary>
        /// Stores the cache of all types that can be converted to all types.
        /// </summary>
        private static Dictionary<Type, Dictionary<Type, ConversionCache>> _Types = new Dictionary<Type, Dictionary<Type, ConversionCache>> ();

        /// <summary>
        /// Try parsing.
        /// </summary>
        /// <param name="s"></param>
        /// <param name="value"></param>
        /// <returns></returns>
        public static bool TryParse ( IComparable s, ref IComparable value )
        {
            // First get the cached conversion method.
            Dictionary<Type, ConversionCache> type1Cache = null;
            ConversionCache type2Cache = null;

            if ( !_Types.ContainsKey ( s.GetType () ) )
            {
                type1Cache = new Dictionary<Type, ConversionCache> ();

                _Types.Add ( s.GetType (), type1Cache );
            }
            else
            {
                type1Cache = _Types[s.GetType ()];
            }

            if ( !type1Cache.ContainsKey ( value.GetType () ) )
            {
                // We havent converted this type before, so create a new conversion
                type2Cache = new ConversionCache ( s.GetType (), value.GetType () );

                // Add to the cache
                type1Cache.Add ( value.GetType (), type2Cache );
            }
            else
            {
                type2Cache = type1Cache[value.GetType ()];
            }

            // Attempt the parse
            return type2Cache.TryParse ( s, ref value );
        }

        /// <summary>
        /// Stores the method to convert from Type1 to Type2
        /// </summary>
        internal class ConversionCache
        {
            internal bool TryParse ( IComparable s, ref IComparable value )
            {
                if ( this._Method != null )
                {
                    // Invoke the cached TryParse method.
                    object[] parameters = new object[] { s, value };
                    bool result = (bool)this._Method.Invoke ( null,  parameters);

                    if ( result )
                        value = parameters[1] as IComparable;

                    return result;
                }
                else
                    return false;

            }

            private MethodInfo _Method;
            internal ConversionCache ( Type type1, Type type2 )
            {
                // Use reflection to get the TryParse method from it.
                this._Method = type2.GetMethod ( "TryParse", new Type[] { type1, type2.MakeByRefType () } );
            }
        }
    }
A: 

So I need a general way to attempt to convert from any type to any type. Easy enough, .Net provides us with the TypeConverter class.

You're asking too much.

class Animal { }
class Dog : Animal { }
class Cat : Animal { }

Should I be able to convert a Cat to a Dog?

You'll find your problem is far easier to solve if you specify more precisely (preferably exactly) what you want the behavior of the method to be. So, write down the expected inputs and what you want the output to be in each possible case. Then your method should write itself.

So right now we have this specification:

If the main datatype is a DateTime, and I am passed a String, I need to

attempt to convert the String to a DateTime to perform a Date comparison. if the String cannot be converted to a DateTime then do a String comparison.

int CompareTo(DateTime d, object o) {
    string s = o as string;
    if(s != null) {
        DateTime dt;
        if(dt.TryParse(s, out dt)) {
            return d.CompareTo(dt);
        }
        else {
            return d.ToString().CompareTo(s);
        }
    }
    throw new InvalidOperationException();
}
Jason
I want a bool TypeConverter.TryConvert( object inObject, out object ConvertedObjected) method. Kinda like DateTime.TryParse.I want to do exactly what the code sample I posted does, but without the Exception being raised.Im not really doing objects here, just basic variable types : ints, strings, DateTimes..I thought my question was fairly clear.
Mongus Pong
@Pongus: It's not clear. What are type are you trying to convert `inObject` to? Are you trying to convert to the type of `ConvertedObjected` (sic)?
Jason
@Jason my apologies. It is a grid control. I need to sort and filter on any data type that is thrown at me as long as it is IComparable.
Mongus Pong
+2  A: 

I would argue that this code really should throw exceptions when it can't figure out a conversion. If the two arguments passed in are DateTime.Now and Color.Fuschsia, you can make no meaningful comparison between them, so any value you return would be wrong. That's the definition of the right time to throw an exception.

If you absolutely need to avoid exceptions, it's not possible to do what you want with arbitrary types. Every type has its own rules about which values it can parse, and the converter has no way to tell that in advance. (That is to say, as you've noticed, it knows that you can sometimes convert a string to a DateTime, but it is not designed to know that "1/1/2010" is a valid DateTime while "Fred" is not.)

Auraseer
In this circumstance it is perfectly valid for the inputs to be a DateTime and Color. Obviously as their raw datatypes they cannot be compared. That is why I want to check to see if they can be converted so that a useful comparison can be made. I just want to check, I dont want this to be an error condition.
Mongus Pong
So what you want is just a method to check whether a conversion would succeed, without actually doing the conversion? This is not possible with arbitrary types. `TypeConverter` is designed to attempt the conversion and throw exceptions on failure. You could check for common types that you know how to handle, like `DateTime` and `Double` and such, and call their `TryParse` methods. But that only works for the types you check.
Auraseer
+3  A: 

If it is not possible to write it without exceptions, you can isolate the problematic code by refactoring it into a method like so:

public static bool TryConvert<T, U>(T t, out U u)
{
    try
    {
        TypeConverter converter = TypeDescriptor.GetConverter(typeof(U));
        if (!converter.CanConvertFrom(typeof(T)))
        {
            u = default(U);
            return false;
        }
        u = (U)converter.ConvertFrom(t);
        return true;
    }
    catch (Exception e)
    {
        if (e.InnerException is FormatException)
        {
            u = default(U);
            return false;
        }

        throw;
    }
}

Ideally you should be passing in nullable types as the output parameter, so that null represents an undefined value (because it couldn't do a conversion) rather than the default (i.e. 0 for int)

ICR
+1  A: 

Are generics an option? Here's a cheeky hack that hunts the TryParse method and calls it via a (cached) delegate:

using System;
using System.Reflection;

static class Program
{
    static void Main()
    {
        int i; float f; decimal d;
        if (Test.TryParse("123", out i)) {
            Console.WriteLine(i);
        }
        if (Test.TryParse("123.45", out f)) {
            Console.WriteLine(f);
        }
        if (Test.TryParse("123.4567", out d)) {
            Console.WriteLine(d);
        }
    }
}
public static class Test
{
    public static bool TryParse<T>(string s, out T value) {
        return Cache<T>.TryParse(s, out value);
    }
    internal static class Cache<T> {
        public static bool TryParse(string s, out T value)
        {
            return func(s, out value);
        }    
        delegate bool TryPattern(string s, out T value);
        private static readonly TryPattern func;
        static Cache()
        {
            MethodInfo method = typeof(T).GetMethod(
                "TryParse", new Type[] { typeof(string), typeof(T).MakeByRefType() });
            if (method == null) {
                func = delegate(string x, out T y) { y = default(T); return false; };
            } else {
                func = (TryPattern) Delegate.CreateDelegate(typeof(TryPattern),method);
            }            
        }
    }
}
Marc Gravell
Yeah, wow that looks just the ticket! Thanks. Will just have to wait until Monday to try it out at work. Like it!
Mongus Pong
Bummer denied.. The solution looked good, but I am only being passed the data as objects. I dont know the types at compile time. I realise I haven't formulated my question very well, hence the confusion. This is close though.. very very close, I can still have a go at using the reflection part of it Im sure.
Mongus Pong
Woohoo! Have twisted it about a bit to come up with the solution. See the edit in my post. Many thanks!
Mongus Pong