Como arredondar decimais usando bc no bash?

45

Um exemplo rápido do que eu quero usar o script bash:

#!/bin/bash
echo "Insert the price you want to calculate:"
read float
echo "This is the price without taxes:"
echo "scale=2; $float/1.18" |bc -l
read -p "Press any key to continue..."
bash scriptname.sh

Supondo que o preço seja: 48,86 A resposta será: 41.406779661 (41.40 na verdade porque estou usando scale=2;)

Minha pergunta é: Como arredondar o segundo decimal para mostrar a resposta dessa maneira ?: 41.41

blackedx
fonte
Acho estranho porque "printf"% 0.2f \ n "41.445" agora funciona, mas "printf"% 0.2f \ n "41.435 e printf"% 0.2f \ n "41.455" funcionam. Mesmo o seu próprio caso funciona (On 12,04), mas não com o 0,445
Luis Alvarado
8
IMHO, ninguém respondeu a essa pergunta satisfatoriamente , talvez porque bcnão consiga alcançar o que está sendo solicitado (ou pelo menos a pergunta que eu estava fazendo quando encontrei este post), que é como arredondar decimais usandobc (que é chamado por bash).
Adam Katz
Sei que essa pergunta é antiga, mas não consigo resistir a um comentário empurrando minha própria implementação do bc: github.com/gavinhoward/bc . Para isso, está na biblioteca interna com a r()função, para que você possa usá-lo bc -le "r($float/1.18, 2)".
Gavin Howard

Respostas:

31

Uma função bash round:

round()
{
echo $(printf %.$2f $(echo "scale=$2;(((10^$2)*$1)+0.5)/(10^$2)" | bc))
};

Usado no seu exemplo de código:

#!/bin/bash
# the function "round()" was taken from 
# http://stempell.com/2009/08/rechnen-in-bash/

# the round function:
round()
{
echo $(printf %.$2f $(echo "scale=$2;(((10^$2)*$1)+0.5)/(10^$2)" | bc))
};

echo "Insert the price you want to calculate:"
read float
echo "This is the price without taxes:"
#echo "scale=2; $float/1.18" |bc -l
echo $(round $float/1.18 2);
read -p "Press any key to continue..."

Boa sorte: o)

