2012-03-02 23 views
24

Es importante tener en cuenta que no estoy buscando una función de redondeo. Estoy buscando una función que devuelva el número de decimales en la representación decimal simplificada de un número arbitrario. Es decir, tenemos los siguientes:¿Existe alguna forma confiable en JavaScript para obtener el número de decimales de un número arbitrario?

decimalPlaces(5555.0);  //=> 0 
decimalPlaces(5555);  //=> 0 
decimalPlaces(555.5);  //=> 1 
decimalPlaces(555.50);  //=> 1 
decimalPlaces(0.0000005); //=> 7 
decimalPlaces(5e-7);  //=> 7 
decimalPlaces(0.00000055); //=> 8 
decimalPlaces(5.5e-7);  //=> 8 

Mi primer instinto era utilizar las representaciones de cadena: división en '.', a continuación, en 'e-', y hacer los cálculos, al igual que (el ejemplo es detallado):

function decimalPlaces(number) { 
    var parts = number.toString().split('.', 2), 
    integerPart = parts[0], 
    decimalPart = parts[1], 
    exponentPart; 

    if (integerPart.charAt(0) === '-') { 
    integerPart = integerPart.substring(1); 
    } 

    if (decimalPart !== undefined) { 
    parts = decimalPart.split('e-', 2); 
    decimalPart = parts[0]; 
    } 
    else { 
    parts = integerPart.split('e-', 2); 
    integerPart = parts[0]; 
    } 
    exponentPart = parts[1]; 

    if (exponentPart !== undefined) { 
    return integerPart.length + 
     (decimalPart !== undefined ? decimalPart.length : 0) - 1 + 
     parseInt(exponentPart); 
    } 
    else { 
    return decimalPart !== undefined ? decimalPart.length : 0; 
    } 
} 

Para mis ejemplos anteriores, esta función funciona. Sin embargo, no estoy satisfecho hasta que haya probado todos los valores posibles, así que eliminé Number.MIN_VALUE.

Number.MIN_VALUE;      //=> 5e-324 
decimalPlaces(Number.MIN_VALUE);  //=> 324 

Number.MIN_VALUE * 100;    //=> 4.94e-322 
decimalPlaces(Number.MIN_VALUE * 100); //=> 324 

Esto parecía razonable al principio, pero luego en una toma doble me di cuenta de que debería haber 5e-324 * 105e-323! Y luego me di cuenta: estoy lidiando con los efectos de la cuantificación de números muy pequeños. No solo se cuantifican los números antes del almacenamiento; adicionalmente, algunos números almacenados en binario tienen representaciones decimales irracionalmente largas, por lo que sus representaciones decimales se están truncando. Esto es desafortunado para mí, porque significa que no puedo obtener su precisión decimal real usando sus representaciones de cadena.

Así que vengo a ti, comunidad de StackOverflow. ¿Alguien entre ustedes conoce una manera confiable de llegar a la precisión de un punto verdadero después del punto decimal?

El propósito de esta función, si alguien pregunta, es para usar en otra función que convierta un flotador en una fracción simplificada (es decir, devuelve el numerador entero relativamente coprimario y el denominador natural distinto de cero). La única pieza que falta en esta función externa es una manera confiable de determinar el número de lugares decimales en el flotador para poder multiplicarlo por la potencia apropiada de 10. Afortunadamente estoy pensando demasiado.

+0

Hay un número infinito de lugares decimales en cualquier número de coma flotante. –

+2

Puede estar interesado en esto: http://blog.thatscaptaintoyou.com/introducing-big-js-arbitrary-precision-math-for-javascript/ –

+0

@LightnessRacesinOrbit: bien, bueno, no pude incluir todos los detalles en ¡el título! Sí especifico en la pregunta, sin embargo, que estoy buscando la representación simplificada. – Milosz

Respuesta

14

Nota histórica: el hilo de comentarios a continuación puede referirse a las implementaciones primera y segunda. Cambié el pedido en septiembre de 2017 ya que liderar con una implementación defectuosa causó confusión.

