Pregunta Encontrar el número de lugares decimales en valor decimal independientemente de la cultura


Me pregunto si hay una forma concisa y precisa para sacar el número de decimales en un valor decimal (como un int) que será seguro de usar en diferentes culturas de información?

Por ejemplo:
19.0 debería devolver 1,
27.5999 debería devolver 4,
19.12 debe devolver 2,
etc.

Escribí una consulta que realizaba una división de cadenas en un período para buscar 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 un separador decimal y, por lo tanto, es muy frágil en diferentes sistemas.


55
2017-11-20 16:31


origen


Respuestas:


solía El camino de Joe para resolver este problema :)

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

120
2017-11-21 12:55



Dado que ninguna de las respuestas suministradas era lo suficientemente buena para el número mágico "-0.01f" convertido a decimal ... es decir: GetDecimal((decimal)-0.01f); 
Solo puedo suponer que un virus colosal de pedo-mente atacó a todos hace 3 años :)
Esto es lo que parece ser una implementación operativa de este malvado y monstruoso problema, el muy complicado problema de contar los decimales después del punto: sin cadenas, sin culturas, sin necesidad de contar los bits y sin necesidad de leer los foros de matemáticas. solo simples matemáticas de 3er 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
}

16
2018-05-13 03:27



Probablemente usaría la solución en @ respuesta de fixagon.

Sin embargo, aunque la estructura Decimal no tiene un método para obtener el número de decimales, puedes llamar Decimal.GetBits para extraer la representación binaria, luego use el valor entero y la escala para calcular el número de decimales.

Esto probablemente sería más rápido que formatearlo como una cadena, aunque tendría que procesar una gran cantidad de decimales para notar la diferencia.

Dejaré la implementación como un ejercicio.


15
2017-11-20 16:46



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 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 consiste en un signo, un valor numérico donde cada dígito en el valor varía de 0 a 9, y un factor de escala que indica la posición de un punto decimal flotante que separa las partes integrales y fraccionarias del valor numérico ".

Y también:

"La representación binaria de un valor decimal consiste en un signo de 1 bit, un número entero de 96 bits y un factor de escala utilizado para dividir el entero de 96 bits y especifica 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. "

En el nivel interno, el valor decimal está representado por cuatro valores enteros.

Decimal internal representation

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 MSDN dice que el factor de escala es implícitamente el número 10, se eleva a un exponente que va de 0 a 28. Esto es exactamente lo que necesitamos.

Por lo tanto, en base a 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 SIGN_MASK para ignorar el signo. Después de lógico y también hemos cambiado 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 al final en un número decimal. Los ceros finales no afectan el valor de un número decimal en operaciones aritméticas u operaciones de comparación. Sin embargo, los ceros finales pueden revelarse mediante el método ToString si se aplica una cadena de formato adecuada.

Esta solución parece la mejor, pero espera, hay más. Por acceder a métodos privados en C # podemos usar expresiones para construir 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 del 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.


13
2017-07-03 08:39



puedes usar InvariantCulture

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

otra posibilidad sería hacer algo como eso:

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

10
2017-11-20 16:35



Y aquí está otra manera, use el tipo SqlDecimal que tiene una propiedad de escala con el recuento de los dígitos a la derecha del decimal. Emite tu valor decimal a SqlDecimal y luego accede a Escala.

((SqlDecimal)(decimal)yourValue).Scale

5
2018-03-07 20:20



Confiar en la representación interna de decimales no es genial.

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

4
2017-09-29 07:08



La mayoría de la gente aquí parece ignorar que el decimal considera que los ceros finales son significativos para el almacenamiento y la impresión.

Entonces, 0.1m, 0.10m y 0.100m se pueden comparar como iguales, se almacenan de forma 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 "demasiada precisión" están realmente informando el correcto precisión, en decimaltérminos de s.

Además, las soluciones basadas en matemáticas (como la multiplicación por potencias de 10) probablemente serán muy lentas (el decimal es ~ 40x más lento que el doble para la aritmética, y tampoco se debe mezclar en punto flotante porque es probable que introduzca imprecisión ) Del mismo modo, lanzando a int o long como medio de truncar es propenso a errores (decimal tiene un alcance mucho mayor que cualquiera de ellos, se basa en un entero de 96 bits).

Si bien no es elegante como tal, la siguiente será una de las maneras 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 solo se trata de ver cuántas posiciones quedan después del punto decimal (si es que hay uno).

Editar: tipo de retorno cambiado a int


4
2017-08-06 08:32



Ayer escribí un pequeño y conciso método que también devuelve el número de decimales sin tener que depender de ninguna división de cuerda o cultura que sea 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;

1
2017-11-21 14:14



Puedes probar:

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

1
2017-11-20 16:42