Подтвердить что ты не робот

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

Мне интересно, есть ли краткий и точный способ вывести число десятичных знаков в десятичном значении (как 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;

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

4b9b3361

Ответ 1

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

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

Ответ 2

Так как ни один из ответов не был достаточно хорош для магического числа "-0.01f", преобразованного в десятичное число. i.e: 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
}

Ответ 3

Я, вероятно, воспользуюсь решением в @fixagon answer.

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

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

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

Ответ 4

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

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

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

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

А также:

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

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

Decimal internal representation

Существует общедоступная функция 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, если применяется соответствующая строка формата.

Это решение выглядит как лучшее, но подождите, это еще не все. получая доступ к закрытым методам в С#, мы можем использовать выражения для создания прямого доступа к полю флагов и избежать создания массива 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);

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

Ответ 5

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

Ответ 6

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

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

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

Ответ 7

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

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

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

Кроме того, решения на основе математики (например, умножение на степеней 10), вероятно, будут очень медленными (десятичное значение ~ 40x медленнее, чем double для арифметики, и вы не хотите смешивать в плавающей точке либо потому, что вероятно ввести неточность). Точно так же отбрасывание на 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;
}

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

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

Ответ 8

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

((SqlDecimal)(decimal)yourValue).Scale

Ответ 9

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

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;

Ответ 10

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

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

Ответ 11

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

int priceDecimalPlaces =
        price.ToString(System.Globalization.CultureInfo.InvariantCulture)
              .Split('.')[1].Length;

Ответ 12

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

  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)

Ответ 13

string number = "123.456789"; // Convert to string
int length = number.Substring(number.IndexOf(".") + 1).Length;  // 6

Ответ 14

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

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

Ответ 15

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

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

Ответ 16

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

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