IsNumeric in C#? Not!
Recently we had another "go around" at the office regarding IsNumeric in C#. We had a forum discussion on this at eggheadcafe.com some time ago, I suggested to use the Microsoft.VisualBasic.Information.IsNumeric method, which can certainly be called from C# or any other .NET language.
Two other developers, both of whom I highly respect, have gone quite a bit deeper into this; J. Ambrose Little here and Justin Rogers here. Only Rogers actually made reference to the fact that the VisualBasic method does a significant amount of type-checking under the hood; type-checking that was not included in either of their test suites. The bottom line is this:
If I put "$1,079.51" into Ambrose's VisualBasic method, it returns true, because that string, indeed, "IS" numeric. Other methods tried will either incorrectly return false, or will actually hang and not return at all. According to the Microsoft definition of "Numeric", depending on the CultureInfo, any valid Currency sign or thousands / decimal delimiter in the correct position in a string of numbers
should be able to be handled by the method.
My point is, comparative test suites designed to illustrate performance are only valid when comparing apples to apples. If the test methodology or assumptions are initially flawed, their validity goes down the toilet, IMHO. Developers who aren't aware of all these little nuances may very well read one of these blogs/articles and proceed to create their own "IsNumeric" method in C# that uses char.IsDigit and other techniques for speed. And when they ship a French version of their app to a client, it may very well blow up. So, the question is, if you are a C# purist, you could spend a very long time writing your own method and never calling into the Microsoft.VisualBasic.dll assembly. But, since you would have to do all the very same things done in the decompiled code below in order for it to be "right", it might end up not being any faster at all!
ISNUMERIC - decompiled to C# (Not all methods used are shown):
public static bool IsNumeric(object Expression)
{
bool flag1;
IConvertible convertible1 = null;
if (Expression is IConvertible)
{
convertible1 = (IConvertible) Expression;
}
if (convertible1 == null)
{
if (Expression is char[])
{
Expression = new string((char[]) Expression);
}
else
{
return false;
}
}
TypeCode code1 = convertible1.GetTypeCode();
if ((code1 != TypeCode.String) && (code1 != TypeCode.Char))
{
return Utils.IsNumericTypeCode(code1);
}
string text1 = convertible1.ToString(null);
try
{
long num2;
if (!StringType.IsHexOrOctValue(text1, ref num2))
{
double num1;
return DoubleType.TryParse(text1, ref num1);
}
flag1 = true;
}
catch (Exception)
{
flag1 = false;
}
return flag1;
}
internal static bool IsNumericTypeCode(TypeCode TypCode)
{
switch (TypCode)
{
case TypeCode.Boolean:
case TypeCode.Byte:
case TypeCode.Int16:
case TypeCode.Int32:
case TypeCode.Int64:
case TypeCode.Single:
case TypeCode.Double:
case TypeCode.Decimal:
{
return true;
}
}
return false;
}
internal static bool TryParse(string Value, ref double Result)
{
bool flag1;
CultureInfo info1 = Utils.GetCultureInfo();
NumberFormatInfo info3 = info1.NumberFormat;
NumberFormatInfo info2 = DecimalType.GetNormalizedNumberFormat(info3);
Value = StringType.ToHalfwidthNumbers(Value, info1);
if (info3 == info2)
{
return double.TryParse(Value, NumberStyles.Any, info2, out Result);
}
try
{
Result = double.Parse(Value, NumberStyles.Any, info2);
flag1 = true;
}
catch (FormatException)
{
flag1 = double.TryParse(Value, NumberStyles.Any, info3, out Result);
}
catch (Exception)
{
flag1 = false;
}
return flag1;
}
internal static CultureInfo GetCultureInfo()
{
return Thread.CurrentThread.CurrentCulture;
}
internal static NumberFormatInfo GetNormalizedNumberFormat(NumberFormatInfo InNumberFormat)
{
NumberFormatInfo info2;
NumberFormatInfo info5 = InNumberFormat;
if (((((info5.CurrencyDecimalSeparator != null) && (info5.NumberDecimalSeparator != null)) && ((info5.CurrencyGroupSeparator != null) && (info5.NumberGroupSeparator != null))) && (((info5.CurrencyDecimalSeparator.Length == 1) && (info5.NumberDecimalSeparator.Length == 1)) && ((info5.CurrencyGroupSeparator.Length == 1) && (info5.NumberGroupSeparator.Length == 1)))) && (((info5.CurrencyDecimalSeparator[0] == info5.NumberDecimalSeparator[0]) && (info5.CurrencyGroupSeparator[0] == info5.NumberGroupSeparator[0])) && (info5.CurrencyDecimalDigits == info5.NumberDecimalDigits)))
{
return InNumberFormat;
}
info5 = null;
NumberFormatInfo info4 = InNumberFormat;
if ((((info4.CurrencyDecimalSeparator != null) && (info4.NumberDecimalSeparator != null)) && ((info4.CurrencyDecimalSeparator.Length == info4.NumberDecimalSeparator.Length) && (info4.CurrencyGroupSeparator != null))) && ((info4.NumberGroupSeparator != null) && (info4.CurrencyGroupSeparator.Length == info4.NumberGroupSeparator.Length)))
{
int num3 = info4.CurrencyDecimalSeparator.Length - 1;
int num1 = 0;
while (num1 <= num3)
{
if (info4.CurrencyDecimalSeparator[num1] != info4.NumberDecimalSeparator[num1])
{
goto Label_019D;
}
num1++;
}
int num2 = info4.CurrencyGroupSeparator.Length - 1;
for (num1 = 0; num1 <= num2; num1++)
{
if (info4.CurrencyGroupSeparator[num1] != info4.NumberGroupSeparator[num1])
{
goto Label_019D;
}
}
return InNumberFormat;
}
info4 = null;
Label_019D:
info2 = (NumberFormatInfo) InNumberFormat.Clone();
NumberFormatInfo info3 = info2;
info3.CurrencyDecimalSeparator = info3.NumberDecimalSeparator;
info3.CurrencyGroupSeparator = info3.NumberGroupSeparator;
info3.CurrencyDecimalDigits = info3.NumberDecimalDigits;
info3 = null;
return info2;
}
Two other developers, both of whom I highly respect, have gone quite a bit deeper into this; J. Ambrose Little here and Justin Rogers here. Only Rogers actually made reference to the fact that the VisualBasic method does a significant amount of type-checking under the hood; type-checking that was not included in either of their test suites. The bottom line is this:
If I put "$1,079.51" into Ambrose's VisualBasic method, it returns true, because that string, indeed, "IS" numeric. Other methods tried will either incorrectly return false, or will actually hang and not return at all. According to the Microsoft definition of "Numeric", depending on the CultureInfo, any valid Currency sign or thousands / decimal delimiter in the correct position in a string of numbers
should be able to be handled by the method.
My point is, comparative test suites designed to illustrate performance are only valid when comparing apples to apples. If the test methodology or assumptions are initially flawed, their validity goes down the toilet, IMHO. Developers who aren't aware of all these little nuances may very well read one of these blogs/articles and proceed to create their own "IsNumeric" method in C# that uses char.IsDigit and other techniques for speed. And when they ship a French version of their app to a client, it may very well blow up. So, the question is, if you are a C# purist, you could spend a very long time writing your own method and never calling into the Microsoft.VisualBasic.dll assembly. But, since you would have to do all the very same things done in the decompiled code below in order for it to be "right", it might end up not being any faster at all!
ISNUMERIC - decompiled to C# (Not all methods used are shown):
public static bool IsNumeric(object Expression)
{
bool flag1;
IConvertible convertible1 = null;
if (Expression is IConvertible)
{
convertible1 = (IConvertible) Expression;
}
if (convertible1 == null)
{
if (Expression is char[])
{
Expression = new string((char[]) Expression);
}
else
{
return false;
}
}
TypeCode code1 = convertible1.GetTypeCode();
if ((code1 != TypeCode.String) && (code1 != TypeCode.Char))
{
return Utils.IsNumericTypeCode(code1);
}
string text1 = convertible1.ToString(null);
try
{
long num2;
if (!StringType.IsHexOrOctValue(text1, ref num2))
{
double num1;
return DoubleType.TryParse(text1, ref num1);
}
flag1 = true;
}
catch (Exception)
{
flag1 = false;
}
return flag1;
}
internal static bool IsNumericTypeCode(TypeCode TypCode)
{
switch (TypCode)
{
case TypeCode.Boolean:
case TypeCode.Byte:
case TypeCode.Int16:
case TypeCode.Int32:
case TypeCode.Int64:
case TypeCode.Single:
case TypeCode.Double:
case TypeCode.Decimal:
{
return true;
}
}
return false;
}
internal static bool TryParse(string Value, ref double Result)
{
bool flag1;
CultureInfo info1 = Utils.GetCultureInfo();
NumberFormatInfo info3 = info1.NumberFormat;
NumberFormatInfo info2 = DecimalType.GetNormalizedNumberFormat(info3);
Value = StringType.ToHalfwidthNumbers(Value, info1);
if (info3 == info2)
{
return double.TryParse(Value, NumberStyles.Any, info2, out Result);
}
try
{
Result = double.Parse(Value, NumberStyles.Any, info2);
flag1 = true;
}
catch (FormatException)
{
flag1 = double.TryParse(Value, NumberStyles.Any, info3, out Result);
}
catch (Exception)
{
flag1 = false;
}
return flag1;
}
internal static CultureInfo GetCultureInfo()
{
return Thread.CurrentThread.CurrentCulture;
}
internal static NumberFormatInfo GetNormalizedNumberFormat(NumberFormatInfo InNumberFormat)
{
NumberFormatInfo info2;
NumberFormatInfo info5 = InNumberFormat;
if (((((info5.CurrencyDecimalSeparator != null) && (info5.NumberDecimalSeparator != null)) && ((info5.CurrencyGroupSeparator != null) && (info5.NumberGroupSeparator != null))) && (((info5.CurrencyDecimalSeparator.Length == 1) && (info5.NumberDecimalSeparator.Length == 1)) && ((info5.CurrencyGroupSeparator.Length == 1) && (info5.NumberGroupSeparator.Length == 1)))) && (((info5.CurrencyDecimalSeparator[0] == info5.NumberDecimalSeparator[0]) && (info5.CurrencyGroupSeparator[0] == info5.NumberGroupSeparator[0])) && (info5.CurrencyDecimalDigits == info5.NumberDecimalDigits)))
{
return InNumberFormat;
}
info5 = null;
NumberFormatInfo info4 = InNumberFormat;
if ((((info4.CurrencyDecimalSeparator != null) && (info4.NumberDecimalSeparator != null)) && ((info4.CurrencyDecimalSeparator.Length == info4.NumberDecimalSeparator.Length) && (info4.CurrencyGroupSeparator != null))) && ((info4.NumberGroupSeparator != null) && (info4.CurrencyGroupSeparator.Length == info4.NumberGroupSeparator.Length)))
{
int num3 = info4.CurrencyDecimalSeparator.Length - 1;
int num1 = 0;
while (num1 <= num3)
{
if (info4.CurrencyDecimalSeparator[num1] != info4.NumberDecimalSeparator[num1])
{
goto Label_019D;
}
num1++;
}
int num2 = info4.CurrencyGroupSeparator.Length - 1;
for (num1 = 0; num1 <= num2; num1++)
{
if (info4.CurrencyGroupSeparator[num1] != info4.NumberGroupSeparator[num1])
{
goto Label_019D;
}
}
return InNumberFormat;
}
info4 = null;
Label_019D:
info2 = (NumberFormatInfo) InNumberFormat.Clone();
NumberFormatInfo info3 = info2;
info3.CurrencyDecimalSeparator = info3.NumberDecimalSeparator;
info3.CurrencyGroupSeparator = info3.NumberGroupSeparator;
info3.CurrencyDecimalDigits = info3.NumberDecimalDigits;
info3 = null;
return info2;
}
I tackled this one a couple of years ago. I tested out several different methods of handling this including importing the VisualBasic namespace. It provides the most complete solution but the drawback was more than I could bear and being a C# aficionado meant I couldn't do that in good conscience.
ReplyDeleteI found that the vb method was in the middle of the pack for performance. The slowest was Convert.Int32 since it was catching exceptions and my tests were for invalid numbers.
One you haven't listed or talked about is using Regular Expressions and I found those to be as good as the VB import. The fastest method was manually checking each character in the string for IsDigit and ignoring the formatting such as "$" and ".".