Redondeo a un número arbitrario de dígitos significativos


83

¿Cómo se puede redondear cualquier número (no solo enteros> 0) a N dígitos significativos?

Por ejemplo, si quiero redondear a tres dígitos significativos, estoy buscando una fórmula que pueda tomar:

1,239,451 y retorno 1,240,000

12.1257 y regreso 12.1

.0681 y devuelve .0681

5 y devuelve 5

Naturalmente, el algoritmo no debería estar codificado para manejar solo N de 3, aunque eso sería un comienzo.


Parece una pregunta demasiado general. Los diferentes lenguajes de programación tienen una función estándar diferente para hacer esto. No es apropiado reinventar la rueda.
Johnny Wong

Respuestas:


106

Aquí está el mismo código en Java sin el error 12.100000000000001 que tienen otras respuestas

También eliminé el código repetido, lo cambié powera un tipo entero para evitar problemas flotantes cuando n - dterminó, y dejé el intermedio largo más claro

El error se produjo al multiplicar un número grande por un número pequeño. En su lugar, divido dos números de tamaño similar.

EDITAR
Se corrigieron más errores. Se agregó una verificación de 0, ya que daría como resultado NaN. Hizo que la función realmente funcione con números negativos (el código original no maneja números negativos porque un registro de un número negativo es un número complejo)

public static double roundToSignificantFigures(double num, int n) {
    if(num == 0) {
        return 0;
    }

    final double d = Math.ceil(Math.log10(num < 0 ? -num: num));
    final int power = n - (int) d;

    final double magnitude = Math.pow(10, power);
    final long shifted = Math.round(num*magnitude);
    return shifted/magnitude;
}

2
Gracias por aceptar mi respuesta. Me acabo de dar cuenta de que mi respuesta es más de un año después de la pregunta. Esta es una de las razones por las que stackoverflow es tan genial. ¡Puede encontrar información útil!
Pirolístico

2
Tenga en cuenta que esto puede fallar ligeramente para valores cercanos al límite de redondeo. Por ejemplo, el redondeo de 1,255 a 3 dígitos significativos debería devolver 1,26 pero devuelve 1,25. Eso es porque 1.255 * 100.0 es 125.499999 ... Pero esto es de esperar cuando se trabaja con dobles
cquezel

Vaya, sé que esto es antiguo, pero estoy tratando de usarlo. Tengo un flotador que quiero mostrar con 3 cifras significativas. Si el valor flotante es 1.0, llamo a su método pero aún así regresa como 1.0, incluso si lanzo el flotante como un doble. Quiero que vuelva como 1. ¿Alguna idea?
Steve W

3
Este fragmento de Java termina en el ejemplo oficial de Android android.googlesource.com/platform/development/+/fcf4286/samples/…
Curious Sam

1
No es perfecto. Para num = -7999999.999999992 y n = 2 devuelve -7999999.999999999 pero debería ser -8000000.
Duncan Calvert

16

Aquí hay una implementación de JavaScript breve y dulce:

function sigFigs(n, sig) {
    var mult = Math.pow(10, sig - Math.floor(Math.log(n) / Math.LN10) - 1);
    return Math.round(n * mult) / mult;
}

alert(sigFigs(1234567, 3)); // Gives 1230000
alert(sigFigs(0.06805, 3)); // Gives 0.0681
alert(sigFigs(5, 3)); // Gives 5

pero 12.1257 da 12.126
Pirolístico

1
Buena respuesta Ates. Quizás agregue un disparador para devolver 0 si n==0:)
sscirrus

¿Hay alguna razón para hacer en Math.log(n) / Math.LN10lugar de hacerlo Math.log10(n)?
Lee

1
@Lee developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/… "Esta es una nueva tecnología, parte del estándar ECMAScript 2015 (ES6)". Entonces, básicamente, problemas de compatibilidad.
Ates Goral

¿Me estoy perdiendo algo o esta respuesta asume eso Math.floor(x) == Math.ceil(x) - 1? Porque no es así cuando xes un número entero. Creo que el segundo argumento de la powfunción debería ser sig - Math.ceil(Math.log(n) / Math.LN10)(o simplemente usar Math.log10)
Paul

