2

I'm just starting to use bash, and I'm having some difficulties with bash arithmetic. Lets say I have a some cities with three temperatures, and I'd like to average the temperatures without using the awk command. How can I do this? I've done it with awk, but I'm practicing with different techniques.

# cityTemp.txt with four cities, three day's temperatures
Toronto 20 25 30
Miami 80 80 110
London 40 20 60
New York 5 10 15 

Lets say I'm trying to write a script to output:

25 Toronto
90 Miami
40 London
10 New York

I've already done this with some piping and the awk command, but I'm having problems doing this without using awk.

Again, I'm new to this. I tried a for loop, but I didn't really know what I was doing.

---edit 1:

Benjamin W. I'm editing again, but right now I'm playing with a loop:

#!/bin/bash

for i in $(cat $1)
do
    echo "i is: ${i}"
done < $1

This is, maybe obviously, just printing every field from cityTemp.txt one at a time.

---edit 2:

This was my ending attempt

while read -a rows
do
    total=0
    sum=0
    for i in "${rows[@]}"
    do
        sum=`expr $sum + $i`
        total=`expr $total + 1`
    done
    average=`expr $sum / $total`
    Output = ${average}
done < $1
echo ${averages}
radiomime
  • 118
  • 1
  • 1
  • 11
  • What does your for loop look like? How did it not work? You can [edit] your question to add some information. – Benjamin W. Feb 02 '16 at 23:43
  • Thanks for the reply. I've had a few things going, but nothing was quite working. This is where I am again. I was trying to use expr() before, but it was hard for me to only view columns 2,3,4 – radiomime Feb 02 '16 at 23:57
  • will the temp always be 3 values or do you need something to work with an arbitrary number of tempreatures – Adel Ahmed Feb 02 '16 at 23:58
  • I've only been working with 3 values, but varying numbers of cities (always formatted:city temp temp temp), though if you could apply range I'd of course like to learn. --edit-- I did put a zip code in there that I put in brackets, but I only got that all to work with awk – radiomime Feb 03 '16 at 00:01
  • Your final version relies on the fact that a string is evaluated as zero in an arithmetic context, as you're treating all the city names as a numbers; cf. `i=York; echo $(( i + 1 ))`. It works, but I'm not sure if you were aware of this happening. – Benjamin W. Feb 03 '16 at 00:56
  • Oh, and instead of editing your question to basically add an answer, you can also add it as a proper, separate answer. – Benjamin W. Feb 03 '16 at 01:15
  • You cannot have whitespace around the equals sign in an assignment. http://shellcheck.net/ is your friend. – tripleee Feb 03 '16 at 05:11
  • Thank you, the white space has been throwing me off with bash scripts. Didn't know I had to pay this much attention to it. – radiomime Feb 04 '16 at 19:15

4 Answers4

0

this should work for a file with an arbitrary nymber of lines and 3 teampreature inputs:

#!/bin/bash
i=1
file=file
nolines=`wc -l $file | cut -d' ' -f1`
while [ $i -lt $nolines ]
do
line=$(sed -n "$i"p $file)
city=$(echo $line | cut -d' ' -f1)
val1=$(echo $line | cut -d' ' -f2)
val2=$(echo $line | cut -d' ' -f3)
val3=$(echo $line | cut -d' ' -f4)
sum=$(($val1 + $val2 + $val3))
avg=$(($sum / 3))
echo $avg $city
i=$[i+1]
done
Adel Ahmed
  • 638
  • 7
  • 24
0

This should work in any Posix shell, so we may as well shebang it with #!/bin/sh.

#!/bin/sh

avg () {
  city=
  while [ $# -gt 3 ]; do
    city="$city $1"
    shift
  done
  echo $((($1 + $2 + $3) / 3)) $city
}

while read line; do
  avg $line
done < cityTemp.txt

This would have been a lot easier if the city was last on each line, lol.

DigitalRoss
  • 143,651
  • 25
  • 248
  • 329
0

This takes advantage of a few Bash features, but ended up rather bulky:

#!/bin/bash                                                                                                                         

# Regex to capture city name and temperatures                                                                                       
re='([[:alpha:] ]*) ([[:digit:] ]*)'                                                                                                

while IFS= read -r line; do                                                                                                         
    unset temps                             # Delete array                                                                                                  
    [[ $line =~ $re ]]                      # Capture city name and temperatures
    city="${BASH_REMATCH[1]}"               # Assign city name                                                                              
    read -a temps <<< "${BASH_REMATCH[2]}"  # Assign temperatures to array                                                          

    sum=0                                                                                                                           

    # Loop over temperatures                                                                                                        
    for temp in "${temps[@]}"; do                                                                                                   
        (( sum += temp ))                                                                                                           
    done                                                                                                                            

    # Print average                                                                                                                 
    printf "%d %s\n" $(( sum / ${#temps[@]} )) "$city"                                                                              
done < "$1"

It would be much simpler if the city name always consisted of the same amount of words, but there is "New York", so I use a regex to separate name and temperatures.

The temperatures are then assigned to an array with read -a, over which I loop to get the total.

Assigning to the array to loop over could be replaced by directly looping over the string and relying on word splitting:

for temp in ${BASH_REMATCH[2]}; do

but then we'd have to keep track of the number of temperature values by updating a counter or something similar.

Benjamin W.
  • 46,058
  • 19
  • 106
  • 116
0

Here is a version that uses Bash's builtin regex to add the arbitrary elements after the city names.

Given:

$ echo "$nums"
Toronto 20 25 30
Miami 80 80 110
London 40 20 60
New York 5 10 15

You can do:

regex="^([a-zA-Z ]+)(.+)$"
echo "$nums" | while read line
do
    [[ $line =~ $regex ]]
    name="${BASH_REMATCH[1]}"
    arr=(${BASH_REMATCH[2]})
    sum=0
    for i in ${arr[@]}; do
        let sum+=$i
    done
    printf "%s %s\n" $(( $sum / ${#arr[@]} )) "$name"
done

Prints:

25 Toronto 
90 Miami 
40 London 
10 New York 

If you want floating point, you can use bc like so.

Given:

$ echo "$nums"
Toronto 22 25 30
Miami 80 80 110
London 40 20 60
New York 5 10 15

You can do:

echo "$nums" | while read line
do
    [[ $line =~ $regex ]]
    name="${BASH_REMATCH[1]}"
    arr=(${BASH_REMATCH[2]})
    avg=$(IFS='+'; echo "scale=2;(${arr[*]})/${#arr[@]}" | bc -l)
    printf "%s %s\n" $avg "$name"   
done

Prints:

25.66 Toronto 
90.00 Miami 
40.00 London 
10.00 New York 
dawg
  • 98,345
  • 23
  • 131
  • 206