Si quieres algo que mapea "0.1e-100" a 101, entonces usted puede intentar algo así como

function decimalPlaces(n) { 
    // Make sure it is a number and use the builtin number -> string. 
    var s = "" + (+n); 
    // Pull out the fraction and the exponent. 
    var match = /(?:\.(\d+))?(?:[eE]([+\-]?\d+))?$/.exec(s); 
    // NaN or Infinity or integer. 
    // We arbitrarily decide that Infinity is integral. 
    if (!match) { return 0; } 
    // Count the number of digits in the fraction and subtract the 
    // exponent to simulate moving the decimal point left by exponent places. 
    // 1.234e+2 has 1 fraction digit and '234'.length - 2 == 1 
    // 1.234e-2 has 5 fraction digit and '234'.length - -2 == 5 
    return Math.max(
     0, // lower limit. 
     (match[1] == '0' ? 0 : (match[1] || '').length) // fraction length 
     - (match[2] || 0)); // exponent 
} 

De acuerdo con la especificación, cualquier solución basada en la orden interna número-> conversión de cadenas sólo puede tener una precisión de 21 lugares más allá del exponente

9.8.1 ToString Applied to the Number Type

  1. De lo contrario, dejar que n, k, y s ser números enteros tales que k ≥ 1, 10k-1 ≤ s < 10k, el valor del número de s × 10n-k es m, y k es lo más pequeño posible. Tenga en cuenta que k es el número de dígitos en la representación decimal de s, que s no es divisible por 10, y que el dígito menos significativo de s no está necesariamente determinado de manera única por estos criterios.
  2. Si k ≤ n ≤ 21, devuelva la cadena que consta de los k dígitos de la representación decimal de s (en orden, sin ceros a la izquierda), seguido de n-k apariciones del carácter '0'.
  3. Si 0 < n ≤ 21, devuelva la cadena que consta de los n dígitos más significativos de la representación decimal de s, seguido de un punto decimal '.', Seguido de los dígitos restantes k-n de la representación decimal de s .
  4. Si -6 < n ≤ 0, devuelva la cadena que consiste en el carácter '0', seguido de un punto decimal '.', Seguido de -n apariciones del carácter '0', seguido de los dígitos k del representación decimal de s.

Nota histórica: La implementación de abajo es problemático. Lo dejo aquí como contexto para el hilo de comentarios.

Según la definición de Number.prototype.toFixed, parece que debería funcionar lo siguiente, pero debido a la representación IEEE-754 de valores dobles, ciertos números producirán resultados falsos. Por ejemplo, decimalPlaces(0.123) devolverá 20.

function decimalPlaces(number) { 
 
    // toFixed produces a fixed representation accurate to 20 decimal places 
 
    // without an exponent. 
 
    // The ^-?\d*\. strips off any sign, integer portion, and decimal point 
 
    // leaving only the decimal fraction. 
 
    // The 0+$ strips off any trailing zeroes. 
 
    return ((+number).toFixed(20)).replace(/^-?\d*\.?|0+$/g, '').length; 
 
} 
 

 
// The OP's examples: 
 
console.log(decimalPlaces(5555.0)); // 0 
 
console.log(decimalPlaces(5555)); // 0 
 
console.log(decimalPlaces(555.5)); // 1 
 
console.log(decimalPlaces(555.50)); // 1 
 
console.log(decimalPlaces(0.0000005)); // 7 
 
console.log(decimalPlaces(5e-7)); // 7 
 
console.log(decimalPlaces(0.00000055)); // 8 
 
console.log(decimalPlaces(5e-8)); // 8 
 
console.log(decimalPlaces(0.123)); // 20 (!)

+0

Muy informativo, gracias. Creo que podría terminar usando Fijo (20) por simplicidad e ignorar los números realmente pequeños. La línea de cuantificación tiene que dibujarse en alguna parte, y 20 no es lo suficientemente baja como para ser molesta para la mayoría de los propósitos. – Milosz

+0

