Calculate System.Decimal Precision and Scale

前端 未结 5 1679
我寻月下人不归
我寻月下人不归 2020-12-01 01:01

Suppose that we have a System.Decimal number.

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

d.ToString() = \"123.4         


        
相关标签:
5条回答
  • 2020-12-01 01:22

    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),
                    };
                }
            }
        }
    }
    
    0 讨论(0)
  • 2020-12-01 01:24

    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();
        }
    }
    
    0 讨论(0)
  • 2020-12-01 01:28
    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.

    0 讨论(0)
  • 2020-12-01 01:34

    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);
        }
    }
    
    0 讨论(0)
  • 2020-12-01 01:46

    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);
         }
    
    0 讨论(0)
提交回复
热议问题