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

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

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

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

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

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

20 ответов

Решение

Я использовал способ Джо, чтобы решить эту проблему:)

decimal argument = 123.456m;
int count = BitConverter.GetBytes(decimal.GetBits(argument)[3])[2];

Поскольку ни один из предоставленных ответов не был достаточно хорош для магического числа "-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
}

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

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

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

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

Полагаться на внутреннее представление десятичных чисел не круто.

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

    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();
    }

Одно из лучших решений для определения количества цифр после десятичной точки показано в записи 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);

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

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

Таким образом, 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

Вы можете использовать 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);
}

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

((SqlDecimal)(decimal)yourValue).Scale

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

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.

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

  • Разные культуры
  • Целые числа
  • Отрицательные числа
  • Завершающий набор нулей в десятичном разряде (например, 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;
    }
}

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

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

    // 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 здесь - это поле flags. Из документации и, как уже упоминалось в других комментариях, мы знаем, что только биты из 16-24 кодируют шкалу и что нам нужно избегать 31-го бита, который кодирует знак. Поскольку int имеет размер 4 байта, мы можем безопасно сделать это:

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

Это должно быть наиболее производительным решением, так как нет выделения GC массива байтов или преобразований 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 но, возможно, кому-то будет полезно иметь их всех.

На самом деле я протестировал большинство решений здесь. Некоторые из них быстрые, но ненадежные, некоторые надежные, но не быстрые. С модификацией ответа @RooiWillie я получаю это достаточно быстро и надежно:

      public static int GetSignificantDecimalPlaces(decimal number)
{
    if (number % 1 == 0) return 0;
    var numstr = number.ToString(CultureInfo.InvariantCulture).TrimEnd('0');
    return numstr.Length - 1 - numstr.IndexOf('.');
}

Примечание. Не учитываются конечные нули.

xUnit тесты:

      [Theory]
[InlineData(0, 0)]
[InlineData(1.0, 0)]
[InlineData(100, 0)]
[InlineData(100.10, 1)]
[InlineData(100.05, 2)]
[InlineData(100.0200, 2)]
[InlineData(0.0000000001, 10)]
[InlineData(-52.12340, 4)]
public void GetSignificantDecimalPlaces(decimal number, int expected)
{
    var actual = GetSignificantDecimalPlaces(number);
    Assert.Equal(expected, actual);
}

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

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;

При условииDecimal.Scaleсвойство было раскрыто (предложение ) в .NET 7, и если принять во внимание трюк Томаса Матерны, мы можем написать:

      public static int GetDecimalPlaces(this decimal number)
{
    if (number.Scale == 0)
        return 0;

    number /= 1.000000000000000000000000000000000m;

    return number.Scale;
}

И (xUnit) тесты все еще проходят:

      Assert.Equal(0, 0.0m.GetDecimalPlaces());
Assert.Equal(0, 1.0m.GetDecimalPlaces());
Assert.Equal(0, (-1.0m).GetDecimalPlaces());

Assert.Equal(2, 0.01m.GetDecimalPlaces());

Assert.Equal(3, 1.123m.GetDecimalPlaces());
Assert.Equal(3, (-1.123m).GetDecimalPlaces());
Assert.Equal(3, 0.001m.GetDecimalPlaces());

Assert.Equal(5, 43.12345m.GetDecimalPlaces());
Assert.Equal(5, 0.00005m.GetDecimalPlaces());
Assert.Equal(5, 0.00001m.GetDecimalPlaces());

Assert.Equal(7, 0.0000001m.GetDecimalPlaces());
Assert.Equal(8, 0.00000001m.GetDecimalPlaces());
Assert.Equal(9, 0.000000001m.GetDecimalPlaces());
Assert.Equal(10, 0.0000000001m.GetDecimalPlaces());
Assert.Equal(11, 0.00000000001m.GetDecimalPlaces());
Assert.Equal(12, 0.000000000001m.GetDecimalPlaces());
Assert.Equal(13, 0.0000000000001m.GetDecimalPlaces());
Assert.Equal(14, 0.00000000000001m.GetDecimalPlaces());
Assert.Equal(15, 0.000000000000001m.GetDecimalPlaces());
Assert.Equal(16, 0.0000000000000001m.GetDecimalPlaces());
Assert.Equal(17, 0.00000000000000001m.GetDecimalPlaces());
Assert.Equal(18, 0.000000000000000001m.GetDecimalPlaces());
Assert.Equal(19, 0.0000000000000000001m.GetDecimalPlaces());
Assert.Equal(20, 0.00000000000000000001m.GetDecimalPlaces());

