代码之家  ›  专栏  ›  技术社区  ›  Jason Kresowaty

计算系统.小数精度和小数位数

  •  22
  • Jason Kresowaty  · 技术社区  · 15 年前

    假设我们有一个系统,十进制数。

    为了进行说明,让我们取一个ToString()表示如下:

    d.ToString() = "123.4500"
    

    关于这个小数点,可以这样说。在这里,比例是指小数点右边的位数。有效刻度相似,但忽略小数部分中出现的任何尾随零。(换句话说,这些参数的定义类似于SQL小数加上一些额外的参数来说明系统。小数部分尾随零的十进制概念。)

    • 精度: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
    

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

    5 回复  |  直到 8 年前
        1
  •  26
  •   Jon Skeet    15 年前

    是的,你需要使用 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();
        }
    }
    
        2
  •  23
  •   Ben Buck    14 年前

    当我需要在将十进制值写入数据库之前验证精度和小数位数时,我遇到了这篇文章。实际上,我使用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
  •  10
  •   Jason Kresowaty    15 年前

    使用toString比jon skeet的解决方案快10倍。虽然速度相当快,但这里的挑战(如果有人愿意的话!)是为了击败托斯林的表现。

    我从以下测试程序中得到的性能结果是: 显示信息239毫秒 FastInfo 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);
        }
    }
    
        4
  •  0
  •   user1785960    8 年前
    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;
        }
    }
    

    我的解决方案与Oracle Precision和Scale Definition for Number(P,S)数据类型兼容:

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

    当做。

        5
  •  0
  •   Andreas    8 年前

    我现在确实有一个类似的问题,但我不仅需要比例,而且还需要作为整数的mantisse。 基于以上的解决方案,请找出我能想到的最快的,如下。 统计数字: “Viabits”需要2000毫秒才能在我的机器上进行70万次检查。 “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://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),
                    };
                }
            }
        }
    }