user85321
fonte
1
btw, se o número for negativo, nós temos que usar-0.5
Aquarius Power
Não foi possível obter o exemplo para trabalhar no console de teste! "Depurar" revelou um pouco que, por exemplo, $2 = 3eu tinha que usar echo $(env printf %.3f $(echo "scale=3;((1000*$1)+0.5)/1000" | bc)). Cuidado com o env antes do printf! Isso vai te ensinar novamente que é sempre importante entender o que você está copiando e colando de outro lugar.
Syntaxerror 23/10
@Aquarius Power obrigado pela inspiração! Agora eu peguei uma versão do script acima, que funcionará com números negativos e positivos.
Syntaxerror
Isso é desnecessariamente complicado e faz todo tipo de aritmética extra para chegar às respostas mais simples fornecidas por migas e zuberuber.
Adam Katz
@AquariusPower +1 sim, é diferente para números negativos. Para testar um número negativo (não inteiro!), Tentei um pouco e aceitei o if [ eco "$ 1/1" | bc` -gt 0] `- existe uma maneira mais elegante de verificar (exceto analisar a string para" - ")?
precisa saber é
30

Solução mais simples:

printf %.2f $(echo "$float/1.18" | bc -l)
migas
fonte
2
Como observado também em um comentário acima ( askubuntu.com/a/179949/512213 ), isso se comportaria incorretamente para números negativos, infelizmente.
RobertG 02/03
2
@RobertG: Por quê? Esta solução não usa +0.5. Tente printf "%.2f\n" "$(bc -l <<<"48.86/1.18")" "$(bc -l <<<"-48.86/1.18")"e você receberá 41.41e -41.41.
Musiphil 12/08/16
1
Também é possível usar um cachimbo:echo "$float/1.18" | bc -l | xargs printf %.2f
dessert
A solução nesta resposta me fornece:, bash: printf: 1.69491525423728813559: invalid numberdependendo da localidade.
Torsten Bronger 12/09
19

Arredondamento de bash / awk:

echo "23.49" | awk '{printf("%d\n",$1 + 0.5)}'  

Se você tem python, pode usar algo como isto:

echo "4.678923" | python -c "print round(float(raw_input()))"
zuberuber
fonte
Obrigado pela dica, mas não resolve o que eu preciso. Eu tenho que fazê-lo apenas usando o bash ...
blackedx
1
O comando python é mais legível e bom para scripts rápidos. Também suporta arredondamentos arbitrários de dígitos adicionando, por exemplo, ", 3" à função de arredondamento. Obrigado
Elle
echo "$float" |awk '{printf "%.2f", $1/1.18}'executará a matemática solicitada da pergunta na percisão solicitada de centésimos. Isso é tanto "usando o bash" quanto a bcchamada da pergunta.
Adam Katz
2
Para Python, você pode usar algo como python -c "print(round($num))"onde num=4.678923. Não há necessidade de mexer com stdin. Você pode também rodada de n dígitos assim: python -c "print(round($num, $n))".
6
Consegui fazer algo bem legal com essa solução, meu problema era 0,5; nesse caso, nem sempre eu conseguia a rodada de que precisava, então eu vim com o seguinte: echo 5 | python -c "print int(round((float(raw_input())/2)-0.5))"(Quando + será arredondado para cima e para baixo).
Yaron
4

Aqui está uma solução puramente bc. Regras de arredondamento: em +/- 0,5, arredondar para zero.

Coloque a escala que você está procurando em $ result_scale; sua matemática deve estar onde $ MATH está localizado na lista de comandos bc:

bc <<MATH
h=0
scale=0

/* the magnitude of the result scale */
t=(10 ^ $result_scale)

/* work with an extra digit */
scale=$result_scale + 1

/* your math into var: m */
m=($MATH)

/* rounding and output */
if (m < 0) h=-0.5
if (m > 0) h=0.5

a=(m * t + h)

scale=$result_scale
a / t
MATH
MetroEast
fonte
1
muito interessante! MATH=-0.34;result_scale=1;bc <<MATH, #ObjectiveAndClear: 5, como explicado aqui :) #
Aquarius Power
Eu o nomeio para "melhor resposta".
Marc Tamsky 18/06
2

Eu sei que é uma pergunta antiga, mas tenho uma solução 'bc' pura sem 'if' ou ramificações:

#!/bin/sh
bcr()
{
    echo "scale=$2+1;t=$1;scale-=1;(t*10^scale+((t>0)-(t<0))/2)/10^scale" | bc -l
}

Use-o como bcr '2/3' 5ou bcr '0.666666' 2-> (expressão seguida de escala)

Isso é possível porque em bc (como C / C ++) é permitido misturar expressões lógicas em seus cálculos. A expressão ((t>0)-(t<0))/2)será avaliada em +/- 0,5, dependendo do sinal de 't' e, portanto, usará o valor correto para arredondar.

Marcus Kolenda
fonte
Parece legal, mas bcr "5 / 2" 0retorna em 2vez de 3. Estou esquecendo de algo?
blujay
1
#!/bin/bash
# - loosely based on the function "round()", taken from 
# http://stempell.com/2009/08/rechnen-in-bash/

# - inspired by user85321 @ askubuntu.com (original author)
#   and Aquarius Power

# the round function (alternate approach):

round2()
{
    v=$1
    vorig=$v
    # if negative, negate value ...
    (( $(bc <<<"$v < 0") == 1 )) && v=$(bc <<<"$v * -1")
    r=$(bc <<<"scale=$3;(((10^$3)*$v/$2)+0.5)/(10^$3)")

    # ... however, since value was only negated to get correct rounding, we 
    # have to add the minus sign again for the resulting value ...

    (( $(bc <<< "$vorig < 0") == 1 )) && r=$(bc <<< "$r * -1")
    env printf %.$3f $r
};

echo "Insert the price you want to calculate:"
read float
echo "This is the price without taxes:"
round2 $float 1.18 2
echo && read -p "Press any key to continue..."

Na verdade, é simples: não há necessidade de adicionar explicitamente uma variante "-0.5" codificada para números negativos. Falado matematicamente, apenas calcularemos o valor absoluto do argumento e ainda adicionaremos 0,5 como normalmente. Mas como (infelizmente) não temos nenhuma abs()função interna à nossa disposição (a menos que codifiquemos uma), simplesmente negaremos o argumento, se for negativo.

Além disso, provou ser muito complicado trabalhar com o quociente como parâmetro (já que, para minha solução, preciso acessar o dividendo e o divisor separadamente). É por isso que meu script tem um terceiro parâmetro adicional.

