Encontre o número de casas decimais no valor decimal, independentemente da cultura

Eu estou querendo saber se existe uma maneira concisa e precisa para retirar o número de casas decimais em um valor decimal (como um int) que será seguro para usar em diferentes informações de cultura?

Por exemplo:
19.0 deve retornar 1,
27.5999 deve retornar 4,
19.12 deve retornar 2,
etc.

Eu escrevi uma consulta que fez uma string dividida em um período para encontrar casas decimais:

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

Mas me ocorre que isso só funcionará em regiões que usam o ‘.’ como um separador decimal e, portanto, é muito frágil em diferentes sistemas.

Eu usei o jeito de Joe para resolver esse problema 🙂

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

Já que nenhuma das respostas fornecidas era boa o suficiente para o número mágico “-0.01f” convertido em decimal … ie: GetDecimal((decimal)-0.01f);
Eu só posso supor que um vírus colossal mente-peido atacou todos há 3 anos 🙂
Aqui está o que parece ser uma implementação de trabalho para este problema mal e monstruoso, o problema muito complicado de contar as casas decimais após o ponto – sem cordas, sem culturas, sem necessidade de contar os bits e sem necessidade de ler fóruns de matemática. matemática simples de 3ª série.

 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 } 

Eu provavelmente usaria a solução na resposta do @fixagon .

No entanto, enquanto a estrutura Decimal não tem um método para obter o número de decimais, você poderia chamar Decimal.GetBits para extrair a representação binária, em seguida, usar o valor inteiro e escala para calcular o número de decimais.

Isso provavelmente seria mais rápido que a formatação como uma string, embora você tenha que processar uma quantidade considerável de decimais para notar a diferença.

Vou deixar a implementação como um exercício.

Uma das melhores soluções para encontrar o número de dígitos após o ponto decimal é mostrada na postagem de burning_LEGION .

Aqui estou usando partes de um artigo do fórum STSdb: Número de dígitos após o ponto decimal .

No MSDN, podemos ler a seguinte explicação:

“Um número decimal é um valor de ponto flutuante que consiste em um sinal, um valor numérico em que cada dígito no valor varia de 0 a 9 e um fator de escala que indica a posição de um ponto decimal flutuante que separa a integral e fracionária. partes do valor numérico. “

E também:

“A representação binária de um valor Decimal consiste em um sinal de 1 bit, um número inteiro de 96 bits e um fator de escala usado para dividir o inteiro de 96 bits e especificar qual parte dele é uma fração decimal. O fator de escala é implicitamente o número 10, elevado a um expoente que varia de 0 a 28. “

No nível interno, o valor decimal é representado por quatro valores inteiros.

Representação interna decimal

Existe uma function GetBits publicamente disponível para obter a representação interna. A function retorna um array int []:

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

O quarto elemento da matriz retornada contém um fator de escala e um sinal. E como o MSDN diz que o fator de escala é implicitamente o número 10, aumentado para um expoente variando de 0 a 28. Isso é exatamente o que precisamos.

Assim, com base em todas as investigações acima, podemos construir nosso método:

 private const int SIGN_MASK = ~Int32.MinValue; public static int GetDigits4(decimal value) { return (Decimal.GetBits(value)[3] & SIGN_MASK) >> 16; } 

Aqui, um SIGN_MASK é usado para ignorar o sinal. Depois de lógico e também mudamos o resultado com 16 bits para a direita para receber o fator de escala real. Este valor, finalmente, indica o número de dígitos após o ponto decimal.

Note que aqui o MSDN também diz que o fator de escala também preserva qualquer zeros à direita em um número Decimal. Zeros à direita não afetam o valor de um número Decimal em operações aritméticas ou de comparação. No entanto, zeros à direita podem ser revelados pelo método ToString se uma seqüência de formato apropriada for aplicada.

Esta solução parece a melhor, mas espere, há mais. Ao acessar methods privados em C # , podemos usar expressões para construir um access direto ao campo flags e evitar a construção da matriz 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 GetDigitsLambda; public DecimalHelper() { GetDigitsLambda = CreateGetDigitsMethod(); GetDigits = GetDigitsLambda.Compile(); } private Expression 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(digits, value); } } 

Este código compilado é atribuído ao campo GetDigits. Observe que a function recebe o valor decimal como ref, portanto, nenhuma cópia real é executada – apenas uma referência ao valor. Usando a function GetDigits do DecimalHelper é fácil:

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

Este é o método mais rápido possível para obter números de dígitos após o ponto decimal para valores decimais.

você pode usar a InvariantCulture

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

Outra possibilidade seria fazer algo assim:

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

E aqui está outra maneira, use o tipo SqlDecimal que tem uma propriedade de escala com a contagem dos dígitos à direita do decimal. Transmita seu valor decimal para SqlDecimal e, em seguida, acesse a escala.

 ((SqlDecimal)(decimal)yourValue).Scale 

Confiar na representação interna de decimais não é legal.

Que tal agora:

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

A maioria das pessoas aqui parece não saber que o decimal considera zeros à direita como significativos para armazenamento e impressão.

Assim, 0,1 m, 0,10 m e 0,100 m podem ser comparados como iguais, eles são armazenados de forma diferente (como valor / escala 1/1, 10/2 e 100/3, respectivamente) e serão impressos como 0,1, 0,10 e 0,100, respectivamente , por ToString() .

Como tal, as soluções que relatam “uma precisão muito alta” estão relatando a precisão correta , em termos decimal .

Além disso, soluções baseadas em matemática (como multiplicar por potências de 10) provavelmente serão muito lentas (decimal é ~ 40x mais lento que o dobro para aritmética, e você não quer misturar em ponto flutuante porque é provável que introduza imprecisão ). Da mesma forma, a conversão para int ou long como um meio de truncamento é propensa a erros (o decimal tem um intervalo muito maior do que qualquer um deles – é baseado em um inteiro de 96 bits).

Apesar de não ser elegante como tal, o seguinte provavelmente será uma das maneiras mais rápidas de obter a precisão (quando definido como “casas decimais excluindo zeros à direita”):

 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 cultura invariável garante um '.' como ponto decimal, zeros à direita são aparados e, em seguida, é apenas uma questão de ver quantas posições permanecem após o ponto decimal (se houver um).

Editar: mudou o tipo de retorno para int

Eu escrevi um pequeno método conciso ontem que também retorna o número de casas decimais sem ter que confiar em nenhuma divisão ou cultura de string que é ideal:

 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; 

Podes tentar:

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

Eu uso o seguinte mecanismo no meu código

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

input decimal = 3,376; var instring = input.ToString ();

chamar GetDecimalLength (instrução)

Eu sugiro usar este método:

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

Usando a recursion, você pode fazer:

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