Skip to content

Instantly share code, notes, and snippets.

@h8rt3rmin8r
Last active July 16, 2020 15:38
Show Gist options
  • Save h8rt3rmin8r/901b449be4287484e0de06f7109ee50e to your computer and use it in GitHub Desktop.
Save h8rt3rmin8r/901b449be4287484e0de06f7109ee50e to your computer and use it in GitHub Desktop.
Calculate future value based on compound interest
#! /usr/bin/env bash
#>------------------------------------------------------------------------------
#>
#> [ future-value.sh ]
#>
#> Calculate future value based on compound interest
#>
#> USAGE:
#>
#> future-value.sh <OPTION>
#>
#> run a suplemental operation where "OPTION" is one of the following:
#>
#> |
#> -h, --help | Print this help text to the terminal
#> |
#> --ta <X>, |
#> --test-account <X> | Test if the account value "X" is a valid input for
#> | the "ACCOUNT" parameter
#> |
#> | See definition of "ACCOUNT" below
#> |
#> --tc <X>, |
#> --test-contrib <X> | Test if the contribution value "X" is a valid input
#> | for the "CONTRIBUTION" parameter
#> |
#> | See the definition of "CONTRIBUTION" below
#> |
#> --ti <X>, |
#> --test-interest <X> | Test if the interest value "X" is a valid input for
#> | the "INTEREST" parameter
#> |
#> | See definition of "INTEREST" below
#> |
#> --tp <X>, |
#> --test-period <X> | Derive the numeric period value for "X"; where "X"
#> | is some time increment in textual form
#> | (examples: s, y, yearly, every second, etc ...)
#> | See definition of "PERIOD" below
#> |
#>
#> future-value.sh \
#> [-p <PERIOD>|--period="<PERIOD>"] \
#> [-a <ACCOUNT>|--account="<ACCOUNT>"] \
#> [-i <INTEREST>|--interest="<INTEREST>"] \
#> (-c <CONTRIBUTION>|--contribution="<CONTRIBUTION>")
#>
#> run a full future value calculation such that the following definitions
#> are true:
#>
#> |
#> "ACCOUNT" | The starting value of an account
#> |
#> "CONTRIBUTION" | The amount manually contributed to the account in
#> | each "PERIOD" of time (optional)
#> |
#> "INTEREST" | The fixed annual interest rate with which to run
#> | compounding value calculations
#> |
#> "PERIOD" | The period of compounding and contributions; may be
#> | exactly or similar to the folowing:
#> |
#> | second, minute, hour, day, week, month, quarter,
#> | semi-annual, year
#> |
#>
#> REFERENCE:
#>
#> # Future Value Calculator
#> https://bit.ly/future-value-calculator
#>
#> # Future Value - Simple
#> https://financeformulas.net/Future_Value.html
#>
#> # Future Value - Continuous Compounding
#> https://financeformulas.net/Future-Value-Continuous-Compounding.html
#>
#> ATTRIBUTION:
#>
#> Created on 20200716 by h8rt3rmin8r ([email protected])
#>
#> SOURCE CODE:
#>
#> Pastebin: https://bit.ly/32qOTrU
#> Github: https://bit.ly/2B5lZCl
#>
#>------------------------------------------------------------------------------
# Declare functions
function fv_calc() {
# Future value calculation function
local IX="${i_acc}"
local RX="${i_int}"
local TX="${i_per}"
local calc_a=$(echo "scale=10; 1 + ${RX}" | bc 2>/dev/null)
local calc_b=$(echo "scale=10; ${calc_a} ^ ${TX}" | bc 2>/dev/null)
echo "scale=10; ${IX} * ${calc_b}" | bc 2>/dev/null
return $?
}
function fv_help() {
# Script help text function
cat "${0}" \
| grep -E '^#[>]' \
| sed 's/^..//'
return $?
}
function fv_parse() {
# General parsing function
## Prints modified values of an indicated type
if [[ "$#" -ne 2 ]]; then
return 1
fi
local e_c=1
case "${1//[-]}" in
d|decimal|a|acc|account|accountvalue|c|con|contribution|i|int|interest|pc|percent)
shift 1
local output=$(grep -Eo '^([0-9])+([.][0-9]+)?' <<<"$@")
if [[ "${output:0:1}" == "." ]]; then
local output="0${output}"
fi
echo "${output}"
local e_c="$?"
;;
p|pd|per|period)
shift 1
grep -E '^(31536000|525600|8760|365|52|12|6|4|1)' <<<"$@"
local e_c="$?"
;;
esac
return ${e_c}
}
function fv_percent_cnv() {
# Percent to decimal conversion function
local i_n="${1//[^0-9.]}"
local output=$(echo "scale=10; ${i_n} / 100" | bc 2>/dev/null)
if [[ "${output:0:1}" == "." ]]; then
local output="0${output}"
fi
echo "${output}"
return $?
}
function fv_period_ref() {
# Converts a period textual name into a corresponding number
if [[ "x${1}" == "x" ]]; then
return 1
fi
local e_c=1
local in_base="${1//[- ]}"
local in_x="${in_base,,}"
case "${in_x}" in
s|sec|second|secondly|everysecond|eachsecond|bysecond|31536000)
echo "31536000"
local e_c=0
;;
min|minute|minutely|everyminute|eachminute|byminute|525600)
echo "525600"
local e_c=0
;;
h|hr|hour|hourly|everyhour|eachhour|byhour|8760)
echo "8760"
local e_c=0
;;
d|day|daily|everyday|eachday|byday|365)
echo "365"
local e_c=0
;;
w|wk|week|weekly|everyweek|eachweek|byweek|52)
echo "52"
local e_c=0
;;
m|mo|month|monthly|everymonth|eachmonth|bymonth|12)
echo "12"
local e_c=0
;;
sa|sannually|semiannually|6)
echo "6"
local e_c=0
;;
q|qt|quarter|quarterly|everyquarter|eachquarter|byquarter|4)
echo "4"
local e_c=0
;;
a|ann|annually|annual|everyyear|eachyear|byyear|year|yearly|yr|y|1)
echo "1"
e_c=0
;;
esac
return ${e_c}
}
function fv_valid() {
# General validation function (emits exit codes only)
if [[ "$#" -ne 2 ]]; then
return 1
fi
local e_c=1
case "${1//[-]}" in
d|decimal|a|acc|account|accountvalue|c|con|contribution|i|int|interest|pc|percent)
shift 1
grep -Eo '^([0-9])+([.][0-9]+)?$' <<<"$@" &>/dev/null
local e_c="$?"
;;
p|pd|per|period)
shift 1
grep -E '^(31536000|525600|8760|365|52|12|6|4|1)$' <<<"$@" &>/dev/null
local e_c="$?"
;;
esac
return ${e_c}
}
function fv_vb() {
# Script verbosity function
echo "$(date '+%s%N')|future-value|$@" &>/dev/stderr
return $?
}
#-------------------------------------------------------------------------------
# Declare variables and arrays
c_a=""
c_b=""
c_x=""
i_acc=""
i_con=""
i_int=""
i_per=""
c_acc=""
c_con=""
c_int=""
c_per=""
c_all=""
#-------------------------------------------------------------------------------
# Execute operations
## Catch basic operational requests (like help text etc.)
case "${1//[-]}" in
h|H|help)
fv_help
exit $?
;;
ta|testacc|testaccount)
shift 1
if [[ "x${1}" == "x" ]]; then
fv_vb "ERROR: Missing required input: <PERIOD_VALUE>"
fv_vb "Use '--help' for more information"
exit 1
fi
test_a="$@"
test_b=$(fv_parse --account "${test_a}")
test_c=$(fv_valid --account "${test_a}"; echo $?)
if [[ "${test_c}" -eq 0 ]]; then
test_c="PASS (0)"
else
test_c="FAIL (1)"
fi
echo "Input: ${1}"
echo "Translation: null"
echo "Parsed Input: ${test_b}"
echo "Validation: ${test_c}"
exit $?
;;
tc|testcon|testcontrib|testcontribution)
shift 1
if [[ "x${1}" == "x" ]]; then
fv_vb "ERROR: Missing required input: <PERIOD_VALUE>"
fv_vb "Use '--help' for more information"
exit 1
fi
test_a="$@"
test_b=$(fv_parse --contribution "${test_a}")
test_c=$(fv_valid --contribution "${test_a}"; echo $?)
if [[ "${test_c}" -eq 0 ]]; then
test_c="PASS (0)"
else
test_c="FAIL (1)"
fi
echo "Input: ${1}"
echo "Translation: null"
echo "Parsed Input: ${test_b}"
echo "Validation: ${test_c}"
exit $?
;;
ti|testint|testinterest)
shift 1
if [[ "x${1}" == "x" ]]; then
fv_vb "ERROR: Missing required input: <PERIOD_VALUE>"
fv_vb "Use '--help' for more information"
exit 1
fi
test_a=$(fv_percent_cnv "$@")
test_b=$(fv_parse --interest "${test_a}")
test_c=$(fv_valid --interest "${test_a}"; echo $?)
if [[ "${test_c}" -eq 0 ]]; then
test_c="PASS (0)"
else
test_c="FAIL (1)"
fi
echo "Input: ${1}"
echo "Translation: ${test_a}"
echo "Parsed Translation: ${test_b}"
echo "Validation: ${test_c}"
exit $?
;;
tp|testper|testperiod)
shift 1
if [[ "x${1}" == "x" ]]; then
fv_vb "ERROR: Missing required input: <PERIOD_VALUE>"
fv_vb "Use '--help' for more information"
exit 1
fi
test_a=$(fv_period_ref "$@")
test_b=$(fv_parse --period "${test_a}")
test_c=$(fv_valid --period "${test_a}"; echo $?)
if [[ "${test_c}" -eq 0 ]]; then
test_c="PASS (0)"
else
test_c="FAIL (1)"
fi
echo "Input: ${1}"
echo "Translation: ${test_a}"
echo "Parsed Translation: ${test_b}"
echo "Validation: ${test_c}"
exit $?
;;
esac
if [[ "$#" -lt 4 ]]; then
## Kill sessions having less than four inputs
fv_vb "ERROR: Inadequate number of input parameters detected"
fv_vb "Use '--help' for more information"
exit 1
fi
## Consume all required input parameters for running future value calculations
while [[ "$#" -gt 0 ]]; do
c_a="${1//[-]}"
if [[ "${c_v}" =~ ^[-]+[a-z]+[=] ]]; then
c_x="${1#[-]}"
c_x="${c_x#[-]}"
c_a="${c_x//[=]*}"
c_b="${c_x//*[=]}"
case "${c_a}" in
a|acc|account)
shift 1
i_acc="${c_b}"
;;
c|cont|contribution)
shift 1
i_con="${c_b}"
;;
i|int|interest)
shift 1
i_int="${c_b}"
;;
p|per|period)
shift 1
i_per="${c_b}"
;;
esac
else
c_b="${2}"
case "${c_a}" in
a|acc|account)
shift 1
i_acc="${c_b}"
shift 1
;;
c|cont|contribution)
shift 1
i_con="${c_b}"
shift 1
;;
i|int|interest)
shift 1
i_int="${c_b}"
shift 1
;;
p|per|period)
shift 1
i_per="${c_b}"
shift 1
;;
esac
fi
done
## Format all inputs to conform with the requirements of the conversion formula
## Improperly formatted inputs will be ignored (causing the script to die in the
## following "if" filter section)
i_acc=$(fv_parse --account "${i_acc}") ## validation
i_con=$(fv_parse --contribution "${i_con}") ## validation
t_int=$(fv_percent_cnv "${i_int}") ## transformation
i_int=$(fv_parse --interest "${t_int}") ## validation
t_per=$(fv_period_ref "${i_per}") ## transformation
i_per=$(fv_parse --period "${t_per}") ## validation
## Verify all required variables have been defined
c_acc=$(grep "." <<<"${i_acc}" &>/dev/null; echo $?)
c_int=$(grep "." <<<"${i_int}" &>/dev/null; echo $?)
c_per=$(grep "." <<<"${i_per}" &>/dev/null; echo $?)
c_all="${c_acc}${c_int}${c_per}"
if [[ "${c_all}" =~ "1" ]]; then
## Determine what is missing and inform the user; then kill the script
if [[ "${c_acc}" -ne 0 ]]; then
fv_vb "ERROR: Required variable is UNDEFINED: account_value"
fv_vb "The input may have been missing or improperly formatted"
fi
if [[ "${c_int}" -ne 0 ]]; then
fv_vb "ERROR: Required variable is UNDEFINED: interest_rate"
fv_vb "The input may have been missing or improperly formatted"
fi
if [[ "${c_per}" -ne 0 ]]; then
fv_vb "ERROR: Required variable is UNDEFINED: period"
fv_vb "The input may have been missing or improperly formatted"
fi
fv_vb "Use '--help' for more information"
fv_vb "Aborting calculation operations"
exit 1
fi
## Run the calculation and kill the script
run_output=$(fv_calc)
run_return=$(fv_calc &>/dev/null; echo $?)
if [[ "${run_return}" -eq 0 ]]; then
echo "${run_output}"
fi
exit ${run_return}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment