JavaScript: Get number of edited/updated inputs

Instead of dividing it by 3 all the time, you can calculate this number dynamically based on number of input fields updated by the student in a row.

Here is the working code:

function getValueAndTotal(element){
  var valueChanged = (element.defaultValue === element.value || element.value === "") ? 0 : 1;  
  return { value: Number(element.value), total: valueChanged };
}

document.getElementById('calcBtn').addEventListener('click', function() {
  var scienceTest1 = getValueAndTotal(document.getElementById('scienceTest1'));
  var scienceTest2 = getValueAndTotal(document.getElementById('scienceTest2'));
  var scienceTest3 = getValueAndTotal(document.getElementById('scienceTest3'));

  var physicsTest1 = getValueAndTotal(document.getElementById('physicsTest1'));
  var physicsTest2 = getValueAndTotal(document.getElementById('physicsTest2'));
  var physicsTest3 = getValueAndTotal(document.getElementById('physicsTest3'));

  var historyTest1 = getValueAndTotal(document.getElementById('historyTest1'));
  var historyTest2 = getValueAndTotal(document.getElementById('historyTest2'));
  var historyTest3 = getValueAndTotal(document.getElementById('historyTest3'));

  var scienceAverage = document.getElementById('scienceAverage');
  var physicsAverage = document.getElementById('physicsAverage');
  var historyAverage = document.getElementById('historyAverage');

  var finalGrade = document.getElementById('finalGrade');
  var scienceTotalTests = scienceTest1.total + scienceTest2.total + scienceTest3.total;
  var physicsTotalTests = physicsTest1.total + physicsTest2.total + physicsTest3.total;
  var historyTotalTests = historyTest1.total + historyTest2.total + historyTest3.total;

  scienceAverage.value = (scienceTotalTests === 0 ? 0 : (scienceTest1.value + scienceTest2.value + scienceTest3.value) / scienceTotalTests);
  physicsAverage.value = (physicsTotalTests === 0 ? 0 : (physicsTest1.value + physicsTest3.value + physicsTest3.value) / physicsTotalTests);
  historyAverage.value = (historyTotalTests === 0 ? 0 : (historyTest1.value + historyTest2.value + historyTest3.value) / historyTotalTests);

  finalGrade.value = (scienceAverage.value * 5 + physicsAverage.value * 3 + historyAverage.value * 2) / 10;
});
<form>
  Science: 
    <input type="number" id="scienceTest1" class="scienceTest">
    <input type="number" id="scienceTest2" class="scienceTest">
    <input type="number" id="scienceTest3" class="scienceTest">
    <output id="scienceAverage"></output>
  <br>Physics: 
    <input type="number" id="physicsTest1">
    <input type="number" id="physicsTest2">
    <input type="number" id="physicsTest3">
    <output id="physicsAverage"></output>
  <br>History: 
    <input type="number" id="historyTest1">
    <input type="number" id="historyTest2">
    <input type="number" id="historyTest3">
    <output id="historyAverage"></output>
  <br>
    <input type="button" value="Calculate" id="calcBtn">
    <output id="finalGrade"></output>
</form>

It looks like you need to check the values of inputs are valid numbers before using them in the arithmetic that calculates the per-course averages. One way to do this would be via the following check:

if (!Number.isNaN(Number.parseFloat(input.value))) {
  /* Use input.value in average calculation */
}

You might also consider adjusting your script and HTML as shown below, which would allow you to generalize and re-use the average calculation for each of the three classes as detailed below:

document.getElementById('calcBtn').addEventListener('click', function() {

  /* Generalise the calculation of updates for specified course type */
  const calculateForCourse = (cls) => {

    let total = 0
    let count = 0

    /* Select inputs with supplied cls selector and iterate each element */
    for (const input of document.querySelectorAll(`input.${cls}`)) {

      if (!Number.isNaN(Number.parseFloat(input.value))) {
      
        /* If input value is non-empty, increment total and count for
        subsequent average calculation */
        total += Number.parseFloat(input.value);
        count += 1;
      }
    }

    /* Cacluate average and return result */
    return { count, average : count > 0 ? (total / count) : 0 }
  }

  /* Calculate averages using shared function for each class type */
  const calcsScience = calculateForCourse('science')
  const calcsPhysics = calculateForCourse('physics')
  const calcsHistory = calculateForCourse('history')
  
  /* Update course averages */
  document.querySelector('output.science').value = calcsScience.average
  document.querySelector('output.physics').value = calcsPhysics.average
  document.querySelector('output.history').value = calcsHistory.average
  
  /* Update course counts */
  document.querySelector('span.science').innerText = `changed:${calcsScience.count}`
  document.querySelector('span.physics').innerText = `changed:${calcsPhysics.count}`
  document.querySelector('span.history').innerText = `changed:${calcsHistory.count}`

  /* Update final grade */
  var finalGrade = document.getElementById('finalGrade');

  finalGrade.value = (calcsScience.average * 5 + calcsPhysics.average * 3 + calcsHistory.average * 2) / 10;
});
<!-- Add class to each of the course types to allow script to distinguish
     between related input and output fields -->
