На самом деле, математически определить точность и масштаб десятичного значения
Я искал способ определения масштаба и точности десятичного числа в 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()
,
ToString(String, IFormatInfo)
на самом деле надежный способ, так как вы можете точно определить формат.- Например, использовать
F
спецификатор и передать культурно-нейтральныйNumberFormatInfo
в котором вы вручную задали все поля, которые влияют на этот конкретный формат.- учитывая
NumberDecimalDigits
поле: тест показывает, что это минимальное число - поэтому установите его0
(документы неясны по этому поводу), - и конечные нули печатаются хорошо, если есть
- учитывая
- Например, использовать
Семантика
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 };
}
}