const fmtUsd = new Intl.NumberFormat( 'en-US', { style: 'currency', currency: 'USD' } );
const fmtPerc = new Intl.NumberFormat( 'en-US', { style: 'percent', minimumFractionDigits: 2, maximumFractionDigits: 2 } );
function compute( e ) {
const errorOutput = getElement( 'errorOutput' , 'div' );
const outputContainer = getElement( 'outputContainer', 'div' );
try {
const principalInput = getElement( 'principal', 'input[type="number"]' );
const rateInput = getElement( 'rate' , 'input[type="range"]' );
const yearsSelect = getElement( 'years' , 'select' );
const principalAmount = getNumberValue( principalInput );
const ratePercDbl = getNumberValue( rateInput ) / 100.0;
const yearsCount = getNumberValue( yearsSelect );
computeInner( principalAmount, ratePercDbl, yearsCount );
// No errors, so show the output div and hide any previous error messages:
outputContainer.hidden = false;
errorOutput .hidden = true;
}
catch( err ) {
// Something went wrong! So hide the normal output <div> and show the error details:
errorOutput.hidden = false;
errorOutput.textContent = ( err || "(Unknown error)" ).toString() + "\r\n\r\n" + ( err.stack );
outputContainer.hidden = true;
}
}
/**
* @param {number} principalAmount
* @param {number} interestRatePerc
* @param {number} yearsCount
*/
function computeInner( principalAmount, interestRatePerc, yearsCount ) {
// Preconditions:
if( typeof principalAmount != 'number' ) throw new Error( "Expected `principalAmount` to be `number` but encountered " + typeof principalAmount );
if( typeof interestRatePerc != 'number' ) throw new Error( "Expected `interestRatePerc` to be `number` but encountered " + typeof interestRatePerc );
if( typeof yearsCount != 'number' || yearsCount <= 0 ) throw new Error( "Expected `yearsCount` to be a positive `number` but encountered " + ( typeof yearsCount ) + " \"" + yearsCount.toString() + "\" instead." );
//
const interest = principalAmount * interestRatePerc * yearsCount;
const totalAmount = interest + principalAmount;
const endYear = new Date().getFullYear() + yearsCount;
//
setOutput( 'principal', principalAmount , fmtUsd );
setOutput( 'rate' , interestRatePerc, fmtPerc );
setOutput( 'years' , endYear );
const amountOut = getElement( 'receiveAmountOut', 'output' );
amountOut.value = fmtUsd.format( totalAmount );
}
/**
* @param {string} outputFor
* @param {number} value
* @param {Intl.NumberFormat} formatter
*/
function setOutput( outputFor, value, formatter ) {
const formattedValue = formatter ? ( formatter.format( value ) ) : ( value.toString() );
const selector = 'output[for~="' + CSS.escape( outputFor ) +'"]';
const outputs = document.querySelectorAll( selector );
for( const outputEl of outputs ) {
outputEl.value = formattedValue;
}
}
/**
* @param {string} id
* @param {string} validationSelector
* @returns {HTMLElement}
*/
function getElement( id, validationSelector ) {
const el = document.getElementById( id );
if( el === null ) throw new Error( "No element with `id=\"" + id + "\"` was found." );
if( !el.matches( validationSelector ) ) throw new Error( "The element with `id=\"" + id + "\"` does not match selector \"" + validationSelector + "\"." );
return el;
}
/**
* @param {HTMLInputElement | HTMLSelectElement} input
* @returns {number}
*/
function getNumberValue( input ) {
// Preconditions:
if( !( input instanceof HTMLElement ) ) throw new Error( 'Expected `input` to be a HTML element.' );
if( !( input.tagName === 'SELECT' || input.tagName === 'INPUT' ) ) throw new Error( 'Expected `input` to be a HTML <input /> or <select> element.' );
//
let valueNumber;
if( input.tagName === 'SELECT' ) {
valueNumber = parseInt( input.value, /*radix:*/ 10 );
}
else if( input.type === 'number' ) { // Can't use valueAsNumber with type="range", grr.
valueNumber = input.valueAsNumber;
}
else {
valueNumber = parseFloat( input.value );
}
if( isNaN( valueNumber ) ) {
throw new Error( "Form <input> or <select> has invalid value: \"" + input.value + "\"." );
}
return valueNumber;
}
body {
background-color: black;
font-family: sans-serif;
color: white
}
.maindiv {
background-color: white;
color: black;
width: 300px;
padding: 20px;
border-radius: 25px;
margin: auto;
}
h1 {
color: grey;
font-family: verdana
}
.field {
padding: 1em;
margin: 1em 0;
background-color: #eee;
border-radius: 5px;
}
.field > label,
.field > input,
.field > div > input {
display: block;
}
#principalField.field > div {
display: flex;
justify-content: stretch;
align-items: stretch;
}
#principalField.field > div > span.symbol {
display: inline-block;
/* Width is 1em with negative right margin of -1.5em to make the currency symbol appear "inside" the <input />. Also, identical font-size, padding and border as Chrome's default style for <input />: */
width: 1em;
margin: 0 -1.5em 0 0;
border: 1px solid transparent;
padding: 1px 2px;
z-index: 10;
pointer-events: none;
user-select: none;
color: #999;
font-size: 90%;
}
#errorOutput {
border: 1px solid red;
border-radius: 7px;
white-space: pre-line;
}
output {
font-weight: bold;
}
output:empty {
display: none;
}
input[type="number"] {
text-align: right;
}
<div class="maindiv">
<h1>Simple Interest Calculator</h1>
<div class="field" id="principalField">
<label for="principal">Principal amount</label>
<div>
<span class="symbol">$</span>
<input type="number" id="principal" oninput="compute(event)" value="123.00" size="6" />
</div>
</div>
<div class="field">
<label for="rate">Interest rate (<output for="rate">10.25%</output>)</label>
<input type="range" id="rate" min="1" max="20" step="0.25" value="10.25" oninput="compute(event)" />
</div>
<div class="field">
<label for="years">No. of Years </label>
<select id="years" oninput="compute(event)">
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
</select>
</div>
<button hidden type="button" onclick="compute(event)">Compute Interest</button>
<div id="outputContainer" hidden>
If you deposit <output for="principal"></output>
at an interest rate of <output for="rate"></output>
you will receive an amount of <output id="receiveAmountOut"></output> in the year <output for="years"></output>.
</div>
<div id="errorOutput" hidden></div>
</div>