1068 lines
39 KiB
Bash
1068 lines
39 KiB
Bash
#!/bin/env bash
|
|
################################################################################
|
|
# shellmath.sh
|
|
# Shell functions for floating-point arithmetic using only builtins
|
|
#
|
|
# Copyright (c) 2020 by Michael Wood. All rights reserved.
|
|
#
|
|
# Usage:
|
|
#
|
|
# source _thisPath_/_thisFileName_
|
|
#
|
|
# # Conventional method: call the APIs by subshelling
|
|
# mySum=$( _shellmath_add 202.895 6.00311 )
|
|
# echo $mySum
|
|
#
|
|
# # Optimized method: use hidden globals to simulate more flexible pass-and-return
|
|
# _shellmath_isOptimized=1
|
|
# _shellmath_add 44.2 -87
|
|
# _shellmath_getReturnValue mySum
|
|
# echo $mySum
|
|
#
|
|
################################################################################
|
|
|
|
|
|
################################################################################
|
|
# Program constants
|
|
################################################################################
|
|
declare -A -r __shellmath_numericTypes=(
|
|
[INTEGER]=0
|
|
[DECIMAL]=1
|
|
)
|
|
|
|
declare -A -r __shellmath_returnCodes=(
|
|
[SUCCESS]="0:Success"
|
|
[FAIL]="1:General failure"
|
|
[ILLEGAL_NUMBER]="2:Invalid argument; decimal number required: '%s'"
|
|
[DIVIDE_BY_ZERO]="3:Divide by zero error"
|
|
)
|
|
|
|
declare -r -i __shellmath_true=1
|
|
declare -r -i __shellmath_false=0
|
|
|
|
declare __shellmath_SUCCESS __shellmath_FAIL __shellmath_ILLEGAL_NUMBER
|
|
|
|
################################################################################
|
|
# Program state
|
|
################################################################################
|
|
declare __shellmath_isOptimized=${__shellmath_false}
|
|
declare __shellmath_didPrecalc=${__shellmath_false}
|
|
|
|
|
|
################################################################################
|
|
# Error-handling utilities
|
|
################################################################################
|
|
function _shellmath_getReturnCode()
|
|
{
|
|
local errorName=$1
|
|
return "${__shellmath_returnCodes[$errorName]%%:*}"
|
|
}
|
|
|
|
function _shellmath_warn()
|
|
{
|
|
# Generate an error message and return control to the caller
|
|
_shellmath_handleError -r "$@"
|
|
return $?
|
|
}
|
|
|
|
function _shellmath_exit()
|
|
{
|
|
# Generate an error message and EXIT THE SCRIPT / interpreter
|
|
_shellmath_handleError "$@"
|
|
}
|
|
|
|
function _shellmath_handleError()
|
|
{
|
|
# Hidden option "-r" causes return instead of exit
|
|
local returnDontExit=$__shellmath_false
|
|
if [[ "$1" == "-r" ]]; then
|
|
returnDontExit=${__shellmath_true}
|
|
shift
|
|
fi
|
|
|
|
# Format of $1: returnCode:msgTemplate
|
|
[[ "$1" =~ ^([0-9]+):(.*) ]]
|
|
returnCode=${BASH_REMATCH[1]}
|
|
msgTemplate=${BASH_REMATCH[2]}
|
|
shift
|
|
|
|
# Display error msg, making parameter substitutions as needed
|
|
msgParameters="$*"
|
|
printf "$msgTemplate" "${msgParameters[@]}"
|
|
|
|
if ((returnDontExit)); then
|
|
return "$returnCode"
|
|
else
|
|
exit "$returnCode"
|
|
fi
|
|
}
|
|
|
|
|
|
################################################################################
|
|
# precalc()
|
|
#
|
|
# Pre-calculates certain global data and by setting the global variable
|
|
# "__shellmath_didPrecalc" records that this routine has been called. As an
|
|
# optimization, the caller should check that global to prevent needless
|
|
# invocations.
|
|
################################################################################
|
|
function _shellmath_precalc()
|
|
{
|
|
# Set a few global constants
|
|
_shellmath_getReturnCode SUCCESS; __shellmath_SUCCESS=$?
|
|
_shellmath_getReturnCode FAIL; __shellmath_FAIL=$?
|
|
_shellmath_getReturnCode ILLEGAL_NUMBER; __shellmath_ILLEGAL_NUMBER=$?
|
|
|
|
# Determine the decimal precision to which we can accurately calculate.
|
|
# To do this we probe for the threshold at which integers overflow and
|
|
# take the integer floor of that number's base-10 logarithm.
|
|
# We check the 64-bit, 32-bit and 16-bit thresholds only.
|
|
if ((2**63 < 2**63-1)); then
|
|
__shellmath_precision=18
|
|
__shellmath_maxValue=$((2**63-1))
|
|
elif ((2**31 < 2**31-1)); then
|
|
__shellmath_precision=9
|
|
__shellmath_maxValue=$((2**31-1))
|
|
else ## ((2**15 < 2**15-1))
|
|
__shellmath_precision=4
|
|
__shellmath_maxValue=$((2**15-1))
|
|
fi
|
|
|
|
__shellmath_didPrecalc=$__shellmath_true
|
|
}
|
|
|
|
|
|
################################################################################
|
|
# Simulate pass-and-return by reference using a secret global storage array
|
|
################################################################################
|
|
|
|
declare -a __shellmath_storage
|
|
|
|
function _shellmath_setReturnValues()
|
|
{
|
|
local -i _i
|
|
|
|
for ((_i=1; _i<=$#; _i++)); do
|
|
__shellmath_storage[_i]="${!_i}"
|
|
done
|
|
|
|
__shellmath_storage[0]=$#
|
|
}
|
|
|
|
function _shellmath_getReturnValues()
|
|
{
|
|
local -i _i
|
|
local evalString
|
|
|
|
for ((_i=1; _i<=$#; _i++)); do
|
|
evalString+=${!_i}="${__shellmath_storage[_i]}"" "
|
|
done
|
|
|
|
eval "$evalString"
|
|
}
|
|
|
|
function _shellmath_setReturnValue() { __shellmath_storage=(1 "$1"); }
|
|
function _shellmath_getReturnValue() { eval "$1"=\"${__shellmath_storage[1]}\"; }
|
|
function _shellmath_getReturnValueCount() { eval "$1"=\"${__shellmath_storage[0]}\"; }
|
|
|
|
################################################################################
|
|
# validateAndParse(numericString)
|
|
# Return Code: SUCCESS or ILLEGAL_NUMBER
|
|
# Return Signature: integerPart fractionalPart isNegative numericType isScientific
|
|
#
|
|
# Validate and parse arguments to the main arithmetic routines
|
|
################################################################################
|
|
|
|
function _shellmath_validateAndParse()
|
|
{
|
|
local n="$1"
|
|
local isNegative=${__shellmath_false}
|
|
local isScientific=${__shellmath_false}
|
|
local numericType returnCode
|
|
|
|
((returnCode = __shellmath_SUCCESS))
|
|
|
|
# Accept decimals: leading digits (optional), decimal point, trailing digits
|
|
if [[ "$n" =~ ^[-]?([0-9]*)\.([0-9]+)$ ]]; then
|
|
local integerPart=${BASH_REMATCH[1]:-0}
|
|
local fractionalPart=${BASH_REMATCH[2]}
|
|
|
|
# Strip superfluous trailing zeros
|
|
if [[ "$fractionalPart" =~ ^(.*[^0])0*$ ]]; then
|
|
fractionalPart=${BASH_REMATCH[1]}
|
|
fi
|
|
|
|
numericType=${__shellmath_numericTypes[DECIMAL]}
|
|
|
|
# Factor out the negative sign if it is present
|
|
if [[ "$n" =~ ^- ]]; then
|
|
isNegative=${__shellmath_true}
|
|
n=${n:1}
|
|
fi
|
|
|
|
_shellmath_setReturnValues "$integerPart" "$fractionalPart" \
|
|
$isNegative "$numericType" $isScientific
|
|
return "$returnCode"
|
|
|
|
# Accept integers
|
|
elif [[ "$n" =~ ^[-]?[0-9]+$ ]]; then
|
|
numericType=${__shellmath_numericTypes[INTEGER]}
|
|
|
|
# Factor out the negative sign if it is present
|
|
if [[ "$n" =~ ^- ]]; then
|
|
isNegative=${__shellmath_true}
|
|
n=${n:1}
|
|
fi
|
|
|
|
_shellmath_setReturnValues "$n" 0 $isNegative "$numericType" $isScientific
|
|
return "$returnCode"
|
|
|
|
# Accept scientific notation: 1e5, 2.44E+10, etc.
|
|
elif [[ "$n" =~ (.*)[Ee](.*) ]]; then
|
|
local significand=${BASH_REMATCH[1]}
|
|
local exponent=${BASH_REMATCH[2]}
|
|
|
|
# Validate the significand: optional sign, integer part,
|
|
# optional decimal point and fractional part
|
|
if [[ "$significand" =~ ^[-]?([0-9]+)(\.([0-9]+))?$ ]]; then
|
|
|
|
isScientific=${__shellmath_true}
|
|
|
|
# Separate the integer and fractional parts
|
|
local sigInteger=${BASH_REMATCH[1]}
|
|
local sigIntLength=${#sigInteger}
|
|
local sigFraction=${BASH_REMATCH[3]}
|
|
|
|
# Strip superfluous trailing zeros
|
|
if [[ "$sigFraction" =~ ^(.*[^0])0*$ ]]; then
|
|
sigFraction=${BASH_REMATCH[1]}
|
|
fi
|
|
|
|
local sigFracLength=${#sigFraction}
|
|
|
|
if [[ "$n" =~ ^- ]]; then
|
|
isNegative=${__shellmath_true}
|
|
n=${n:1}
|
|
fi
|
|
|
|
# Rewrite the scientifically-notated number in ordinary decimal notation.
|
|
# IOW, realign the integer and fractional parts. Separate with a space
|
|
# so they can be returned as two separate values
|
|
if ((exponent > 0)); then
|
|
local zeroCount integer fraction
|
|
((zeroCount = exponent - sigFracLength))
|
|
if ((zeroCount > 0)); then
|
|
printf -v zeros "%0*d" "$zeroCount" 0
|
|
n=${sigInteger}${sigFraction}${zeros}" 0"
|
|
numericType=${__shellmath_numericTypes[INTEGER]}
|
|
elif ((zeroCount < 0)); then
|
|
n=${sigInteger}${sigFraction:0:exponent}" "${sigFraction:exponent}
|
|
numericType=${__shellmath_numericTypes[DECIMAL]}
|
|
else
|
|
n=${sigInteger}${sigFraction}" 0"
|
|
numericType=${__shellmath_numericTypes[INTEGER]}
|
|
fi
|
|
integer=${n% *}; fraction=${n#* }
|
|
_shellmath_setReturnValues "$integer" "$fraction" $isNegative "$numericType" $isScientific
|
|
return "$returnCode"
|
|
|
|
elif ((exponent < 0)); then
|
|
local zeroCount integer fraction
|
|
((zeroCount = -exponent - sigIntLength))
|
|
if ((zeroCount > 0)); then
|
|
printf -v zeros "%0*d" "$zeroCount" 0
|
|
n="0 "${zeros}${sigInteger}${sigFraction}
|
|
numericType=${__shellmath_numericTypes[DECIMAL]}
|
|
elif ((zeroCount < 0)); then
|
|
n=${sigInteger:0:-zeroCount}" "${sigInteger:(-zeroCount)}${sigFraction}
|
|
numericType=${__shellmath_numericTypes[DECIMAL]}
|
|
else
|
|
n="0 "${sigInteger}${sigFraction}
|
|
numericType=${__shellmath_numericTypes[DECIMAL]}
|
|
fi
|
|
integer=${n% *}; fraction=${n#* }
|
|
_shellmath_setReturnValues "$integer" "$fraction" $isNegative "$numericType" $isScientific
|
|
return "$returnCode"
|
|
|
|
else
|
|
# exponent == 0 means the number is already aligned as desired
|
|
numericType=${__shellmath_numericTypes[DECIMAL]}
|
|
_shellmath_setReturnValues "$sigInteger" "$sigFraction" $isNegative "$numericType" $isScientific
|
|
return "$returnCode"
|
|
fi
|
|
|
|
# Reject strings like xxx[Ee]yyy where xxx, yyy are not valid numbers
|
|
else
|
|
((returnCode = __shellmath_ILLEGAL_NUMBER))
|
|
_shellmath_setReturnValues ""
|
|
return "$returnCode"
|
|
fi
|
|
|
|
# Reject everything else
|
|
else
|
|
((returnCode = __shellmath_ILLEGAL_NUMBER))
|
|
_shellmath_setReturnValues ""
|
|
return "$returnCode"
|
|
fi
|
|
}
|
|
|
|
|
|
################################################################################
|
|
# numToScientific (integerPart, fractionalPart)
|
|
#
|
|
# Format conversion utility function
|
|
################################################################################
|
|
function _shellmath_numToScientific()
|
|
{
|
|
local integerPart=$1 fractionalPart=$2
|
|
local exponent head tail scientific
|
|
|
|
if ((integerPart > 0)); then
|
|
((exponent = ${#integerPart}-1))
|
|
head=${integerPart:0:1}
|
|
tail=${integerPart:1}${fractionalPart}
|
|
elif ((integerPart < 0)); then
|
|
((exponent = ${#integerPart}-2)) # skip "-" and first digit
|
|
head=${integerPart:0:2}
|
|
tail=${integerPart:2}${fractionalPart}
|
|
else
|
|
[[ "$fractionalPart" =~ ^[-]?(0*)([^0])(.*)$ ]]
|
|
exponent=$((-(${#BASH_REMATCH[1]} + 1)))
|
|
head=${BASH_REMATCH[2]}
|
|
tail=${BASH_REMATCH[3]}
|
|
fi
|
|
|
|
# Remove trailing zeros
|
|
[[ $tail =~ ^.*[^0] ]]; tail=${BASH_REMATCH[0]:-0}
|
|
|
|
printf -v scientific "%d.%de%d" "$head" "$tail" "$exponent"
|
|
|
|
_shellmath_setReturnValue "$scientific"
|
|
}
|
|
|
|
|
|
################################################################################
|
|
# _shellmath_add (addend_1, addend_2)
|
|
################################################################################
|
|
function _shellmath_add()
|
|
{
|
|
local n1="$1"
|
|
local n2="$2"
|
|
|
|
if ((! __shellmath_didPrecalc)); then
|
|
_shellmath_precalc; __shellmath_didPrecalc=$__shellmath_true
|
|
fi
|
|
|
|
local isVerbose=$(( __shellmath_isOptimized == __shellmath_false ))
|
|
|
|
# Is the caller itself an arithmetic function?
|
|
local isSubcall=${__shellmath_false}
|
|
local isMultiplication=${__shellmath_false}
|
|
if [[ "${FUNCNAME[1]}" =~ shellmath_(add|subtract|multiply|divide)$ ]]; then
|
|
isSubcall=${__shellmath_true}
|
|
if [[ "${BASH_REMATCH[1]}" == multiply ]]; then
|
|
isMultiplication=${__shellmath_true}
|
|
fi
|
|
fi
|
|
|
|
# Handle corner cases where argument count is not 2
|
|
local argCount=$#
|
|
if ((argCount == 0)); then
|
|
echo "Usage: ${FUNCNAME[0]} addend_1 addend_2"
|
|
return "$__shellmath_SUCCESS"
|
|
elif ((argCount == 1)); then
|
|
# Note the result as-is, print if running "normally", and return
|
|
_shellmath_setReturnValue "$n1"
|
|
(( isVerbose && ! isSubcall )) && echo "$n1"
|
|
return "$__shellmath_SUCCESS"
|
|
elif ((argCount > 2 && !isSubcall)); then
|
|
local recursiveReturn
|
|
|
|
# Use a binary recursion tree to add everything up
|
|
# 1) left branch
|
|
_shellmath_add "${@:1:$((argCount/2))}"; recursiveReturn=$?
|
|
_shellmath_getReturnValue n1
|
|
if (( recursiveReturn != __shellmath_SUCCESS )); then
|
|
_shellmath_setReturnValue "$n1"
|
|
return "$recursiveReturn"
|
|
fi
|
|
# 2) right branch
|
|
_shellmath_add "${@:$((argCount/2+1))}"; recursiveReturn=$?
|
|
_shellmath_getReturnValue n2
|
|
if (( recursiveReturn != __shellmath_SUCCESS )); then
|
|
_shellmath_setReturnValue "$n2"
|
|
return "$recursiveReturn"
|
|
fi
|
|
# 3) head node
|
|
local sum
|
|
_shellmath_add "$n1" "$n2"; recursiveReturn=$?
|
|
_shellmath_getReturnValue sum
|
|
_shellmath_setReturnValue "$sum"
|
|
if (( isVerbose && ! isSubcall )); then
|
|
echo "$sum"
|
|
fi
|
|
return "$recursiveReturn"
|
|
fi
|
|
|
|
local integerPart1 fractionalPart1 integerPart2 fractionalPart2
|
|
local isNegative1 type1 isScientific1 isNegative2 type2 isScientific2
|
|
local flags
|
|
|
|
if ((isMultiplication)); then
|
|
integerPart1="$1"
|
|
fractionalPart1="$2"
|
|
integerPart2="$3"
|
|
fractionalPart2="$4"
|
|
|
|
type1=${__shellmath_numericTypes[DECIMAL]}
|
|
type2=${__shellmath_numericTypes[DECIMAL]}
|
|
isNegative1=$__shellmath_false
|
|
isNegative2=$__shellmath_false
|
|
isScientific1=$__shellmath_false
|
|
isScientific2=$__shellmath_false
|
|
else
|
|
# Check and parse the arguments
|
|
_shellmath_validateAndParse "$n1"; flags=$?
|
|
_shellmath_getReturnValues integerPart1 fractionalPart1 isNegative1 type1 isScientific1
|
|
if ((flags == __shellmath_ILLEGAL_NUMBER)); then
|
|
_shellmath_warn "${__shellmath_returnCodes[ILLEGAL_NUMBER]}" "$n1"
|
|
return $?
|
|
fi
|
|
_shellmath_validateAndParse "$n2"; flags=$?
|
|
_shellmath_getReturnValues integerPart2 fractionalPart2 isNegative2 type2 isScientific2
|
|
if ((flags == __shellmath_ILLEGAL_NUMBER)); then
|
|
_shellmath_warn "${__shellmath_returnCodes[ILLEGAL_NUMBER]}" "$n2"
|
|
return $?
|
|
fi
|
|
fi
|
|
|
|
# Quick add & return for integer adds
|
|
if ((type1==type2 && type1==__shellmath_numericTypes[INTEGER])); then
|
|
((isNegative1)) && ((integerPart1*=-1))
|
|
((isNegative2)) && ((integerPart2*=-1))
|
|
local sum=$((integerPart1 + integerPart2))
|
|
if (( (!isSubcall) && (isScientific1 || isScientific2) )); then
|
|
_shellmath_numToScientific $sum ""
|
|
_shellmath_getReturnValue sum
|
|
fi
|
|
_shellmath_setReturnValue $sum
|
|
if (( isVerbose && ! isSubcall )); then
|
|
echo "$sum"
|
|
fi
|
|
return "$__shellmath_SUCCESS"
|
|
fi
|
|
|
|
# Right-pad both fractional parts with zeros to the same length
|
|
local fractionalLen1=${#fractionalPart1}
|
|
local fractionalLen2=${#fractionalPart2}
|
|
if ((fractionalLen1 > fractionalLen2)); then
|
|
# Use printf to zero-pad. This avoids mathematical side effects.
|
|
printf -v fractionalPart2 %-*s "$fractionalLen1" "$fractionalPart2"
|
|
fractionalPart2=${fractionalPart2// /0}
|
|
elif ((fractionalLen2 > fractionalLen1)); then
|
|
printf -v fractionalPart1 %-*s "$fractionalLen2" "$fractionalPart1"
|
|
fractionalPart1=${fractionalPart1// /0}
|
|
fi
|
|
local unsignedFracLength=${#fractionalPart1}
|
|
|
|
# Implement a sign convention that will enable us to detect carries by
|
|
# comparing string lengths of addends and sums: propagate the sign across
|
|
# both numeric parts (whether unsigned or zero).
|
|
if ((isNegative1)); then
|
|
fractionalPart1="-"$fractionalPart1
|
|
integerPart1="-"$integerPart1
|
|
fi
|
|
if ((isNegative2)); then
|
|
fractionalPart2="-"$fractionalPart2
|
|
integerPart2="-"$integerPart2
|
|
fi
|
|
|
|
local integerSum=0
|
|
local fractionalSum=0
|
|
|
|
((integerSum = integerPart1+integerPart2))
|
|
|
|
# Summing the fractional parts is tricky: We need to override the shell's
|
|
# default interpretation of leading zeros, but the operator for doing this
|
|
# (the "10#" operator) cannot work directly with negative numbers. So we
|
|
# break it all down.
|
|
if ((isNegative1)); then
|
|
((fractionalSum += (-1) * 10#${fractionalPart1:1}))
|
|
else
|
|
((fractionalSum += 10#$fractionalPart1))
|
|
fi
|
|
if ((isNegative2)); then
|
|
((fractionalSum += (-1) * 10#${fractionalPart2:1}))
|
|
else
|
|
((fractionalSum += 10#$fractionalPart2))
|
|
fi
|
|
|
|
unsignedFracSumLength=${#fractionalSum}
|
|
if [[ "$fractionalSum" =~ ^[-] ]]; then
|
|
((unsignedFracSumLength--))
|
|
fi
|
|
|
|
# Restore any leading zeroes that were lost when adding
|
|
if ((unsignedFracSumLength < unsignedFracLength)); then
|
|
local lengthDiff=$((unsignedFracLength - unsignedFracSumLength))
|
|
local zeroPrefix
|
|
printf -v zeroPrefix "%0*d" "$lengthDiff" 0
|
|
if ((fractionalSum < 0)); then
|
|
fractionalSum="-"${zeroPrefix}${fractionalSum:1}
|
|
else
|
|
fractionalSum=${zeroPrefix}${fractionalSum}
|
|
fi
|
|
fi
|
|
|
|
# Carry a digit from fraction to integer if required
|
|
if ((10#$fractionalSum!=0 && unsignedFracSumLength > unsignedFracLength)); then
|
|
local carryAmount
|
|
((carryAmount = isNegative1?-1:1))
|
|
((integerSum += carryAmount))
|
|
# Remove the leading 1-digit whether the fraction is + or -
|
|
fractionalSum=${fractionalSum/1/}
|
|
fi
|
|
|
|
# Transform the partial sums from additive to concatenative. Example: the
|
|
# pair (-2,3) is not -2.3 but rather (-2)+(0.3), i.e. -1.7 so we want to
|
|
# transform (-2,3) to (-1,7). This transformation is meaningful when
|
|
# the two parts have opposite signs, so that's what we look for.
|
|
if ((integerSum < 0 && 10#$fractionalSum > 0)); then
|
|
((integerSum += 1))
|
|
((fractionalSum = 10#$fractionalSum - 10**unsignedFracSumLength))
|
|
elif ((integerSum > 0 && 10#$fractionalSum < 0)); then
|
|
((integerSum -= 1))
|
|
((fractionalSum = 10**unsignedFracSumLength + 10#$fractionalSum))
|
|
fi
|
|
# This last case needs to function either as an "else" for the above,
|
|
# or as a coda to the "if" clause when integerSum is -1 initially.
|
|
if ((integerSum == 0 && 10#$fractionalSum < 0)); then
|
|
integerSum="-"$integerSum
|
|
((fractionalSum *= -1))
|
|
fi
|
|
|
|
# Touch up the numbers for display
|
|
local sum
|
|
((10#$fractionalSum < 0)) && fractionalSum=${fractionalSum:1}
|
|
if (( (!isSubcall) && (isScientific1 || isScientific2) )); then
|
|
_shellmath_numToScientific "$integerSum" "$fractionalSum"
|
|
_shellmath_getReturnValue sum
|
|
elif ((10#$fractionalSum)); then
|
|
printf -v sum "%s.%s" "$integerSum" "$fractionalSum"
|
|
else
|
|
sum=$integerSum
|
|
fi
|
|
|
|
# Note the result, print if running "normally", and return
|
|
_shellmath_setReturnValue $sum
|
|
if (( isVerbose && ! isSubcall )); then
|
|
echo "$sum"
|
|
fi
|
|
|
|
return "$__shellmath_SUCCESS"
|
|
}
|
|
|
|
|
|
################################################################################
|
|
# subtract (subtrahend, minuend)
|
|
################################################################################
|
|
function _shellmath_subtract()
|
|
{
|
|
local n1="$1"
|
|
local n2="$2"
|
|
local isVerbose=$(( __shellmath_isOptimized == __shellmath_false ))
|
|
|
|
if ((! __shellmath_didPrecalc)); then
|
|
_shellmath_precalc; __shellmath_didPrecalc=$__shellmath_true
|
|
fi
|
|
|
|
if (( $# == 0 || $# > 2 )); then
|
|
echo "Usage: ${FUNCNAME[0]} subtrahend minuend"
|
|
return "$__shellmath_SUCCESS"
|
|
elif (( $# == 1 )); then
|
|
# Note the value as-is and return
|
|
_shellmath_setReturnValue "$n1"
|
|
((isVerbose)) && echo "$n1"
|
|
return "$__shellmath_SUCCESS"
|
|
fi
|
|
|
|
# Symbolically negate the second argument
|
|
if [[ "$n2" =~ ^- ]]; then
|
|
n2=${n2:1}
|
|
else
|
|
n2="-"$n2
|
|
fi
|
|
|
|
# Calculate, note the result, print if running "normally", and return
|
|
local difference
|
|
_shellmath_add "$n1" "$n2"
|
|
_shellmath_getReturnValue difference
|
|
if ((isVerbose)); then
|
|
echo "$difference"
|
|
fi
|
|
|
|
return $?
|
|
}
|
|
|
|
|
|
################################################################################
|
|
# reduceOuterPairs (two integer parts [, two fractional parts])
|
|
#
|
|
# Examines the magnitudes of two numbers in advance of a multiplication
|
|
# and either chops off their lowest-order digits or pushes them to the
|
|
# corresponding lower-order parts in order to prevent overflow in the product.
|
|
# The choice depends on whether 2 or 4 arguments are supplied.
|
|
################################################################################
|
|
function _shellmath_reduceOuterPairs()
|
|
{
|
|
local value1="$1" value2="$2" subvalue1="$3" subvalue2="$4"
|
|
|
|
local digitExcess value1Len=${#value1} value2Len=${#value2}
|
|
((digitExcess = value1Len + value2Len - __shellmath_precision))
|
|
|
|
# Be very precise about detecting overflow. The "digit excess" underestimates
|
|
# this: floor(log_10(longLongMax)). We don't want to needlessly lose precision
|
|
# when a product barely squeezes under the exact threshold.
|
|
if ((digitExcess>1 || (digitExcess==1 && value1 > __shellmath_maxValue/value2) )); then
|
|
|
|
# Identify the digit-tails to be pruned off and either discarded or
|
|
# pushed past the decimal point. In pruning the two values we want to
|
|
# retain as much "significance" as possible, so we try to equalize the
|
|
# lengths of the remaining digit sequences.
|
|
local tail1 tail2
|
|
local lengthDiff leftOver
|
|
|
|
# Which digit string is longer, and by how much?
|
|
((lengthDiff = value1Len - value2Len))
|
|
if ((lengthDiff > 0)); then
|
|
if ((digitExcess <= lengthDiff)); then
|
|
# Chop digits from the longer string only
|
|
tail1=${value1:(-digitExcess)}
|
|
tail2="" # do not chop anything
|
|
else
|
|
# Chop more digits from the longer string so the two strings
|
|
# end up as nearly-equal in length as possible
|
|
((leftOver = digitExcess - lengthDiff))
|
|
tail1=${value1:(-(lengthDiff + leftOver/2))}
|
|
tail2=${value2:(-((leftOver+1)/2))}
|
|
fi
|
|
else
|
|
((lengthDiff *= -1))
|
|
# Mirror the above code block but swap 1 and 2
|
|
if ((digitExcess <= lengthDiff)); then
|
|
tail1=""
|
|
tail2=${value2:(-digitExcess)}
|
|
else
|
|
((leftOver = digitExcess - lengthDiff))
|
|
tail1=${value1:(-((leftOver+1)/2))}
|
|
tail2=${value2:(-(lengthDiff + leftOver/2))}
|
|
fi
|
|
fi
|
|
|
|
# Discard the least-significant digits or move them past the decimal point
|
|
value1=${value1%${tail1}}
|
|
[[ -n "$subvalue1" ]] && subvalue1=${tail1}${subvalue1%0} # remove placeholder zero
|
|
value2=${value2%${tail2}}
|
|
[[ -n "$subvalue2" ]] && subvalue2=${tail2}${subvalue2%0}
|
|
else
|
|
# Signal the caller that no rescaling was actually done
|
|
((digitExcess = 0))
|
|
fi
|
|
|
|
_shellmath_setReturnValues "$value1" "$value2" \
|
|
"$subvalue1" "$subvalue2" "$digitExcess"
|
|
}
|
|
|
|
################################################################################
|
|
# rescaleValue(value, rescaleFactor)
|
|
#
|
|
# Upscales a decimal value by "factor" orders of magnitude: 3.14159 --> 3141.59
|
|
################################################################################
|
|
function _shellmath_rescaleValue()
|
|
{
|
|
local value="$1" rescalingFactor="$2"
|
|
local head tail zeroCount zeroTail
|
|
|
|
[[ "$value" =~ ^(.*)\.(.*)$ ]]
|
|
head=${BASH_REMATCH[1]}
|
|
tail=${BASH_REMATCH[2]}
|
|
((zeroCount = rescalingFactor - ${#tail}))
|
|
if ((zeroCount > 0)); then
|
|
printf -v zeroTail "%0*d" "$zeroCount" 0
|
|
value=${head}${tail}${zeroTail}
|
|
elif ((zeroCount < 0)); then
|
|
value=${head}${tail:0:rescalingFactor}"."${tail:rescalingFactor}
|
|
else
|
|
value=${head}${tail}
|
|
fi
|
|
|
|
_shellmath_setReturnValue "$value"
|
|
}
|
|
|
|
################################################################################
|
|
# reduceCrossPairs (two integer parts, two fractional parts)
|
|
#
|
|
# Examines the precision of the inner products (of "multiplication by parts")
|
|
# and if necessary truncates the fractional part(s) to prevent overflow
|
|
################################################################################
|
|
function _shellmath_reduceCrossPairs()
|
|
{
|
|
local value1="$1" value2="$2" subvalue1="$3" subvalue2="$4"
|
|
|
|
local digitExcess value1Len=${#value1} value2Len=${#value2}
|
|
local subvalue1Len=${#subvalue1} subvalue2Len=${#subvalue2}
|
|
|
|
# Check BOTH cross-products
|
|
((digitExcess = value1Len + subvalue2Len - __shellmath_precision))
|
|
if ((digitExcess > 1 || (digitExcess==1 && value1 > __shellmath_maxValue/subvalue2) )); then
|
|
subvalue2=${subvalue2:0:(-digitExcess)}
|
|
fi
|
|
((digitExcess = value2Len + subvalue1Len - __shellmath_precision))
|
|
if ((digitExcess > 1 || (digitExcess==1 && value2 > __shellmath_maxValue/subvalue1) )); then
|
|
subvalue1=${subvalue1:0:(-digitExcess)}
|
|
fi
|
|
|
|
_shellmath_setReturnValues "$subvalue1" "$subvalue2"
|
|
}
|
|
|
|
|
|
function _shellmath_round()
|
|
{
|
|
local number="$1" digitCount="$2"
|
|
local nextDigit=${number:digitCount:1}
|
|
|
|
number=${number:0:digitCount}
|
|
if ((nextDigit >= 5)); then
|
|
printf -v number "%0*d" "$digitCount" $((10#$number + 1))
|
|
fi
|
|
|
|
_shellmath_setReturnValue "$number"
|
|
}
|
|
|
|
################################################################################
|
|
# multiply (multiplicand, multiplier)
|
|
################################################################################
|
|
function _shellmath_multiply()
|
|
{
|
|
local n1="$1"
|
|
local n2="$2"
|
|
|
|
if ((! __shellmath_didPrecalc)); then
|
|
_shellmath_precalc; __shellmath_didPrecalc=$__shellmath_true
|
|
fi
|
|
|
|
local isVerbose=$(( __shellmath_isOptimized == __shellmath_false ))
|
|
|
|
# Is the caller itself an arithmetic function?
|
|
local isSubcall=${__shellmath_false}
|
|
if [[ "${FUNCNAME[1]}" =~ shellmath_(add|subtract|multiply|divide)$ ]]; then
|
|
isSubcall=${__shellmath_true}
|
|
fi
|
|
|
|
# Handle corner cases where argument count is not 2
|
|
local argCount=$#
|
|
if ((argCount == 0)); then
|
|
echo "Usage: ${FUNCNAME[0]} factor_1 factor_2"
|
|
return "$__shellmath_SUCCESS"
|
|
elif ((argCount == 1)); then
|
|
# Note the value as-is and return
|
|
_shellmath_setReturnValue "$n1"
|
|
(( isVerbose && ! isSubcall )) && echo "$n1"
|
|
return "$__shellmath_SUCCESS"
|
|
elif ((argCount > 2)); then
|
|
local recursiveReturn
|
|
|
|
# Use a binary recursion tree to multiply everything out
|
|
# 1) left branch
|
|
_shellmath_multiply "${@:1:$((argCount/2))}"; recursiveReturn=$?
|
|
_shellmath_getReturnValue n1
|
|
if (( recursiveReturn != __shellmath_SUCCESS )); then
|
|
_shellmath_setReturnValue "$n1"
|
|
return "$recursiveReturn"
|
|
fi
|
|
# 2) right branch
|
|
_shellmath_multiply "${@:$((argCount/2+1))}"; recursiveReturn=$?
|
|
_shellmath_getReturnValue n2
|
|
if (( recursiveReturn != __shellmath_SUCCESS )); then
|
|
_shellmath_setReturnValue "$n2"
|
|
return "$recursiveReturn"
|
|
fi
|
|
# 3) head node
|
|
local product
|
|
_shellmath_multiply "$n1" "$n2"; recursiveReturn=$?
|
|
_shellmath_getReturnValue product
|
|
_shellmath_setReturnValue "$product"
|
|
if (( isVerbose && ! isSubcall )); then
|
|
echo "$product"
|
|
fi
|
|
return "$recursiveReturn"
|
|
fi
|
|
|
|
local integerPart1 fractionalPart1 integerPart2 fractionalPart2
|
|
local isNegative1 type1 isScientific1 isNegative2 type2 isScientific2
|
|
local flags
|
|
|
|
# Check and parse the arguments
|
|
_shellmath_validateAndParse "$n1"; flags=$?
|
|
_shellmath_getReturnValues integerPart1 fractionalPart1 isNegative1 type1 isScientific1
|
|
if ((flags == __shellmath_ILLEGAL_NUMBER)); then
|
|
_shellmath_warn "${__shellmath_returnCodes[ILLEGAL_NUMBER]}" "$n1"
|
|
return $?
|
|
fi
|
|
_shellmath_validateAndParse "$n2"; flags=$?
|
|
_shellmath_getReturnValues integerPart2 fractionalPart2 isNegative2 type2 isScientific2
|
|
if ((flags == __shellmath_ILLEGAL_NUMBER)); then
|
|
_shellmath_warn "${__shellmath_returnCodes[ILLEGAL_NUMBER]}" "$n2"
|
|
return $?
|
|
fi
|
|
|
|
# Overflow / underflow detection and accommodation
|
|
local rescalingFactor=0
|
|
if ((${#integerPart1} + ${#integerPart2} + ${#fractionalPart1} + ${#fractionalPart2} >= ${__shellmath_precision})); then
|
|
_shellmath_reduceOuterPairs "$integerPart1" "$integerPart2" "$fractionalPart1" "$fractionalPart2"
|
|
_shellmath_getReturnValues integerPart1 integerPart2 fractionalPart1 fractionalPart2 rescalingFactor
|
|
if ((10#$fractionalPart1)); then type1=${__shellmath_numericTypes[DECIMAL]}; fi
|
|
if ((10#$fractionalPart2)); then type2=${__shellmath_numericTypes[DECIMAL]}; fi
|
|
|
|
_shellmath_reduceCrossPairs "$integerPart1" "$integerPart2" "$fractionalPart1" "$fractionalPart2"
|
|
_shellmath_getReturnValues fractionalPart1 fractionalPart2
|
|
|
|
_shellmath_reduceOuterPairs "$fractionalPart1" "$fractionalPart2"
|
|
_shellmath_getReturnValues fractionalPart1 fractionalPart2
|
|
fi
|
|
|
|
# Quick multiply & return for integer multiplies
|
|
if ((type1==type2 && type1==__shellmath_numericTypes[INTEGER])); then
|
|
((isNegative1)) && ((integerPart1*=-1))
|
|
((isNegative2)) && ((integerPart2*=-1))
|
|
local product=$((integerPart1 * integerPart2))
|
|
if ((rescalingFactor > 0)); then
|
|
_shellmath_rescaleValue "$product" "$rescalingFactor"
|
|
_shellmath_getReturnValue product
|
|
fi
|
|
if (( (!isSubcall) && (isScientific1 || isScientific2) )); then
|
|
_shellmath_numToScientific $product ""
|
|
_shellmath_getReturnValue product
|
|
fi
|
|
_shellmath_setReturnValue $product
|
|
if (( isVerbose && ! isSubcall )); then
|
|
echo "$product"
|
|
fi
|
|
return "$__shellmath_SUCCESS"
|
|
fi
|
|
|
|
# The product has four components per the distributive law
|
|
local intProduct floatProduct innerProduct1 innerProduct2
|
|
# Widths of the decimal parts
|
|
local floatWidth fractionalWidth1 fractionalWidth2
|
|
|
|
# Compute the integer and floating-point components
|
|
((intProduct = integerPart1 * integerPart2))
|
|
|
|
fractionalWidth1=${#fractionalPart1}
|
|
fractionalWidth2=${#fractionalPart2}
|
|
((floatWidth = fractionalWidth1 + fractionalWidth2))
|
|
((floatProduct = 10#$fractionalPart1 * 10#$fractionalPart2))
|
|
if ((${#floatProduct} < floatWidth)); then
|
|
printf -v floatProduct "%0*d" "$floatWidth" "$floatProduct"
|
|
fi
|
|
|
|
# Compute the inner products: First integer-multiply, then rescale
|
|
((innerProduct1 = integerPart1 * 10#$fractionalPart2))
|
|
((innerProduct2 = integerPart2 * 10#$fractionalPart1))
|
|
|
|
# Rescale the inner products back to decimals so we can shellmath_add() them
|
|
if ((fractionalWidth2 <= ${#innerProduct1})); then
|
|
local innerInt1=${innerProduct1:0:(-$fractionalWidth2)}
|
|
local innerFloat1=${innerProduct1:(-$fractionalWidth2)}
|
|
integerPart1=${innerInt1}
|
|
fractionalPart1=${innerFloat1}
|
|
else
|
|
integerPart1=0
|
|
printf -v fractionalPart1 "%0*d" "$fractionalWidth2" "$innerProduct1"
|
|
fi
|
|
if ((fractionalWidth1 <= ${#innerProduct2})); then
|
|
local innerInt2=${innerProduct2:0:(-$fractionalWidth1)}
|
|
local innerFloat2=${innerProduct2:(-$fractionalWidth1)}
|
|
integerPart2=${innerInt2}
|
|
fractionalPart2=${innerFloat2}
|
|
else
|
|
integerPart2=0
|
|
printf -v fractionalPart2 "%0*d" "$fractionalWidth1" "$innerProduct2"
|
|
fi
|
|
|
|
# Combine the distributed parts
|
|
local innerSum product
|
|
# Add the inner products to get the inner sum
|
|
_shellmath_add "$integerPart1" "$fractionalPart1" "$integerPart2" "$fractionalPart2"
|
|
_shellmath_getReturnValue innerSum
|
|
[[ "$innerSum" =~ (.*)\.(.*) ]]
|
|
integerPart1=${BASH_REMATCH[1]}
|
|
fractionalPart1=${BASH_REMATCH[2]}
|
|
# Add inner sum + outer sum
|
|
_shellmath_add "$integerPart1" "$fractionalPart1" "$intProduct" "$floatProduct"
|
|
_shellmath_getReturnValue product
|
|
|
|
# Determine the sign of the product
|
|
if ((isNegative1 != isNegative2)); then
|
|
product="-"$product
|
|
fi
|
|
|
|
# When we pre-detect overflow in the integer part of the computation,
|
|
# we compensate by shrinking the inputs by some order of magnitude.
|
|
# Having now finished the computation, we revert to the original magnitude.
|
|
if ((rescalingFactor > 0)); then
|
|
_shellmath_rescaleValue "$product" "$rescalingFactor"
|
|
_shellmath_getReturnValue product
|
|
fi
|
|
|
|
# Convert to scientific notation if appropriate
|
|
if (( (!isSubcall) && (isScientific1 || isScientific2) )); then
|
|
_shellmath_numToScientific "${product%.*}" "${product#*.}"
|
|
_shellmath_getReturnValue product
|
|
fi
|
|
|
|
# Note the result, print if running "normally", and return
|
|
_shellmath_setReturnValue $product
|
|
if (( isVerbose && ! isSubcall )); then
|
|
echo "$product"
|
|
fi
|
|
|
|
return "$__shellmath_SUCCESS"
|
|
}
|
|
|
|
|
|
################################################################################
|
|
# divide (dividend, divisor)
|
|
################################################################################
|
|
function _shellmath_divide()
|
|
{
|
|
local n1="$1"
|
|
local n2="$2"
|
|
local integerPart1 fractionalPart1 integerPart2 fractionalPart2
|
|
local isNegative1 type1 isScientific1 isNegative2 type2 isScientific2
|
|
|
|
if ((! __shellmath_didPrecalc)); then
|
|
_shellmath_precalc; __shellmath_didPrecalc=$__shellmath_true
|
|
fi
|
|
|
|
local isVerbose=$(( __shellmath_isOptimized == __shellmath_false ))
|
|
|
|
local isTesting=${__shellmath_false}
|
|
if [[ "${FUNCNAME[1]}" == "_shellmath_assert_functionReturn" ]]; then
|
|
isTesting=${__shellmath_true}
|
|
fi
|
|
|
|
if [[ $# -eq 0 || $# -gt 2 ]]; then
|
|
echo "Usage: ${FUNCNAME[0]} dividend divisor"
|
|
return "$__shellmath_SUCCESS"
|
|
elif [[ $# -eq 1 ]]; then
|
|
# Note the value as-is and return
|
|
_shellmath_setReturnValue "$n1"
|
|
((isVerbose)) && echo "$n1"
|
|
return "$__shellmath_SUCCESS"
|
|
fi
|
|
|
|
# Check and parse the arguments
|
|
local flags
|
|
_shellmath_validateAndParse "$n1"; flags=$?
|
|
_shellmath_getReturnValues integerPart1 fractionalPart1 isNegative1 type1 isScientific1
|
|
if ((flags == __shellmath_ILLEGAL_NUMBER)); then
|
|
_shellmath_warn "${__shellmath_returnCodes[ILLEGAL_NUMBER]}" "$n1"
|
|
return $?
|
|
fi
|
|
_shellmath_validateAndParse "$n2"; flags=$?
|
|
_shellmath_getReturnValues integerPart2 fractionalPart2 isNegative2 type2 isScientific2
|
|
if ((flags == __shellmath_ILLEGAL_NUMBER)); then
|
|
_shellmath_warn "${__shellmath_returnCodes[ILLEGAL_NUMBER]}" "$n2"
|
|
return $?
|
|
fi
|
|
|
|
# Throw error on divide by zero
|
|
if ((integerPart2 == 0 && 10#$fractionalPart2 == 0)); then
|
|
_shellmath_warn "${__shellmath_returnCodes[DIVIDE_BY_ZERO]}" "$n2"
|
|
return $?
|
|
fi
|
|
|
|
# Convert the division problem to an *integer* division problem by rescaling
|
|
# both inputs so as to lose their decimal points. To obtain maximal precision,
|
|
# we scale up the numerator further, padding with as many zeros as it can hold
|
|
local numerator denominator quotient
|
|
local rescaleFactor zeroCount zeroTail
|
|
|
|
if ((integerPart1 == 0)); then
|
|
integerPart1=""
|
|
fi
|
|
((zeroCount = __shellmath_precision - ${#integerPart1} - ${#fractionalPart1}))
|
|
((rescaleFactor = __shellmath_precision - ${#integerPart1} - ${#fractionalPart2}))
|
|
if ((zeroCount > 0)); then
|
|
printf -v zeroTail "%0*d" "$zeroCount" 0
|
|
fi
|
|
|
|
# Rescale and rewrite the fraction to be computed, and compute it
|
|
numerator=${integerPart1}${fractionalPart1}${zeroTail}
|
|
denominator=${integerPart2}${fractionalPart2}
|
|
((quotient = 10#$numerator / 10#$denominator))
|
|
|
|
# For greater precision, re-divide by the remainder to get the next digits of the quotient
|
|
local remainder quotient_2
|
|
((remainder = 10#$numerator % 10#$denominator)) # cannot exceed numerator or thus, maxValue
|
|
((zeroCount = __shellmath_precision - ${#remainder}))
|
|
if ((zeroCount > 0)); then
|
|
printf -v zeroTail "%0*d" "$zeroCount" 0
|
|
else
|
|
zeroTail=""
|
|
fi
|
|
# Derive the new numerator from the remainder. Do not change the denominator.
|
|
numerator=${remainder}${zeroTail}
|
|
((quotient_2 = 10#$numerator / 10#$denominator))
|
|
quotient=${quotient}${quotient_2}
|
|
((rescaleFactor += ${#quotient_2}))
|
|
|
|
# Rescale back. For aesthetic reasons we also round off at the "precision"th decimal place
|
|
((zeroCount = rescaleFactor - ${#quotient}))
|
|
if ((zeroCount >= 0)); then
|
|
local zeroPrefix="" fractionalPart
|
|
if ((zeroCount > 0)); then
|
|
printf -v zeroPrefix "%0*d" "$((rescaleFactor - ${#quotient}))" 0
|
|
fi
|
|
fractionalPart=${zeroPrefix}${quotient}
|
|
_shellmath_round "$fractionalPart" $__shellmath_precision
|
|
_shellmath_getReturnValue fractionalPart
|
|
quotient="0."${fractionalPart}
|
|
else
|
|
fractionalPart=${quotient:(-$rescaleFactor)}
|
|
_shellmath_round "$fractionalPart" $__shellmath_precision
|
|
_shellmath_getReturnValue fractionalPart
|
|
quotient=${quotient:0:(-$rescaleFactor)}"."${fractionalPart}
|
|
fi
|
|
|
|
# Determine the sign of the quotient
|
|
if ((isNegative1 != isNegative2)); then
|
|
quotient="-"$quotient
|
|
fi
|
|
|
|
if ((isTesting)); then
|
|
# Trim zeros. (Requires decimal point and zero tail.)
|
|
if [[ "$quotient" =~ [\.].*0$ ]]; then
|
|
# If the decimal point IMMEDIATELY precedes the 0s, remove that too
|
|
[[ $quotient =~ [\.]?0+$ ]]
|
|
quotient=${quotient%${BASH_REMATCH[0]}}
|
|
fi
|
|
fi
|
|
|
|
# Convert to scientific notation if appropriate
|
|
if ((isScientific1 || isScientific2)); then
|
|
_shellmath_numToScientific "${quotient%.*}" "${quotient#*.}"
|
|
_shellmath_getReturnValue quotient
|
|
fi
|
|
|
|
# Note the result, print if running "normally", and return
|
|
_shellmath_setReturnValue "$quotient"
|
|
if ((isVerbose)); then
|
|
echo "$quotient"
|
|
fi
|
|
|
|
return "$__shellmath_SUCCESS"
|
|
}
|
|
|