Parse a decimal number without losing significant digits.

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

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

This works great for very large (or very small) numbers, as the Parse method throws an OverflowException in these 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 of 1.2345678912345678912345678912(29 significant digits).

This complies with the specification that decimal has an accuracy of 28-29 significant digits.

However, I need to be able to detect (and reject) numbers that will be truncated in the analysis, since the loss of significant digits is unacceptable in my case.

What is the best way to do this?


Note that pre-parsing or post-validating by comparing strings is not an easy way, since I need to support all kinds of culture-specific input and all kinds of numeric styles (spaces, thousands of delimiters, brackets, exponent syntax, etc.) .d.).

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


28 . , 27 ( 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;
}
+4
4

, . , , , , .

, , ( , ) System.Number . , decimal TryParseDecimal ParseDecimal, -

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

NumberBuffer - struct. , TryStringToNumber, . NumberBuffer precision, .

, / , -. , :

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;
}

, , , , Expression. , , System.Reflection.Emit. :

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>));
    }
}

:)

+1

, , 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;

Parse .

+1

, , .. Decimal myNumber = Decimal.Parse(myInput) , 28 .

, , :

//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!");

, # , .

+1

BigRational. (?) .Net framework, BigInteger TryParse. , , BigRational .

+1
source

Source: https://habr.com/ru/post/1621906/


All Articles