Calculate System.Decimal Precision and Scale

拟墨画扇 提交于 2019-12-17 08:31:34

问题


Suppose that we have a System.Decimal number.

For illustration, let's take one whose ToString() representation is as follows:

d.ToString() = "123.4500"

The following can be said about this Decimal. For our purposes here, scale is defined as the number of digits to the right of the decimal point. Effective scale is similar but ignores any trailing zeros that occur in the fractional part. (In other words, these parameters are defined like SQL decimals plus some additional parameters to account for the System.Decimal concept of trailing zeros in the fractional part.)

  • Precision: 7
  • Scale: 4
  • EffectivePrecision: 5
  • EffectiveScale: 2

Given an arbitrary System.Decimal, how can I compute all four of these parameters efficiently and without converting to a String and examining the String? The solution probably requires Decimal.GetBits.

Some more examples:

Examples Precision  Scale  EffectivePrecision  EffectiveScale
0        1 (?)      0      1 (?)               0
0.0      2 (?)      1      1 (?)               0
12.45    4          2      4                   2
12.4500  6          4      4                   2
770      3          0      3                   0

(?) Alternatively interpreting these precisions as zero would be fine.


回答1:


Yes, you'd need to use Decimal.GetBits. Unfortunately, you then have to work with a 96-bit integer, and there are no simple integer type in .NET which copes with 96 bits. On the other hand, it's possible that you could use Decimal itself...

Here's some code which produces the same numbers as your examples. Hope you find it useful :)

using System;

public class Test
{
    static public void Main(string[] x)
    {
        ShowInfo(123.4500m);
        ShowInfo(0m);
        ShowInfo(0.0m);
        ShowInfo(12.45m);
        ShowInfo(12.4500m);
        ShowInfo(770m);
    }

    static void ShowInfo(decimal dec)
    {
        // We want the integer parts as uint
        // C# doesn't permit int[] to uint[] conversion,
        // but .NET does. This is somewhat evil...
        uint[] bits = (uint[])(object)decimal.GetBits(dec);


        decimal mantissa = 
            (bits[2] * 4294967296m * 4294967296m) +
            (bits[1] * 4294967296m) +
            bits[0];

        uint scale = (bits[3] >> 16) & 31;

        // Precision: number of times we can divide
        // by 10 before we get to 0        
        uint precision = 0;
        if (dec != 0m)
        {
            for (decimal tmp = mantissa; tmp >= 1; tmp /= 10)
            {
                precision++;
            }
        }
        else
        {
            // Handle zero differently. It's odd.
            precision = scale + 1;
        }

        uint trailingZeros = 0;
        for (decimal tmp = mantissa;
             tmp % 10m == 0 && trailingZeros < scale;
             tmp /= 10)
        {
            trailingZeros++;
        }

        Console.WriteLine("Example: {0}", dec);
        Console.WriteLine("Precision: {0}", precision);
        Console.WriteLine("Scale: {0}", scale);
        Console.WriteLine("EffectivePrecision: {0}",
                          precision - trailingZeros);
        Console.WriteLine("EffectiveScale: {0}", scale - trailingZeros);
        Console.WriteLine();
    }
}



回答2:


I came across this article when I needed to validate precision and scale before writing a decimal value to a database. I had actually come up with a different way to achieve this using System.Data.SqlTypes.SqlDecimal which turned out to be faster that the other two methods discussed here.

 static DecimalInfo SQLInfo(decimal dec)

     {

         System.Data.SqlTypes.SqlDecimal x;
         x = new  System.Data.SqlTypes.SqlDecimal(dec);                     
         return new DecimalInfo((int)x.Precision, (int)x.Scale, (int)0);
     }



回答3:


Using ToString is about 10x faster than Jon Skeet's solution. While this is reasonably fast, the challenge here (if there are any takers!) is to beat the performance of ToString.

The performance results I get from the following test program are: ShowInfo 239 ms FastInfo 25 ms

using System;
using System.Diagnostics;
using System.Globalization;

