LinuxQuestions.org
Go Job Hunting at the LQ Job Marketplace
Go Back   LinuxQuestions.org > Forums > Linux Forums > Linux - General > LinuxQuestions.org Member Success Stories
User Name
Password
LinuxQuestions.org Member Success Stories Just spent four hours configuring your favorite program? Just figured out a Linux problem that has been stumping you for months?
Post your Linux Success Stories here.

Notices

Reply
 
Search this Thread
Old 03-29-2009, 12:26 AM   #1
anonguy9
LQ Newbie
 
Registered: Mar 2009
Posts: 25

Rep: Reputation: 16
Talking Rounding in pure bash


Keep in mind that I've only started looking at bash this week. All of my previous attempts were the standard find|grep foo sort of thing. =)

Once again I decided to tackle everyday functionality which ought to exist out of the box in bash but was intentionally omitted for some reason.

Once again, this proves that bash isn't a real programming language. =)


----


The logic for this code was taken from http://en.wikipedia.org/wiki/Roundin...to-even_method

The method is "Round-to-even", aka unbiased rounding, convergent rounding, statistician's rounding, Dutch rounding, Gaussian rounding, or bankers' rounding.

I was very strict in following the logic given in the Wikipedia article. This means that I intentionally avoided certain programming structures/styles/optimizations. It also seems to make the code easier to read, adapt or change.

I added in additional error-checking, and I decided to do a lot more work to deal with some technically-valid but unusual cases.

I also ended up doing another little test function. I do believe there are some bash tools which can do this sort of thing, but I have no intention of continuing with bash much further. This was a fun but really strange experience.

As an aside, this code also happens to append additional zeros if it needs to.


Tests:
Code:
 Wikipedia cases
pass - 3.016 (2) => 3.02
pass - 3.013 (2) => 3.01
pass - 3.015 (2) => 3.02
pass - 3.045 (2) => 3.04
pass - 3.04501 (2) => 3.05
 My cases
pass - 1 () => 1
pass - 2 (0) => 2
pass - 3.00 () => 3
pass - .44 (4) => 0.44
pass - 5.0000 (1) => 5.0
pass - 6.0000 (2) => 6.00
pass - 7. (2) => 7.00
0 failures

Code:
replace_character() {
  unset searchstring_success
  until [ "sky" = "falling" ]; do
  # 2 parameters, no blanks, first parameter  must be one character.
  if [ ! "$#" -eq 3 ] || [ "$1" = "" ] || [ "$2" = "" ] || [ "$3" = "" ] || [ `expr length $1` -gt 1 ]; then echo "Needs three parameters: a character, a string and a position"; break ; fi
  expr $3 + 1 &> /dev/null ; result=$?
  if [ $result -ne 0 ]; then echo $3 is not a number. ; break ; fi
  character="$1"
  string="$2"
  position="$3"
  length=${#string}
  i=0
  unset newstring
  until [ $i -eq $length ]; do
    if [ $i -eq $position ]; then
      newstring=$newstring$character
    else
      newstring=$newstring${string:$i:1}
    fi
    ((i++))
  done
  echo $newstring

  break
  done
}


# TODO: deal with "+" or "-" characters.  Should be easy.

round() {
  until [ "sky" = "falling" ]; do
    if [ "$#" -lt 1 ] || [ "$#" -gt 2 ] || [ "$1" = "" ]; then echo "Needs one or two parameters: a number and an optional position"; break ; fi

    # Check $1
    # If I've only been given a number, then what the heck am I being called upon to do?  Bail out.
    if [[ "$1" =~ '^([0-9]+)$' ]]; then echo $1 ; break ; fi
    if [[ "$1" =~ '^([0-9]+\.)$' ]]; then
      left=${1%.*}
      # FIXME: This coding cannot deal with a bad $2 ($rounding_digit_location) since that checking is done later on!!
      number=`for i in {1..${#2}}; do printf 0; done`
    # ".123" => "0.123"
    elif [[ "$1" =~ '^(\.[0-9]+)$' ]]; then
      left=0
      number=${1#*.}
    # If it's a number in the form of nnn.nnn
    elif [[ "$1" =~ '^([0-9]+\.[0-9]+)$' ]]; then
      left=${1%.*}
      number=${1#*.}
    else
      echo $1 is not a number. ; break
    fi

    # Check $2
    if [ "$2" = "" ]; then
      # By default round to this many digits
      rounding_digit_location=0
    else
      if [[ ! "$2" =~ '^([0-9]+)$' ]]; then echo $2 is not a number. ; break ; fi
      rounding_digit_location=$2
      # If the rounding digit is longer than what's available to work with, make it the maximum length.
      if [ "$rounding_digit_location" -gt ${#number} ]; then rounding_digit_location=${#number} ; fi
    fi

    # If I've been given a boring number, don't even bother to do rounding
    if [ $number = 0 ]; then
      # there a much better way to do this with some kind of {} thing, but I can't remember where I saw that note.
      until [ $rounding_digit_location -eq -1 ]; do
        final=$final"0"
        ((rounding_digit_location--))
      done
      echo $left"."$final
      break
    fi

    # Above is my error and hedge-case programming.
    # Now we begin the actual code.
    # It was translated as literally as possible from the Wikipedia English explanation.
    # Their original notes are included next to each ##

    ## Decide which is the last digit to keep.
    # Convert it to a count-from-zero

    ((rounding_digit_location--))
    item=${number:$rounding_digit_location:1}
    
    ## Increase it by 1 if the next digit is 6 or more, or a 5 followed by one or more non-zero digits.
    ## ... the next digit
    item_after=${number:$(( $rounding_digit_location + 1 )):1}
    if [ "$item_after" = "" ]; then item_after=0 ; fi
    ## ... is 6 or more
    if [ "$item_after" -ge 6 ]; then
      result=$(( $item + 1 ))
    fi
    ## ... or a 5 followed by one or more non-zero digits.
    if [ $item_after -eq 5 ]; then
      ## ... followed by one or more non-zero digits
      string_after=${number:$(( $rounding_digit_location + 2))}
      # and if I run past the edge of the string, it's 0
      if [ "$string_after" = "" ]; then string_after=0 ; fi
      if [ $string_after -gt 0 ]; then
        result=$(( $item + 1 ))
      fi
    fi
    
    ## Leave it the same if the next digit is 4 or less
    if [ $item_after -le 4 ]; then result=$item ; fi
    
    ## Otherwise, if all that follows the last digit is a 5 and possibly trailing zeroes; then increase the rounded digit if it is currently odd; else, if it is already even, leave it alone.
    if [ $item_after -eq 5 ]; then
      ## ... followed by one or more zero digits
      string_after=${number:$(( $rounding_digit_location + 2))}
      # and if I run past the edge of the string, it's 0
      if [ "$string_after" = "" ]; then string_after=0 ; fi
      if [ $string_after -eq 0 ]; then
        if [ $(( $item % 2 )) -ne 0 ]; then
          # ... then increase the rounded digit if it is currently odd
          result=$(( $item + 1 ))
        else
          # ... else, if it is already even, leave it alone.
          result=$item
        fi
      fi
    fi
    
    # take the rounded digit and slap it overtop of the original:
    final=`replace_character $result $number $rounding_digit_location`
    
    # Truncate everything past the rounded digit:
    truncate=${final:$(( $rounding_digit_location + 1 ))}
    final=${final%$truncate*}

    if [ "$left" = "" ]; then
      echo $final
    else
      if [ "$final" = "" ]; then
        # If I was given nnn.nnn but rounded to 0 digits:
        echo $left
      else
        # If I was given nnn.nnn:
        echo $left"."$final
      fi
    fi
    break
  done
}


# --------------
# Testing
# --------------
round_test() {
  result=`round $1 $2`
  expected=$3
  if [ "$result" = "$expected" ]; then printf pass ; else ((fail_count++)) ; printf fail - got $result ; fi
  echo " - $1 ($2) => $3"
}

# Cases taken from http://en.wikipedia.org/wiki/Rounding#Round-to-even_method
round_test_cases() {
  fail_count=0

  echo " Wikipedia cases"
  # 3.016 rounded to hundredths is 3.02 (because the next digit (6) is 6 or more)
  round_test 3.016 2 3.02
  # 3.013 rounded to hundredths is 3.01 (because the next digit (3) is 4 or less)
  round_test 3.013 2 3.01
  # 3.015 rounded to hundredths is 3.02 (because the next digit is 5, and the hundredths digit (1) is odd)
  round_test 3.015 2 3.02
  # 3.045 rounded to hundredths is 3.04 (because the next digit is 5, and the hundredths digit (4) is even)
  round_test 3.045 2 3.04
  # 3.04501 rounded to hundredths is 3.05 (because the next digit is 5, but it is followed by non-zero digits)
  round_test 3.04501 2 3.05

  echo " My cases"
  round_test 1 "" 1
  round_test 2 0 2
  round_test 3.00 "" 3
  round_test .44 4 0.44 # careful: this should not become 0.4400
  round_test 5.0000 1 5.0
  round_test 6.0000 2 6.00
  round_test 7. 2 7.00

  if [ $fail_count = 1 ]; then
    echo $fail_count failure
  else
    echo $fail_count failures
  fi
}

Last edited by anonguy9; 03-29-2009 at 01:38 AM. Reason: minor update
 
Old 03-29-2009, 01:44 AM   #2
colucix
Moderator
 
Registered: Sep 2003
Location: Bologna
Distribution: CentOS 6.5 OpenSuSE 12.3
Posts: 10,458

Rep: Reputation: 1941Reputation: 1941Reputation: 1941Reputation: 1941Reputation: 1941Reputation: 1941Reputation: 1941Reputation: 1941Reputation: 1941Reputation: 1941Reputation: 1941
Nice. However, what about something like this...?
Code:
printf "%4.2f\n" 3.04501
or even
Code:
for i in 3.016 3.013 3.015 3.045 3.04501
> do
>   printf "pass - %f (%1d) => %4.2f\n" $i 2 $i
> done
Moreover, please take in mind that a decimal digit of 5 has to be rounded to the upper integer: 3.045 rounds up to 3.05, not down to 3.04 as in the Wikipedia example.
 
1 members found this post helpful.
Old 03-29-2009, 02:51 AM   #3
anonguy9
LQ Newbie
 
Registered: Mar 2009
Posts: 25

Original Poster
Rep: Reputation: 16
Quote:
Originally Posted by colucix View Post
Code:
printf "%4.2f\n" 3.04501
or even
Code:
for i in 3.016 3.013 3.015 3.045 3.04501
> do
>   printf "pass - %f (%1d) => %4.2f\n" $i 2 $i
> done
Wow, I didn't know printf could do that kind of stuff! I went looking for documentation, but I don't have printf's info pages installed.

For reference for anyone else who was in that position, this is a good site:

- ou800.doc.sco.com (format modifiers)

I do notice that printf is part of awk/gawk. I wanted to make a bash-only solution. I didn't realise printf was an external command. I guess I should stick with echo -n from now on.


Quote:
Originally Posted by colucix View Post
Moreover, please take in mind that a decimal digit of 5 has to be rounded to the upper integer: 3.045 rounds up to 3.05, not down to 3.04 as in the Wikipedia example.
This is how I learned things too, but Apparently if the digit to the right of the target digit is a five there are special rules. The target digit only increments if it would become even. I even implemented it the way I knew from memory, but I scrapped all of that to do it the way I read. It was also an exercise in translating an English procedure into pseudocode and then real code.

This was more of a learning experience anyway.. I'm not going to put the code to significant (if any) use. I wouldn't want to be pedantic and implement all the variations, even though that would be a simple side project.
 
Old 03-29-2009, 09:54 AM   #4
colucix
Moderator
 
Registered: Sep 2003
Location: Bologna
Distribution: CentOS 6.5 OpenSuSE 12.3
Posts: 10,458

Rep: Reputation: 1941Reputation: 1941Reputation: 1941Reputation: 1941Reputation: 1941Reputation: 1941Reputation: 1941Reputation: 1941Reputation: 1941Reputation: 1941Reputation: 1941
Quote:
Originally Posted by anonguy9 View Post
I do notice that printf is part of awk/gawk. I wanted to make a bash-only solution. I didn't realise printf was an external command. I guess I should stick with echo -n from now on.
You're right. However printf (different from the awk counterpart) is either a shell built-in and an external program. For pure bash programming you have to stick with the shell built-ins. On most system you will find the complete list in
Code:
man bashbuiltins
or something similar, depending on the linux distro. If you're in doubt and don't know if your shell is currently using the built-in or the external command, use type, e.g.
Code:
$ type echo
echo is a shell builtin
$ type printf
printf is a shell builtin
$ type type
type is a shell builtin
$ type cut
cut is /usr/bin/cut
in the list above, only the last command is an external one. Anyway you can get a different result on your system for the echo and printf commands.

Cheers!
 
Old 03-29-2009, 12:20 PM   #5
anonguy9
LQ Newbie
 
Registered: Mar 2009
Posts: 25

Original Poster
Rep: Reputation: 16
No joy on man bashbuiltins, but man builtins does give me the list.

You're right, printf is a builtin for me!

Well that's hilarious. I really did hack together some legacy crap that's been replaced by a oneliner. =)

But I guess that's just the way it is. The included man page "documentation" (used loosely) as well as the official documentation as well as the best tutorials and blog postings didn't mention rounding.

Just like my grandpa could say "back in my day we had to read the source code, and all the comments were misleading too", one day I could tell my grandkids "back in my day the documentation was written by the *programmer*, and all their examples sucked".

Community documentation ftw. I'm so glad wikis are getting popular for online program documentation, and even more glad that MediaWiki gets chosen more ThanThoseOtherCrappyWikis so I can actually help.

Urr, ok.. ranting concluded.


----


For reference, man builtins:

Code:
NAME
       bash,  :,  ., [, alias, bg, bind, break, builtin, cd, command, compgen,
       complete, continue, declare, dirs, disown, echo,  enable,  eval,  exec,
       exit,  export,  fc,  fg, getopts, hash, help, history, jobs, kill, let,
       local, logout, popd, printf, pushd, pwd, read, readonly,  return,  set,
       shift,  shopt,  source,  suspend,  test,  times,  trap,  type, typeset,
       ulimit, umask, unalias, unset,  wait  -  bash  built-in  commands,  see
       bash(1)

SEE ALSO
       bash(1), sh(1)
 
  


Reply

Tags
bash, character, number, numbers, printf, replace


Thread Tools Search this Thread
Search this Thread:

Advanced Search

Posting Rules
You may not post new threads
You may not post replies
You may not post attachments
You may not edit your posts

BB code is On
Smilies are On
[IMG] code is Off
HTML code is Off


Similar Threads
Thread Thread Starter Forum Replies Last Post
c++ - how to avoid truncating or rounding a double numerical value babag Programming 9 05-18-2008 05:00 PM
Need Assitence wiht rounding to nearest inch Timberwolf753 Programming 6 10-12-2006 06:14 PM
rounding a large double to 2 decimal places linuxmandrake Programming 2 03-17-2006 03:31 PM
C++ Rounding and Truncation Opeth Programming 4 09-17-2005 07:16 PM
Java Precission rounding. Tru_Messiah Programming 4 05-14-2004 11:23 PM


All times are GMT -5. The time now is 04:13 AM.

Main Menu
My LQ
Write for LQ
LinuxQuestions.org is looking for people interested in writing Editorials, Articles, Reviews, and more. If you'd like to contribute content, let us know.
Main Menu
Syndicate
RSS1  Latest Threads
RSS1  LQ News
Twitter: @linuxquestions
identi.ca: @linuxquestions
Facebook: linuxquestions Google+: linuxquestions
Open Source Consulting | Domain Registration