@Milosz, sí. Si estuviera escribiendo una biblioteca, iría con el segundo enfoque, pero para aplicaciones específicas, la primera puede ser suficiente. Sin embargo, una cosa a tener en cuenta es que el primero no trata tan bien con 'NaN' y ±' Infinity'. –

+7

Esto no funciona para mí en Google Chrome versión 30. La entrada de 555.50 da como resultado una salida de 1, pero 555.20 da como resultado una salida de 20. – user1

3

esto funciona para un número menor de e-17:

function decimalPlaces(n){ 
    var a; 
    return (a=(n.toString().charAt(0)=='-'?n-1:n+1).toString().replace(/^-?[0-9]+\.?([0-9]+)$/,'$1').length)>=1?a:0; 
} 
+0

Esto me recuerda que olvidé incluir la comprobación de signos menos al comienzo del número. ¡Gracias! También disfruto de cuán concisa es esta función. Presionando el número pasado 1 en valor absoluto para evitar el exponente negativo es inteligente (aunque no entiendo por qué JavaScript cambia a la notación de exponente mucho más tarde para exponentes positivos). Si nada de lo que funciona después de la e-17 aparece, es probable que sea con esto. – Milosz

+0

Incorrectamente devuelve '0' para' 1.2' – serg

+1

A partir de la última edición (1 de abril de 2013) en esta respuesta, devuelve (correctamente) 1 para 1.2. Si howerver devuelve 1 (en lugar de cero) para los números sin decimales – Hoffmann

1

No sólo son números que se cuantifican antes de su almacenamiento; adicionalmente, algunos números almacenados en binario tienen representaciones decimales irracionalmente largas, por lo que sus representaciones decimales se están truncando.

JavaScript representa los números usando IEEE-754 formato de doble precisión (64 bit). Según tengo entendido, esto le da una precisión de 53 bits, o de quince a dieciséis dígitos decimales.

Por lo que para cualquier número con más dígitos, usted acaba de obtener una aproximación. Existen algunas bibliotecas para manejar números grandes con más precisión, incluidos los mencionados en this thread.

11

Bueno, yo uso una solución basada en el hecho de que si multiplicas un número de coma flotante por la potencia correcta de 10, obtienes un número entero.

Por ejemplo, si multiplicas 3.14 * 10^2, obtienes 314 (un número entero). El exponente representa entonces el número de decimales que tiene el número de coma flotante.

Por lo tanto, pensé que si gradualmente multiplico un punto flotante aumentando las potencias de 10, finalmente llegaría a la solución.

let decimalPlaces = function() { 
 
    function isInt(n) { 
 
     return typeof n === 'number' && 
 
      parseFloat(n) == parseInt(n, 10) && !isNaN(n); 
 
    } 
 
    return function (n) { 
 
     const a = Math.abs(n); 
 
     let c = a, count = 1; 
 
     while (!isInt(c) && isFinite(c)) { 
 
     c = a * Math.pow(10, count++); 
 
     } 
 
     return count - 1; 
 
    }; 
 
}(); 
 

 
for (const x of [ 
 
    0.0028, 0.0029, 0.0408, 
 
    0, 1.0, 1.00, 0.123, 1e-3, 
 
    3.14, 2.e-3, 2.e-14, -3.14e-21, 
 
    5555.0, 5555, 555.5, 555.50, 0.0000005, 5e-7, 0.00000055, 5e-8, 
 
    0.000006, 0.0000007, 
 
    0.123, 0.121, 0.1215 
 
]) console.log(x, '->', decimalPlaces(x));

+2

+1 por no usar expresiones de reqular y por una solución que no está limitada a ningún número específico de decimales. El único problema que veo son los números con un número infinito de decimales como 1/3. –

+0

@ DaniëlTulp Los números pares con notación decimal infinita deben representarse con un número finito de decimales en la memoria de la computadora. Supongo que este enfoque te diría cuántos decimales tiene esa notación finita. Supongo que para representar con precisión 1/3 tendríamos que usar fracciones, ya que la notación decimal no funcionaría. –

+1

este ha sido el enfoque más consistente para mí. ¡Me siento más cómodo usando matemáticas que convirtiéndome en cadenas, accesorios! – SuckerForMayhem

