Convert.ChangeType doesn't accept "1E-5" scientific notation for Decimal, how to fix?

Viewed 162

MRE: https://dotnetfiddle.net/M7WeMH

I have this generic utility:

    static T readValue<T>(string value) 
    {
        return (T)Convert.ChangeType(value, typeof(T));
    }

Reading some data from a file that should all be decimal values, I get an exception when the text is in scientific notation e.g. "1E-5" - this occurs rarely in the files. Testing further I find that decimal.Parse has the same problem:

using System;
                    
public class Program
{
    static T readValue<T>(string value) 
    {
        return (T)Convert.ChangeType(value, typeof(T));
    }

    public static void Main()
    {
        try
        {
            Console.WriteLine($"{readValue<decimal>("1E-5")}");
        }
        catch(Exception e)
        {
            Console.WriteLine(e);
        }
        try
        {
            Console.WriteLine($"{decimal.Parse("1E-5")}");
        }
        catch(Exception e)
        {
            Console.WriteLine(e);
        }
    }
}

System.FormatException: Input string was not in a correct format.
at System.Number.ThrowOverflowOrFormatException(ParsingStatus status, TypeCode type) at System.Number.ParseDecimal(ReadOnlySpan`1 value, NumberStyles styles, NumberFormatInfo info) at System.Convert.ToDecimal(String value, IFormatProvider provider) at System.String.System.IConvertible.ToDecimal(IFormatProvider provider) at System.Convert.ChangeType(Object value, Type conversionType, IFormatProvider provider) at System.Convert.ChangeType(Object value, Type conversionType) at Program.readValue[T](String value)
at Program.Main()

System.FormatException: Input string was not in a correct format.
at System.Number.ThrowOverflowOrFormatException(ParsingStatus status, TypeCode type) at System.Number.ParseDecimal(ReadOnlySpan`1 value, NumberStyles styles, NumberFormatInfo info) at System.Decimal.Parse(String s) at Program.Main()

This question explains how to fix it for decimal.Parse (Parse a Number from Exponential Notation) but I this generic method is used for loading all sorts of file data from CSV files in many places... is there an equivalent fix to avoid a lot of code refactoring?

2 Answers

This is ugly as all get out, but here's an example of how specific type behavior could be done:

static T readValue<T>(string value) where T : struct
{
    var type = typeof(T);
    if (type == typeof(decimal))
    {
        // Put this return statement in a block that verifies the content of the string is scientific notation.
        return (T)Convert.ChangeType(decimal.Parse(value, NumberStyles.Float), typeof(T));
    }
    return (T)Convert.ChangeType(value, typeof(T));
}

The reason is because the default style used by Decimal.Parse and Decimal.TryParse is like this

[ws][sign][digits,]digits[.fractional-digits][ws]

which doesn't allow the exponent. If you read Parse's documentation you'll see that the full number format is

[ws][$][sign][digits,]digits[.fractional-digits][e[sign]digits][ws]

where

e: The 'e' or 'E' character, which indicates that the value is represented in exponential notation. The s parameter can represent a number in exponential notation if style includes the NumberStyles.AllowExponent flag.

That means you need to use NumberStyles.Float (which includes NumberStyles.AllowExponent) as above to make it work. Some simple demo:

var style = System.Globalization.NumberStyles.Float;
var culture = CultureInfo.GetCultureInfo("en-US");
string s = "1E-5";
var i = decimal.Parse(s, style);
decimal dec = 0;
decimal.TryParse(s, style, culture, out dec);
Related