erro de sintaxe
fonte
@ muru sobre suas edições recentes: herestrings em vez de echo .. |aritmética de shell, qual é o objetivo echo $()? É fácil de explicar: suas strings here sempre exigirão um diretório gravável/tmp ! E a minha solução também irá trabalhar em um ambiente somente de leitura, por exemplo, um shell de root de emergência, onde /é não sempre gravável por padrão. Portanto, havia uma boa razão pela qual eu codifiquei dessa maneira.
Syntaxerror 17/04/2015
Herestrings, eu concordo. Mas quando é echo $()necessário? Isso e indentação levaram minhas edições, as herestrings simplesmente aconteceram.
Muru
@ muru Bem, o caso mais inútil echo $()disso estava atrasado para ser consertado era, de fato, a penúltima linha, lol. Obrigado pelo aviso. Pelo menos este parece muito melhor agora, não posso negar. Enfim, adorei a lógica disso. Muitas coisas booleanas, menos "se" s supérfluas (o que certamente aumentaria o código em 50%).
Syntaxerror 17/04/2015
1

Ainda estou procurando uma bcresposta pura para arredondar apenas um valor dentro de uma função, mas aqui está uma bashresposta pura :

#!/bin/bash

echo "Insert the price you want to calculate:"
read float
echo "This is the price without taxes:"

embiggen() {
  local int precision fraction=""
  if [ "$1" != "${1#*.}" ]; then  # there is a decimal point
    fraction="${1#*.}"       # just the digits after the dot
  fi
  int="${1%.*}"              # the float as a truncated integer
  precision="${#fraction}"   # the number of fractional digits
  echo $(( 10**10 * $int$fraction / 10**$precision ))
}

# round down if negative
if [ "$float" != "${float#-}" ]
  then round="-5000000000"
  else round="5000000000"
fi

# calculate rounded answer (sans decimal point)
answer=$(( ( `embiggen $float` * 100 + $round ) / `embiggen 1.18` ))

int=${answer%??}  # the answer as a truncated integer

