Найдите количество десятичных знаков в десятичном значении независимо от культуры

Мне интересно, есть ли краткий и точный способ вывести количество десятичных знаков в десятичном значении (как int), которое можно было бы безопасно использовать в различной информации о культуре?

Например:
19.0 должен вернуть 1,
27.5999 должен вернуть 4,
19.12 должен вернуть 2,
и т. Д.

Я написал запрос, который разделил строку на точку, чтобы найти десятичные разряды:

int priceDecimalPlaces = price.ToString().Split('.').Count() > 1 
                  ? price.ToString().Split('.').ToList().ElementAt(1).Length 
                  : 0;

Но мне приходит в голову, что это будет работать только в регионах, которые используют '.' как десятичный разделитель и поэтому очень хрупкий в различных системах.


person Jesse Carter    schedule 20.11.2012    source источник
comment
Десятичная дробь в соответствии с заголовком вопроса   -  person Jesse Carter    schedule 20.11.2012
comment
Как насчет сопоставления с образцом до разделения? Обычно \ d + (\ D) \ d +, где \ D возвращает разделитель (. И т. Д.)   -  person Anshul    schedule 20.11.2012
comment
Это не закрытый вопрос, как может показаться на первый взгляд. Запрос 19.0 на возврат 1 - это деталь реализации, касающаяся внутренней памяти значения 19.0. Дело в том, что программа может хранить это как 190×10⁻¹, 1900×10⁻² или 19000×10⁻³. Все они равны. Тот факт, что он использует первое представление, когда ему задано значение 19.0M, и это раскрывается при использовании ToString без спецификатора формата, просто совпадение и радость. За исключением того, что это неприятно, когда люди полагаются на показатель степени в тех случаях, когда им не следует этого делать.   -  person ErikE    schedule 22.08.2015
comment
Если вам нужен тип, который может нести количество десятичных знаков, используемых при его создании, чтобы вы могли надежно отличить 19M от 19.0M от 19.00M, вам необходимо создать новый класс, который объединяет базовое значение как одно свойство и число десятичных знаков как другое свойство.   -  person ErikE    schedule 22.08.2015
comment
Несмотря на то, что класс Decimal может различать 19 м, от 19,0 м от 19,00 м? Значимые цифры - это один из основных вариантов его использования. Что такое 19,0 м * 1,0 м? Кажется, он говорит 19.00 мин, но, может быть, разработчики C # неправильно вычисляют: P? Опять же значащие цифры - настоящая вещь. Если вам не нравятся значащие цифры, вам, вероятно, не следует использовать класс Decimal.   -  person Nicholi    schedule 22.08.2015
comment
Что должно whatever(654.32100m) вернуть?   -  person Solomon Ucko    schedule 11.02.2020


Ответы (16)


Я использовал способ Джо для решения этой проблемы :)

