0

I am trying to create an array of objects and populate it with specific calculations. I am currently stuck and wonder how I can use data from the same iteration to do calculations.

I am pretty sure that the first three columns are working as intended. However, the last two columns "TotInterest" and "Balance" are not.


To calculate the "balance":
I take the balance from the previous iteration/period and add the current payment and interest.

To calculate the "totInterest":
this should just be the sum of the current and all previous interests.


Since the first period is special, I have tried to add it before the loop. I think this is causing some kind of issues, but I cant figure out how.

The area of js-code to focus on is commented /** UPDATED TABLE OBJECT **/


Any help would be appreciated!

EDIT: There is no error in the code, but the expected behaviour is not correct. Here is an image of the expected/correct results: https://i.sstatic.net/HyrUp.png
The balance should sum the balance from the previous one iteration and add the current interest and payment. Balance should be something like this:
balance: calcTable[i-1].balance + A + calcTable[i].interest
The total Interest should add the current interest with all of the previous iterations

//************* CALCULATIONS *************

var Pre = document.getElementById("calc_P").value;
var P = parseFloat(Pre);
var r = document.getElementById("calc_r").value / 100;
var n = 12;
var tre = document.getElementById("calc_t").value;
var t = parseFloat(tre);
var Are = document.getElementById("calc_A").value;
var A = parseFloat(Are);
var p = 12;

var nper = p * t;
var rate = (1 + r / n) ** (n / p) - 1;
var F = P * (1 + rate) ** nper + (A * ((1 + rate) ** nper - 1)) / rate;

//************* DEFAULTS ****************

document.getElementById("finalValue").innerHTML = F;

//************* LINK INPUTS *************

// Select all inputs and add event listener.
var inputsArray = document.querySelectorAll("input");
inputsArray.forEach(el => el.addEventListener("input", linkValue));

// Link the range with number inputs
function linkValue(e) {
  var sibling = e.target.previousElementSibling || e.target.nextElementSibling;
  sibling.value = e.target.value;
}

//************* DEFAULT TABLE OBJECT *************

var tableTotalt = [
  {
    Period: 0,
    Payment: "123",
    PPayment: "154",
    Interest: "143",
    CuInterest: "4423",
    Balance: "123"
  },
  {
    Period: 1,
    Payment: "123",
    PPayment: "154",
    Interest: "143",
    CuInterest: "4423",
    Balance: "123"
  },
  {
    Period: 2,
    Payment: "123",
    PPayment: "154",
    Interest: "143",
    CuInterest: "4423",
    Balance: "123"
  },
  {
    Period: 3,
    Payment: "123",
    PPayment: "154",
    Interest: "143",
    CuInterest: "4423",
    Balance: "123"
  },
  {
    Period: 4,
    Payment: "123",
    PPayment: "154",
    Interest: "143",
    CuInterest: "4423",
    Balance: "123"
  },
  {
    Period: 5,
    Payment: "123",
    PPayment: "154",
    Interest: "143",
    CuInterest: "4423",
    Balance: "123"
  }
];

//************* UPDATED TABLE OBJECT *************

//test-array
var calcTable = new Array(nper);

calcTable.unshift({
  period: 0,
  payment: 0,
  totPayment: 10000,
  interest: 0,
  totInterest: 0,
  balance: 10000
});
console.log(calcTable);
for (let i = 0; i < nper; i += 1) {
  calcTable[i + 1] = {
    period: i + 1,
    payment: A,
    totPayment: (P += A),
    interest: calcTable[i].balance * rate,
    totInterest: calcTable[i].balance * rate + calcTable[i].interest,
    balance: calcTable[i].balance + A + calcTable[i].interest
  };
}

//************* CREATE HTML TABLE *************

var selectorTotalt = "TblCalc";

//call the jsonToTable Function
jsonToTable(calcTable, selectorTotalt);

function addEl(parent, nodeName, className) {
  var element = document.createElement(nodeName);
  if (className) element.className = className;
  if (parent) parent.appendChild(element);
  return element;
}
function addText(parent, text) {
  parent.appendChild(document.createTextNode(text));
}
function jsonToTable(json, selector) {
  var table = addEl(null, "table", "tbl-container");
  var tbody = addEl(table, "tbody");
  var thr = addEl(tbody, "tr", "tbl-headers");

  //loop through the property names of the first object
  for (var propertyName in json[0]) {
    addText(addEl(thr, "th"), propertyName);
  }

  //loop through the array of objects
  for (var ind = 0; ind < json.length; ind++) {
    var item = json[ind];
    var tr = addEl(tbody, "tr", "tbl-rows");
    //loop though each of the objects properties
    var first = ind != json.length - 1; // no first item for last row
    for (var key in item) {
      var el = addEl(tr, "td");
      if (first) {
        // <---- point of interest
        el = addEl(el, "div", "tbl-name-container ");
        el = addEl(el, "div", "tbl-name-text");
        first = false;
      }
      //append the table data containing the objects property value
      addText(el, "" + item[key]);
    }
  }

  var target = document.getElementById(selector);
  target.innerHTML = "";
  target.appendChild(table);
}
<div class="finalValue">Final value: <span id="finalValue">0</span></div>

