计算 System.Decimal 精度和比例

Posted

技术标签:

【中文标题】计算 System.Decimal 精度和比例【英文标题】:Calculate System.Decimal Precision and Scale 【发布时间】:2009-04-18 18:52:20 【问题描述】:

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

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

d.ToString() = "123.4500"

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

精度:7 规模:4 有效精度:5 有效量表:2

给定一个任意的 System.Decimal,我怎样才能有效地计算所有这四个参数而不转换为字符串并检查字符串?该解决方案可能需要 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

(?) 或者将这些精度解释为零也可以。

【问题讨论】:

【参考方案1】:

是的,您需要使用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();
    

【讨论】:

谢谢,这很有趣。不像我在另一篇文章中展示的那样从 ToString 中抓取信息快。 乔恩,如果你认为你的 (uint[])(object) 演员表是邪恶的(我同意),那么你为什么不使用更简洁和明确的方式呢? @Joren:因为 C# 不允许您在 int[] 和 uint[] 之间进行直接转换,即使 CLR 可以。您想到了哪种更简洁、更明确的方式? 类似Select(x =&gt; (uint)x).ToArray();。我认为这很明确。它非常简单,并且不违反 C# 转换规则,所以我认为它也很整洁。 @Joren:虽然我不是一个微优化的人,但我认为在演员实际工作时无偿创建一个新的迭代器、一个新的数组等有点多。我认为评论非常清楚地解释了发生了什么——我发现评论+现有代码比在这种情况下使用 LINQ 更清晰。我想我们必须同意不同意。【参考方案2】:

当我需要在将十进制值写入数据库之前验证精度和比例时,我遇到了这篇文章。实际上,我想出了一种不同的方法来使用 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);
     

【讨论】:

【参考方案3】:

使用 ToString 比 Jon Skeet 的解决方案快大约 10 倍。虽然这相当快,但这里的挑战(如果有任何接受者的话!)是击败 ToString 的性能。

我从以下测试程序中得到的性能结果是: 显示信息 239 毫秒 快速信息 25 毫秒

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

【讨论】:

我并不完全感到惊讶 - 由于缺少 96 位整数类型,我们正在使用小数进行大量运算。如果您通过完全忽略前 32 位来使用 ulong 而不是小数作为尾数,它会比 FastInfo 稍快 - 但当然,它不适用于所有小数!我怀疑我们可以通过在一个循环中计算精度和尾随零来提高速度(每次都除以 10)。 基于字符串的算法会为带有前导零的数字(即 0.555)和在小数位和有效数字之间有零的数字(即 0.0005)产生错误的结果。 谢谢,我已经修改了代码。我还在 FastInfo 方法上方的代码中添加了注释。这是为了指出该方法使用 SQL 精度和比例定义,而不是通常的科学记数法。 (在原始问题中提到这是处理 SQL 小数。) 鉴于 SQL 以错误的精度存储十进制值,我不确定他们是否有任何业务定义自己的精度、比例等标准。我仍然必须为我的程序使用 Jon 的算法,因为它给了我预期的结果,但感谢您修复您的代码。 +1【参考方案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;
    

我的解决方案与 NUMBER(p,s) 数据类型的 Oracle 精度和比例定义兼容:

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

问候。

【讨论】:

【参考方案5】:

我目前确实有类似的问题,但我不仅需要刻度,还需要尾数作为整数。 根据上面的解决方案,请在下面找到我能想到的最快的解决方案。 统计数据: “ViaBits”在我的机器上进行 7,000,000 次检查需要 2,000 毫秒。 “ViaString”同样的任务需要 4000 毫秒。

    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://***.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),
                ;
            
        
    

【讨论】:

以上是关于计算 System.Decimal 精度和比例的主要内容,如果未能解决你的问题,请参考以下文章

精度损失而引发的 bug

lua中精度计算

怎么计算混淆矩阵的消费者精度

NSDecimalNumber 值的比例和精度

精度和规模有啥区别?

Hive 数据类型:双精度和比例