decimal argument = 123.456m;
int count = BitConverter.GetBytes(decimal.GetBits(argument)[3])[2];
person burning_LEGION    schedule 21.11.2012
comment
Хорошо, я не ожидал, что реализация этого будет такой простой. - person Jesse Carter; 21.11.2012
comment
После того, как я посмотрел на это дальше и увидел в действии, я отмечаю это как ответ, потому что, на мой взгляд, это самый лаконичный и элегантный метод возврата десятичных разрядов, который я видел здесь. Я бы снова +1, если бы мог: D - person Jesse Carter; 21.11.2012
comment
+1. Хорошее решение, я никогда не мог представить себе что-то подобное. - person Felipe Oriani; 30.07.2013
comment
Недавно у меня возникла проблема с этим решением. Проблема в том, что он также считает нули в конце. Например. для аргумента var = 123,4560м; результат будет 4. - person raznagul; 18.12.2013
comment
decimal сохраняет счетную цифру после запятой, поэтому вы обнаружите эту проблему, вам нужно преобразовать десятичное число в двойное и снова в десятичное для исправления: BitConverter.GetBytes (decimal.GetBits ((десятичный) (двойной) аргумент) [3]) [2 ]; - person burning_LEGION; 18.12.2013
comment
У меня это не сработало. Значение, возвращаемое из SQL, составляет 21,17, в нем 4 цифры. Тип данных определяется как DECIMAL (12,4), так что, возможно, это так (с использованием Entity Framework). - person PeterX; 18.06.2014
comment
@raznagul - Я бы предположил, что для 123.4560 правильным результатом будет 4, потому что это число с точностью до 4 dp. Например, число от 123,456 до 4dps может быть 123,4558. - person Lee Oades; 02.09.2014
comment
@ m.edmondson Заметили ли вы, что (Decimal) 0.01f фактически создает десятичную дробь с 3 цифрами? Console.WriteLine ((Десятичное) 0,01f); На самом деле это очень ХОРОШЕЕ решение, потому что GetBits очень хорошо определен в том, что он возвращает. Microsoft не может просто изменить документацию GetBits по прихоти, так как это нарушит определенный ими интерфейс и то, что он возвращает. Это не изменение реализации ... это буквально изменение интерфейса. Даже если в будущем в него будут внесены изменения, это, вероятно, будет частью очень ХОРОШО ОПРЕДЕЛЕННОГО критического изменения для ГЛАВНОГО обновления. - person Nicholi; 21.08.2015
comment
@Nicholi - Нет, это исключительно плохо, потому что метод полагается на размещение нижележащих битов десятичной дроби - то, что имеет много способов для обозначения того же числа. Вы бы не стали тестировать класс на основе состояния его частных полей, не так ли? - person m.edmondson; 21.08.2015
comment
Согласитесь, это ужасный способ сделать это, потому что 10 × 10⁻³ = 1 × 10⁻². Одинаковое число, два разных представления. GetBits отвечает не на тот вопрос, который в большинстве случаев является правильным ответом только по совпадению. - person ErikE; 22.08.2015
comment
Если вопрос в том, сколько цифр в объекте Decimal, GetBits предоставляет это решение. И снова, ЕСЛИ базовое представление Decimal должно было измениться, реализация GetBits должна была бы измениться, потому что она имеет определенное и задокументированное возвращаемое значение. (Десятичный) 0.01f возвращает 3 цифры, ПОТОМУ ЧТО ЭТО ДЕСЯТИЧНЫЙ ОБЪЕКТ С ТРЕМЯ ЦИФРАМИ. Если бы вопрос заключался в том, сколько цифр в double / float, то да, приведение к Decimal и использование GetBits может не привести к желаемому ответу. Поскольку преобразование / приведение из double / float будет неточным. - person Nicholi; 22.08.2015
comment
Также очень важным применением класса Decimal являются так называемые значащие цифры (я предлагаю вам прочитать о них). В то время как вы можете подумать, что 0,01 и 0,010 - это одно и то же, не все остальные. Это большая цель класса Decimal. Вы бы сказали, что буквальное значение 0,010 м состоит из 2 или 3 цифр? - person Nicholi; 22.08.2015
comment
Не уверен, что в этом должно быть элегантно или приятно. Это примерно так же запутано, как и получается. Кто знает, работает ли это вообще во всех случаях. Невозможно убедиться. - person usr; 27.12.2015
comment
Допустимо для чисел с плавающей запятой и удвоения? var d10 = 54321.98M; var f10 = 54321.98f; var double10 = 54321.98; - person Kiquenet; 22.02.2017
comment
@Kiquenet, нет, только десятичная дробь - person burning_LEGION; 22.02.2017
comment
Это плохая идея - person Waruna Manjula; 22.03.2018
comment
Хотя этот метод гениален, он не работает должным образом. Он считает, что десятичное число, инициализированное значением 1234.5600M, имеет 4dp. См. dotnetfiddle.net/FsP49s. - person Rob; 05.07.2019
comment
похоже, вы взяли решение отсюда: - person TheDude; 22.12.2020

Поскольку ни один из предоставленных ответов не был достаточно хорош для преобразования магического числа "-0.01f" в десятичное ... то есть: GetDecimal((decimal)-0.01f);
Я могу только предположить, что колоссальный вирус мысленного пердения атаковал всех 3 года назад :)
Вот то, что кажется работающей реализацией этой злой и чудовищной проблемы, очень сложной проблемы подсчета десятичных знаков после точки - без строк, без культур, без необходимости считать биты и без необходимости читать математические форумы. просто простая математика для 3-го класса.

public static class MathDecimals
{
    public static int GetDecimalPlaces(decimal n)
    {
        n = Math.Abs(n); //make sure it is positive.
        n -= (int)n;     //remove the integer part of the number.
        var decimalPlaces = 0;
        while (n > 0)
        {
            decimalPlaces++;
            n *= 10;
            n -= (int)n;
        }
        return decimalPlaces;
    }
}

