计算System.Decimal精度和比例

Jas*_*aty 23 .net decimal

假设我们有一个System.Decimal数字.

为了说明,我们来看一个ToString()表示如下:

d.ToString() = "123.4500"
Run Code Online (Sandbox Code Playgroud)

关于这个十进制,可以说以下内容.对于我们这里的目的,比例定义为小数点右边的位数.有效比例相似但忽略了小数部分中出现的任何尾随零.(换句话说,这些参数定义为SQL小数加上一些额外的参数,以说明小数部分中尾随零的System.Decimal概念.)

  • 精度:7
  • 比例:4
  • EffectivePrecision:5
  • EffectiveScale:2

给定一个任意的System.Decimal,如何有效地计算所有这四个参数而不转换为String并检查String?该解决方案可能需要Decimal.GetBits.

更多例子:

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
Run Code Online (Sandbox Code Playgroud)

(?)或者将这些精度解释为零也没关系.

Jon*_*eet 27

是的,你需要使用Decimal.GetBits.不幸的是,你必须使用96位整数,并且.NET中没有简单的整数类型可以处理96位.另一方面,你有可能Decimal自己使用......

这里有一些代码与您的示例生成相同的数字.希望你觉得它有用 :)

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();
    }
}
Run Code Online (Sandbox Code Playgroud)

  • @Joren:虽然我不是微优化的人,但我觉得无意中创建一个新的迭代器,一个新的数组等,当一个演员实际工作时会有点多.我认为该评论很清楚地解释了发生了什么 - 我发现评论+现有代码比在这种情况下使用LINQ更清晰.我想我们必须同意不同意. (3认同)

小智 24

当我需要在将十进制值写入数据库之前验证精度和比例时,我遇到了这篇文章.我实际上想出了一种不同的方法来实现这一点,使用System.Data.SqlTypes.SqlDecimal,结果比这里讨论的其他两种方法更快.

 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);
     }
Run Code Online (Sandbox Code Playgroud)


Jas*_*aty 11

使用ToString比Jon Skeet的解决方案快约10倍.虽然速度相当快,但这里的挑战(如果有任何参与者!)是要击败ToString的性能.

我从以下测试程序得到的性能结果是: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);
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 基于字符串的算法对具有前导零的数字(即0.555)以及在十进制和有效数字之间具有零的数字(即0.0005)产生不正确的结果. (3认同)
  • 我并不完全惊讶 - 因为缺少96位整数类型,我们正在进行大量的小数运算.如果你通过完全忽略前32位对尾数使用ulong而不是decimal,它比FastInfo略快 - 但当然,它不适用于所有小数!我怀疑我们可以通过计算一个循环中的精度和尾随零来提高速度(因为每次都除以10). (2认同)