public class Test
{
    static public void Main(string[] x)
    {
        Stopwatch sw1 = new Stopwatch();
        Stopwatch sw2 = new Stopwatch();

        sw1.Start();
        for (int i = 0; i < 10000; i++)
        {
            ShowInfo(123.4500m);
            ShowInfo(0m);
            ShowInfo(0.0m);
            ShowInfo(12.45m);
            ShowInfo(12.4500m);
            ShowInfo(770m);
        }
        sw1.Stop();

        sw2.Start();
        for (int i = 0; i < 10000; i++)
        {
            FastInfo(123.4500m);
            FastInfo(0m);
            FastInfo(0.0m);
            FastInfo(12.45m);
            FastInfo(12.4500m);
            FastInfo(770m);
        }
        sw2.Stop();

        Console.WriteLine(sw1.ElapsedMilliseconds);
        Console.WriteLine(sw2.ElapsedMilliseconds);
        Console.ReadLine();
    }

    // Be aware of how this method handles edge cases.
    // A few are counterintuitive, like the 0.0 case.
    // Also note that the goal is to report a precision
    // and scale that can be used to store the number in
    // an SQL DECIMAL type, so this does not correspond to
    // how precision and scale are defined for scientific
    // notation. The minimal precision SQL decimal can
    // be calculated by subtracting TrailingZeros as follows:
    // DECIMAL(Precision - TrailingZeros, Scale - TrailingZeros).
    //
    //     dec Precision Scale TrailingZeros
    // ------- --------- ----- -------------
    //   0             1     0             0
    // 0.0             2     1             1
    // 0.1             1     1             0
    // 0.01            2     2             0 [Diff result than ShowInfo]
    // 0.010           3     3             1 [Diff result than ShowInfo]
    // 12.45           4     2             0
    // 12.4500         6     4             2
    // 770             3     0             0
    static DecimalInfo FastInfo(decimal dec)
    {
        string s = dec.ToString(CultureInfo.InvariantCulture);

        int precision = 0;
        int scale = 0;
        int trailingZeros = 0;
        bool inFraction = false;
        bool nonZeroSeen = false;

        foreach (char c in s)
        {
            if (inFraction)
            {
                if (c == '0')
                    trailingZeros++;
                else
                {
                    nonZeroSeen = true;
                    trailingZeros = 0;
                }

                precision++;
                scale++;
            }
            else
            {
                if (c == '.')
                {
                    inFraction = true;
                }
                else if (c != '-')
                {
                    if (c != '0' || nonZeroSeen)
                    {
                        nonZeroSeen = true;
                        precision++;
                    }
                }
            }
        }

        // Handles cases where all digits are zeros.
        if (!nonZeroSeen)
            precision += 1;

        return new DecimalInfo(precision, scale, trailingZeros);
    }

    struct DecimalInfo
    {
        public int Precision { get; private set; }
        public int Scale { get; private set; }
        public int TrailingZeros { get; private set; }

        public DecimalInfo(int precision, int scale, int trailingZeros)
            : this()
        {
            Precision = precision;
            Scale = scale;
            TrailingZeros = trailingZeros;
        }
    }

    static DecimalInfo ShowInfo(decimal dec)
    {
        // We want the integer parts as uint
        // C# doesn't permit int[] to uint[] conversion,
        // but .NET does. This is somewhat evil...
        uint[] bits = (uint[])(object)decimal.GetBits(dec);


        decimal mantissa =
            (bits[2] * 4294967296m * 4294967296m) +
            (bits[1] * 4294967296m) +
            bits[0];

        uint scale = (bits[3] >> 16) & 31;

        // Precision: number of times we can divide
        // by 10 before we get to 0 
        uint precision = 0;
        if (dec != 0m)
        {
            for (decimal tmp = mantissa; tmp >= 1; tmp /= 10)
            {
                precision++;
            }
        }
        else
        {
            // Handle zero differently. It's odd.
            precision = scale + 1;
        }

        uint trailingZeros = 0;
        for (decimal tmp = mantissa;
            tmp % 10m == 0 && trailingZeros < scale;
            tmp /= 10)
        {
            trailingZeros++;
        }

        return new DecimalInfo((int)precision, (int)scale, (int)trailingZeros);
    }
}



回答4:


public static class DecimalExtensions
{
    public static int GetPrecision(this decimal value)
    {
        return GetLeftNumberOfDigits(value) + GetRightNumberOfDigits(value);
    }

    public static int GetScale(this decimal value)
    {
        return GetRightNumberOfDigits(value);
    }
    /// <summary>
    /// Number of digits to the right of the decimal point without ending zeros
    /// </summary>
    /// <param name="value"></param>
    /// <returns></returns>
    public static int GetRightNumberOfDigits(this decimal value)
    {
        var text = value.ToString(System.Globalization.CultureInfo.InvariantCulture).TrimEnd('0');
        var decpoint = text.IndexOf(System.Globalization.CultureInfo.InvariantCulture.NumberFormat.NumberDecimalSeparator);
        if (decpoint < 0)
            return 0;
        return text.Length - decpoint - 1;
    }

