Arredondando para um número arbitrário de dígitos significativos

83

Como você pode arredondar qualquer número (não apenas inteiros> 0) para N dígitos significativos?

Por exemplo, se eu quiser arredondar para três dígitos significativos, estou procurando uma fórmula que possa ter:

1.239.451 e retorno 1.240.000

12,1257 e retorno 12,1

0,0681 e retorno 0,0681

5 e retorno 5

Naturalmente, o algoritmo não deve ser codificado para lidar apenas com N de 3, embora isso fosse um começo.

DougN
fonte
Parece questão muito geral. Diferentes linguagens de programação têm diferentes funções padrão para fazer isso. Não é apropriado reinventar a roda, de fato.
Johnny Wong

Respostas:

106

Aqui está o mesmo código em Java sem o bug 12.100000000000001 que outras respostas têm

Também removi o código repetido, mudei powerpara um tipo inteiro para evitar problemas de flutuação quando n - dterminar e tornei o intermediário longo mais claro

O bug foi causado pela multiplicação de um grande número por um pequeno número. Em vez disso, divido dois números de tamanho semelhante.

EDIT
Corrigido mais bugs. Adicionada verificação de 0, pois resultaria em NaN. Fez a função funcionar realmente com números negativos (o código original não lida com números negativos porque um log de um número negativo é um número complexo)

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;
}
Pirolístico
fonte
2
Obrigado por aceitar minha resposta. Acabei de perceber que minha resposta é mais de um ano após a pergunta. Esta é uma das razões pelas quais o stackoverflow é tão legal. Você pode encontrar informações úteis!
Pirolística de
2
Observe que isso pode falhar ligeiramente para valores próximos ao limite da rodada. Por exemplo, o arredondamento de 1,255 para 3 dígitos significativos deve retornar 1,26, mas retorna 1,25. Isso ocorre porque 1.255 * 100.0 é 125.499999 ... Mas isso é esperado ao trabalhar com duplas
cquezel
Uau, eu sei que isso é antigo, mas estou tentando usar. Eu tenho um float que desejo exibir com 3 algarismos significativos. Se o valor do float for 1.0, eu chamo seu método, mas ele ainda retorna como 1.0, mesmo se eu converter o float como um double. Eu quero que ele retorne como 1. Alguma ideia?
Steve W
3
Este snippet de java termina no exemplo oficial do Android android.googlesource.com/platform/development/+/fcf4286/samples/…
Curious Sam
1
Imperfeito. Para num = -7999999.999999992 e n = 2, retorna -7999999.999999999, mas deve ser -8000000.
Duncan Calvert
16

Aqui está uma implementação de JavaScript curta e agradável:

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
Ates Goral
fonte
mas 12,1257 dá 12,126
Pirolístico
1
Boa resposta, Ates. Talvez adicione um acionador para retornar 0 se n==0:)
sscirrus
há uma razão para fazer em Math.log(n) / Math.LN10vez de Math.log10(n)?
Lee
1
@Lee developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/… "Esta é uma nova tecnologia, parte do padrão ECMAScript 2015 (ES6)." Então, basicamente, problemas de compatibilidade.
Ates Goral
Estou perdendo alguma coisa ou essa resposta presume isso Math.floor(x) == Math.ceil(x) - 1? Porque não faz quando xé um inteiro. Acho que o segundo argumento da powfunção deveria ser sig - Math.ceil(Math.log(n) / Math.LN10)(ou apenas usar Math.log10)
Paulo
15

RESUMO:

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

Portanto, você precisa encontrar a casa decimal do primeiro dígito diferente de zero, salvar os próximos N-1 dígitos e arredondar o enésimo dígito com base no resto.

Podemos usar o log para fazer o primeiro.

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

Portanto, para números> 0, calcule o teto do log. Para números <0, tome o chão do log.

Agora temos o dígito d: 7 no primeiro caso, 2 no segundo, -2 no terceiro.