15

RESUMEN:

double roundit(double num, double N)
{
    double d = log10(num);
    double power;
    if (num > 0)
    {
        d = ceil(d);
        power = -(d-N);
    }
    else
    {
        d = floor(d); 
        power = -(d-N);
    }

    return (int)(num * pow(10.0, power) + 0.5) * pow(10.0, -power);
}

Por lo tanto, debe encontrar el lugar decimal del primer dígito distinto de cero, luego guardar los siguientes dígitos N-1 y luego redondear el dígito N en función del resto.

Podemos usar log para hacer lo primero.

log 1239451 = 6.09
log 12.1257 = 1.08
log 0.0681  = -1.16

Entonces, para números> 0, tome el techo del registro. Para números <0, tome el piso del registro.

Ahora tenemos el dígito d: 7 en el primer caso, 2 en el segundo, -2 en el tercero.

Tenemos que redondear el (d-N)décimo dígito. Algo como:

double roundedrest = num * pow(10, -(d-N));

pow(1239451, -4) = 123.9451
pow(12.1257, 1)  = 121.257
pow(0.0681, 4)   = 681

Luego haz el redondeo estándar:

roundedrest = (int)(roundedrest + 0.5);

Y deshacer el pow.

roundednum = pow(roundedrest, -(power))

Donde potencia es la potencia calculada anteriormente.


Acerca de la precisión: la respuesta de Pyrolistical está más cerca del resultado real. Pero tenga en cuenta que no puede representar 12.1 exactamente en cualquier caso. Si imprime las respuestas de la siguiente manera:

System.out.println(new BigDecimal(n));

Las respuestas son:

Pyro's: 12.0999999999999996447286321199499070644378662109375
Mine: 12.10000000000000142108547152020037174224853515625
Printing 12.1 directly: 12.0999999999999996447286321199499070644378662109375

Entonces, ¡usa la respuesta de Pyro!


1
Este algoritmo parece propenso a errores de punto flotante. Cuando se implementa con JavaScript, obtengo: 0.06805 -> 0.06810000000000001 y 12.1 -> 12.100000000000001
Ates Goral

12.1 por sí solo no se puede representar con precisión utilizando punto flotante; no es el resultado de este algoritmo.
Claudiu

1
Este código en Java produce 12.100000000000001 y utiliza dobles de 64 bits que pueden presentar 12.1 exactamente.
Pirolístico

4
No importa si es de 64 bits o de 128 bits. No se puede representar la fracción 1/10 usando una suma finita de potencias de 2, y así es como se representan los números de punto flotante
Claudiu

2
para aquellos que intervienen, básicamente la respuesta de Pyrolistical es más precisa que la mía, de modo que el algoritmo de impresión de números de punto flotante imprime '12 .1 'en lugar de '12 .100000000000001'. su respuesta es mejor, aunque técnicamente estaba en lo cierto en que no se puede representar '12 .1 'exactamente.
Claudiu

10

¿No es la implementación de JavaScript "breve y sencilla"?

Number(n).toPrecision(sig)

p.ej

