Floating number comparison using BC - bash

Background
Beginner here trying to learn some Bash basics.
My question is my attempt at modifying an existing textbook example to include simple input validation. The validation works if one or more inputs are inappropriate but when all three inputs are appropriate, I instead get a syntax error warning and then an answer.
Code is at the end, but below are my thoughts. Appreciate any corrections wherever they are wrong.
Much thanks.
Thoughts
I have three conditions to check for. If any one of them fails, a message is displayed and the program terminates. This works. However, if all 3 conditions are met, then I receive a syntax error.
I thought the error might be related to expansion so I manually ran the commands and supplied hard inputs using the command line. E.g. bc <<< "0.5 > 0" and these seemed to work as intended.
Subsequently, it seems that my problems only arise when I involve the $interest variable with its decimal points. However, I used BC because my understanding is that Bash only does Integers. What else have I missed out?
Code
# loan-calc: script to calculate monthly loan payments
# formulae may be mathematically wrong, example copied from textbook
# reference only for bash scripting, not math
PROGNAME="${0##*/}"
usage () {
cat <<- EOF
Usage: $PROGNAME PRINCIPAL INTEREST MONTHS
Where:
PRINCIPAL is the amount of the loan.
INTEREST is the APR as a number (7% = 0.07)
MONTHS is the length of the loan's term.
EOF
}
read -p "Enter principal amount > " principal
read -p "Enter interest rate (E.g. 7.5% = 0.075) > " interest
read -p "Enter term length in months > " months
# Principal, interest rate, and months must be more than 0.
if (( "$principal <= 0" | bc )) || (( "$months <= 0" | bc )) || (( "$interest <= 0" | bc )); then
usage
exit 1
fi
result=$(bc <<- EOF
scale = 10
i = $interest / 12
p = $principal
n = $months
p * ((i * ((1 + i) ^ n)) / (((1 + i) ^ n) - 1))
EOF
)
printf "%0.2f\n" $result

Shell Arithmetic
(( 1 | bc )) does not pipe 1 into bc. When you are evaluating expressions inside of (( expression )), | is the bitwise OR operator, not a pipe.
(( bc )) evaluates to 1 every time, so all of your conditional tests are just OR'ing the number with 1, not piping the number into bc.
Your expression inside of the parentheses should be the output from echoing the mathematical string into bc using a pipe, e.g. (( $(echo "$variable <= 0"| bc) )).
This can be wrapped in a named function so the if statement is more readable.
notValid() {
(( $(echo "$1 <= 0" | bc) ))
}
# Principal, interest rate, and months must be more than 0.
if notValid "$principal" || notValid "$months" || notValid "$interest"; then
usage
exit 1
fi

Related

How to turn a function output, into a number [duplicate]

