Last active
July 16, 2020 15:38
-
-
Save h8rt3rmin8r/901b449be4287484e0de06f7109ee50e to your computer and use it in GitHub Desktop.
Calculate future value based on compound interest
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#! /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