private static void Main(string[] args)
{
    Console.WriteLine(1/3m); //this is 0.3333333333333333333333333333
    Console.WriteLine(1/3f); //this is 0.3333333

    Console.WriteLine(MathDecimals.GetDecimalPlaces(0.0m));                  //0
    Console.WriteLine(MathDecimals.GetDecimalPlaces(1/3m));                  //28
    Console.WriteLine(MathDecimals.GetDecimalPlaces((decimal)(1 / 3f)));     //7
    Console.WriteLine(MathDecimals.GetDecimalPlaces(-1.123m));               //3
    Console.WriteLine(MathDecimals.GetDecimalPlaces(43.12345m));             //5
    Console.WriteLine(MathDecimals.GetDecimalPlaces(0));                     //0
    Console.WriteLine(MathDecimals.GetDecimalPlaces(0.01m));                 //2
    Console.WriteLine(MathDecimals.GetDecimalPlaces(-0.001m));               //3
    Console.WriteLine(MathDecimals.GetDecimalPlaces((decimal)-0.00000001f)); //8
    Console.WriteLine(MathDecimals.GetDecimalPlaces((decimal)0.0001234f));   //7
    Console.WriteLine(MathDecimals.GetDecimalPlaces((decimal)0.01f));        //2
    Console.WriteLine(MathDecimals.GetDecimalPlaces((decimal)-0.01f));       //2
}
person G.Y    schedule 13.05.2015
comment
Ваше решение не сработает в ряде случаев, когда в конце есть нули, а цифры являются ЗНАЧИТЕЛЬНЫМИ. 0,01 м * 2,0 м = 0,020 м. Должно быть 3 цифры, ваш метод возвращает 2. Кажется, вы неправильно понимаете, что происходит, когда вы приводите 0,01f к Decimal. Плавающие точки по своей природе неточны, поэтому фактическое двоичное значение, сохраненное для 0,01f, неточно. Когда вы приводите к Decimal (очень структурированное представление чисел), вы можете не получить 0,01 м (на самом деле вы получите 0,010 м). Решение GetBits действительно правильно для получения количества цифр из десятичного числа. Ключевым моментом является то, как вы конвертируете в десятичный формат. - person Nicholi; 21.08.2015
comment
@Nicholi 0,020 м равно 0,02 м .. нули в конце не имеют значения. OP спрашивает, независимо от культуры в названии, и даже более конкретно объясняет ... что будет безопасно использовать в различных культурных данных ... - поэтому я думаю, что мой ответ остается даже более верным, чем другие. - person G.Y; 05.09.2015
comment
OP конкретно сказал: 19.0 должен вернуть 1. Этот код не работает в этом случае. - person daniloquio; 15.01.2016
comment
возможно, это не то, что хотел OP, но этот ответ лучше соответствует моим потребностям, чем главный ответ на этот вопрос - person Arsen Zahray; 11.05.2017
comment
Первые две строки следует заменить на n = n % 1; if (n < 0) n = -n;, потому что значение больше int.MaxValue вызовет OverflowException, например 2147483648.12345. - person Loathing; 21.09.2017
comment
Из того, что я могу понять в вопросе, похоже, что вместо того, чтобы возвращать 0 для целого числа, они хотят, чтобы минимальное значение было 1. Поэтому я бы поменял возвращение на это return Math.Max(1,decimalPlaces) Если бы они хотели, чтобы 0,0 возвращал 0, который мог бы быть лишним условием. - person Pete; 25.05.2018

Я бы, вероятно, использовал решение в ответе @ fixagon.

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

Это, вероятно, будет быстрее, чем форматирование в виде строки, хотя вам придется обрабатывать очень много десятичных знаков, чтобы заметить разницу.

Я оставлю реализацию в качестве упражнения.

person Joe    schedule 20.11.2012
comment
Спасибо, @Joe, это действительно отличный подход. В зависимости от того, как мой начальник думает об использовании другого решения, я постараюсь реализовать вашу идею. Определенно было бы забавным упражнением :) - person Jesse Carter; 20.11.2012

Одно из лучших решений для определения количества цифр после десятичной точки показано в сообщении burn_LEGION.

Здесь я использую части из статьи форума STSdb: Количество цифр после десятичной точки.

В MSDN мы можем прочитать следующее объяснение:

"Десятичное число - это значение с плавающей запятой, состоящее из знака, числового значения, где каждая цифра в значении находится в диапазоне от 0 до 9, и коэффициента масштабирования, который указывает положение десятичной запятой с плавающей запятой, разделяющей целая и дробная части числового значения. "