<form>
  Science:
  <input type="number" class="science" id="scienceTest1">
  <input type="number" class="science" id="scienceTest2">
  <input type="number" class="science" id="scienceTest3">
  <output id="scienceAverage" class="science"></output>
  <span class="science"></span>
  <br> Physics:
  <input type="number" class="physics" id="physicsTest1">
  <input type="number" class="physics" id="physicsTest2">
  <input type="number" class="physics" id="physicsTest3">
  <output id="physicsAverage" class="physics"></output>
  <span class="physics"></span>
  <br> History:
  <input type="number" class="history" id="historyTest1">
  <input type="number" class="history" id="historyTest2">
  <input type="number" class="history" id="historyTest3">
  <output id="historyAverage" class="history"></output>
  <span class="history"></span>
  <br>
  <input type="button" value="Calculate" id="calcBtn">
  <output id="finalGrade"></output>
</form>

Update

To extend on the first answer, please see the documentation in the snippet below responding to your question's update:

document.getElementById('calcBtn').addEventListener('click', function() {
  var test1 = document.getElementById('test1').value;
  var test2 = document.getElementById('test2').value;
  var test3 = document.getElementById('test3').value;
  var average = document.getElementById('average');
  
  /* This variable counts the number of inputs that have changed */
  var changesDetected = 0;
  
  /* If value of test1 field "not equals" the empty string, then 
  we consider this a "changed" field, so we'll increment our 
  counter variable accordinly */
  if(test1 != '') {
    changesDetected = changesDetected + 1;
  }
  /* Apply the same increment as above for test2 field */
  if(test2 != '') {
    changesDetected = changesDetected + 1;
  }
  /* Apply the same increment as above for test3 field */
  if(test3 != '') {
    changesDetected = changesDetected + 1;
  }
  
  /* Calculate average from changesDetected counter.
  We need to account for the case where no changes
  have been detected to prevent a "divide by zero" */
  if(changesDetected != 0) {
    average.value = (Number(test1) + Number(test2) + Number(test3)) / changesDetected;
  }
  else {
    average.value = 'Cannot calculate average'
  }
  
  /* Show a dialog to box to display the number of fields changed */
  alert("Detected that " + changesDetected + " inputs have been changed")
});
<form>
  <input type="number" id="test1">
  <input type="number" id="test2">
  <input type="number" id="test3">
  <output id="average"></output>
  <br>
  <input type="button" value="Calculate" id="calcBtn">
</form>

Update 2

The prior Update can be simplified with a loop like so:

document.getElementById('calcBtn').addEventListener('click', function() {
  
  let changesDetected = 0;
  let total = 0;
  const ids = ['test1', 'test2', 'test3'];
  
  for(const id of ids) {
    const value = document.getElementById(id).value;
    if(value != '') {
      changesDetected += 1;
      total += Number(value);
    }
  }
  
  var average = document.getElementById('average');
  
  if(changesDetected != 0) {
    average.value = total / changesDetected;
  }
  else {
    average.value = 'Cannot calculate average'
  }
    
  alert("Detected that " + changesDetected + " inputs have been changed")
});
<form>
  <input type="number" id="test1">
  <input type="number" id="test2">
  <input type="number" id="test3">
  <output id="average"></output>
  <br>
  <input type="button" value="Calculate" id="calcBtn">
</form>

Update 3

Another concise approach based on your JSFiddle would be the following:

document.getElementById('calculator').addEventListener('click', function() {
  var physicsAverage = document.getElementById('physicsAverage'),
    historyAverage = document.getElementById('historyAverage');

  physicsAverage.value = calculateAverageById('physics')
  historyAverage.value = calculateAverageById('history');
});

function calculateAverageById(id) {
  /* Get all input descendants of element with id */
  const inputs = document.querySelectorAll(`#${id} input`);

  /* Get all valid grade values from selected input elements */
  const grades = Array.from(inputs)
    .map(input => Number.parseFloat(input.value))
    .filter(value => !Number.isNaN(value));

  /* Return average of all grades, or fallback message if no valid grades present */
  return grades.length ? (grades.reduce((sum, grade) => (sum + grade), 0) / grades.length) : 'No assessment made!'
}
<form>
  <p id="physics">
    Physics:
    <input type="number">
    <input type="number">
    <input type="number">
    <output id="physicsAverage"></output>
  </p>
  <p id="history">
    History:
    <input type="number">
    <input type="number">
    <input type="number">
    <output id="historyAverage"></output>
  </p>
  <button type="button" id="calculator">Calculate</button>
