Вычислить точность и масштаб системы.

Предположим, что у нас есть System.Decimal number.

Для иллюстрации возьмем тот, чье представление ToString() выглядит следующим образом:

d.ToString() = "123.4500"

В этом десятичном разряде можно сказать следующее. Для наших целей масштаб определяется как число цифр справа от десятичной точки. Эффективная шкала аналогична, но игнорирует любые конечные нули, которые встречаются в дробной части. (Другими словами, эти параметры определены как децималы SQL плюс некоторые дополнительные параметры для учета концепции System.Decimal конечных нулей в дробной части.)

  • Точность: 7
  • Масштаб: 4
  • Эффективная точность: 5
  • Эффективная шкала: 2

Для произвольной System.Decimal, как я могу вычислить все четыре из этих параметров эффективно и без преобразования в 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

(?) В качестве альтернативы интерпретация этих прецизионных значений как нуля будет хорошей.

Ответ 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 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

Я столкнулся с этой статьей, когда мне нужно было проверить точность и масштаб, прежде чем записывать десятичное значение в базу данных. Я действительно придумал другой способ добиться этого, используя 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 примерно в 10 раз быстрее, чем решение Jon Skeet. Хотя это достаточно быстро, задача здесь (если есть какие-либо приемники!) Состоит в том, чтобы превзойти производительность ToString.

Результаты работы, полученные из следующей тестовой программы: ShowInfo 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 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;
    }
}

Мое решение совместимо с определением точности и масштабирования Oracle для NUMBER (p, s) DataType:

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

С уважением.

Ответ 5

У меня в настоящее время есть аналогичная проблема, но мне нужен не только масштаб, но также и мантисс как целое. Основываясь на вышеизложенных решениях, найдите самый быстрый, я мог бы придумать, ниже. Статистика: "ViaBits" занимает 2 000 мс для 7 000 000 проверок на моей машине. "ViaString" занимает 4 000 мс для одной и той же задачи.

    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/info/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),
                };
            }
        }
    }
}