简体   繁体   中英

Parse decimal number without loosing significant digits

I need to parse user input as a number and store it in a decimal variable.

It is important for me to not accept any user input that cannot be properly represented by a decimal value.

This works fine for very large (or very small) numbers, since the Parse method throws an OverflowException in those cases.

However, when a number has too many significant digits the Parse method will silently return a truncated (or rounded?) value.

For example, parsing 1.23456789123456789123456789123 (30 significant digits) results in a value equal to 1.2345678912345678912345678912 (29 significant digits).

This is according to the specification that says that a decimal value has a precision of 28-29 significant digits.

However, I need to be able to detect (and reject) numbers that will be truncated when parsed, since loosing significant digits is unacceptable in my case.

What is the best way to go about this?


Please notice, that pre-parsing or post-validation by string comparison is not a simple way to go since I need to support all kinds of culture-specific input and all kinds of number styles (whitespace, thousand separators, parenthesis, exponent syntax, etc).

Therefore, I'm looking for a solution to this without duplicating the parsing code as provided by .NET.


I'm currently using this workaround to detect input with 28 or more significant digits. While this works, it effectively limit all input to at most 27 significant digits (instead of 28-29):

/// <summary>
///     Determines whether the specified value has 28 or more significant digits, 
///     in which case it must be rejected since it may have been truncated when 
///     we parsed it.
/// </summary>
static bool MayHaveBeenTruncated(decimal value)
{
    const string format = "#.###########################e0";
    string str = value.ToString(format, CultureInfo.InvariantCulture);
    return (str.LastIndexOf('e') - str.IndexOf('.')) > 27;
}

Assuming the input is a string and it has been validated as numeric, you can use String.Split:

text = text.Trim().Replace(",", "");
bool neg = text.Contains("-");
if (neg) text = text.Replace("-", "");
while (text.Substring(0, 1) == 0 && text.Substring(0, 2) != "0." && text != "0")
    text = text.Substring(1);
if (text.Contains("."))
{
    while (text.Substring(text.Length - 1) == "0")
        text = text.Substring(0, text.Length - 1);
}
if (text.Split(".")[0].Length + text.Split(".")[1].Length + (neg ? 1 : 0) <= 29)
    valid = true;

You could override or replace Parse and include this check.

The problem is that the rounding is taken care of when you do the conversation ie Decimal myNumber = Decimal.Parse(myInput) will always return in a rounded number if there are more than 28 decimals.

You don't want to create a big parser either so what I would do is compare the input string value with the new decimal value as a string:

//This is the string input from the user
string myInput = "1.23456789123456789123456789123";

//This is the decimal conversation in your application
Decimal myDecimal = Decimal.Parse(myInput);

//This is the check to see if the input string value from the user is the same 
//after we parsed it to a decimal value. Now we need to parse it back to a string to verify
//the two different string values:
if(myInput.CompareTo(myDecimal.ToString()) == 0)
    Console.WriteLine("EQUAL: Have NOT been rounded!");
else
    Console.WriteLine("NOT EQUAL: Have been rounded!");

This way C# will handle all the number stuff and you will only do a quick check.

You should have a look at the BigRational impelmentation. It is not (yet?) part of the .Net framework, but it is the aquivalent to the BigInteger class and provides a TryParse method. This way you should be able to compare if your parsed BigRational is equal to the parsed decimal.

Let me first state that there is no "official" solution. Normally I would not rely on internal implementation, so I'm providing you the following just because you said it's very important to you to get that resolved.

If you take a look at the reference source, you'll see that all parse methods are implemented in a (unfortunately internal) System.Number class. Further investigating, the decimal related methods are TryParseDecimal and ParseDecimal , and they both use something like this

byte* buffer = stackalloc byte[NumberBuffer.NumberBufferBytes];
var number = new NumberBuffer(buffer);
if (TryStringToNumber(s, styles, ref number, numfmt, true))
{
   // other stuff
}                        

where NumberBuffer is another internal struct . The key point is that the whole parsing happens inside the TryStringToNumber method and the result is used to produce the result. What we are interested is a NumberBuffer field called precision which is populated by the above method.

With all that in mind, we can generate a similar method just to extract the precision after calling the base decimal method to ensure a normal validation/exceptions before we do our post processing. So the method would be like this

