awk aritmética de alta precisão

11

Estou procurando uma maneira de dizer ao awk para fazer aritmética de alta precisão em uma operação de substituição. Isso envolve, ler um campo de um arquivo e substituí-lo por um incremento de 1% nesse valor. No entanto, estou perdendo precisão lá. Aqui está uma reprodução simplificada do problema:

 $ echo 0.4970436865354813 | awk '{gsub($1, $1*1.1)}; {print}'
   0.546748

Aqui, tenho 16 dígitos após a precisão decimal, mas o awk fornece apenas seis. Usando printf, estou obtendo o mesmo resultado:

$ echo 0.4970436865354813 | awk '{gsub($1, $1*1.1)}; {printf("%.16G\n", $1)}'
0.546748

Alguma sugestão de como obter a precisão desejada?

mkc
fonte
Talvez o awk tenha uma resolução mais alta, mas é apenas sua formatação de saída está truncando. Use printf.
dubiousjim
Nenhuma alteração no valor do resultado após o uso de printf. Pergunta editada em conformidade.
Mkc
Como o @manatwork apontou, isso gsubé desnecessário. O problema é que gsubfunciona em seqüências de caracteres, não em números, portanto, uma conversão é feita primeiro CONVFMT, e o valor padrão para isso é %.6g.
jw013
@ jw013, como mencionei na pergunta, meu problema original requer o gsub, pois preciso substituir um número com um incremento de 1%. Concordado, no exemplo simplificado, não é necessário.
Mkc

Respostas:

12
$ echo 0.4970436865354813 | awk -v CONVFMT=%.17g '{gsub($1, $1*1.1)}; {print}'
0.54674805518902947

Ou melhor, aqui:

$ echo 0.4970436865354813 | awk '{printf "%.17g\n", $1*1.1}'
0.54674805518902947

é provavelmente o melhor que você pode conseguir. Use em bcvez disso para precisão arbitrária.

$ echo '0.4970436865354813 * 1.1' | bc -l
.54674805518902943
Stéphane Chazelas
fonte
Se você quiser precisão arbitrária em AWKque você pode usar a -Mbandeira e definir o PRECvalor a um grande número
Robert Benson
3
@RobertBenson, apenas com o GNU awk e apenas com versões recentes (4.1 ou superior, portanto não no momento em que a resposta foi escrita) e somente quando o MPFR foi ativado no tempo de compilação.
Stéphane Chazelas
2

Para maior precisão com o (GNU) awk (com o bignum compilado), use:

$ echo '0.4970436865354813' | awk -M -v PREC=100 '{printf("%.18f\n", $1)}'
0.497043686535481300

O PREC = 100 significa 100 bits em vez dos 53 bits padrão.
Se esse awk não estiver disponível, use bc

$ echo '0.4970436865354813*1.1' | bc -l
.54674805518902943

Ou você precisará aprender a conviver com a imprecisão inerente dos carros alegóricos.


Nas suas linhas originais, existem vários problemas:

  • Um fator de 1,1 é um aumento de 10%, não 1% (deve ser um multiplicador de 1,01). Eu vou usar 10%.
  • O formato de conversão de uma sequência para um número (flutuante) é fornecido pelo CONVFMT. Seu valor padrão é %.6g. Isso limita os valores a 6 dígitos decimais (após o ponto). Isso é aplicado ao resultado da alteração gsub de $1.

    $ a='0.4970436865354813'
    $ echo "$a" | awk '{printf("%.16f\n", $1*1.1)}'
    0.5467480551890295
    
    $ echo "$a" | awk '{gsub($1, $1*1.1)}; {printf("%.16f\n", $1)}'
    0.5467480000000000
  • O formato printf gremove zeros à direita:

    $ echo "$a" | awk '{gsub($1, $1*1.1)}; {printf("%.16g\n", $1)}'
    0.546748
    
    $ echo "$a" | awk '{gsub($1, $1*1.1)}; {printf("%.17g\n", $1)}'
    0.54674800000000001

    Ambos os problemas podem ser resolvidos com:

    $ echo "$a" | awk '{printf("%.17g\n", $1*1.1)}'
    0.54674805518902947

    Ou

    $ echo "$a" | awk -v CONVFMT=%.30g '{gsub($1, $1*1.1)}; {printf("%.17f\n", $1)}'
    0.54674805518902947 

Mas não pense que isso significa maior precisão. A representação interna do número ainda é uma bóia em tamanho duplo. Isso significa 53 bits de precisão e, com isso, você só pode ter certeza de 15 dígitos decimais corretos, mesmo que muitas vezes até 17 dígitos pareçam corretos. Isso é uma miragem.

$ echo "$a" | awk -v CONVFMT=%.30g '{gsub($1, $1*1.1}; {printf("%.30f\n", $1)}'
0.546748055189029469325134868996

O valor correto é:

$ echo "scale=18; 0.4970436865354813 * 1.1" | bc
.54674805518902943

O que também pode ser calculado com (GNU) awk se a biblioteca bignum tiver sido compilada em:

$ echo "$a" | awk -M -v PREC=100 -v CONVFMT=%.30g '{printf("%.30f\n", $1)}'
0.497043686535481300000000000000
NotAnUnixNazi
fonte