Estou me perguntando se existe uma maneira concisa e precisa de extrair 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 dividia a string em um ponto 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 separador decimal e, portanto, muito frágil em diferentes sistemas.
c#
decimal
cultureinfo
Jesse Carter
fonte
fonte
19.0
para retornar1
é um detalhe de implementação referente ao armazenamento interno do valor19.0
. O fato é que é perfeitamente legítimo que o programa armazene isso como190×10⁻¹
ou1900×10⁻²
ou19000×10⁻³
. Todos esses são iguais. O fato de que ele usa a primeira representação quando recebe um valor de19.0M
e isso é exposto ao usarToString
sem um especificador de formato é apenas uma coincidência, e uma coisa feliz. Exceto que não fica feliz quando as pessoas confiam no expoente em casos onde não deveriam.19M
a partir19.0M
de19.00M
, você precisa criar uma nova classe que empacota o valor subjacente como uma propriedade eo número de casas decimais como outra propriedade.Respostas:
Usei a maneira de Joe para resolver esse problema :)
decimal argument = 123.456m; int count = BitConverter.GetBytes(decimal.GetBits(argument)[3])[2];
fonte
decimal
mantém o dígito de contagem após a vírgula, é por isso que você encontra este "problema", você deve converter o decimal em duplo e novamente em decimal para corrigir: BitConverter.GetBytes (decimal.GetBits ((decimal) (double) argument) [3]) [ 2];Uma vez que nenhuma das respostas fornecidas foram boas 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 de peido mental atacou a todos há 3 anos :)
Aqui está o que parece estar funcionando implementação para este problema maligno 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 .. apenas 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 }
fonte
n = n % 1; if (n < 0) n = -n;
porque um valor maior queint.MaxValue
causará umOverflowException
, por exemplo2147483648.12345
.Eu provavelmente usaria a solução na resposta de @fixagon .
No entanto, embora a estrutura Decimal não tenha um método para obter o número de decimais, você pode chamar Decimal.GetBits para extrair a representação binária e, em seguida, usar o valor inteiro e a escala para calcular o número de decimais.
Isso provavelmente seria mais rápido do que formatar como uma string, embora você tenha que processar uma quantidade enorme de decimais para notar a diferença.
Vou deixar a implementação como um exercício.
fonte
Uma das melhores soluções para encontrar o número de dígitos após a vírgula decimal é mostrada na postagem de burn_LEGION .
Aqui estou usando partes de um artigo do fórum STSdb: Número de dígitos após a vírgula 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 o integral do fracionário 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.
Há uma função GetBits disponível publicamente para obter a representação interna. A função retorna uma matriz 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 diz o MSDN, o fator de escala é implicitamente o número 10, elevado a um expoente que varia 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 à direita para receber o fator de escala real. Este valor, por fim, indica o número de dígitos após a vírgula decimal.
Observe que aqui o MSDN também informa que o fator de escala também preserva quaisquer zeros à direita em um número decimal. Os zeros à direita não afetam o valor de um número decimal em operações aritméticas ou de comparação. No entanto, zeros finais podem ser revelados pelo método ToString se uma seqüência de caracteres de formato apropriada for aplicada.
Esta solução parece ser a melhor, mas espere, tem mais. Ao acessar métodos privados em C # podemos usar as expressões de construir um acesso directo ao campo de bandeiras e evitar construir a 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<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); } }
Este código compilado é atribuído ao campo GetDigits. Observe que a função recebe o valor decimal como ref, portanto, nenhuma cópia real é executada - apenas uma referência ao valor. Usar a função 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 o número de dígitos após a vírgula decimal para valores decimais.
fonte
0.01
e0.010
são números exatamente iguais . Além disso, a ideia de que um tipo de dado numérico tem algum tipo de semântica de "número de dígitos usados" que pode ser confiável é completamente errada (não deve ser confundida com "número de dígitos permitidos". Não confunda a apresentação (a exibição de o valor de um número em uma base particular, por exemplo, a expansão decimal do valor indicado pela expansão binária 111) com o valor subjacente! Para reiterar, os números não são dígitos, nem são compostos de dígitos .Depender da 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(); }
fonte
você pode usar o 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); }
fonte
.
.A maioria das pessoas aqui parece não estar ciente de que o decimal considera os zeros à direita como significativos para armazenamento e impressão.
Portanto, 0,1m, 0,10m e 0,100m 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, na verdade, relatando a precisão correta , em
decimal
termos de.Além disso, as soluções baseadas em matemática (como a multiplicação por potências de 10) provavelmente serão muito lentas (decimal é ~ 40x mais lento do que o dobro para aritmética, e você não quer misturar em ponto flutuante porque isso provavelmente introduzirá imprecisão ) Da mesma forma, a conversão para
int
oulong
como meio de truncamento está sujeita a erros (decimal
tem um intervalo muito maior do que qualquer um deles - é baseado em um número inteiro de 96 bits).Embora não seja 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, os zeros à direita são aparados, e então é apenas uma questão de ver quantas posições permanecem após o ponto decimal (se houver um).
Editar: tipo de retorno alterado para int
fonte
E aqui está outra maneira, use o tipo SqlDecimal que possui uma propriedade de escala com a contagem dos dígitos à direita do decimal. Converta seu valor decimal para SqlDecimal e acesse Escala.
((SqlDecimal)(decimal)yourValue).Scale
fonte
GetBytes
portanto, aloca a matriz de bytes em vez de acessar os bytes em um contexto não seguro. Existe até uma nota e um código comentado no código de referência, informando isso e como eles poderiam fazer isso. Por que eles não fizeram isso é um mistério para mim. Eu ficaria longe disso e acessaria os bits de escala diretamente, em vez de esconder o GC Alloc neste elenco, já que não é muito óbvio o que ele faz por baixo do capô.Até agora, quase todas as soluções listadas estão alocando memória GC, que é a maneira C # de fazer as coisas, mas está longe de ser ideal em ambientes de desempenho crítico. (Aqueles que não alocam usam loops e também não levam os zeros à direita em consideração.)
Portanto, para evitar alocações de GC, você pode apenas acessar os bits de escala em um contexto não seguro. Isso pode parecer frágil, mas de acordo com a fonte de referência da Microsoft , o layout da estrutura do decimal é Sequencial e ainda tem um comentário lá, para não alterar a ordem dos campos:
// 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;
Como você pode ver, o primeiro int aqui é o campo de sinalizadores. Pela documentação e conforme mencionado em outros comentários aqui, sabemos que apenas os bits de 16-24 codificam a escala e que precisamos evitar o 31º bit que codifica o sinal. Como int tem o tamanho de 4 bytes, podemos fazer isso com segurança:
internal static class DecimalExtensions { public static byte GetScale(this decimal value) { unsafe { byte* v = (byte*)&value; return v[2]; } } }
Esta deve ser a solução de melhor desempenho, uma vez que não há alocação de GC da matriz de bytes ou conversões de ToString. Eu testei com .Net 4.xe .Net 3.5 no Unity 2019.1. Se houver alguma versão em que isso falhe, por favor me avise.
Editar:
Agradeço a @Zastai por me lembrar da possibilidade de usar um layout de estrutura explícito para praticamente atingir a mesma lógica de ponteiro fora do código não seguro:
[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; } }
Para resolver o problema original, você poderia remover todos os campos além de
Value
e,Scale
mas talvez pudesse ser útil para alguém ter todos eles.fonte
[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; }
const decimal Foo = 1.0000000000000000000000000000m;
, dividir um decimal por aquele irá redimensioná-lo para a escala mais baixa possível (ou seja, não incluindo mais os zeros decimais à direita). Eu não fiz o benchmarking para ver se é ou não mais rápido do que a abordagem baseada em strings que sugeri em outro lugar.Estou usando algo muito semelhante à resposta de Clemente:
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); }
Lembre-se de usar System.Windows.Forms para obter acesso a Application.CurrentCulture
fonte
Eu escrevi um pequeno método conciso ontem que também retorna o número de casas decimais sem ter que contar com nenhuma divisão de string ou culturas, o 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;
fonte
19.0 should return 1
. Esta solução sempre assumirá uma quantidade mínima de 1 casa decimal e ignorará os zeros à direita. decimal pode ter esses, pois usa um fator de escala. O fator de escala pode ser acessado como nos bytes 16-24 do elemento com índice 3 no array obtido deDecimal.GetBytes()
ou usando a lógica de ponteiro.Eu uso o seguinte mecanismo em 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; }
entrada decimal = 3,376; var instring = input.ToString ();
chame GetDecimalLength (instruindo)
fonte
Usando a recursão, você pode fazer:
private int GetDecimals(decimal n, int decimals = 0) { return n % 1 != 0 ? GetDecimals(n * 10, decimals + 1) : decimals; }
fonte
19.0 should return 1
. Esta solução irá ignorar os zeros finais. decimal pode ter esses, pois usa um fator de escala. O fator de escala pode ser acessado como nos bytes 16-24 do elemento com índice 3 noDecimal.GetBytes()
array ou usando a lógica de ponteiro.string number = "123.456789"; // Convert to string int length = number.Substring(number.IndexOf(".") + 1).Length; // 6
fonte
Podes tentar:
int priceDecimalPlaces = price.ToString(System.Globalization.CultureInfo.InvariantCulture) .Split('.')[1].Length;
fonte
[1]
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); }
fonte
Como um método de extensão decimal que leva em consideração:
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; } }
fonte