</form>

The main differences here are:

  • the use of document.querySelectorAll(#${id} input); with a template literal to extract the input elements of a element with id
  • the use of Array.from(inputs) for a more readable means of converting the result of the query to an array
  • the use of Number.parseFloat and Number.isNaN when transforming and filtering input elements to valid numeric values for the subsequent average calculation

Hope that helps!


A good start is to change your ID to Class to put your inputs into logical groups. The next step is to get the inputs from a particular group that has a value that is not null. We can do this by selecting for example .scienceTest and then filtering out empty string items.

I added a helper function values to extract the values from a nodelist and put them into a normal Array.

We can use a Boolean to test the empty strings. We also cast all strings to numbers using Number. This is done in the onlyNumbers function.

Next, we need to calculate the averages of each group. This is easy since we have a filtered list of numbers. All we do is calculate the sum and divide by the Array length. This is done with our little avrg function.

 

document.getElementById('calcBtn').addEventListener('click', function() {
  var scienceTest = getGrades('.scienceTest')
  var physicsTest = getGrades('.physicsTest')
  var historyTest = getGrades('.historyTest')
  
  var scienceAverage = document.getElementById('scienceAverage');
  var physicsAverage = document.getElementById('physicsAverage');
  var historyAverage = document.getElementById('historyAverage');
  
  var finalGrade = document.getElementById('finalGrade');
  
  scienceAverage.value = avrg(scienceTest)
  physicsAverage.value = avrg(physicsTest)
  historyAverage.value = avrg(historyTest)
  
  finalGrade.value = (scienceAverage.value * 5 + physicsAverage.value * 3 + historyAverage.value * 2) / 10;
  
});

function avrg(list) {
	return list.length ? list.reduce((acc, i) => acc + i, 0) / list.length : 0
}

function getGrades(selector) {
	return onlyNumbers(values(document.querySelectorAll(selector)))
}
function onlyNumbers(list) {
		return list.filter(Boolean).map(Number)
}

function values(nodelist) {
		return Array.prototype.map.call(nodelist, (node) => node.value)
}
<form>
  Science: <input type="number" class="scienceTest">
  <input type="number" class="scienceTest">
  <input type="number" class="scienceTest">
  <output id="scienceAverage"></output>
  <br> Physics: <input type="number" class="physicsTest">
  <input type="number" class="physicsTest">
  <input type="number" class="physicsTest">
  <output id="physicsAverage"></output>
  <br> History: <input type="number" class="historyTest">
  <input type="number" class="historyTest">
  <input type="number" class="historyTest">
  <output id="historyAverage"></output>
  <br>
  <input type="button" value="Calculate" id="calcBtn">
  <output id="finalGrade"></output>
</form>

Update: Simplified example

document.getElementById('calcBtn').addEventListener('click', function() {
  var test1 = document.getElementById('test1').value;
  var test2 = document.getElementById('test2').value;
  var test3 = document.getElementById('test3').value;
  var average = document.getElementById('average');
  // Put all field values in array, Filter empty values out, cast values to Number
  var rowValues = [test1, test2, test3].filter(Boolean).map(Number)

  console.log('Number of changed fields', rowValues.length)

  // calculate average by reducing the array to the sum of its remaining values then divide by array length
  average.value = rowValues.reduce((sum, grade) => sum + grade, 0) / rowValues.length;
});
<form>
  <input type="number" id="test1">
  <input type="number" id="test2">
  <input type="number" id="test3">
  <output id="average"></output>
  <br>
  <input type="button" value="Calculate" id="calcBtn">
</form>

Update Extra: Based on OP's jsfiddle example in the comments

document.getElementById('calculator').addEventListener('click', function() {
  var physicsAverage = document.getElementById('physicsAverage'),
    historyAverage = document.getElementById('historyAverage');

  physicsAverage.value = calculateAverageById('physics')
  historyAverage.value = calculateAverageById('history');
});

function calculateAverageById(id) {
	// Get all inputs under Id
  var inputs = document.getElementById(id).getElementsByTagName('input')

  var values =
    Array.prototype.slice.call(inputs) // From HTMLCollection to Array
    .map(e => e.value.trim()) // Return all .value from input elements
    .filter(Boolean) // Filter out any empty strings ""
    .map(Number) // convert remaining values to Numbers
  return (values.length) ? // if length is greater then 0
    values.reduce((sum, grade) => sum + grade, 0) / values.length // Return average
    :
    'No assessment made!' // else return this message
}
    <form>
  <p id="physics">
    Physics:
    <input type="number">
    <input type="number">
    <input type="number">
    <output id="physicsAverage"></output>
  </p>
  <p id="history">
    History:
    <input type="number">
    <input type="number">
    <input type="number">
    <output id="historyAverage"></output>
  </p>
  <button type="button" id="calculator">Calculate</button>
</form>