<!-- INPUTS -->
<div>
  P: <input id="calc_P" type="number" value="10000">
  <input type="range" value="10000"><br>
</div>
<div>
  A: <input id="calc_A" type="number" value="1000">
  <input type="range" value="1000"><br>
</div>
<div>
  t: <input id="calc_t" type="number" value="10">
  <input type="range" value="10"><br>
</div>
<div>
  r: <input id="calc_r" type="number" value="10">
  <input type="range" value="10"><br>
</div>


<!-- TABLE -->
<div id="TblCalc" class="card table-card"></div>


<!-- JS -->
<script type="text/javascript" src="test.js"></script>

5
  • What's the point of sibling.value = sibling.value? Commented Sep 6, 2019 at 23:16
  • Oh, I probably tried some other code there and forgot to delete it after solving the "LINK INPUTS". 'sibling.value = sibling.value' can be deleted :) The area where the main problem is the section commented '**** UPDATED TABLE OBJECT ******' Commented Sep 6, 2019 at 23:20
  • What is not working? Based on the calculations you are running, the results I'm seeing are correct. What are the expected values of balance and totInterest for the first few iterations? Commented Sep 6, 2019 at 23:29
  • There is no error in the code, but the expected behaviour is not correct. Here is an image of the expected/correct results: i.imgur.com/04prTMO.png Commented Sep 6, 2019 at 23:33
  • If the code is not correct, there's an error in the code Commented Sep 7, 2019 at 1:07

2 Answers 2

1

OK, this stems from the fact that your calculations were a little off, here is the correct code:

//************* UPDATED TABLE OBJECT *************

//test-array
var calcTable = new Array(nper);

calcTable.unshift({
  period: 0,
  payment: 0,
  totPayment: 10000,
  interest: 0,
  totInterest: 0,
  balance: 10000
});

for (let i = 0; i < nper; i += 1) {
  var interest = calcTable[i].balance * rate;
  calcTable[i + 1] = {
    period: i + 1,
    payment: A,
    totPayment: (P += A),
    interest: interest,
    totInterest: interest + calcTable[i].totInterest,
    balance: calcTable[i].balance + A + interest
  };

}

function setPrecision(table) {
  for (let i = 1; i < table.length; i++) {
    for (let key in table[i]) {
      if (key === 'interest' || key === 'totInterest' || key === 'balance') {
        table[i][key] = table[i][key].toFixed(2);
      }
    }
  }
}

setPrecision(calcTable)

function localeSet(table) {
  for (let i = 1; i < table.length; i++) {
    for (let key in table[i]) {
      if (key === 'interest' || key === 'totInterest' || key === 'balance') {
        table[i][key] = new Intl.NumberFormat('fr', {minimumFractionDigits: 2}).format(table[i][key]);
      } else {
        table[i][key] = new Intl.NumberFormat('fr').format(table[i][key]);
      }
    }
  }
}

localeSet(calcTable);

This portion in your original was causing the incorrect calculations:

totInterest: calcTable[i].balance * rate + calcTable[i].interest,
balance: calcTable[i].balance + A + calcTable[i].interest

You want to add the current interest rate to the balance, not the interest from the previous calculation. I just stored this in a variable and passed it around that way, so you wouldn't have to retype the calculation:

var interest = calcTable[i].balance * rate;

I added some helper functions setPrecision and localeSet that will get the formatting to be like the example you shared in the comments.

Here is what you want:

enter image description here

And here is what my version will get:

enter image description here

I also think that you could refactor a little and the section in question would be a little easier to understand

//test-array
let calcTable = [];

calcTable.push({
  period: 0,
  payment: 0,
  totPayment: 10000,
  interest: 0,
  totInterest: 0,
  balance: 10000
});

for (let i = 1; i < nper; i++) {
  let previous = i - 1;
  let interest = calcTable[previous].balance * rate;
  calcTable[i] = {
    period: i,
    payment: A,
    totPayment: (P += A),
    interest,
    totInterest: interest + calcTable[previous].totInterest,
    balance: calcTable[previous].balance + A + interest
  };
}

You don't really need to set the length of the array with:

let calc = new Array(nper);

You can just make calc an empty array:

let calc = [];

Then, there is no need to call unshift, you can just push the first calculation onto the empty array. In your 'for' loop, you start with index 0, even though you don't really need it, just start with index 1, and then create a local variable 'previous' that reads the previous index, it will make the logic look less confusing I think:

for (let i = 1; i < nper; i++) {
  let previous = i - 1;
Sign up to request clarification or add additional context in comments.

7 Comments

I am very certain, that it is not a formatting issue. The second row and sixth column, p1:balance, should add 10000 from the previous iteration p0:balance and then add 1000 from p1:payment and add 83,33 from p1:interest. The result is not because of rounding/decimals. The fifth column, "totInterest" is supposed to sum the current and previous interest. Eg. p5:totInterest should be the sum of p0-p5 interest.
If you look at p2 balance, it is currently the sum of = previous periods balance(correct) + current periods payment (correct) + previous periods interest(wrong!), should be the current periods interest.
I got you, new answer coming with correct calculations.
balance: calcTable[i].balance + A + calcTable[i].interest if you want the current period's interest then it should be: balance: calcTable[i].balance + A + (calcTable[i].balance * rate) right?
@MattAft correct, I just stored that calculation in the interest variable.
|
1

Didn't do a complete refactor (or comb your code thoroughly), but give this a try. I think the majority of the issue was in the calculation loop, which you were pointing to the previous value for total interest and interest and not the current value.

This had been updated and the loop now starts at the first index to make it easier to think back from the current (via -1) than to start at the previous and think forward to the current (via +1).

I took a few other liberties with Bootstrap and calculating on changes. Feel free to update and modify.

// Globals
const TIMERS = {}; // holds timers

calculate(); // First run (populate on load)


// Link Inputs
let inputsArray = document.querySelectorAll('input');
inputsArray.forEach(el => el.addEventListener('input', linkValue));


function calculate(){

  /*
  A = final balance
  P = principal
  r = rate
  n = number of annual interst payments
  t = years
  */

  // Calculations
  let P = parseFloat(document.querySelector('#calc_P').value);
  let r = document.querySelector('#calc_r').value / 100;
  let n = 12;
  let t = parseFloat(document.querySelector('#calc_t').value);
  let A = parseFloat(document.querySelector('#calc_A').value);
  let p = 12;

  let nper = p * t;
  let rate = (1 + r / n) ** (n / p) - 1;
  let F = P * (1 + rate) ** nper + (A * ((1 + rate) ** nper - 1)) / rate;


  // Initialize
  document.querySelector('#finalValue').innerHTML = F.toFixed(2);

  // Fields that will require special formatting (rounding)
  let roundFields = ['interest','totInterest','balance'];

  // Seed Data
  let data = [{
    period: 0,
    payment: P || 0,
    totPayment: P || 10000,
    interest: 0,
    totInterest: 0,
    balance: P || 10000
  }];

  // Create Data Objects
  for (let i = 1; i < nper + 1; i++) {
    let prevRow = i-1;
    let interest = data[prevRow].balance * rate;

    data.push({
      period      : i,
      payment     : A,
      totPayment  : (P += A),
      interest    : interest,
      totInterest : data[prevRow].totInterest + interest,
      balance     : data[prevRow].balance + A + interest
    });
  }




  //************* CREATE HTML TABLE *************

  let tableSelector = '#TblCalc';

  buildHtmlTable(data, tableSelector);


  function buildHtmlTable(data, tableSelector) {

    let fragment = document.createDocumentFragment();
    let table    = addElement(fragment, 'table', 'table table-sm table-hover tbl-container');
    let thead    = addElement(table, 'thead', 'thead-dark');
    let header   = addElement(thead, 'tr', 'tbl-headers');
    let tbody    = addElement(table, 'tbody');

    // Create Headers
    for (let propertyName in data[0]) {
      let th = addElement(header, 'th'); 
      addText(th, propertyName);
    }

    // Create Rows
    data.forEach((item,i,array)=>{

      let tr = addElement(tbody, 'tr', 'tbl-row');

      // Create Cells
      for (let key in item) {
        let td  = addElement(tr, 'td');
        let val = item[key];
        
        // format output for float fields
        if (roundFields.includes(key))
          val = round(val).toFixed(2);

        addText(td, '' + val);
      }


    }); // End: Create Rows


    // Add to DOM
    let target = document.querySelector(tableSelector);
    target.innerHTML = '';
    target.appendChild(fragment);
  }
  
}


//************ Event Handlers *************

// Link the range with number inputs
function linkValue(e) {
  let target = e.target;
  
  if( typeof target['linkedElement'] !== 'undefined' ){
    target['linkedElement'].value = target.value;
  }
  else {
  
    let ancestor = target.closest('.row');
    let inputs   = ancestor.querySelectorAll('input');

    for (let input of inputs){
      if (target !== input){
        // establish bi-directional links
        target['linkedElement'] = input;
        input['linkedElement'] = target;

        // set values
        input.value = target.value;
        
        calculate();
        break;
      }
    }
  }
  
  // Reduce lag for certain sliders by adding a timeout
  if (target.type == 'range' ){
    if ( ['calc_t','calc_r'].includes(target.id) || 
         ['calc_t','calc_r'].includes(target.linkedElement.id) ){ 
      let id = 'range';

      // clear any open timers (stop unecessarily generating table)
      if (TIMERS[id]){
        clearTimeout(TIMERS[id]);
        TIMERS[id] = null;
      }

      TIMERS[id] = window.setTimeout(function(id){
        calculate();
        TIMERS[id]=null;
      }.bind(this,id), 10);
    }
    else {
      // not all sliders require a delay
      calculate();
    }
  }
  else {
    // immediately execute
    calculate();
  }
}




//************ Utility Functions *************

function addElement(parent, nodeName, className) {
  let element = document.createElement(nodeName);
  if (className) element.className = className;
  if (parent) parent.appendChild(element);
  return element;
}

function addText(parent, text) {
  parent.appendChild( document.createTextNode(text) );
}

function round(num){
  return Math.round( ( num + Number.EPSILON ) * 100 ) / 100;
}
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">


<div class="finalValue">Final value: <span id="finalValue">0</span></div>

<!-- INPUTS -->
<div class="row">
  <div class="col-1 text-right">P:</div>
  <div class="col-3">
    <input id="calc_P" type="number" value="10000" max="10000" class="form-control form-control-sm">
  </div>
  <div class="col-3">
    <input type="range" value="10000" max="10000" class="form-control form-control-sm">
  </div>
</div>

<div class="row">
  <div class="col-1 text-right">A:</div>
  <div class="col-3"><input id="calc_A" type="number" value="1000" max="1000" class="form-control form-control-sm">
  </div>
  <div class="col-3">
    <input type="range" value="1000" max="1000" class="form-control form-control-sm">
  </div>
</div>

<div class="row">
  <div class="col-1 text-right">t:</div>
  <div class="col-3"><input id="calc_t" type="number" value="10" class="form-control form-control-sm">
  </div>
  <div class="col-3">
    <input type="range" value="10" class="form-control form-control-sm">
  </div>
</div>

<div class="row">
  <div class="col-1 text-right">r:</div>
  <div class="col-3"><input id="calc_r" type="number" value="10" class="form-control form-control-sm">
  </div>
  <div class="col-3">
    <input type="range" value="10" class="form-control form-control-sm">
  </div>
</div>


<!-- TABLE -->
<div id="TblCalc" class="card table-card"></div>


<!-- JS -->
<script type="text/javascript" src="test.js"></script>

12 Comments

@Frank351 try this update out. I don't know what F represents (final balance?) and I don't know what the expected behavior is because as I move the sliders the 'F' doesn't match the actual final balance. This looks like a financial equation for compounded interests, but I only recall the Pert method and that seems to slightly deviate. I think it would help if you didn't use abbreviated variables and instead do the readable method of naming your variables what they're intended to represent ("principal" if P is principal).
Also, I'm more curious about your browser lag issue. I tested on my laptop (already weaker hardware) that is 8 years old (even weaker hardware) and it handled fine. I added a document fragment that might reduce some operations when creating the elements, but there's several things you should do otherwise. (1) create the elements before appending them to another; (2) big tables are demanding on browsers, there are libraries that make it easier to deal with efficiently rendering and handling big data in the browser.
It's best to pursue the other libraries and your intuitions are right about the input vs change (or mouseup). I've quickly modified the event handler to cancel the render for the 'r' and 't' sliders. Take a look. The "lag" isn't really bad. -- Tables have a lot of overhead and if you're adding 1000 rows; that's 1000 tr elements + 1000x6 td cells + 1000x6 textnodes (13,000 els), each with their own sets of DOM attributes. But your isn't just adding/removing rows; it's clearing the table (innerHTML='') and reconstructing it each time. There are more efficient ways, but take above. @Frank351
Neither of those will limit it and I think I already put max values on the elements. Step will only change the incremental value, not how many times the event is fired by click-and-drag sliding.
Also, if you’re gonna use that trick, it’d be better not to use the ID’s. Instead add a class to the slider, like “smart-render” and scan for that in the event action (or apply different event functions and entirely remove the conditional logic from the event). The thing is I only did some slight refactoring to your code. Play with it and make it yours.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.