¿Cómo redondear decimales usando bc en bash?


45

Un ejemplo rápido de lo que quiero usar bash scripting:

#!/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

Suponiendo que el precio es: 48.86 La respuesta será: 41.406779661 (41.40 en realidad porque estoy usando scale=2;)

Mi pregunta es: ¿Cómo redondeo el segundo decimal para mostrar la respuesta de esta manera ?: 41.41


Lo encuentro extraño porque "printf"% 0.2f \ n "41.445" ahora funciona pero "printf"% 0.2f \ n "41.435 e printf"% 0.2f \ n "41.455" sí. Incluso su propio caso funciona (el 12.04) pero no con el .445
Luis Alvarado

8
En mi humilde opinión, nadie ha respondido esta pregunta satisfactoriamente , tal vez porque bcno puede lograr lo que se solicita (o al menos la pregunta que estaba haciendo cuando encontré esta publicación), que es cómo redondear decimales usandobc (eso se llama por bash).
Adam Katz

Sé que esta pregunta es antigua, pero no puedo resistir un comentario que empuja mi propia implementación de bc: github.com/gavinhoward/bc . Para eso, está en la biblioteca incorporada con la r()función, por lo que puede usar bc -le "r($float/1.18, 2)".
Gavin Howard

Respuestas:


31

Una función bash round:

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

Usado en su ejemplo 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..."

Buena suerte: o)


1
por cierto, si el número es negativo, tenemos que usar-0.5
Aquarius Power

¡No se pudo obtener el ejemplo para trabajar en la consola de prueba! "Depuración" reveló un poco que, por ejemplo $2 = 3, tuve que usar echo $(env printf %.3f $(echo "scale=3;((1000*$1)+0.5)/1000" | bc)). ¡Cuidado con el ambiente antes de printf! Esto te enseñará de nuevo que siempre es importante entender lo que estás copiando de otras partes.
syntaxerror

@Aquarius Power gracias por la inspiración! Ahora he bifurcado una versión del script anterior que funcionará con números negativos y positivos.
syntaxerror

Esto es innecesariamente complicado y hace todo tipo de aritmética adicional para llegar a las respuestas más simples proporcionadas por migas y zuberuber.
Adam Katz

@AquariusPower +1 sí, es diferente para números negativos. Para probar un número negativo (¡no entero!), Intenté un poco y me decidí por if [ echo "$ 1/1" | bc` -gt 0] `- ¿hay alguna forma más elegante de verificar (excepto analizar la cadena para" - ")?
RobertG

30

La solución más simple:

printf %.2f $(echo "$float/1.18" | bc -l)

2
Como se señaló también en un comentario anterior ( askubuntu.com/a/179949/512213 ), lamentablemente , esto se comportaría incorrectamente para los números negativos.
RobertG

2
@RobertG: ¿Por qué? Esta solución no usa +0.5. Intenta printf "%.2f\n" "$(bc -l <<<"48.86/1.18")" "$(bc -l <<<"-48.86/1.18")"y obtendrás 41.41y -41.41.
musiphil

1
También es posible usar una pipa:echo "$float/1.18" | bc -l | xargs printf %.2f
postre

La solución en esta respuesta me da: bash: printf: 1.69491525423728813559: invalid numberdependiendo de la configuración regional.
Torsten Bronger

19

Bash / awk redondeo:

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

Si tienes Python puedes usar algo como esto:

echo "4.678923" | python -c "print round(float(raw_input()))"

Gracias por el consejo, pero no resuelve lo que necesito. Tengo que hacerlo simplemente usando bash ...
blackedx

1
El comando python es más legible y bueno para scripts rápidos. También admite el redondeo de dígitos arbitrario al agregar, por ejemplo, ", 3" a la función de redondeo. Gracias
Elle

echo "$float" |awk '{printf "%.2f", $1/1.18}'realizará la matemática solicitada de la pregunta a la precisión solicitada de centésimas. Eso es tanto "usar bash" como la bcllamada en la pregunta.
Adam Katz

2
Para Python puedes usar algo como python -c "print(round($num))"dónde num=4.678923. No hay necesidad de fastidiar con stdin. También puede redonda de n dígitos, así: python -c "print(round($num, $n))".
Seis

Logré hacer algo bastante bueno con esa solución, mi problema era 0.5, en este caso no siempre obtuve la ronda que necesitaba, así que se me ocurrió lo siguiente: echo 5 | python -c "print int(round((float(raw_input())/2)-0.5))"(Cuando + redondeará hacia arriba y - redondeará hacia abajo).
Yaron

4

Aquí hay una solución puramente bc. Reglas de redondeo: a +/- 0.5, redondeado desde cero.

Pon la escala que estás buscando en $ result_scale; sus matemáticas deben estar donde se encuentra $ MATH en la lista de comandos de 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

1
¡muy interesante! MATH=-0.34;result_scale=1;bc <<MATH, #ObjectiveAndClear: 5 como se explica aquí :)
Aquarius Power

Nomino esto para "mejor respuesta".
Marc Tamsky

2

Sé que es una vieja pregunta, pero tengo una solución 'bc' pura sin 'if' o ramas:

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

Úselo como bcr '2/3' 5o bcr '0.666666' 2-> (expresión seguida de escala)

Eso es posible porque en bc (como C / C ++) está permitido mezclar expresiones lógicas en sus cálculos. La expresión ((t>0)-(t<0))/2)se evaluará a +/- 0.5 dependiendo del signo de 't' y, por lo tanto, usará el valor correcto para redondear.


Esto se ve bien, pero bcr "5 / 2" 0regresa en 2lugar de 3. ¿Me estoy perdiendo 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..."