А также:

«Двоичное представление десятичного значения состоит из 1-битового знака, 96-битного целого числа и коэффициента масштабирования, используемого для деления 96-битного целого числа и указания того, какая его часть является десятичной дробью. коэффициент масштабирования неявно представляет собой число 10, возведенное в степень в диапазоне от 0 до 28 ".

На внутреннем уровне десятичное значение представлено четырьмя целыми числами.

Десятичное внутреннее представление

Существует общедоступная функция GetBits для получения внутреннего представления. Функция возвращает массив int []:

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

Четвертый элемент возвращаемого массива содержит коэффициент масштабирования и знак. И, как сообщает MSDN, коэффициент масштабирования неявно представляет собой число 10, возведенное в степень в диапазоне от 0 до 28. Это именно то, что нам нужно.

Таким образом, на основе всех вышеперечисленных исследований мы можем построить наш метод:

private const int SIGN_MASK = ~Int32.MinValue;

public static int GetDigits4(decimal value)
{
    return (Decimal.GetBits(value)[3] & SIGN_MASK) >> 16;
}

Здесь SIGN_MASK используется для игнорирования знака. После логического и мы также сдвинули результат на 16 бит вправо, чтобы получить фактический масштабный коэффициент. Это значение, наконец, указывает количество цифр после десятичной точки.

Обратите внимание, что здесь MSDN также сообщает, что коэффициент масштабирования также сохраняет любые конечные нули в десятичном числе. Завершающие нули не влияют на значение десятичного числа в арифметических операциях или операциях сравнения. Однако конечные нули могут быть обнаружены методом ToString, если применяется соответствующая строка формата.

Это решение выглядит лучшим, но подождите, это еще не все. Путем доступа к закрытым методам в C # мы можем использовать выражения для создания прямого доступа к полю флагов и избежать построения массива int:

public delegate int GetDigitsDelegate(ref Decimal value);

public class DecimalHelper
{
    public static readonly DecimalHelper Instance = new DecimalHelper();

    public readonly GetDigitsDelegate GetDigits;
    public readonly Expression<GetDigitsDelegate> GetDigitsLambda;

    public DecimalHelper()
    {
        GetDigitsLambda = CreateGetDigitsMethod();
        GetDigits = GetDigitsLambda.Compile();
    }

    private Expression<GetDigitsDelegate> CreateGetDigitsMethod()
    {
        var value = Expression.Parameter(typeof(Decimal).MakeByRefType(), "value");

        var digits = Expression.RightShift(
            Expression.And(Expression.Field(value, "flags"), Expression.Constant(~Int32.MinValue, typeof(int))), 
            Expression.Constant(16, typeof(int)));

        //return (value.flags & ~Int32.MinValue) >> 16

        return Expression.Lambda<GetDigitsDelegate>(digits, value);
    }
}

Этот скомпилированный код присваивается полю GetDigits. Обратите внимание, что функция получает десятичное значение как ref, поэтому фактическое копирование не выполняется - только ссылка на значение. Использовать функцию GetDigits из DecimalHelper просто:

decimal value = 3.14159m;
int digits = DecimalHelper.Instance.GetDigits(ref value);

Это самый быстрый способ получить количество цифр после десятичной точки для десятичных значений.