Temos que arredondar o (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

Em seguida, faça o arredondamento padrão:

roundedrest = (int)(roundedrest + 0.5);

E desfaça o pow.

roundednum = pow(roundedrest, -(power))

Onde a potência é a potência calculada acima.


Sobre a precisão: a resposta da pirólise está de fato mais próxima do resultado real. Mas observe que você não pode representar 12,1 exatamente em qualquer caso. Se você imprimir as respostas da seguinte forma:

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

As respostas são:

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

Então, use a resposta do Pyro!

Claudiu
fonte
1
Este algoritmo parece sujeito a erros de ponto flutuante. Quando implementado com JavaScript, obtenho: 0,06805 -> 0,06810000000000001 e 12,1 -> 12,100000000000001
Ates Goral
12.1 por si só não pode ser representado com precisão usando ponto flutuante - não é um resultado deste algoritmo.
Claudiu de
1
Este código em Java produz 12.100000000000001 e está usando duplos de 64 bits que podem apresentar 12.1 exatamente.
Pirolístico
4
Não importa se é de 64 bits ou 128 bits. Você não pode representar a fração 1/10 usando uma soma finita de potências de 2, e é assim que os números de ponto flutuante são representados
Claudiu,
2
para os que estão participando, basicamente a resposta da Pirolística é mais precisa do que a minha, de modo que o algoritmo de impressão do número de ponto flutuante imprime '12 .1 'em vez de '12 .100000000000001'. a resposta dele é melhor, mesmo que eu esteja tecnicamente correto que você não pode representar '12 .1 'exatamente.
Claudiu
10

Não é a implementação "curta e agradável" de JavaScript

Number(n).toPrecision(sig)

por exemplo

alert(Number(12345).toPrecision(3)

?

Desculpe, não estou sendo jocoso aqui, é só que usar a função "roundit" de Claudiu e .toPrecision em JavaScript me dá resultados diferentes, mas apenas no arredondamento do último dígito.

JavaScript:

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

.INTERNET

roundit(8.14301,4) == 8.144
Justin Wignall
fonte
1
Number(814301).toPrecision(4) == "8.143e+5". Geralmente não é o que você deseja se estiver mostrando aos usuários.
Zaz
Muito verdadeiro Josh sim, eu geralmente recomendaria .toPrecision () apenas para números decimais e a resposta aceita (com edição) deve ser usada / revisada de acordo com seus requisitos individuais.
Justin Wignall
8

A solução pirolística (muito boa!) Ainda tem um problema. O valor duplo máximo em Java é da ordem de 10 ^ 308, enquanto o valor mínimo é da ordem de 10 ^ -324. Portanto, você pode ter problemas ao aplicar a função roundToSignificantFiguresa algo que está dentro de algumas potências de dez de Double.MIN_VALUE. Por exemplo, quando você liga

roundToSignificantFigures(1.234E-310, 3);

então a variável powerterá o valor 3 - (-309) = 312. Consequentemente, a variável magnitudese tornará Infinity, e é tudo lixo a partir de então. Felizmente, esse não é um problema intransponível: é apenas o fator magnitude que está transbordando. O que importa mesmo é o produto num * magnitude , e isso não transborda. Uma maneira de resolver isso é dividir a multiplicação pelo fator magintudeem duas etapas:


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

Thomas Becker
fonte
6

Que tal esta solução java:

double roundToSignificantFigure (double num, int precision) {
 retornar novo BigDecimal (num)
            .round (novo MathContext (precisão, RoundingMode.HALF_EVEN))
            .doubleValue (); 
}
Wolfgang Grinfeld
fonte
3

Aqui está uma versão modificada do JavaScript de Ates que lida com 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;
 }
Jason Swank
fonte
2

Isso veio 5 anos atrasado, mas vou compartilhar com outras pessoas que ainda têm o mesmo problema. Eu gosto porque é simples e sem cálculos no lado do código. Consulte Métodos integrados para exibição de figuras significativas para obter mais informações.

Isso se você quiser apenas imprimi-lo.

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

Se você quiser convertê-lo:

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

Aqui está um exemplo disso em ação:

BigDecimal bd = toSignificantFigures(BigDecimal.valueOf(0.0681), 2);
JackDev
fonte
Isso exibirá números "grandes" em notação científica, por exemplo, 15k como 1,5e04.
Mateus
2

JavaScript:

Number( my_number.toPrecision(3) );

A Numberfunção mudará a saída do formulário "8.143e+5"para "814300".

Zaz
fonte
1

Você já tentou codificá-lo da maneira como faria à mão?

  1. Converta o número em uma string
  2. Começando no início da string, conte os dígitos - zeros à esquerda não são significativos, todo o resto é.
  3. Quando você chegar ao "enésimo" dígito, dê uma olhada no próximo dígito e, se for 5 ou mais, arredonde para cima.
  4. Substitua todos os dígitos finais por zeros.
Mark Bessey
fonte
1

[Corrigido, 26/10/2009]

Essencialmente, para N dígitos fracionários significativos :

• Multiplique o número por 10 N
• Some 0,5
• Trunque os dígitos da fração (ou seja, trunque o resultado em um inteiro)
• Divida por 10 N

Para N dígitos integrais significativos (não fracionários):

• Divida o número por 10 N
• Some 0,5
• Trunque os dígitos da fração (ou seja, trunque o resultado em um inteiro)
• Multiplique por 10 N

Você pode fazer isso em qualquer calculadora, por exemplo, que tenha um operador "INT" (truncamento de inteiro).

David R Tribble
fonte
Não. Leia a pergunta novamente. 1239451 com 3 sig figs usando seu algoritmo resultaria incorretamente em 123951
Pirolística de
Sim, eu corrigido para distinguir entre o arredondamento para um fraccionada número de dígitos (para a direita do ponto decimal) versus um integrante número de dígitos (para a esquerda).
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);
}
Valeri Shibaev
fonte
1

Aqui está o código da Pirolística (atualmente a melhor resposta) em Visual Basic.NET, caso alguém precise:

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
Michael Zlatkovsky - Microsoft
fonte
0

Este é um que eu criei em 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
SomeGuy
fonte
0

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

Duncan Calvert
fonte
0

Eu precisava disso no Go, o que era um pouco complicado pela falta de biblioteca padrão do Go math.Round()(antes do go1.10). Então eu tive que agitar isso também. Aqui está a minha tradução da excelente resposta da Pirolística :

// 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
}
Michael Hampton
fonte
Isso teve um downvote misterioso! Mas não consigo encontrar um erro ou problema nele. O que está acontecendo aqui?
Michael Hampton
0

Você pode evitar fazer todos esses cálculos com potências de 10 etc. usando apenas FloatToStrF.

FloatToStrF permite que você (entre outras coisas) escolha a precisão (número de algarismos significativos) no valor de saída (que será uma string). Claro, você poderia então aplicar StrToFloat a isso para obter seu valor arredondado como um float.

Veja aqui:

http://docs.embarcadero.com/products/rad_studio/delphiAndcpp2009/HelpUpdate2/EN/html/delphivclwin32/SysUtils_FloatToStrF@Extended@TFloatFormat@[email protected]

George Soden-Freeth
fonte
-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 usa a função de formatação embutida que é transformada em uma função de arredondamento

Harikrishnan
fonte