Tricky shell arithmetic
Inspired by amigo's IQ4sh project but unable to dissect its code I decided to try my luck and roll my own script from scratch, limited to multiplication.
Goal: Floating point multiplication for factors of arbitrary length, coded with nothing but POSIX compatible shell built-ins and constructs.
Obstacles: shell arithmetic, e.g $((2*5))
, allows only integers and is restricted to values <=9223372036854775807
Purpose: Well...hmmmm. Let's say curiosity. It's like performing heart surgery with nothing but a Swiss army knife. Fun for the doctor, no advantage for the patient.
I figured that by simulating paper-and-pencil multiplication, which is simple and knows no value restrictions, I could eventually achieve my goal. Floats would have to be stripped of their fraction points to make them digestible for the shell and finally the point would have to be reinserted into what technically is a text string.
The following code was my first shot, still very bare bone and still restricted, but functional and - I hope - easy to understand (consecutive versions would eliminate the restrictions but are more complex and may blur the basic concept). It's written for busybox ash, making it decently fast.
Restrictions:
- One of the 2 factors may be of any length, the other should not exceed 17 digits. That's already sufficient for most real life calculations
- Multiple factors not supported
- No syntax checking
- No scaling to a user defined precision
- No rounding
Usage: <scriptname> num1 num2
Examples (assuming that the script was named "m" - can't think of a shorter name ):
Code: Select all
# m 2 3
6
# m 2 0.03
0.06
# m 9.22337203685477580792233720368547758079223372036854775807 0.92233720368547758
8.5070591730234615791340362699272532179134036269927253217828333035262490706
Script:
Code: Select all
#!/bin/ash
# Bare bone floating point multiplication, using only POSIX compatible shell builtins and constructs
# Usage from command line: <scriptname> num1 num2
# Restrictions: Only 2 arguments allowed; one may be of any length, the other should be <= 17 digits
mmul() {
a=$1 #1st argument
b=$2 #2nd argument
## Determine sign of end result
case $a$b in
*-*-*)sign= ;;
*-*)sign=- ;;
esac
## Strip any signs
a=${a#[+-]}
b=${b#[+-]}
## If 1st argument contains fractions ...
case $a in *.* )
frca=${a#*.} #extract fraction portion
fcnt=${#frca} #count fraction digits
a=${a%.*}$frca ;; #remove '.'
esac
## If 2nd argument contains fractions ...
case $b in *.* )
frcb=${b#*.}
fcnt=$((fcnt+${#frcb})) #add to fcnt of $a
b=${b%.*}$frcb ;;
esac
## Remove any leading zeros (avoids octal interpretation)
a=${a#${a%%[!0]*}}
b=${b#${b%%[!0]*}}
## Let the longest factor be $a
[ ${#b} -gt ${#a} ] && c=$a a=$b b=$c
## Perform digit-wise multiplication
while [ $a ] ;do #while loop will chop off digit after digit from $a until $a is empty
m=$((${a#${a%?}}*b+cov)) #multiply last digit of $a with factor $b and add any remainder carried over from previous multiplication
res=$((m%10))$res #prepend last digit of m to accumulated result
cov=$((m/10)) #carry-over for next loop: all digits of m except last. Returns '0' for single digit result.
a=${a%?} #cut last digit of $a
done
res=${cov#0}$res #prepend any remaining carry-over value if not 0
## If both input arguments were integers, it's done!
[ -z $fcnt ] && echo ${sign}${res} && return
## If at least one of the input arguments was floating point number ...
while [ $fcnt -gt ${#res} ];do #pad with leading zeros if result has not enough digits to return fraction part (happens if argument 0.0<whatever> e.g. 2*0.002 (fcnt=3) => res 4 => must be padded to 004
res=0$res
done
## Split the result $res (still a pure integer) into digits before and after fraction point
int=$res
c=0
while [ $c -lt $fcnt ];do #chop off characters from right (fraction part) until only integer part remains
int=${int%?}
c=$((c+1))
done
frac=${res#$int} #fraction part
echo ${sign}${int:-0}.${frac} #return result
}
mmul $@