person Kristiyan Dimitrov    schedule 03.07.2014
comment
десятичное r = (десятичное) -0.01f; и решение не удается. (на все ответы, которые я видел на этой странице ...) :) - person G.Y; 13.05.2015
comment
ПРИМЕЧАНИЕ: Что касается всего (Decimal) 0.01f, вы преобразуете плавающую точку, по сути НЕ ТОЧНУЮ, в нечто очень структурированное, например, Decimal. Взгляните на вывод Console.WriteLine ((Decimal) 0.01f). Десятичное число, формируемое в приведении, ДЕЙСТВИТЕЛЬНО состоит из 3 цифр, поэтому все предлагаемые решения говорят 3 вместо 2. На самом деле все работает так, как ожидалось, проблема в том, что вы ожидаете, что значения с плавающей запятой будут точными. Они не. - person Nicholi; 21.08.2015
comment
@Nicholi Ваша точка зрения не выполняется, когда вы понимаете, что 0.01 и 0.010 в точности равны числам. Кроме того, идея о том, что числовой тип данных имеет какое-то количество используемых цифр, на которое можно положиться, полностью ошибочна (не путать с допустимым количеством цифр. Не путайте представление ( отображение значения числа в определенном основании, например, десятичное расширение значения, указанного двоичным расширением 111) с базовым значением! Повторюсь, числа не являются цифрами и не состоят из цифр . - person ErikE; 22.08.2015
comment
Правильно думать об этом так: числа могут быть представлены цифрами, назначенными для представления определенной базы. (То, что цифры являются числовыми, - это всего лишь случайность, например, в шестнадцатеричном формате цифры с A по F не являются числовыми.) Поэтому мне пришлось бы абсолютно и категорически сказать, что они не работают должным образом, потому что GetBits методы задают неправильный вопрос. вопрос, а именно, какова экспонента текущего представления числа, что в большинстве случаев является правильным ответом только по совпадению. (продолжение) - person ErikE; 22.08.2015
comment
Правильный вопрос: какова позиция последней ненулевой цифры дробной части числа? GetBits не может дать надежного ответа на этот вопрос, поскольку 10 × 10⁻³ = 1 × 10⁻². Одно и то же число, два разных представления, получение степени этого дает ответ на неправильный вопрос. - person ErikE; 22.08.2015
comment
Они эквивалентны по стоимости, но не в значащих цифрах. Это крупный вариант использования класса Decimal. Если бы я спросил, сколько цифр в буквальном значении 0,010 м, вы бы ответили только 2? Даже если множество учителей математики и естественных наук по всему миру скажут вам, что итоговый 0 имеет значение? Проблема, о которой мы говорим, проявляется в преобразовании чисел с плавающей запятой в Decimal. Не использование самого GetBits, которое делает именно то, что задокументировано. Если вас не интересуют значащие цифры, то да, у вас есть проблема, и, вероятно, не следует использовать класс Decimal в первую очередь. - person Nicholi; 22.08.2015
comment
@kris, почему private const int SIGN_MASK = ~Int32.MinValue вместо более простого private const int SIGN_MASK = Int32.MaxValue? Есть ли подвох, которого я не вижу? - person the berserker; 27.11.2019
comment
@theberserker Насколько я помню, не было никакого подвоха - все должно работать в обоих направлениях. - person Kristiyan Dimitrov; 28.11.2019

Опираться на внутреннее представление десятичных дробей - это не круто.

Как насчет этого:

    int CountDecimalDigits(decimal n)
    {
        return n.ToString(System.Globalization.CultureInfo.InvariantCulture)
                //.TrimEnd('0') uncomment if you don't want to count trailing zeroes
                .SkipWhile(c => c != '.')
                .Skip(1)
                .Count();
    }
person Clement    schedule 29.09.2015

вы можете использовать InvariantCulture

string priceSameInAllCultures = price.ToString(System.Globalization.CultureInfo.InvariantCulture);

другая возможность - сделать что-то вроде этого:

private int GetDecimals(decimal d, int i = 0)
{
    decimal multiplied = (decimal)((double)d * Math.Pow(10, i));
    if (Math.Round(multiplied) == multiplied)
        return i;
    return GetDecimals(d, i+1);
}
person fixagon    schedule 20.11.2012
comment
Как это помогает мне найти количество десятичных знаков в десятичной дроби? У меня нет проблем с преобразованием десятичной дроби в строку, которая подходит для любой культуры. В соответствии с вопросом я пытаюсь найти количество десятичных знаков, которые были в десятичном - person Jesse Carter; 20.11.2012
comment
@JesseCarter: Это означает, что вы всегда можете разделить на .. - person Austin Salonen; 20.11.2012
comment
@AustinSalonen Правда? Я не знал, что использование InvariantCulture приведет к принудительному использованию точки в качестве десятичного разделителя. - person Jesse Carter; 20.11.2012
comment
как и раньше, он всегда будет приводить цену к строке с. как десятичный разделитель. но, на мой взгляд, это не самый элегантный способ ... - person fixagon; 20.11.2012
comment
@JesseCarter: NumberFormatInfo.NumberDecimalSeparator - person Austin Salonen; 20.11.2012
comment
Хмммм, приятно знать, что я могу реализовать это таким образом. Я надеялся, что может быть какой-то метод, предоставляемый Decimal, чтобы просто получить количество десятичных знаков, которые вообще не полагались на строки - person Jesse Carter; 20.11.2012
comment
Хорошая функция! Мне помогли, по крайней мере, ничего плохого в этом не нашла;) - person GrandaS; 28.06.2017

