1
0
Fork 0
bash/examples/shellmath/shellmath.sh
Daniel Baumann fa1b3d3922
Adding upstream version 5.2.37.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
2025-06-21 06:49:21 +02:00

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"
}