    /// <summary>
    /// Number of digits to the left of the decimal point without starting zeros
    /// </summary>
    /// <param name="value"></param>
    /// <returns></returns>
    public static int GetLeftNumberOfDigits(this decimal value)
    {
        var text = Math.Abs(value).ToString(System.Globalization.CultureInfo.InvariantCulture).TrimStart('0');
        var decpoint = text.IndexOf(System.Globalization.CultureInfo.InvariantCulture.NumberFormat.NumberDecimalSeparator);
        if (decpoint == -1)
            return text.Length;
        return decpoint;
    }
}

My solution is compatible with Oracle precision and scale definition for NUMBER(p,s) DataType:

https://docs.oracle.com/cd/B28359_01/server.111/b28318/datatype.htm#i16209

Regards.




回答5:


I do currently have a similar issue, but I do not only need the scale, but also need the mantisse as integer. Based on the solutions above, please find the fastest, I could come up with, below. Statistics: "ViaBits" takes 2,000ms for 7,000,000 checks on my machine. "ViaString" takes 4,000ms for the same task.

    public class DecimalInfo {

    public BigInteger Mantisse { get; private set; }
    public SByte Scale { get; private set; }
    private DecimalInfo() {
    }

    public static DecimalInfo Get(decimal d) {
        //ViaBits is faster than ViaString.
        return ViaBits(d);
    }

    public static DecimalInfo ViaBits(decimal d) {
        //This is the fastest, I can come up with.
        //Tested against the solutions from http://stackoverflow.com/questions/763942/calculate-system-decimal-precision-and-scale
        if (d == 0) {
            return new DecimalInfo() {
                Mantisse = 0,
                Scale = 0,
            };
        } else {
            byte scale = (byte)((Decimal.GetBits(d)[3] >> 16) & 31);
            //Calculating the mantisse from the bits 0-2 is slower.
            if (scale > 0) {
                if ((scale & 1) == 1) {
                    d *= 10m;
                }
                if ((scale & 2) == 2) {
                    d *= 100m;
                }
                if ((scale & 4) == 4) {
                    d *= 10000m;
                }
                if ((scale & 8) == 8) {
                    d *= 100000000m;
                }
                if ((scale & 16) == 16) {
                    d *= 10000000000000000m;
                }
            }
            SByte realScale = (SByte)scale;
            BigInteger scaled = (BigInteger)d;
            //Just for bigger steps, seems reasonable.
            while (scaled % 10000 == 0) {
                scaled /= 10000;
                realScale -= 4;
            }
            while (scaled % 10 == 0) {
                scaled /= 10;
                realScale--;
            }
            return new DecimalInfo() {
                Mantisse = scaled,
                Scale = realScale,
            };
        }
    }

    public static DecimalInfo ViaToString(decimal dec) {
        if (dec == 0) {
            return new DecimalInfo() {
                Mantisse = 0,
                Scale = 0,
            };
        } else {
            //Is slower than "ViaBits".
            string s = dec.ToString(CultureInfo.InvariantCulture);

            int scale = 0;
            int trailingZeros = 0;
            bool inFraction = false;
            foreach (char c in s) {
                if (inFraction) {
                    if (c == '0') {
                        trailingZeros++;
                    } else {
                        trailingZeros = 0;
                    }
                    scale++;
                } else {
                    if (c == '.') {
                        inFraction = true;
                    } else if (c != '-') {
                        if (c == '0'){
                            trailingZeros ++;
                        } else {
                            trailingZeros = 0;
                        }
                    }
                }
            }

            if (inFraction) {
                return new DecimalInfo() {
                    Mantisse = BigInteger.Parse(s.Replace(".", "").Substring(0, s.Length - trailingZeros - 1)),
                    Scale = (SByte)(scale - trailingZeros),
                };
            } else {
                return new DecimalInfo() {
                    Mantisse = BigInteger.Parse(s.Substring(0, s.Length - trailingZeros)),
                    Scale = (SByte)(scale - trailingZeros),
                };
            }
        }
    }
}


来源:https://stackoverflow.com/questions/763942/calculate-system-decimal-precision-and-scale

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!