Большинство людей здесь, кажется, не подозревают, что decimal считает конечные нули важными для хранения и печати.

Таким образом, 0,1 м, 0,10 м и 0,100 м могут сравниваться как равные, они хранятся по-разному (как значение / масштаб 1/1, 10/2 и 100/3 соответственно) и будут напечатаны как 0,1, 0,10 и 0,100 соответственно. , автор ToString().

Таким образом, решения, которые сообщают о «слишком высокой точности», фактически сообщают о правильной точности на условиях decimal.

Кроме того, математические решения (например, умножение на 10), вероятно, будут очень медленными (десятичное число примерно в 40 раз медленнее, чем двойное для арифметики, и вы также не хотите смешивать числа с плавающей запятой, потому что это может привести к неточности ). Точно так же приведение к int или long в качестве средства усечения подвержено ошибкам (decimal имеет гораздо больший диапазон, чем любой из них - он основан на 96-битном целом числе).

Хотя это и не изящно как таковое, следующее, вероятно, будет одним из самых быстрых способов получить точность (при определении как «десятичные разряды, исключая конечные нули»):

public static int PrecisionOf(decimal d) {
  var text = d.ToString(System.Globalization.CultureInfo.InvariantCulture).TrimEnd('0');
  var decpoint = text.IndexOf('.');
  if (decpoint < 0)
    return 0;
  return text.Length - decpoint - 1;
}

Инвариантная культура гарантирует "." как десятичная точка, завершающие нули обрезаются, а затем просто нужно увидеть, сколько позиций осталось после десятичной точки (если она вообще есть).

Изменить: изменен тип возвращаемого значения на int

person Zastai    schedule 06.08.2015
comment
@mvmorten Не уверен, почему вы сочли необходимым изменить возвращаемый тип на int; byte более точно представляет возвращаемое значение: беззнаковый и малый диапазон (на практике 0–29). - person Zastai; 29.10.2018
comment
Я согласен с тем, что итерационные решения и решения на основе вычислений работают медленно (за исключением того, что не учитывают конечные нули). Однако выделение для этого строки и работа с ней - тоже не самая эффективная вещь, особенно в критических для производительности контекстах и ​​с медленным сборщиком мусора. Доступ к шкале с помощью логики указателя происходит намного быстрее и не требует выделения памяти. - person Martin Tilo Schmitz; 05.02.2019
comment
Да, получить масштаб можно гораздо эффективнее, но это будет включать в себя завершающие нули. И для их удаления требуется произвести арифметические действия с целой частью. - person Zastai; 14.02.2020

А вот еще один способ - использовать тип SqlDecimal, у которого есть свойство scale с подсчетом цифр справа от десятичной дроби. Преобразуйте десятичное значение в SqlDecimal, а затем откройте Scale.

((SqlDecimal)(decimal)yourValue).Scale
person RitchieD    schedule 07.03.2017
comment
Глядя на справочный код Microsoft, приведение к SqlDecimal внутренне использует GetBytes, поэтому он выделяет массив байтов вместо доступа к байтам в небезопасном контексте. В справочном коде даже есть примечание и закомментированный код, в котором говорится об этом и о том, как они могут это сделать. Почему они этого не сделали, для меня загадка. Я бы держался подальше от этого и обращался к битам масштабирования напрямую, вместо того, чтобы скрывать GC Alloc в этом приведении, поскольку просто не очень очевидно, что он делает под капотом. - person Martin Tilo Schmitz; 05.02.2019

На данный момент почти все перечисленные решения выделяют память GC, что в значительной степени соответствует способу работы C #, но далеко не идеально в средах, критичных к производительности. (Те, которые не выделяют циклы использования, а также не принимают во внимание конечные нули.)

Таким образом, чтобы избежать GC Allocs, вы можете просто получить доступ к битам масштабирования в небезопасном контексте. Это может показаться хрупким, но согласно справочному источнику Microsoft, структура макет десятичной дроби - Последовательный и даже имеет комментарий, чтобы не менять порядок полей:

    // NOTE: Do not change the order in which these fields are declared. The
    // native methods in this class rely on this particular order.
    private int flags;
    private int hi;
    private int lo;
    private int mid;

Как видите, первое int здесь - это поле флагов. Из документации и, как упоминалось в других комментариях здесь, мы знаем, что только биты из 16-24 кодируют масштаб и что нам нужно избегать 31-го бита, который кодирует знак. Поскольку int имеет размер 4 байта, мы можем безопасно сделать это:

