awk aritmética de alta precisão

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 a leitura de um campo de um arquivo e a sua substituição por um incremento de 1% nesse valor. No entanto, estou perdendo precisão aí. Aqui está uma reprodução simplificada do problema:

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

Aqui, eu tenho 16 dígitos após a precisão decimal, mas awk dá 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 sobre como obter a precisão desejada?

Comentários

  • Talvez o awk tenha resolução mais alta, mas ' apenas a formatação de saída está truncada. Use printf.
  • Nenhuma mudança no valor do resultado após usar printf. Questão editada de acordo.
  • Como @manatwork apontou, que gsub é desnecessário. O problema é que gsub funciona em strings, não em números, então uma conversão é feita primeiro usando CONVFMT, e o valor padrão para isso é %.6g.
  • @ jw013, Como mencionei na pergunta, meu problema original requer gsub, pois preciso substituir um número por um incremento de 1%. Concordado, no exemplo simplificado, não é necessário.

Resposta

$ 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 bc em vez de precisão arbitrária.

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

Comentários

  • Se quiser precisão arbitrária em AWK, você pode usar o sinalizador -M e definir o PREC valor para um grande número
  • @RobertBenson, apenas com 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 MPFR foi habilitado na compilação entretanto.

Resposta

Para maior precisão com (GNU) awk (com bignum compilado em), 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 flutuadores.


Em suas linhas originais, há vários problemas:

  • Um fator de 1,1 é 10% de aumento, não 1% (deveria ser um Multiplicador 1,01). Usarei 10%.
  • O formato de conversão de uma string para um número (flutuante) é fornecido por 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 g remove os zeros finais:

    $ 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 do número interno ainda é um float em tamanho duplo. Isso significa 53 bits de precisão e com isso você só poderia ter certeza de 15 dígitos decimais corretos, mesmo se muitas vezes até 17 dígitos parecerem 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 

Que poderia também pode ser calculado com (GNU) awk se a biblioteca bignum foi compilada em:

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

Resposta

Meu script awk é maior do que apenas uma linha, então usei a combinação das respostas de Stéphane Chazelas e Isaac:

  1. Eu defini CONVFMT que cuidará globalmente da formatação de saída
  2. Eu também uso o parâmetro bignum -M junto com PREC variável

Trecho de exemplo:

#!/usr/bin/awk -M -f BEGIN { FS="<|>" CONVFMT="%.18g" PREC=100 } { if ($2 == "LatitudeDegrees") { CORR = $3 // redacted specific corrections print(" <LatitudeDegrees>" CORR "</LatitudeDegrees>"); } else if ($2 == "LongitudeDegrees") { CORR = $3 // redacted specific corrections print(" <LongitudeDegrees>" CORR "</LongitudeDegrees>"); } else { print($0); } } END { } 

OP simplificou seu exemplo, mas se o O script awk não é um liner que você não deseja poluir com printf s, mas defina o formato como este na variável. Da mesma forma, a precisão para que não se perca na chamada real da linha de comando.

Deixe uma resposta

O seu endereço de email não será publicado. Campos obrigatórios marcados com *