Assert.Equal(19, 0.00000000000000000010m.GetDecimalPlaces());
Assert.Equal(18, 0.00000000000000000100m.GetDecimalPlaces());
Assert.Equal(17, 0.00000000000000001000m.GetDecimalPlaces());
Assert.Equal(16, 0.00000000000000010000m.GetDecimalPlaces());
Assert.Equal(15, 0.00000000000000100000m.GetDecimalPlaces());
Assert.Equal(14, 0.00000000000001000000m.GetDecimalPlaces());
Assert.Equal(13, 0.00000000000010000000m.GetDecimalPlaces());
Assert.Equal(12, 0.00000000000100000000m.GetDecimalPlaces());
Assert.Equal(11, 0.00000000001000000000m.GetDecimalPlaces());
Assert.Equal(10, 0.00000000010000000000m.GetDecimalPlaces());
Assert.Equal(9, 0.00000000100000000000m.GetDecimalPlaces());
Assert.Equal(8, 0.00000001000000000000m.GetDecimalPlaces());
Assert.Equal(7, 0.00000010000000000000m.GetDecimalPlaces());
Assert.Equal(6, 0.00000100000000000000m.GetDecimalPlaces());
Assert.Equal(5, 0.00001000000000000000m.GetDecimalPlaces());
Assert.Equal(4, 0.00010000000000000000m.GetDecimalPlaces());
Assert.Equal(3, 0.00100000000000000000m.GetDecimalPlaces());
Assert.Equal(2, 0.01000000000000000000m.GetDecimalPlaces());
Assert.Equal(1, 0.10000000000000000000m.GetDecimalPlaces());

Кроме того, есть также новое предложение добавить запрошенную функцию . Жаль, что https://github.com/dotnet/runtime/issues/25715#issue-558361050 был закрыт, так как предложения были хорошими.

Насколько смелым нужно быть, чтобы использовать этот фрагмент, еще предстоит определить :)

С помощью рекурсии вы можете сделать:

private int GetDecimals(decimal n, int decimals = 0)  
{  
    return n % 1 != 0 ? GetDecimals(n * 10, decimals + 1) : decimals;  
}

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

  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)

Ты можешь попробовать:

int priceDecimalPlaces =
        price.ToString(System.Globalization.CultureInfo.InvariantCulture)
              .Split('.')[1].Length;
string number = "123.456789"; // Convert to string
int length = number.Substring(number.IndexOf(".") + 1).Length;  // 6

Начиная с .Net 5,decimal.GetBitsимеет перегрузку, которая занимаетSpan<int>в качестве пункта назначения. Это позволяет избежать выделения нового массива в куче GC без необходимости возиться с отражением в закрытых членах .

      static int GetDecimalPlaces(decimal value)
{
    Span<int> data = stackalloc int[4];
    decimal.GetBits(value, data);
    // extract bits 16-23 of the flags value
    const int mask = (1 << 8) - 1;
    return (data[3] >> 16) & mask;
}

Обратите внимание, что это отвечает на случай, указанный в вопросе, где 19.0 указано для возврата 1. Это соответствует тому, как .NetSystem.Decimalstruct хранит десятичные разряды, включая конечные нули (которые могут рассматриваться как важные для некоторых приложений, например, для представления измерений с заданной точностью).

Ограничение здесь заключается в том, что это очень специфично для десятичного формата .Net, и преобразования из других типов с плавающей запятой могут дать не то, что вы ожидаете. Например, случай преобразования значения0.01f(который на самом деле хранит число 0,00999999977648258209228515625), чтобы получить значение0.010mскорее, чем0.01m(это можно увидеть, передав значение вToString()), и, таким образом, на выходе будет 3, а не 2. Получение значения десятичных разрядов вdecimalзначение, исключая конечные нули, - это другой вопрос.

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

    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); 
    }
Другие вопросы по тегам