alert(Number(12345).toPrecision(3)

?

Lo siento, no estoy bromeando, es solo que usar la función "roundit" de Claudiu y .toPrecision en JavaScript me da resultados diferentes, pero solo en el redondeo del último dígito.

JavaScript:

Number(8.14301).toPrecision(4) == 8.143

.RED

roundit(8.14301,4) == 8.144

1
Number(814301).toPrecision(4) == "8.143e+5". Generalmente no es lo que desea si se lo muestra a los usuarios.
Zaz

Muy cierto Josh, sí, generalmente recomendaría .toPrecision () solo para números decimales y la respuesta aceptada (con edición) debe usarse / revisarse según sus requisitos individuales.
Justin Wignall

8

La solución de Pyrolistical (¡muy buena!) Todavía tiene un problema. El valor doble máximo en Java es del orden de 10 ^ 308, mientras que el valor mínimo es del orden de 10 ^ -324. Por lo tanto, puede tener problemas al aplicar la función roundToSignificantFiguresa algo que se encuentre dentro de unas pocas potencias de diez de Double.MIN_VALUE. Por ejemplo, cuando llamas

roundToSignificantFigures(1.234E-310, 3);

entonces la variable powertendrá el valor 3 - (-309) = 312. En consecuencia, la variable magnitudese convertirá Infinity, y de ahí en adelante todo será basura. Afortunadamente, este no es un problema insuperable: es solo el factor magnitude que se desborda. Lo que realmente importa es el producto num * magnitude , y eso no se desborda. Una forma de resolver esto es dividiendo la multiplicación por el factor magintudeen dos pasos:


 public static double roundToNumberOfSignificantDigits(double num, int n) {

    final double maxPowerOfTen = Math.floor(Math.log10(Double.MAX_VALUE));

    if(num == 0) {
        return 0;
    }

    final double d = Math.ceil(Math.log10(num < 0 ? -num: num));
    final int power = n - (int) d;

    double firstMagnitudeFactor = 1.0;
    double secondMagnitudeFactor = 1.0;
    if (power > maxPowerOfTen) {
        firstMagnitudeFactor = Math.pow(10.0, maxPowerOfTen);
        secondMagnitudeFactor = Math.pow(10.0, (double) power - maxPowerOfTen);
    } else {
        firstMagnitudeFactor = Math.pow(10.0, (double) power);
    }

    double toBeRounded = num * firstMagnitudeFactor;
    toBeRounded *= secondMagnitudeFactor;

    final long shifted = Math.round(toBeRounded);
    double rounded = ((double) shifted) / firstMagnitudeFactor;
    rounded /= secondMagnitudeFactor;
    return rounded;
}


6

¿Qué tal esta solución de Java:

double roundToSignificantFigure (número doble, precisión int) {
 devolver nuevo BigDecimal (num)
            .round (new MathContext (precisión, RoundingMode.HALF_EVEN))
            .doubleValue (); 
}

3

Aquí hay una versión modificada del JavaScript de Ates que maneja números negativos.

function sigFigs(n, sig) {
    if ( n === 0 )
        return 0
    var mult = Math.pow(10,
        sig - Math.floor(Math.log(n < 0 ? -n: n) / Math.LN10) - 1);
    return Math.round(n * mult) / mult;
 }

2

Esto llegó 5 años tarde, pero lo compartiré con otros que todavía tienen el mismo problema. Me gusta porque es simple y sin cálculos en el lado del código. Consulte Métodos integrados para mostrar cifras significativas para obtener más información.

Esto es si solo desea imprimirlo.

public String toSignificantFiguresString(BigDecimal bd, int significantFigures){
    return String.format("%."+significantFigures+"G", bd);
}

Esto es si quieres convertirlo:

public BigDecimal toSignificantFigures(BigDecimal bd, int significantFigures){
    String s = String.format("%."+significantFigures+"G", bd);
    BigDecimal result = new BigDecimal(s);
    return result;
}

Aquí tienes un ejemplo en acción:

BigDecimal bd = toSignificantFigures(BigDecimal.valueOf(0.0681), 2);

Esto mostrará números "grandes" en notación científica, por ejemplo, 15k como 1.5e04.
Matt

2

JavaScript:

Number( my_number.toPrecision(3) );

La Numberfunción cambiará la salida del formulario "8.143e+5"a "814300".


1

¿Ha intentado codificarlo de la forma en que lo haría a mano?

  1. Convierte el número en una cadena
  2. Comenzando por el principio de la cadena, cuente los dígitos; los ceros iniciales no son significativos, todo lo demás sí lo es.
  3. Cuando llegue al "n-ésimo" dígito, mire el siguiente dígito y, si es 5 o más, redondee hacia arriba.
  4. Reemplace todos los dígitos finales con ceros.

1

[Corregido, 2009-10-26]

Esencialmente, para N dígitos fraccionarios significativos :

• Multiplica el número por 10 N
• Suma 0.5
• Trunca los dígitos de la fracción (es decir, trunca el resultado en un número entero)
• Divide por 10 N

Para N dígitos integrales significativos (no fraccionarios):

• Dividir el número entre 10 N
• Sumar 0,5
• Truncar los dígitos de la fracción (es decir, truncar el resultado en un número entero)
• Multiplicar por 10 N

Puede hacer esto en cualquier calculadora, por ejemplo, que tenga un operador "INT" (truncamiento de enteros).


No Vuelve a leer la pregunta. 1239451 con 3 sig figs usando su algoritmo produciría incorrectamente 123951
Pyrolistical

Sí, lo corrigí para distinguir entre redondear a una fracción de dígitos (a la derecha del punto decimal) versus un número entero de dígitos (a la izquierda).
David R Tribble

1
/**
 * Set Significant Digits.
 * @param value value
 * @param digits digits
 * @return
 */
public static BigDecimal setSignificantDigits(BigDecimal value, int digits) {
    //# Start with the leftmost non-zero digit (e.g. the "1" in 1200, or the "2" in 0.0256).
    //# Keep n digits. Replace the rest with zeros.
    //# Round up by one if appropriate.
    int p = value.precision();
    int s = value.scale();
    if (p < digits) {
        value = value.setScale(s + digits - p); //, RoundingMode.HALF_UP
    }
    value = value.movePointRight(s).movePointLeft(p - digits).setScale(0, RoundingMode.HALF_UP)
        .movePointRight(p - digits).movePointLeft(s);
    s = (s > (p - digits)) ? (s - (p - digits)) : 0;
    return value.setScale(s);
}

1

Aquí está el código de Pyrolistical (actualmente la mejor respuesta) en Visual Basic.NET, si alguien lo necesita:

Public Shared Function roundToSignificantDigits(ByVal num As Double, ByVal n As Integer) As Double
    If (num = 0) Then
        Return 0
    End If

    Dim d As Double = Math.Ceiling(Math.Log10(If(num < 0, -num, num)))
    Dim power As Integer = n - CInt(d)
    Dim magnitude As Double = Math.Pow(10, power)
    Dim shifted As Double = Math.Round(num * magnitude)
    Return shifted / magnitude
End Function

0

Este es uno que se me ocurrió en VB:

Function SF(n As Double, SigFigs As Integer)
    Dim l As Integer = n.ToString.Length
    n = n / 10 ^ (l - SigFigs)
    n = Math.Round(n)
    n = n * 10 ^ (l - SigFigs)
    Return n
End Function

0

return new BigDecimal(value, new MathContext(significantFigures, RoundingMode.HALF_UP)).doubleValue();


0

Necesitaba esto en Go, que era un poco complicado por la falta de la biblioteca estándar de Go math.Round()(antes de go1.10). Así que también tuve que preparar eso. Aquí está mi traducción de la excelente respuesta de Pyrolistical :

// TODO: replace in go1.10 with math.Round()
func round(x float64) float64 {
    return float64(int64(x + 0.5))
}

// SignificantDigits rounds a float64 to digits significant digits.
// Translated from Java at https://stackoverflow.com/a/1581007/1068283
func SignificantDigits(x float64, digits int) float64 {
    if x == 0 {
        return 0
    }

    power := digits - int(math.Ceil(math.Log10(math.Abs(x))))
    magnitude := math.Pow(10, float64(power))
    shifted := round(x * magnitude)
    return shifted / magnitude
}

¡Esto tiene un voto negativo misterioso! Pero no puedo encontrar un error o problema en él. ¿Que está pasando aqui?
Michael Hampton

0

Puede evitar hacer todos estos cálculos con potencias de 10, etc., simplemente usando FloatToStrF.

FloatToStrF le permite (entre otras cosas) elegir la precisión (número de cifras significativas) en el valor de salida (que será una cadena). Por supuesto, puede aplicar StrToFloat a esto para obtener su valor redondeado como flotante.

Mira aquí:

http://docs.embarcadero.com/products/rad_studio/delphiAndcpp2009/HelpUpdate2/EN/html/delphivclwin32/SysUtils_FloatToStrF@Extended@TFloatFormat@Integer@Integer.html


-1
public static double roundToSignificantDigits(double num, int n) {
    return Double.parseDouble(new java.util.Formatter().format("%." + (n - 1) + "e", num).toString());
}

Este código utiliza la función de formato incorporada que se convierte en una función de redondeo

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.