Introduction
This is probably pretty obscure--what if you want to compare two objects and you don't know their types? What if one of those types might be an enum? What if one of those types might be a string containing a number, and you want to compare it to another number? What if both objects might be numbers and you want to compare them at the appropriate precision? Well, if you ever have to do something so strange, this little class is for you.
Why do this? Well, it was pretty much just a silly exercise, but IComparable
only works if:
- implemented by the "compare with" object.
- the "compare to" object is of the same type.
For example:
int a=5;
a.CompareTo(6.2);
throws an exception! What I've done is made a smarter (and slower!) object comparer, one that attempts to optimally match types before making the comparison.
How It Works
The best way to describe how it works is with a flowchart. But before going into that, I want to point out a few things. Comparing strings is the least desirable comparison, so the program strives to convert strings to either a numeric value type or, at worst case, the type of the object being compared to. This is true even when comparing two strings--if they are both numeric values, the algorithm will convert them to a type double. A nifty little regex expression:
[+-\.,]?\d*[\.,]?\d*e?[+-\.,]?\d+[\.,]?\d*
is used to tell if the string is a numeric value. I wrote this expression, so it may not be the best in town.
If the comparer isn't dealing with strings, but they're both value types, then it attempts to convert one of the objects to the same type as the other object, whichever has the larger range. So, when an int
object is compared to a double
object, the int
is converted to a double
, and thus precision is not lost.
This also works with enum
s. Unfortunately, the ConvertTo
and ConvertFrom
methods of the TypeConverter
class don't automatically handle conversion of enum
s, so I have to explicitly use the Convert.ToDouble()
method (again, converting the enum
to a double
).
The value type conversion is done based on what I determined to be the precedence of the different value types. If a value type is unsigned while the other value type is not, then the unsigned type is converted to a decimal type and the normal precedence determination is made, so that comparing an unsigned byte of 255 to a signed byte of 127 will return the result that 255 > 127.
The precedence that I've established is:
protected ArrayList precedence=new ArrayList(new string[]
{
"System.Boolean",
"System.Byte",
"System.Sbyte",
"System.Char",
"System.UInt16",
"System.Int16",
"System.UInt32",
"System.Int32",
"System.UInt64",
"System.Int64",
"System.Decimal",
"System.Float",
"System.Double",
}
);
Booleans are also a bane, as the TypeConverter
class will not successfully convert a boolean to an integer type using Convert.To
, nor will an integer type successfully convert from a boolean, using ConvertFrom
. More special case code!
Here's the flowchart:
Probably the most interesting code is the precedence code and the conversion of one object to another.
Precedence Algorithm
protected bool MatchTypesByPrecision(object a, object b, Type typea,
Type typeb, out object a2, out object b2)
{
a2=a;
b2=b;
bool ret=false;
if (a is Enum)
{
a=Convert.ToDouble(a);
typea=typeof(System.Double);
}
if (b is Enum)
{
b=Convert.ToDouble(b);
typeb=typeof(System.Double);
}
int uidxa=unsignedTypes.IndexOf(typea.FullName);
int uidxb=unsignedTypes.IndexOf(typeb.FullName);
if ( (uidxa != -1) && (uidxb == -1) )
{
a=Convert.ToDecimal(a);
typea=typeof(System.Decimal);
}
else if ( (uidxb != -1) && (uidxa == -1) )
{
b=Convert.ToDecimal(b);
typeb=typeof(System.Decimal);
}
int idxa=precedence.IndexOf(typea.FullName);
int idxb=precedence.IndexOf(typeb.FullName);
if (idxa < idxb)
{
ret=MatchTypesToB(a, b, typea, typeb, out a2, out b2);
}
else if (idxa > idxb)
{
ret=MatchTypesToA(a, b, typea, typeb, out a2, out b2);
}
else
{
a2=a;
b2=b;
ret=true;
}
return ret;
}
Converting One Object To Another
protected bool MatchTypesToA(object a, object b, Type typea,
Type typeb, out object a2, out object b2)
{
bool ret=false;
a2=a;
b2=b;
TypeConverter tcb=TypeDescriptor.GetConverter(typeb);
if (tcb.CanConvertTo(typea))
{
b2=tcb.ConvertTo(b, typea);
ret=true;
}
else
{
TypeConverter tca=TypeDescriptor.GetConverter(typea);
if (tca.CanConvertFrom(typeb))
{
b2=tca.ConvertFrom(b);
ret=true;
}
}
return ret;
}
Note that both ConvertTo
and ConvertFrom
attempts are made, as sometimes the source object will not implement a ConvertTo
for the target object, but the target object will implement a ConvertFrom
for the source object. There is a complementary "MatchTypesToB
" method, which looks very similar.
Unit Testing
I've written 30 unit tests (using the AUT engine, download from here) trying out various comparisons, but this is by no means exhaustive. If you find a problem, let me know!
Conclusion
What can I say? This was a fun exercise, but even I don't have a use for this right now! Do you?