Me pregunto si hay una forma concisa y precisa de extraer el número de lugares decimales en un valor decimal (como un int) que sea seguro de usar en diferentes informaciones culturales.
Por ejemplo:
19.0 debería devolver 1,
27.5999 debería devolver 4,
19.12 debería devolver 2,
etc.
Escribí una consulta que dividió una cadena en un período para encontrar lugares decimales:
int priceDecimalPlaces = price.ToString().Split('.').Count() > 1
? price.ToString().Split('.').ToList().ElementAt(1).Length
: 0;
Pero se me ocurre que esto solo funcionará en regiones que usan el '.' como separador decimal y, por lo tanto, es muy frágil en diferentes sistemas.
c#
decimal
cultureinfo
Jesse Carter
fuente
fuente
19.0
devolución1
es un detalle de implementación con respecto al almacenamiento interno del valor19.0
. El hecho es que es perfectamente legítimo que el programa almacene esto como190×10⁻¹
o1900×10⁻²
o19000×10⁻³
. Todos esos son iguales. El hecho de que use la primera representación cuando se le da un valor de19.0M
y esto se expone cuando se usaToString
sin un especificador de formato es solo una coincidencia, y algo feliz. Excepto que no es feliz cuando la gente confía en el exponente en los casos en que no debería.19M
a partir19.0M
de19.00M
, tendrá que crear una nueva clase que agrupa el valor subyacente como una propiedad y el número de lugares decimales como otra propiedad.Respuestas:
Solía manera de Joe para resolver este problema :)
decimal argument = 123.456m; int count = BitConverter.GetBytes(decimal.GetBits(argument)[3])[2];
fuente
decimal
mantiene el recuento dígito después del coma, es por eso que encuentra este "problema", tiene que convertir decimal a doble y a decimal nuevamente para corregirlo: BitConverter.GetBytes (decimal.GetBits ((decimal) (argumento doble)) [3]) [ 2];Dado que ninguna de las respuestas proporcionadas fue lo suficientemente buena para el número mágico "-0.01f" convertido a decimal ... es decir
GetDecimal((decimal)-0.01f);
, solo puedo asumir que un colosal virus de pedo mental atacó a todos hace 3 años :)
Aquí está lo que parece ser un implementación a este malvado y monstruoso problema, el muy complicado problema de contar los lugares decimales después del punto - sin cadenas, sin culturas, sin necesidad de contar los bits y sin necesidad de leer foros de matemáticas ... simplemente matemáticas de tercer grado.
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 }
fuente
n = n % 1; if (n < 0) n = -n;
porque un valor mayor queint.MaxValue
causará unOverflowException
, por ejemplo2147483648.12345
.Probablemente usaría la solución en la respuesta de @ fixagon .
Sin embargo, aunque la estructura Decimal no tiene un método para obtener el número de decimales, puede llamar a Decimal.GetBits para extraer la representación binaria, luego usar el valor entero y la escala para calcular el número de decimales.
Esto probablemente sería más rápido que formatear como una cadena, aunque tendría que estar procesando una gran cantidad de decimales para notar la diferencia.
Dejaré la implementación como ejercicio.
fuente
Una de las mejores soluciones para encontrar el número de dígitos después del punto decimal se muestra en la publicación de burning_LEGION .
Aquí estoy usando partes de un artículo del foro de STSdb: Número de dígitos después del punto decimal .
En MSDN podemos leer la siguiente explicación:
"Un número decimal es un valor de coma flotante que consta de un signo, un valor numérico en el que cada dígito del valor varía de 0 a 9 y un factor de escala que indica la posición de un punto decimal flotante que separa la integral y la fraccional. partes del valor numérico ".
Y también:
"La representación binaria de un valor decimal consta de un signo de 1 bit, un número entero de 96 bits y un factor de escala que se utiliza para dividir el entero de 96 bits y especificar qué parte de él es una fracción decimal. El factor de escala es implícitamente el número 10, elevado a un exponente que va de 0 a 28 ".
A nivel interno, el valor decimal está representado por cuatro valores enteros.
Hay una función GetBits disponible públicamente para obtener la representación interna. La función devuelve una matriz int []:
[__DynamicallyInvokable] public static int[] GetBits(decimal d) { return new int[] { d.lo, d.mid, d.hi, d.flags }; }
El cuarto elemento de la matriz devuelta contiene un factor de escala y un signo. Y como dice el MSDN, el factor de escala es implícitamente el número 10, elevado a un exponente que va de 0 a 28. Esto es exactamente lo que necesitamos.
Por lo tanto, basándonos en todas las investigaciones anteriores, podemos construir nuestro método:
private const int SIGN_MASK = ~Int32.MinValue; public static int GetDigits4(decimal value) { return (Decimal.GetBits(value)[3] & SIGN_MASK) >> 16; }
Aquí se usa un SIGN_MASK para ignorar el signo. Después de lógica y también hemos desplazado el resultado con 16 bits a la derecha para recibir el factor de escala real. Este valor, finalmente, indica el número de dígitos después del punto decimal.
Tenga en cuenta que aquí MSDN también dice que el factor de escala también conserva los ceros finales en un número decimal. Los ceros finales no afectan el valor de un número decimal en operaciones aritméticas o de comparación. Sin embargo, el método ToString puede revelar ceros finales si se aplica una cadena de formato adecuada.
Esta solución parece la mejor, pero espere, hay más. Al acceder a métodos privados en C # , podemos usar expresiones para crear un acceso directo al campo de banderas y evitar construir la 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 se asigna al campo GetDigits. Tenga en cuenta que la función recibe el valor decimal como ref, por lo que no se realiza ninguna copia real, solo una referencia al valor. Usar la función GetDigits de DecimalHelper es fácil:
decimal value = 3.14159m; int digits = DecimalHelper.Instance.GetDigits(ref value);
Este es el método más rápido posible para obtener el número de dígitos después del punto decimal para valores decimales.
fuente
0.01
y0.010
son exactamente números iguales . Además, la idea de que un tipo de datos numérico tiene algún tipo de semántica de "número de dígitos utilizados" en la que se puede confiar es completamente errónea (no debe confundirse con "número de dígitos permitido". No confunda presentación (la visualización de el valor de un número en una base particular, por ejemplo, la expansión decimal del valor indicado por la expansión binaria 111) con el valor subyacente. Para reiterar, los números no son dígitos, ni están formados por dígitos .Confiar en la representación interna de decimales no está bien.
Qué tal esto:
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(); }
fuente
puedes usar InvariantCulture
string priceSameInAllCultures = price.ToString(System.Globalization.CultureInfo.InvariantCulture);
otra posibilidad sería hacer algo así:
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); }
fuente
.
.La mayoría de la gente aquí parece no saber que decimal considera que los ceros finales son importantes para el almacenamiento y la impresión.
Por lo tanto, 0,1 m, 0,10 my 0,100 m pueden compararse como iguales, se almacenan de manera diferente (como valor / escala 1/1, 10/2 y 100/3, respectivamente) y se imprimirán como 0,1, 0,10 y 0,100, respectivamente. , por
ToString()
.Como tal, las soluciones que informan "una precisión demasiado alta" en realidad informan la precisión correcta , en
decimal
los términos.Además, las soluciones basadas en matemáticas (como multiplicar por potencias de 10) probablemente serán muy lentas (el decimal es ~ 40 veces más lento que el doble para la aritmética, y tampoco desea mezclar el punto flotante porque es probable que introduzca imprecisión ). Del mismo modo, la fundición a
int
olong
como un medio de truncar es propenso a errores (decimal
tiene una gama mucho mayor que cualquiera de los que - que está basado en torno a un número entero de 96 bits).Si bien no es elegante como tal, lo siguiente probablemente será una de las formas más rápidas de obtener la precisión (cuando se define como "lugares decimales excluyendo ceros finales"):
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; }
La cultura invariante garantiza un '.' como punto decimal, los ceros finales se recortan, y luego es solo una cuestión de ver cuántas posiciones quedan después del punto decimal (si es que hay uno).
Editar: cambió el tipo de retorno a int
fuente
Y aquí hay otra forma, use el tipo SqlDecimal que tiene una propiedad de escala con el recuento de los dígitos a la derecha del decimal. Transmita su valor decimal a SqlDecimal y luego acceda a Scale.
((SqlDecimal)(decimal)yourValue).Scale
fuente
GetBytes
por lo que asigna la matriz de bytes en lugar de acceder a los bytes en un contexto inseguro. Incluso hay una nota y un código comentado en el código de referencia, indicando eso y cómo podrían hacerlo en su lugar. Por qué no lo hicieron es un misterio para mí. Me mantendría alejado de esto y accedería a los bits de escala directamente en lugar de esconder el GC Alloc en este elenco, ya que no es muy obvio lo que hace debajo del capó.Hasta ahora, casi todas las soluciones enumeradas están asignando memoria GC, que es en gran medida la forma de C # para hacer las cosas, pero está lejos de ser ideal en entornos de rendimiento crítico. (Los que no asignan utilizan bucles y tampoco tienen en cuenta los ceros finales).
Entonces, para evitar GC Allocs, puede acceder a los bits de escala en un contexto inseguro. Eso puede sonar frágil, pero según la fuente de referencia de Microsoft , el diseño de estructura de decimal es secuencial e incluso tiene un comentario allí, no para cambiar el orden de los 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 puede ver, el primer int aquí es el campo flags. Por la documentación y como se menciona en otros comentarios aquí, sabemos que solo los bits de 16-24 codifican la escala y que debemos evitar el bit 31 que codifica el signo. Dado que int es del tamaño de 4 bytes, podemos hacer esto con seguridad:
internal static class DecimalExtensions { public static byte GetScale(this decimal value) { unsafe { byte* v = (byte*)&value; return v[2]; } } }
Esta debería ser la solución de mayor rendimiento, ya que no hay ninguna asignación GC de la matriz de bytes o conversiones ToString. Lo probé contra .Net 4.xy .Net 3.5 en Unity 2019.1. Si hay alguna versión en la que esto falle, hágamelo saber.
Editar:
Gracias a @Zastai por recordarme la posibilidad de usar un diseño de estructura explícito para lograr prácticamente la misma lógica de puntero fuera del código inseguro:
[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 el problema original, se podía tira de lejos todos los campos aparte
Value
yScale
aunque tal vez podría ser útil para alguien que tiene a todos.fuente
[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;
entonces dividir un decimal por eso lo reescalará a la escala más baja posible (es decir, ya no se incluirán los ceros decimales finales). Sin embargo, no he comparado esto para ver si es o no más rápido que el enfoque basado en cadenas que sugerí en otro lugar.Ayer escribí un pequeño método conciso que también devuelve el número de lugares decimales sin tener que depender de divisiones o culturas de cadenas, lo cual es 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;
fuente
19.0 should return 1
. Esta solución siempre asumirá una cantidad mínima de 1 lugar decimal e ignorará los ceros finales. decimal puede tenerlos ya que usa un factor de escala. Se puede acceder al factor de escala como en los bytes 16-24 del elemento con índice 3 en la matriz obtenidaDecimal.GetBytes()
o mediante la lógica de puntero.Estoy usando algo muy similar a la respuesta de Clement:
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); }
Recuerde usar System.Windows.Forms para obtener acceso a Application.CurrentCulture
fuente
Puedes probar:
int priceDecimalPlaces = price.ToString(System.Globalization.CultureInfo.InvariantCulture) .Split('.')[1].Length;
fuente
[1]
Utilizo el siguiente mecanismo en mi 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 ();
llamar a GetDecimalLength (instring)
fuente
Usando la recursividad puedes hacer:
private int GetDecimals(decimal n, int decimals = 0) { return n % 1 != 0 ? GetDecimals(n * 10, decimals + 1) : decimals; }
fuente
19.0 should return 1
. Esta solución ignorará los ceros finales. decimal puede tenerlos ya que usa un factor de escala. Se puede acceder al factor de escala como en los bytes 16-24 del elemento con índice 3 en laDecimal.GetBytes()
matriz o utilizando la lógica de puntero.string number = "123.456789"; // Convert to string int length = number.Substring(number.IndexOf(".") + 1).Length; // 6
fuente
Sugiero 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); }
fuente
Como método de extensión decimal que tiene en cuenta:
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; } }
fuente