How to Presenti Large Numbers Attractively using shell script

Posted on 7:09 PM by Bharathvn

A common mistake that programmers make is to present the results of calculations to the user without first formatting them. It's difficult for users to ascertain whether 43245435 goes into the millions without manually counting from right to left and mentally inserting a comma every three digits. Use this script to format your results.

The Code
#!/bin/sh
# nicenumber -- Given a number, shows it in comma-separated form.
# Expects DD and TD to be instantiated. Instantiates nicenum
# or, if a second arg is specified, the output is echoed to stdout.

nicenumber()
{
# Note that we assume that '.' is the decimal separator in
# the INPUT value to this script. The decimal separator in the output value is
# '.' unless specified by the user with the -d flag

integer=$(echo $1 | cut -d. -f1) # left of the decimal
decimal=$(echo $1 | cut -d. -f2) # right of the decimal

if [ $decimal != $1 ]; then
# There's a fractional part, so let's include it.
result="${DD:="."}$decimal"
fi

thousands=$integer

while [ $thousands -gt 999 ]; do
remainder=$(($thousands % 1000)) # three least significant digits

while [ ${#remainder} -lt 3 ] ; do # force leading zeros as needed
remainder="0$remainder"
done

thousands=$(($thousands / 1000)) # to left of remainder, if any
result="${TD:=","}${remainder}${result}" # builds right to left
done

nicenum="${thousands}${result}"
if [ ! -z $2 ] ; then
echo $nicenum
fi
}

DD="." # decimal point delimiter, to separate integer and fractional values
TD="," # thousands delimiter, to separate every three digits

while getopts "d:t:" opt; do
case $opt in
d ) DD="$OPTARG" ;;
t ) TD="$OPTARG" ;;
esac
done
shift $(($OPTIND - 1))

if [ $# -eq 0 ] ; then
echo "Usage: $(basename $0) [-d c] [-t c] numeric value"
echo " -d specifies the decimal point delimiter (default '.')"
echo " -t specifies the thousands delimiter (default ',')"
exit 0
fi

nicenumber $1 1 # second arg forces nicenumber to 'echo' output

exit 0

How It Works
The heart of this script is the while loop within the nicenumber function, which takes the numeric value and iteratively splits it into the three least significant digits (the three that'll go to the right of the next comma) and the remaining numeric value. These least significant digits are then fed through the loop again.

Running the Code
To run this script, simply specify a very large numeric value, and the script will add a decimal point and thousands separators as needed, using either the default values or the characters specified as flags.

Because the function outputs a numeric result, the result can be incorporated within an output message, as demonstrated here:

echo "Do you really want to pay $(nicenumber $price) dollars?"

The Results
$ nicenumber 5894625
5,894,625
$ nicenumber 589462532.433
589,462,532.433
$ nicenumber -d, -t. 589462532.433
589.462.532,433

Hacking the Script
Different countries use different characters for the thousands and decimal delimiters, hence the addition of flexible calling flags to this script. For example, Germans and Italians would use -d "." and -t ",". The French use -d "," and -t" ", and the Swiss, who have four national languages, use -d "." and -t "'". This is a great example of a situation in which flexible is better than hard-coded, so that the tool is useful to the largest possible user community.

On the other hand, I did hard-code the "." as the decimal separator for input values, so if you are anticipating fractional input values using a different delimiter, you can change the two calls to cut that specify a "." as the decimal delimiter. Here's one solution:

integer=$(echo $1 | cut "-d$DD" -f1) # left of the decimal
decimal=$(echo $1 | cut "-d$DD" -f2) # right of the decimal

This works, but it isn't particularly elegant if a different decimal separator character is used. A more sophisticated solution would include a test just before these two lines to ensure that the expected decimal separator was the one requested by the user. We could add this test by using the same basic concept shown in Script #2: Cut out all the digits and see what's left:

separator="$(echo $1 | sed 's/[[:digit:]]//g')"
if [ ! -z "$separator" -a "$separator" != "$DD" ] ; then
echo "$0: Unknown decimal separator $separator encountered." >&2
exit 1
fi