static unsafe bool GetPrecision(string s, NumberStyles style, NumberFormatInfo numfmt)
{
    byte* buffer = stackalloc byte[Number.NumberBuffer.NumberBufferBytes];
    var number = new NumberBuffer(buffer);
    TryStringToNumber(s, styles, ref number, numfmt, true);
    return number.precision;
}

But remember, those types are internal, as well as their methods, so it's difficult to apply the normal reflection, delegate or Expression based techniques. Fortunately, it's not so hard to write such a method using System.Reflection.Emit . The full implementation is as follows

public static class DecimalUtils
{
    public static decimal ParseExact(string s, NumberStyles style = NumberStyles.Number, IFormatProvider provider = null)
    {
        // NOTE: Always call base method first 
        var value = decimal.Parse(s, style, provider);
        if (!IsValidPrecision(s, style, provider))
            throw new InvalidCastException(); // TODO: throw appropriate exception
        return value;
    }

    public static bool TryParseExact(string s, out decimal result, NumberStyles style = NumberStyles.Number, IFormatProvider provider = null)
    {
        // NOTE: Always call base method first 
        return decimal.TryParse(s, style, provider, out result) && !IsValidPrecision(s, style, provider);
    }

    static bool IsValidPrecision(string s, NumberStyles style, IFormatProvider provider)
    {
        var precision = GetPrecision(s, style, NumberFormatInfo.GetInstance(provider));
        return precision <= 29;
    }

    static readonly Func<string, NumberStyles, NumberFormatInfo, int> GetPrecision = BuildGetPrecisionFunc();
    static Func<string, NumberStyles, NumberFormatInfo, int> BuildGetPrecisionFunc()
    {
        const BindingFlags Flags = BindingFlags.Public | BindingFlags.NonPublic;
        const BindingFlags InstanceFlags = Flags | BindingFlags.Instance;
        const BindingFlags StaticFlags = Flags | BindingFlags.Static;

        var numberType = typeof(decimal).Assembly.GetType("System.Number");
        var numberBufferType = numberType.GetNestedType("NumberBuffer", Flags);

        var method = new DynamicMethod("GetPrecision", typeof(int),
            new[] { typeof(string), typeof(NumberStyles), typeof(NumberFormatInfo) },
            typeof(DecimalUtils), true);

        var body = method.GetILGenerator();
        // byte* buffer = stackalloc byte[Number.NumberBuffer.NumberBufferBytes];
        var buffer = body.DeclareLocal(typeof(byte*));
        body.Emit(OpCodes.Ldsfld, numberBufferType.GetField("NumberBufferBytes", StaticFlags));
        body.Emit(OpCodes.Localloc);
        body.Emit(OpCodes.Stloc, buffer.LocalIndex);
        // var number = new Number.NumberBuffer(buffer);
        var number = body.DeclareLocal(numberBufferType);
        body.Emit(OpCodes.Ldloca_S, number.LocalIndex);
        body.Emit(OpCodes.Ldloc, buffer.LocalIndex);
        body.Emit(OpCodes.Call, numberBufferType.GetConstructor(InstanceFlags, null,
            new[] { typeof(byte*) }, null));
        // Number.TryStringToNumber(value, options, ref number, numfmt, true);
        body.Emit(OpCodes.Ldarg_0);
        body.Emit(OpCodes.Ldarg_1);
        body.Emit(OpCodes.Ldloca_S, number.LocalIndex);
        body.Emit(OpCodes.Ldarg_2);
        body.Emit(OpCodes.Ldc_I4_1);
        body.Emit(OpCodes.Call, numberType.GetMethod("TryStringToNumber", StaticFlags, null,
            new[] { typeof(string), typeof(NumberStyles), numberBufferType.MakeByRefType(), typeof(NumberFormatInfo), typeof(bool) }, null));
        body.Emit(OpCodes.Pop);
        // return number.precision;
        body.Emit(OpCodes.Ldloca_S, number.LocalIndex);
        body.Emit(OpCodes.Ldfld, numberBufferType.GetField("precision", InstanceFlags));
        body.Emit(OpCodes.Ret);

        return (Func<string, NumberStyles, NumberFormatInfo, int>)method.CreateDelegate(typeof(Func<string, NumberStyles, NumberFormatInfo, int>));
    }
}

Use it on your own risk :)

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM