Esta resposta à primeira pergunta vinculada tem a linha quase descartável no final:
Consulte também %g
para arredondar para um número especificado de dígitos significativos.
Então você pode simplesmente escrever
printf "%.2g" "$n"
(mas consulte a seção abaixo no separador decimal e na localidade e observe que o não-Bash printf
não precisa suportar %f
e %g
).
Exemplos:
$ printf "%.2g\n" 76543 0.0076543
7.7e+04
0.0077
Claro, agora você tem representação de mantissa-expoente em vez de decimal puro, portanto, você deve converter novamente:
$ printf "%0.f\n" 7.7e+06
7700000
$ printf "%0.7f\n" 7.7e-06
0.0000077
Reunindo tudo isso e agrupando-o em uma função:
# Function round(precision, number)
round() {
n=$(printf "%.${1}g" "$2")
if [ "$n" != "${n#*e}" ]
then
f="${n##*e-}"
test "$n" = "$f" && f= || f=$(( ${f#0}+$1-1 ))
printf "%0.${f}f" "$n"
else
printf "%s" "$n"
fi
}
(Nota - essa função é escrita no shell portátil (POSIX), mas assume que printf
lida com as conversões de ponto flutuante. O Bash tem um built-in printf
que sim, então você está bem aqui, e a implementação do GNU também funciona, portanto, a maioria do GNU Sistemas Linux podem usar com segurança o Dash).
Casos de teste
radix=$(printf %.1f 0)
for i in $(seq 12 | sed -e 's/.*/dc -e "12k 1.234 10 & 6 -^*p"/e' -e "y/_._/$radix/")
do
echo $i "->" $(round 2 $i)
done
Resultado dos testes
.000012340000 -> 0.000012
.000123400000 -> 0.00012
.001234000000 -> 0.0012
.012340000000 -> 0.012
.123400000000 -> 0.12
1.234 -> 1.2
12.340 -> 12
123.400 -> 120
1234.000 -> 1200
12340.000 -> 12000
123400.000 -> 120000
1234000.000 -> 1200000
Uma observação sobre separador decimal e localidade
Todo o trabalho acima pressupõe que o caractere de raiz (também conhecido como separador decimal) seja .
, como na maioria das localidades em inglês. Outros locais usam em ,
vez disso, e alguns shells têm um built-in printf
que respeita o local. Nessas shells, pode ser necessário definir LC_NUMERIC=C
para forçar o uso .
como caractere radix ou escrever /usr/bin/printf
para impedir o uso da versão interna. Este último é complicado pelo fato de que (pelo menos algumas versões) parecem sempre analisar argumentos usando .
, mas imprimindo usando as configurações atuais de localidade.
%f
/%g
, mas esse é oprintf
argumento, e não é necessário um POSIXprintf
para ter um shell POSIX. Eu acho que você deveria ter comentado em vez de editar lá.printf %g
não pode ser usado em um script POSIX. É verdade que tudoprintf
depende do utilitário, mas esse utilitário está embutido na maioria dos shells. O OP foi marcado como bash, portanto, usar um shebang do bash é uma maneira fácil de obter um printf compatível com% g. Caso contrário, você precisa adicionar um assumindo que o seu printf (ou o builtin printf do seush
casoprintf
é builtin lá) suporta a não-padrão (mas bastante comum)%g
...dash
tem um builtinprintf
(que suporta%g
). Nos sistemas GNU,mksh
é provavelmente o único shell hoje em dia que não terá um builtinprintf
.bash
) e relegar um pouco disso para as anotações - parece correto agora?TL; DR
Basta copiar e usar a função
sigf
na seçãoA reasonably good "significant numbers" function:
. Está escrito (como todo o código nesta resposta) para trabalhar com o hífen .Dará a
printf
aproximação à parte inteira de N com$sig
dígitos.Sobre o separador decimal.
O primeiro problema a ser resolvido com printf é o efeito e o uso da "marca decimal", que nos EUA é um ponto e no DE é uma vírgula (por exemplo). É um problema porque o que funciona para um código de idioma (ou shell) falhará com outro código de idioma. Exemplo:
Uma solução comum (e incorreta) é definir
LC_ALL=C
o comando printf. Mas isso define a marca decimal para um ponto decimal fixo. Para locais onde uma vírgula (ou outro) é o caractere usado comum que é um problema.A solução é descobrir dentro do script para o shell que está executando qual é o separador decimal da localidade. Isso é bem simples:
Removendo zeros:
Esse valor é usado para alterar o arquivo com a lista de testes:
Isso torna as execuções em qualquer shell ou código de idioma automaticamente válidas.
Alguns princípios.
Deve ser intuitivo cortar o número a ser formatado com o formato
%.*e
ou mesmo%.*g
de printf. A principal diferença entre usar%.*e
ou%.*g
é como eles contam dígitos. Um usa a contagem completa, o outro precisa da contagem menos 1:Isso funcionou bem para 4 dígitos significativos.
Depois que o número de dígitos foi cortado, precisamos de uma etapa adicional para formatar números com expoentes diferentes de 0 (como era acima).
Isso funciona corretamente. A contagem da parte inteira (à esquerda da marca decimal) é apenas o valor do expoente ($ exp). A contagem de casas decimais necessária é o número de dígitos significativos ($ sig) menos a quantidade de dígitos já usada na parte esquerda do separador decimal:
Como a parte integrante do
f
formato não tem limite, de fato não há necessidade de declará-lo explicitamente e este código (mais simples) funciona:Primeiro julgamento.
Uma primeira função que poderia fazer isso de uma maneira mais automatizada:
Essa primeira tentativa funciona com muitos números, mas falhará com números para os quais a quantidade de dígitos disponíveis é menor que a contagem significativa solicitada e o expoente é menor que -4:
Ele adicionará muitos zeros que não são necessários.
Segundo julgamento.
Para resolver isso, precisamos limpar N do expoente e quaisquer zeros à direita. Então, podemos obter o tamanho efetivo dos dígitos disponíveis e trabalhar com isso:
No entanto, isso é usar a matemática do ponto flutuante e "nada é simples no ponto flutuante": Por que meus números não são somados?
Mas nada no "ponto flutuante" é simples.
Contudo:
Por quê?:
E, também, o comando
printf
é construído com muitas conchas.Quais
printf
impressões podem mudar com o shell:Uma função razoavelmente boa de "números significativos":
E os resultados são:
fonte
Se você já tiver o número como uma sequência, ou seja, "3456" ou "0,003756", poderá fazer isso apenas usando a manipulação de sequências. O seguinte está fora do topo da minha cabeça, e não foi completamente testado, e usa sed, mas considere:
Onde basicamente você tira e salva qualquer material "-0.000" no início, depois use uma operação simples de substring no restante. Uma ressalva sobre o exposto acima é que vários 0s iniciais não são removidos. Vou deixar isso como um exercício.
fonte