internal static class DecimalExtensions
{
  public static byte GetScale(this decimal value)
  {
    unsafe
    {
      byte* v = (byte*)&value;
      return v[2];
    }
  }
}

Это должно быть наиболее производительное решение, поскольку для массива байтов или преобразований ToString не используется сборщик мусора. Я тестировал его на .Net 4.x и .Net 3.5 в Unity 2019.1. Если есть какие-либо версии, в которых это не удается, сообщите мне.

Изменить:

Спасибо @Zastai за напоминание о возможности использования явного макета структуры для практического достижения той же логики указателя за пределами небезопасного кода:

[StructLayout(LayoutKind.Explicit)]
public struct DecimalHelper
{
    const byte k_SignBit = 1 << 7;

    [FieldOffset(0)]
    public decimal Value;

    [FieldOffset(0)]
    public readonly uint Flags;
    [FieldOffset(0)]
    public readonly ushort Reserved;
    [FieldOffset(2)]
    byte m_Scale;
    public byte Scale
    {
        get
        {
            return m_Scale;
        }
        set
        {
            if(value > 28)
                throw new System.ArgumentOutOfRangeException("value", "Scale can't be bigger than 28!")
            m_Scale = value;
        }
    }
    [FieldOffset(3)]
    byte m_SignByte;
    public int Sign
    {
        get
        {
            return m_SignByte > 0 ? -1 : 1;
        }
    }
    public bool Positive
    {
        get
        {
            return (m_SignByte & k_SignBit) > 0 ;
        }
        set
        {
            m_SignByte = value ? (byte)0 : k_SignBit;
        }
    }
    [FieldOffset(4)]
    public uint Hi;
    [FieldOffset(8)]
    public uint Lo;
    [FieldOffset(12)]
    public uint Mid;

    public DecimalHelper(decimal value) : this()
    {
        Value = value;
    }

    public static implicit operator DecimalHelper(decimal value)
    {
        return new DecimalHelper(value);
    }

    public static implicit operator decimal(DecimalHelper value)
    {
        return value.Value;
    }
}

Чтобы решить исходную проблему, вы можете удалить все поля, кроме Value и Scale, но, возможно, кому-то будет полезно иметь их все.

person Martin Tilo Schmitz    schedule 04.02.2019
comment
Вы также можете избежать небезопасного кода, написав свою собственную структуру с явным макетом - поместите десятичную дробь в позицию 0, а затем байты / целые числа в соответствующих местах. Что-то вроде: [StructLayout(LayoutKind.Explicit)] public struct DecimalHelper { [FieldOffset(0)] public decimal Value; [FieldOffset(0)] public uint Flags; [FieldOffset(0)] public ushort Reserved; [FieldOffset(2)] public byte Scale; [FieldOffset(3)] public DecimalSign Sign; [FieldOffset(4)] public uint ValuePart1; [FieldOffset(8)] public ulong ValuePart2; } - person Zastai; 05.02.2019
comment
Спасибо @Zastai, хорошее замечание. Я также включил этот подход. :) - person Martin Tilo Schmitz; 05.02.2019
comment
Одно замечание: установка шкалы за пределы диапазона 0-28 приводит к поломке. ToString () имеет тенденцию работать, но арифметические операции не выполняются. - person Zastai; 06.02.2019
comment
Еще раз спасибо @Zastai, я добавил чек :) - person Martin Tilo Schmitz; 07.02.2019
comment
Другое дело: здесь несколько человек не захотели учитывать конечные десятичные нули. Если вы определяете const decimal Foo = 1.0000000000000000000000000000m;, то деление десятичной дроби на нее приведет к изменению масштаба до наименьшего возможного масштаба (т. Е. Больше не будет включать конечные десятичные нули). Я не тестировал это, чтобы увидеть, быстрее ли он, чем основанный на строках подход, который я предлагал в другом месте. - person Zastai; 07.02.2019

Я использую что-то очень похожее на ответ Клемента:

private int GetSignificantDecimalPlaces(decimal number, bool trimTrailingZeros = true)
{
  string stemp = Convert.ToString(number);

  if (trimTrailingZeros)
    stemp = stemp.TrimEnd('0');

  return stemp.Length - 1 - stemp.IndexOf(
         Application.CurrentCulture.NumberFormat.NumberDecimalSeparator);
}

Не забудьте использовать System.Windows.Forms для доступа к Application.CurrentCulture.

person RooiWillie    schedule 18.10.2019

