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


Me pregunto si hay una manera concisa y precisa de extraer el número de lugares decimales en un valor decimal (como un int) que será seguro de usar en diferentes información de la cultura?

Por ejemplo:
19.0 debe devolver 1,
27.5999 debe devolver 4,
19.12 debe devolver 2,
sucesivamente.

Escribí una consulta que dividió una cadena en un punto 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 la'."como separador decimal y por lo tanto es muy frágil a través de diferentes sistemas.

Author: Austin Salonen, 2012-11-20

13 answers

Usé Joe's way para resolver este problema:)

decimal argument = 123.456m;
int count = BitConverter.GetBytes(decimal.GetBits(argument)[3])[2];
 124
Author: burning_LEGION,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 10:31:31

Ya que ninguna de las respuestas suministradas era lo suficientemente buena para el número mágico "-0.01 f" 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 una implementación de trabajo a este problema malvado y monstruoso, el problema muy complicado de contar los lugares 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 simple 3er grado matemáticas.

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
}
 18
Author: G.Y,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-05-13 03:27:55

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

Sin embargo, mientras que 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, utilice 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.

Me iré the implementation as an exercise.

 15
Author: Joe,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 11:54:58

Una de las mejores soluciones para encontrar el número de dígitos después del punto decimal se muestra en Burning_legion's post.

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 integral y fraccional 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 especificar qué porción de ella 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.

Representación interna decimal

Existe 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, sobre la base de todas las investigaciones anteriores que puede 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 una máscara de SIGNO 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 al valor de un número decimal en operaciones aritméticas o de comparación. Sin embargo, los ceros finales podrían ser revelados por el método toString si se aplica una cadena de formato adecuada.

Esta solución parece la mejor, pero espera, hay más. accediendo a métodos privados en C# podemos usar expresiones para construir un acceso directo al campo flags y evitar construir el array 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 hay copia real es realizado - 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 los valores decimales.

 13
Author: Kris,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 12:03:08

Puede usar la cultura invariante

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);
}
 10
Author: fixagon,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-11-20 17:12:07

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();
    }
 5
Author: Clement,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-09-29 07:08:16

Y aquí hay otra manera, use el tipo SqlDecimal que tiene una propiedad de escala con el recuento de los dígitos a la derecha del decimal. Convierta su valor decimal en SqlDecimal y luego acceda a la escala.

((SqlDecimal)(decimal)yourValue).Scale
 5
Author: RitchieD,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-03-07 23:22:51

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

Así que 0.1 m, 0.10 m y 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 reportan "una precisión demasiado alta" en realidad reportan la precisión correcta, en términos de decimal.

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 en coma flotante porque es probable que introduzca imprecisión). Del mismo modo, la conversión a int o long como un medio de truncar es propensa a errores (decimal tiene un rango mucho mayor que cualquiera de ellos-se basa en un 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 "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 a '.'como punto decimal, se recortan los ceros finales, y luego es solo cuestión de ver cuántas posiciones quedan después del punto decimal (si incluso hay una).

Editar: se ha cambiado el tipo de retorno a int

 4
Author: Zastai,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-10-15 18:19:16

Ayer escribí un pequeño método conciso que también devuelve el número de decimales sin tener que depender de ninguna división de cadenas o culturas, 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;
 1
Author: Jesse Carter,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-11-21 14:14:17

Puedes probar:

int priceDecimalPlaces =
        price.ToString(System.Globalization.CultureInfo.InvariantCulture)
              .Split('.')[1].Length;
 1
Author: NicoRiff,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-08-16 15:05:55

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 = entrada.toString ();

Call GetDecimalLength (instring)

 1
Author: Srikanth,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-02-10 06:06:09

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); 
    }
 0
Author: Veysel Özdemir,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-02-13 11:10:55

Usando recursión puedes hacer:

private int GetDecimals(decimal n, int decimals = 0)  
{  
    return n % 1 != 0 ? GetDecimals(n * 10, decimals + 1) : decimals;  
}
 0
Author: Mars,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-12-20 08:59:18