echo $int.${answer#$int}  # reassemble with correct precision

read -p "Press any key to continue..."

Basicamente, isso extrai cuidadosamente os decimais, multiplica tudo por 100 bilhões (10¹⁰, 10**10pol bash), ajusta a precisão e o arredondamento, executa a divisão real, divide de volta à magnitude apropriada e reinsere o decimal.

Passo a passo:

A embiggen()função atribui a forma inteira truncada de seu argumento $inte salva os números após o ponto $fraction. O número de dígitos fracionários é anotado em $precision. A matemática multiplica 10¹⁰ pela concatenação de $inte $fractionajusta-a para corresponder à precisão (por exemplo, embiggen 48.86torna-se 10 × 4886000000004886/100 e retorna 488.600.000.000).

Como queremos uma precisão final de centésimos, multiplicamos o primeiro número por 100, adicionamos 5 para fins de arredondamento e dividimos o segundo número. Essa tarefa $answernos deixa cem vezes a resposta final.

Agora precisamos adicionar o ponto decimal. Atribuímos um novo $intvalor para $answerexcluir seus dois dígitos finais, depois echocom um ponto e $answerexcluindo o $intvalor que já foi resolvido. (Não importa o erro de destaque da sintaxe que faz com que isso pareça um comentário)

(Bashism: exponenciação não é POSIX, então isso é um basismo. Uma solução POSIX pura exigiria que loops adicionassem zeros em vez de usar potências de dez. Além disso, "embiggen" é uma palavra perfeitamente cromulante.)


Um dos principais motivos pelos quais uso zshcomo shell é o suporte à matemática de ponto flutuante. A solução para esta pergunta é bastante direta em zsh:

printf %.2f $((float/1.18))

(Adoraria ver alguém adicionar um comentário a esta resposta com o truque para ativar a aritmética de ponto flutuante bash, mas tenho certeza de que esse recurso ainda não existe.)

Adam Katz
fonte
1

se você tiver o resultado, por exemplo, considere 2.3747888

Tudo que você tem a fazer é:

d=$(echo "(2.3747888+0.5)/1" | bc); echo $d

isso arredonda o número corretamente exemplo:

(2.49999 + 0.5)/1 = 2.99999 

os decimais são removidos bce, por isso, arredondam para 2, como deveria

user525061
fonte
1

Eu tive que calcular a duração total de uma coleção de arquivos de áudio.

Então eu tive que:

A. obtenha a duração de cada arquivo ( não mostrado )

B. some todas as durações (elas estavam em NNN.NNNNNN(fp) segundos)

C. separar horas, minutos, segundos, subsegundos.

D. produz uma string de HR: MIN: SEC: FRAMES, em que frame = 1/75 seg.

(Os quadros vêm do código SMPTE usado nos estúdios.)


R: use ffprobee analise a linha de duração em um número fp (não mostrado)

B:

 # add them up as a series of strings separated by "+" and send it to bc

arr=( "${total[@]}" )  # copy array

# IFS is "Internal Field Separator"
# the * in arr[*] means "all of arr separated by IFS" 
# must have been made for this
IFS='+' sum=$(echo "scale=3; ${arr[*]} "| bc -l)# (-l= libmath for fp)
echo $sum 

C:

# subtract each amount of time from tt and store it    
tt=$sum   # tt is a running var (fp)


hrs=$(echo "$tt / 3600" | bc)

tt=$(echo "$tt - ( $hrs * 3600 )" | bc )

min=$(echo "$tt / 60" | bc )

tt=$(echo "$tt - ($min *60)" | bc )

sec=$(echo "$tt/1" | bc )

tt=$(echo "$tt - $sec" | bc )

frames=$(echo "$tt * 75"  | bc ) # 75 frames /sec 
frames=$(echo "$frames/1" | bc ) # truncate to whole #

D:

#convert to proper format with printf (bash builtin)        
hrs=$(printf "%02d\n" $hrs)  # format 1 -> 01 

min=$(printf "%02d\n" $min)

sec=$(printf "%02d\n" $sec)

frames=$(printf "%02d\n" $frames)

timecode="$hrs:$min:$sec:$frames"

# timecode "01:13:34:54"
Chris Reid
fonte
1

Aqui está uma versão abreviada do seu script, corrigida para fornecer a saída desejada:

#!/bin/bash
float=48.86
echo "You asked for $float; This is the price without taxes:"
echo "scale=3; price=$float/1.18 +.005; scale=2; price/1 " | bc

Observe que arredondar para o número inteiro mais próximo equivale a adicionar 0,5 e pegar o piso ou arredondar para baixo (para números positivos).

Além disso, o fator de escala é aplicado no momento da operação; então (estes são bccomandos, você pode colá-los no seu terminal):

float=48.86; rate=1.18; 
scale=2; p2=float/rate
scale=3; p3=float/rate
scale=4; p4=float/rate
print "Compare:  ",p2, " v ", p3, " v ", p4
Compare:  41.40 v 41.406 v 41.4067

# however, scale does not affect an entered value (nor addition)
scale=0
a=.005
9/10
0
9/10+a
.005

# let's try rounding
scale=2
p2+a
41.405
p3+a
41.411
(p2+a)/1
41.40
(p3+a)/1
41.41
toddkaufmann
fonte
1

Implementação bc pura conforme solicitado

define ceil(x) { auto os,xx;x=-x;os=scale;scale=0 xx=x/1;if(xx>x).=xx-- scale=os;return(-xx) }

se você colocar isso em um arquivo chamado functions.bc, poderá arredondar para

echo 'ceil(3.1415)' | bc functions.bc

Código para a implementação bc encontrado em http://phodd.net/gnu-bc/code/funcs.bc

Aethalides
fonte
0
bc() {
        while :
        do
                while IFS='$\n' read i
                do
                        /usr/bin/bc <<< "scale=2; $i" | sed 's/^\./0&/'
                done
        done
}
sjas
fonte
0

Você pode usar o awk, sem necessidade de pipes:

$> PRICE=48.86
$> awk -vprice=$PRICE "BEGIN{printf(\"%.2f\n\",price/1.18+0.005)}"
41.41

Defina o preço antecipadamente com a variável de ambiente PRICE=<val>.

  • Em seguida, chame o awk - $PRICEentrará no awk como uma variável do awk price.

  • Em seguida, é arredondado para o centésimo mais próximo com +,005.

  • A opção de formatação printf %.2flimita a escala a duas casas decimais.

Se você precisar fazer isso muito desta maneira, será muito mais eficiente que a resposta selecionada:

time for a in {1..1000}; do echo $(round 48.86/1.18 2) > /dev/random; done
real    0m8.945s
user    0m6.067s
sys 0m4.277s

time for a in {1..1000}; do awk -vprice=$PRICE "BEGIN{printf(\"%.2f\n\",price/1.18+0.005)}">/dev/random; done
real    0m2.628s
user    0m1.462s
sys 0m1.275s
A.Danischewski
fonte