This question already has answers here:
How can I compare two floating point numbers in Bash?
(22 answers)
Floating point comparison in shell
(7 answers)
Closed 4 years ago.
So want to turn this function, whose output is a number. The number is curled through some json file thats irrelevant.
#error input 1
if (($(masterfunc) >= 1)); then
#I've also tried
#error input 2
if (($(masterfunc | bc -l) >= 1)); then
I get these this error, which I'm assuming is because its outputing it as a letter or command and not as a number.
#error output 1
((: 1.00048333447157914468 >= 1: syntax error: invalid arithmetic
operator (error token is ".00048333447157914468 >= 1")
#error output 2
((: .99989817794934530799 >= 1: syntax error: operand expected (error
token is ".99989817794934530799 >= 1")
I'm assuming this is some floating point arithmetic problem, but then it should of been solved through bc?
I'm new to bash so if the problem is some unrelated syntax error I apologize.
This is actually rather complicated. The shell doesn't understand real numbers at all, so you have to get something else (like bc) to do the comparison and output something simpler that bash can understand. The simplest way I see to do this is:
if [ $(echo "$(masterfunc) >= 1" | bc) -eq 1 ]; then
Explanation, step by step:
echo "$(masterfunc) >= 1" runs the masterfunc function, adds ">= 1" to its output, and sends the result (something like "1.00048333447157914468 >= 1") to standard output.
echo "$(masterfunc) >= 1" | bc runs the above and pipes it to bc, which will do the comparison and print "1" if the masterfunc output is greater than or equal to 1, "0" if it's less. Note: you can try running this by hand and see how it works.
This "1"/"0" output is more along the lines of what bash can understand, but we still need to actually tell bash what to make of it.
[ $(echo "$(masterfunc) >= 1" | bc) -eq 1 ] runs the above, captures its output with $( ), and embeds that in a test expression. Basically, depending on the output from bc, this is equivalent to either [ 1 -eq 1 ] or [ 0 -eq 1 ].
Use this:
if (( $(printf '%s >= 1\n' "$(masterfunc)" | bc -l) )); then ...

Syntax error in BASH script near `%` operator

I am writing a simple bash script to count the number of occurrences of random draws of cards. I store those in an array, and when printing out the results, for every 10 times that card is pulled, I print one single '*' in a sort of Histogram style of output.
Although, I keep receiving this error when compiling on Terminal:
"task1.sh: line 29: % 10 : syntax error: operand expected (error token is "% 10 ")
task1.sh: line 33: % 10: syntax error: operand expected (error token is "% 10")"
Can't seem to figure out why though. Thank you in advance for any help.
#!/bin/bash
randomdraw(){
Suits="Clubs Diamonds Hearts Spades"
suit=($Suits)
Denominations="2 3 4 5 6 7 8 9 10 Jack Queen King Ace"
denomination=($Denominations)
num_suits=${#suit[*]}
num_denominations=${#denomination[*]}
declare -a numoccurences
declare -a suitoccurences
for ((x=0 ; $x<$loopnum ; x=$x+1));
do
(( numoccurences[$(( RANDOM%num_denominations ))]++ ))
(( suitoccurences[$(( RANDOM%num_suits ))]++ ))
done
}
echo "How Many Random Draws?: "
read loopnum
randomdraw loopnum
for ((x=0 ; $x<$num_denominations ; x=$x+1));
do
let "rounder=$(( ${numoccurences[x]} % 10 ))"
if [ $rounder -ge 5 ];
then
let "starnum=$(( $(( ${numoccurences[x]} / 10 )) + 1 ))"
else
let "starnum=$(( ${numoccurences[x]} / 10 ))"
fi
echo "${denomination[x]}: "
for ((k=0 ; $k<$starnum ; k=$k+1));
do
echo "*"
done
done
Your num_denominations array is mostly empty and the
let "rounder=$(( ${numoccurences[x]} % 10 ))"
is evaluated to
let "rounder=$(( % 10 ))"
Print numoccurences and suitoccurences before asking for loop number for debugging.
You should try to be consistent in the way you write arithmetic expressions in bash. You don't need to use $ to introduce a variable inside an arithmetic expression. And you don't need to use ${array[idx]} either. There's no reason to use let if you have arithmetic evaluation, either. So instead of
let "rounder=$(( ${numoccurences[x]} % 10 ))"
You could write:
(( rounder = numoccurences[x] % 10 ))
These don't quite do the same thing. In the first one, ${numoccurences[x]} will be substituted with nothing if numoccurrences doesn't have a value corresponding to the key $x. In the second one, numoccurrence[x] will be replaced by 0, which is what you actually want. (That has nothing to do with the unnecessary let, since the $((...)) arithmetic expression is evaluated before let is run.)
There are many other places in that script where you would be well advised to simplify your style. For example,
let "starnum=$(( $(( ${numoccurences[x]} / 10 )) + 1 ))"
would be more robust and more readable as
(( starnum = numoccurences[x] / 10 + 1 ))

How to round a large number in Shell command?

In Mac terminal, I would like to round a large number.
For example,
At 10^13th place:
1234567812345678 --> 1230000000000000
Or at 10^12th place:
1234567812345678 --> 1235000000000000
So I would like to specify the place, and then get the rounded number.
How do I do this?
You can use arithmetic expansion:
$ val=1234567812345678
$ echo $(( ${val: -13:1} < 5 ? val - val % 10**13 : val - val % 10**13 + 10**13 ))
1230000000000000
$ echo $(( ${val: -12:1} < 5 ? val - val % 10**12 : val - val % 10**12 + 10**12 ))
1235000000000000
This checks if the most significant removed digit is 5 or greater, and if it is, the last significant unremoved digit is increased by one; then we subtract the division remainder from the (potentially modified) initial value.
If you don't want to have to write it this way, you can wrap it in a little function:
round () {
echo $(( ${1: -$2:1} < 5 ? $1 - $1 % 10**$2 : $1 - $1 % 10**$2 + 10**$2 ))
}
which can then be used like this:
$ round "$val" 13
1230000000000000
$ round "$val" 12
1235000000000000
Notice that quoting $val isn't strictly necessary here, it's just a good habit.
If the one-liner is too cryptic, this is a more readable version of the same:
round () {
local rounded=$(( $1 - $1 % 10**$2 )) # Truncate
# Check if most significant removed digit is >= 5
if (( ${1: -$2:1} >= 5 )); then
(( rounded += 10**$2 ))
fi
echo $rounded
}
Apart from arithmetic expansion, this also uses parameter expansion to get a substring: ${1: -$2:1} stands for "take $1, count $2 from the back, take one character". There has to be a space before -$2 (or is has to be in parentheses) because otherwise it would be interpreted as a different expansion, checking if $1 is unset or null, which we don't want.
awk's [s]printf function can do rounding for you, within the limits of double-precision floating-point arithmetic:
$ for p in 13 12; do
awk -v p="$p" '{ n = sprintf("%.0f", $0 / 10^p); print n * 10^p }' <<<1234567812345678
done
1230000000000000
1235000000000000
For a pure bash implementation, see Benjamin W.'s helpful answer.
Actually, if you want to round to n significant digits you might be best served by mixing up traditional math and strings.
Serious debugging is left to the student, but this is what I quickly came up with for bash shell and hope MAC is close enough:
function rounder
{
local value=$1;
local digits=${2:-3};
local zeros="$( eval "printf '0%.0s' {1..$digits}" )"; #proper zeros
# a bit of shell magic that repats the '0' $digits times.
if (( value > 1$zeros )); then
# large enough to require rounding
local length=${#value};
local digits_1=$(( $digits + 1 )); #digits + 1
local tval="${value:0:$digits_1}"; #leading digits, plus one
tval=$(( $tval + 5 )); #half-add
local tlength=${#tval}; #check if carried a digit
local zerox="";
if (( tlength > length )); then
zerox="0";
fi
value="${tval:0:$digits}${zeros:0:$((length-$digits))}$zerox";
fi
echo "$value";
}
See how this can be done much shorter, but that's another exercise for the student.
Avoiding floating point math due to the inherit problems within.
All sorts of special cases, like negative numbers, are not covered.

shell script + numbers sum

what the best simple elegant way to sum number in ksh or bash
my example is about let command , but I want to find better way to summary all numbers
for example
num1=1232
num2=24
num3=444
.
.
.
let SUM=$num1+num2+num3.........
How about:
num1=1232
num2=24
num3=444
sum=$((num1+num2+num3))
echo $sum # prints 1700
Agree with ghostdog74. I once used $(( )) built-in function, but I changed to bc because the format the we receive data is not very "number-formated". Check below:
jyzuz#dev:/tmp> echo $(( 017 + 2 ))
17
jyzuz#dev:/tmp> echo $(( 17 + 2 ))
19
jyzuz#dev:/tmp>
Seems that in the 1st case it understands as binary or hex numbers.. not very sure.
So I changed to bc. You can choose wich way you prefer:
bc << EOF
$num1 + $num2 + $num3
EOF
or
bc <<< "$num1 + $num2 + $num3"
There are other cools ways to do this...but it would be good if you send more details, like if you're performing division also, you'll need to add bc -l argument, to load math lib.
You can eliminate the last dollar sign and freely space the operands and operators (including the variable and the assignment operator) for readability if you move the double parentheses all the way to the outside.
num1=1232
num2=24
num3=444
(( sum = num1 + num2 + num3 ))
(( count++ ))
(( sum += quantity ))
You can't use the increment style operators (*= /= %= += -= <<= >>= &= ^= |= ++ --) unless you use let or the outside (()) form (or you're incrementing variables or making assignments on the right hand side).
you can use $(()) syntax, but if you have decimal numbers, use awk, or bc/dc to do your maths, "portably".

More simple math help in bash!

In the same thread as this question, I am giving this another shot and ask SO to help address how I should take care of this problem. I'm writing a bash script which needs to perform the following:
I have a circle in x and y with radius r.
I specify resolution which is the distance between points I'm checking.
I need to loop over x and y (from -r to r) and check if the current (x,y) is in the circle, but I loop over discrete i and j instead.
Then i and j need to go from -r/resolution to +r/resolution.
In the loop, what will need to happen is echo "some_text i*resolution j*resolution 15.95 cm" (note lack of $'s because I'm clueless). This output is what I'm really looking for.
My best shot so far:
r=40.5
resolution=2.5
end=$(echo "scale=0;$r/$resolution") | bc
for (( i=-end; i<=end; i++ ));do
for (( j=-end; j<=end; j++ ));do
x=$(echo "scale=5;$i*$resolution") | bc
y=$(echo "scale=5;$j*$resolution") | bc
if (( x*x + y*y <= r*r ));then <-- No, r*r will not work
echo "some_text i*resolution j*resolution 15.95 cm"
fi
done
done
I've had just about enough with bash and may look into ksh like was suggested by someone in my last question, but if anyone knows a proper way to execute this, please let me know! What ever the solution to this, it will set my future temperament towards bash scripting for sure.
You may want to include the pipe into bc in the $()'s. Instead of.
end=$(echo "scale=0;$r/$resolution") | bc
use
end=$(echo "scale=0;$r/$resolution" | bc)
should help a bit.
EDIT And here's a solution.
r=40.5
resolution=2.5
end=$(echo "scale=0;$r/$resolution" | bc)
for i in $(seq -${end} ${end}); do
for j in $(seq -${end} ${end}); do
x=$(echo "scale=5;$i*$resolution" | bc)
y=$(echo "scale=5;$j*$resolution" | bc)
check=$(echo "($x^2+$y^2)<=$r^2" | bc)
if [ ${check} -eq '1' ]; then
iRes=$(echo "$i*$resolution" | bc)
jRes=$(echo "$j*$resolution" | bc)
echo "some_text $iRes $jRes 15.95 cm"
fi
done
done
As already mentioned this problem is probably best solved using bc, awk, ksh or another scripting language.
Pure Bash. Simple problems which actually need floating point arithmetic sometimes can be transposed to some sort of fixed point arithmetic using only integers. The following solution simulates 2 decimal places after the decimal point.
There is no need for pipes and external processes inside the loops if this precision is sufficient.
factor=100 # 2 digits after the decimal point
r=4050 # the representation of 40.50
resolution=250 # the representation of 2.50
end=$(( (r/resolution)*factor )) # correct the result of the division
for (( i=-end; i<=end; i+=factor )); do
for (( j=-end; j<=end; j+=factor )); do
x=$(( (i*resolution)/factor )) # correct the result of the division
y=$(( (j*resolution)/factor )) # correct the result of the division
if [ $(( x*x + y*y )) -le $(( r*r )) ] ;then # no correction needed
echo "$x $y ... "
fi
done
done
echo -e "resolution = $((resolution/factor)).$((resolution%factor))"
echo -e "r = $((r/factor)).$((r%factor))"
you haven't heard of (g)awk ??. then you should go learn about it. It will benefit you for the long run. Translation of your bash script to awk.
awk 'BEGIN{
r=40.5
resol=2.5
end = r/resol
print end
for (i=-end;i<=end;i++) {
for( j=-end;j<=end;j++ ){
x=sprintf("%.5d",i*resol)
y=sprintf("%.5d",j*resol)
if ( x*x + y*y <= r*r ){
print ".......blah blah ......"
}
}
}
}'
It's looking more like a bc script than a Bash one any way, so here goes:
#!/usr/bin/bc -q
/* -q suppresses a welcome banner - GNU extension? */
r = 40.5
resolution = 2.5
scale = 0
end = r / resolution
scale = 5
for ( i = -end; i <= end; i++ ) {
/* moved x outside the j loop since it only changes with i */
x = i * resolution
for ( j = -end; j <= end; j++ ) {
y = j * resolution
if ( x^2 * y^2 <= r^2 ) {
/*
the next few lines output on separate lines, the quote on
a line by itself causes a newline to be created in the output
numeric output includes newlines automatically
you can comment this out and uncomment the print statement
to use it which is a GNU extension
*/
/* */
"some_text
"
i * resolution
j * resolution
"15.95 cm
"
/* */
/* non-POSIX:
print "some_text ", i * resolution, " ", j * resolution, " 15.95 cm\n"
*/
}
}
}
quit

Resources