En realidad es simple: no es necesario agregar explícitamente una variante "-0.5" codificada para números negativos. Hablando matemáticamente, simplemente calcularemos el valor absoluto del argumento y aún sumaremos 0.5 como lo haríamos normalmente. Pero dado que (desafortunadamente) no tenemos una abs()función incorporada a nuestra disposición (a menos que codifiquemos una), simplemente negaremos el argumento si es negativo.

Además, resultó muy engorroso trabajar con el cociente como parámetro (ya que para mi solución, debo poder acceder al dividendo y al divisor por separado). Es por eso que mi script tiene un tercer parámetro adicional.


@muru sobre sus ediciones recientes: herejes en lugar de echo .. |aritmética de concha, ¿de qué sirve echo $()? Esto es fácil de explicar: ¡sus cadenas aquí siempre requerirán un directorio de escritura/tmp ! Y mi solución también funciona en un entorno de sólo lectura, por ejemplo, un intérprete de comandos de emergencia donde /es que no siempre se puede escribir de forma predeterminada. Así que había una buena razón por la que lo codifiqué de esa manera.
syntaxerror

Aquí, estoy de acuerdo. Pero cuando echo $()alguna vez se necesita? Eso y la sangría provocaron mis ediciones, las herejías simplemente ocurrieron.
muru

@muru Bueno, el caso más inútil de echo $()eso que se debió arreglar fue, de hecho, la penúltima línea, jajaja. Gracias por el aviso. Al menos este se ve mucho mejor ahora, no puedo negarlo. De todos modos, me encantó la lógica en esto. Un montón de cosas booleanas, menos "superfluos" (que seguramente explotarían el código en un 50 por ciento).
syntaxerror

1

Todavía estoy buscando una bcrespuesta pura sobre cómo redondear solo un valor dentro de una función, pero aquí hay una bashrespuesta 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..."

Básicamente, esto extrae cuidadosamente los decimales, multiplica todo por 100 mil millones (10¹⁰, 10**10in bash), ajusta la precisión y el redondeo, realiza la división real, divide de nuevo a la magnitud apropiada y luego reinserta el decimal.

Paso a paso:

La embiggen()función asigna la forma entera truncada de su argumento $inty guarda los números después del punto $fraction. El número de dígitos fraccionarios se indica en $precision. La matemática multiplica 10¹⁰ por la concatenación de $inty $fractionluego ajusta eso para que coincida con la precisión (por ejemplo, se embiggen 48.86convierte en 10¹⁰ × 4886/100 y devuelve 488600000000488,600,000,000).

Queremos una precisión final de centésimos, por lo que multiplicamos el primer número por 100, sumamos 5 para redondear y luego dividimos el segundo número. Esta asignación de $answernos deja en cien veces la respuesta final.

Ahora necesitamos agregar el punto decimal. Asignamos un nuevo $intvalor a la $answerexclusión de sus dos dígitos finales, luego echocon un punto y $answerexcluyendo el $intvalor que ya se ha solucionado. (No importa el error de resaltado de sintaxis que hace que esto parezca un comentario)

(Bashismo: la exponenciación no es POSIX, por lo que es un bashismo. Una solución POSIX pura requeriría bucles para agregar ceros en lugar de usar potencias de diez. Además, "embiggen" es una palabra perfectamente cromulante).


Una de las razones principales que uso zshcomo mi shell es que admite matemáticas de punto flotante. La solución a esta pregunta es bastante sencilla en zsh:

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

(Me encantaría ver a alguien agregar un comentario a esta respuesta con el truco para habilitar la aritmética de punto flotante bash, pero estoy bastante seguro de que dicha característica aún no existe).


1

si tiene el resultado, por ejemplo considere 2.3747888

todo lo que tienes que hacer es:

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

esto redondea el número correctamente ejemplo:

(2.49999 + 0.5)/1 = 2.99999 

los decimales se eliminan bcy se redondea a 2 como debería


1

Tuve que calcular la duración total de una colección de archivos de audio.

Entonces tuve que:

A. obtener la duración de cada archivo ( no se muestra )

B. sume todas las duraciones (cada una en NNN.NNNNNNsegundos (fp))

C. separar horas, minutos, segundos, segundos.

D. genera una cadena de HR: MIN: SEC: MARCOS, donde frame = 1/75 sec.

(Los marcos provienen del código SMPTE utilizado en los estudios).


A: usa ffprobey analiza la línea de duración en un número fp (no se muestra)

SI:

 # 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 #

RE:

#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"

1

Aquí hay una versión abreviada de su script, corregida para proporcionar la salida que desea:

#!/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

Tenga en cuenta que redondear al entero más cercano es equivalente a sumar .5 y tomar el piso, o redondear hacia abajo (para números positivos).

Además, el factor de escala se aplica en el momento de la operación; entonces (estos son bccomandos, puedes pegarlos en tu 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

1

Implementación pura de bc según lo solicitado

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

si coloca eso en un archivo llamado functions.bc, puede redondear con

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

Código para la implementación de bc que se encuentra en http://phodd.net/gnu-bc/code/funcs.bc


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

0

Puede usar awk, no se necesitan tuberías:

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

Establezca el precio de antemano con la variable de entorno PRICE=<val>.

  • Luego llame a awk: $PRICEentrará en awk como una variable awk price.

  • Luego se redondea al centésimo más cercano con +.005.

  • La opción de formato printf %.2flimita la escala a dos decimales.

Si tiene que hacer esto de esta manera, es mucho más eficiente que la respuesta seleccionada:

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
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.