Вчера я написал краткий небольшой метод, который также возвращает количество десятичных знаков без необходимости полагаться на какие-либо разбиения строк или культуры, что идеально:

public int GetDecimalPlaces(decimal decimalNumber) { // 
try {
    // PRESERVE:BEGIN
        int decimalPlaces = 1;
        decimal powers = 10.0m;
        if (decimalNumber > 0.0m) {
            while ((decimalNumber * powers) % 1 != 0.0m) {
                powers *= 10.0m;
                ++decimalPlaces;
            }
        }
return decimalPlaces;
person Jesse Carter    schedule 21.11.2012
comment
@ fix-like-codings, аналогичные вашему второму ответу, хотя для чего-то вроде этого я предпочитаю итеративный подход, а не использование рекурсии - person Jesse Carter; 21.11.2012
comment
В исходном сообщении говорится, что: 19.0 should return 1. Это решение всегда предполагает минимальное количество знаков после запятой и игнорирует конечные нули. decimal может иметь их, поскольку использует масштабный коэффициент. К коэффициенту масштабирования можно получить доступ, как в байтах 16-24 элемента с индексом 3 в массиве, полученном из Decimal.GetBytes(), или с помощью логики указателя. - person Martin Tilo Schmitz; 05.02.2019

Я использую в своем коде следующий механизм

  public static int GetDecimalLength(string tempValue)
    {
        int decimalLength = 0;
        if (tempValue.Contains('.') || tempValue.Contains(','))
        {
            char[] separator = new char[] { '.', ',' };
            string[] tempstring = tempValue.Split(separator);

            decimalLength = tempstring[1].Length;
        }
        return decimalLength;
    }

десятичный ввод = 3,376; var instring = input.ToString ();

вызвать GetDecimalLength (instring)

person Srikanth    schedule 10.02.2014
comment
Это не работает для меня, поскольку представление десятичного значения ToString () добавляет 00 в конец моих данных - я использую тип данных Decimal (12,4) из SQL Server. - person PeterX; 18.06.2014
comment
Можете ли вы преобразовать свои данные в десятичный тип С # и попробовать решение. Для меня, когда я использую Tostring () для десятичного значения С #, я никогда не вижу 00. - person Srikanth; 19.06.2014

Используя рекурсию, вы можете:

private int GetDecimals(decimal n, int decimals = 0)  
{  
    return n % 1 != 0 ? GetDecimals(n * 10, decimals + 1) : decimals;  
}
person Mars    schedule 18.12.2017
comment
В исходном сообщении говорится, что: 19.0 should return 1. Это решение игнорирует конечные нули. decimal может иметь их, поскольку использует масштабный коэффициент. К коэффициенту масштабирования можно получить доступ, как в байтах 16-24 элемента с индексом 3 в массиве Decimal.GetBytes(), или с помощью логики указателя. - person Martin Tilo Schmitz; 05.02.2019

В качестве метода десятичного расширения, учитывающего:

  • Разные культуры
  • Целые числа
  • Отрицательные числа
  • Завершающий набор нулей в десятичном разряде (например, 1,2300M вернет 2, а не 4)
public static class DecimalExtensions
{
    public static int GetNumberDecimalPlaces(this decimal source)
    {
        var parts = source.ToString(CultureInfo.InvariantCulture).Split('.');

        if (parts.Length < 2)
            return 0;

        return parts[1].TrimEnd('0').Length;
    }
}
person bytedev    schedule 09.07.2020

Можешь попробовать:

int priceDecimalPlaces =
        price.ToString(System.Globalization.CultureInfo.InvariantCulture)
              .Split('.')[1].Length;
person NicoRiff    schedule 20.11.2012
comment
Разве это не сработает, если десятичная дробь - целое число? [1] - person Silvermind; 20.11.2012

Предлагаю воспользоваться этим методом:

    public static int GetNumberOfDecimalPlaces(decimal value, int maxNumber)
    {
        if (maxNumber == 0)
            return 0;

        if (maxNumber > 28)
            maxNumber = 28;

        bool isEqual = false;
        int placeCount = maxNumber;
        while (placeCount > 0)
        {
            decimal vl = Math.Round(value, placeCount - 1);
            decimal vh = Math.Round(value, placeCount);
            isEqual = (vl == vh);

            if (isEqual == false)
                break;

            placeCount--;
        }
        return Math.Min(placeCount, maxNumber); 
    }
person Veysel Ozdemir    schedule 13.02.2017