4

I need to count a percentage change between two values.

The code I have here:

echo $time1
echo $time2
pc=$(( (($time2 - $time1)/$time1 * 100) ))
echo $pc

Brings such output in the console (with set -xe option)

+ echo 1800
1800
+ echo 1000
1000
+ pc=0
+ echo 0

The code inside the math expression seems to be written properly, still, I get -80 or so. Why is this happening to me?

The second part of the question. I have no access and I will have no access to the bc command. From what I heard it can give me the absolute value of the number I have.

So.. Without the bc command - will this be a good idea for an IF condition?

if (( (( "$pc" > 20 || (( "$pc" < -20 )); then...
hc0re
  • 1,806
  • 2
  • 26
  • 61
  • Bash only does integer arithmetic, so if `$time2 - $time1` is less than `$time1` then the division will always be zero. However, if you reorder the expression slightly, like multiplying before dividing, the result should be a little better. – Some programmer dude Dec 03 '14 at 09:29
  • 1
    Is it a requirement that this is done in pure bash? – Tom Fenech Dec 03 '14 at 09:30
  • @TomFenech, no, it can be done in any tool aviable for me. Could be done in perl for example. – hc0re Dec 03 '14 at 09:33
  • Have you taken a look at the answers? Do you require any additional clarification? – Tom Fenech Dec 03 '14 at 11:06

5 Answers5

6

As you have mentioned that it's not necessary to do this in bash, I would recommend using awk:

awk -v t1="$time1" -v t2="$time2" 'BEGIN{print (t2-t1)/t1 * 100}'

Normally, awk is designed to process files but you can use the BEGIN block to perform calculations without needing to pass any files to it. Shell variables can be passed to it using the -v switch.

If you would like the result to be rounded, you can always use printf:

awk -v t1="$time1" -v t2="$time2" 'BEGIN{printf "%.0f", (t2-t1)/t1 * 100}'

The %.0f format specifier causes the result to be rounded to an integer (floating point with 0 decimal places).

Tom Fenech
  • 72,334
  • 12
  • 107
  • 141
  • Depending on how the values are obtained in the first place and what the conditional code should be doing, it is possible that you may be able to do the whole thing in awk. – Tom Fenech Dec 03 '14 at 09:48
  • That is a nice solution, and Im upvoting it, nah, Im marking it as a solution, but I need to protect myself against some cases, so I'm currently working on a perl script. But still, thanks for the answer! – hc0re Dec 03 '14 at 11:55
  • No problem. For quick floating point calculations involving shell variables, awk can be useful but it's a bit of an abuse of the `BEGIN` block, so for more complex things, Perl may be a better option. On the other hand, if the two variables are actually being read from a file, using awk may be the best solution. – Tom Fenech Dec 03 '14 at 12:25
  • @TomFenech I was looking for a method to calculate the percentage change. It doesnt really matter - a rounded value is doing its job. Ok, this could be more precise with `bc` or other method without `bc`. 99% of my script is written in bash, so I needed an output compatibile with bash. In this case - an integer :) – hc0re Dec 03 '14 at 14:20
  • 1
    Careful: if `$time1` is zero (for whatever reason) that would lead to division by zero error in awk, but this can be caught before the `awk` dance of course. – ckujau Jan 10 '23 at 16:04
2

Some notes:

  • The math is integer based, so you're getting zero early because you divide
  • The $(( )) syntax will expand variables so use time1 instead of $time1
  • The percentage answer is -44 not -80

Here are some examples:

echo $(( time2-time1 )) # Output: -800
echo $(( (time2-time1)/time1 )) # Output: 0. see it's already zero!
echo $(( (time2-time1)/time1*100 )) # Output: 0. it's still zero and still 'incorrect'
echo $(( (time2-time1)*100 )) # Output: -80000. fix by multiplying before dividing
echo $(( (time2-time1)*100/time1 )) # Output: -44. this is the 'correct' answer
echo $(( (time2-time1)*100/time2 )) # Output: -80. note this is also an 'incorrect' answer

So, in short, the corrected answer is:

time1=1800
time2=1000
pc=$(( (time2 - time1) * 100 / time1 )) # -44

Note that the (( notation )) can also be used to handle both expression and the assignment. i.e.

time1=1800
time2=1000
(( pc=((time2 - time1) * 100 / time1 ) )) # -44
Stephen Quan
  • 21,481
  • 4
  • 88
  • 75
1

With correct rounding, nicer whitespace, and without redundant $ signs:

pc=$(( ((((time2 - time1) * 1000) / time1) + (time2 > time1 ? 5 : -5)) / 10 ))
mirabilos
  • 5,123
  • 2
  • 46
  • 72
1

Maybe you want to prefer bc+awk mixed line, something like this:

total=70
item=30
percent=$(echo %$(echo "scale = 2; ($item / $total)" | bc -l | awk -F '.' '{print $2}'))
echo $percent
mboyar
  • 11
  • 2
0

Solution with awk:

echo "$time1" "$time2" | awk '{print ($1-$2)/$1*100}'

To avoid a negative number as a result, I would suggest to use the higher number for time1 and the lowest for time2 variable.

Feriman
  • 503
  • 8
  • 17