На самом деле, математически определить точность и масштаб десятичного значения

Я искал способ определения масштаба и точности десятичного числа в C#, что привело меня к нескольким SO-вопросам, но ни у одного из них, похоже, нет правильных ответов или нет вводящих в заблуждение названий (на самом деле они касаются SQL-сервера или некоторых других). другие базы данных, а не C#) или любые ответы вообще. Следующий пост, я думаю, ближе всего к тому, что я ищу, но даже это кажется неправильным:

Определить десятичную точность входного числа

Во-первых, кажется, что существует разница между масштабом и точностью. На Google (на MSDN):

"Точность - это количество цифр в числе. Шкала - это количество цифр справа от десятичной точки в числе".

С этим, как говорится, число 12345.67890M будет иметь масштаб 5 и точность 10. Я не обнаружил ни одного примера кода, который бы точно рассчитал это в C#.

Я хочу сделать два вспомогательных метода, decimal.Scale(), а также decimal.Precision(), так что следующий модульный тест проходит:

    [TestMethod]
    public void ScaleAndPrecisionTest()
    {
        //arrange 
        var number = 12345.67890M;

        //act
        var scale = number.Scale();
        var precision = number.Precision();

        //assert
        Assert.IsTrue(precision == 10);
        Assert.IsTrue(scale == 5);
    }

... но я еще не нашел фрагмент, который будет делать это, хотя несколько человек предложили использовать decimal.GetBits()и другие сказали, преобразовать его в строку и проанализировать его.

Преобразование в строку и ее синтаксический анализ, на мой взгляд, ужасная идея, даже если не учитывать проблему локализации с десятичной точкой. Математика за GetBits() метод, однако, как греческий для меня.

Кто-нибудь может описать, как будут выглядеть расчеты для определения масштаба и точности в decimal значение для C#?

3 ответа

Решение

Вот как вы получаете масштаб, используя GetBits() функция:

decimal x = 12345.67890M;
int[] bits = decimal.GetBits(x);
byte scale = (byte) ((bits[3] >> 16) & 0x7F); 

И лучший способ получить точность - это удалить точку дроби (т. Е. Использовать десятичный конструктор для восстановления десятичного числа без масштаба, упомянутого выше), а затем использовать логарифм:

decimal x = 12345.67890M;
int[] bits = decimal.GetBits(x);
//We will use false for the sign (false =  positive), because we don't care about it.
//We will use 0 for the last argument instead of bits[3] to eliminate the fraction point.
decimal xx = new Decimal(bits[0], bits[1], bits[2], false, 0);
int precision = (int)Math.Floor(Math.Log10((double)xx)) + 1;

Теперь мы можем поместить их в расширения:

public static class Extensions{
    public static int GetScale(this decimal value){
    if(value == 0)
            return 0;
    int[] bits = decimal.GetBits(value);
    return (int) ((bits[3] >> 16) & 0x7F); 
    }

    public static int GetPrecision(this decimal value){
    if(value == 0)
        return 0;
    int[] bits = decimal.GetBits(value);
    //We will use false for the sign (false =  positive), because we don't care about it.
    //We will use 0 for the last argument instead of bits[3] to eliminate the fraction point.
    decimal d = new Decimal(bits[0], bits[1], bits[2], false, 0);
    return (int)Math.Floor(Math.Log10((double)d)) + 1;
    }
}

А вот и скрипка.

Прежде всего, решите "физическую" проблему: как вы будете решать, какие цифры значимы. Дело в том, что "точность" не имеет физического значения, если вы не знаете или не угадываете абсолютную ошибку.


Теперь есть 2 основных способа определения каждой цифры (и, следовательно, их числа):

  • получить + интерпретировать значимые части
  • рассчитать математически

2-й способ не может обнаружить конечные нули в дробной части (что может или не может быть значительным в зависимости от вашего ответа на "физическую" проблему), поэтому я не буду покрывать его, если не будет запрошено.

Для первого, в Decimal В интерфейсе я вижу 2 основных способа получения частей: ToString() (несколько перегрузок) и GetBits(),

  1. ToString(String, IFormatInfo) на самом деле надежный способ, так как вы можете точно определить формат.

  2. Семантика GetBits() результат четко задокументирован в его статье MSDN (таким образом, такие жалобы, как "это греческий для меня" не подойдет;)). Декомпиляция с помощью ILSpy показывает, что на самом деле это кортеж полей необработанных данных объекта:

    public static int[] GetBits(decimal d)
    {
        return new int[]
        {
            d.lo,
            d.mid,
            d.hi,
            d.flags
        };
    }
    

    И их семантика:

    • |high|mid|low| - двоичные цифры (96 бит), интерпретируемые как целое число (= выровнено по правому краю)
    • flags:
      • биты 16 в 23 - "сила 10, чтобы разделить целое число" (= количество дробных десятичных цифр)
        • (таким образом, (flags>>16)&0xFF это необработанное значение этого поля)
      • немного 31 - подписать (нас не касается)

    как вы можете видеть, это очень похоже на поплавки IEEE 754.

    Таким образом, число дробных цифр является показателем степени. Количество общих цифр - это количество цифр в десятичном представлении 96-разрядного целого числа.

Ответ Racil дает вам значение внутренней шкалы decimal это правильно, хотя, если внутреннее представление когда-либо изменится, это будет интересно.

В текущем формате прецизионная часть decimal фиксируется на 96 битах, что составляет от 28 до 29 десятичных цифр в зависимости от номера. Все.NET decimal значения разделяют эту точность. Так как это константа, нет внутренней ценности, которую вы можете использовать для ее определения.

Что вы, видимо, после, хотя количество цифр, которое мы можем легко определить из строкового представления. Мы также можем получить масштаб в то же время или, по крайней мере, используя тот же метод.

public struct DecimalInfo
{
    public int Scale;
    public int Length;

    public override string ToString()
    {
        return string.Format("Scale={0}, Length={1}", Scale, Length);
    }
}

public static class Extensions
{
    public static DecimalInfo GetInfo(this decimal value)
    {
        string decStr = value.ToString().Replace("-", "");
        int decpos = decStr.IndexOf(".");
        int length = decStr.Length - (decpos < 0 ? 0 : 1);
        int scale = decpos < 0 ? 0 : length - decpos;
        return new DecimalInfo { Scale = scale, Length = length };
    }
}
Другие вопросы по тегам