-1

Basado en respuesta gion_13 me ocurrió esto:

function decimalPlaces(n){ 
 
let result= /^-?[0-9]+\.([0-9]+)$/.exec(n); 
 
return result === null ? 0 : result[1].length; 
 
} 
 

 
for (const x of [ 
 
    0, 1.0, 1.00, 0.123, 1e-3, 3.14, 2.e-3, -3.14e-21, 
 
    5555.0, 5555, 555.5, 555.50, 0.0000005, 5e-7, 0.00000055, 5e-8, 
 
    0.000006, 0.0000007, 
 
    0.123, 0.121, 0.1215 
 
]) console.log(x, '->', decimalPlaces(x));

Se fija el 1 de regresar cuando no hay cifras decimales. Hasta donde puedo decir, esto funciona sin errores.

+0

Funciona sin errores solo en los pocos casos de prueba que ha incluido :) –

1

Actualización de 2017

Aquí hay una versión simplificada basada en la respuesta de Edwin.Tiene un conjunto de pruebas y devuelve el número correcto de decimales para casos de esquina, incluidos NaN, Infinity, notaciones de exponente y números con representaciones problemáticas de sus fracciones sucesivas, como 0.0029 o 0.0408. Esto cubre la gran mayoría de aplicaciones financieras, donde 0.0408 que tiene 4 decimales (no 6) es más importante que 3.14e-21 que tiene 23.

function decimalPlaces(n) { 
 
    function hasFraction(n) { 
 
    return Math.abs(Math.round(n) - n) > 1e-10; 
 
    } 
 

 
    let count = 0; 
 
    // multiply by increasing powers of 10 until the fractional part is ~ 0 
 
    while (hasFraction(n * (10 ** count)) && isFinite(10 ** count)) 
 
    count++; 
 
    return count; 
 
} 
 

 
for (const x of [ 
 
    0.0028, 0.0029, 0.0408, 0.1584, 4.3573, // corner cases against Edwin's answer 
 
    11.6894, 
 
    0, 1.0, 1.00, 0.123, 1e-3, -1e2, -1e-2, -0.1, 
 
    NaN, 1E500, Infinity, Math.PI, 1/3, 
 
    3.14, 2.e-3, 2.e-14, 
 
    1e-9, // 9 
 
    1e-10, // should be 10, but is below the precision limit 
 
    -3.14e-13, // 15 
 
    3.e-13, // 13 
 
    3.e-14, // should be 14, but is below the precision limit 
 
    123.123456789, // 14, the precision limit 
 
    5555.0, 5555, 555.5, 555.50, 0.0000005, 5e-7, 0.00000055, 5e-8, 
 
    0.000006, 0.0000007, 
 
    0.123, 0.121, 0.1215 
 
]) console.log(x, '->', decimalPlaces(x));

la desventaja es que el método es limitado hasta un máximo de 10 decimales garantizados. Puede devolver más decimales correctamente, pero no confíe en eso. Los números menores que 1e-10 pueden considerarse cero, y la función devolverá 0. Ese valor particular fue elegido para resolver correctamente el caso de esquina 11.6894, para el cual falla el método simple de multiplicar por potencias de 10 (devuelve 5 en lugar de 4)

Sin embargo, esta es la caja de la esquina the 5th que he descubierto, después de 0.0029, 0.0408, 0.1584 y 4.3573. Después de cada uno, tuve que reducir la precisión en un decimal. No sé si hay otros números con menos de 10 decimales para los cuales esta función puede devolver un número incorrecto de decimales. Para estar seguro, busque un arbitrary precision library.

Tenga en cuenta que convertir a cadena y dividir por . es solo una solución de hasta 7 decimales. String(0.0000007) === "7e-7". ¿O tal vez incluso menos? La representación del punto flotante no es intuitiva.

+1

+1 Aunque esta función tiene limitaciones conocidas, funciona para lo que necesito (cualquier cantidad razonable de decimales para el consumo humano, es decir, 0 - 4 en mi aplicación) – Johannes

Cuestiones relacionadas