Scenario
Every semester my students need to take at least one science, one physics and one history test. The following form gives the right average grades as well as the final grade of a student:
document.getElementById('calcBtn').addEventListener('click', function() {
var scienceTest1 = document.getElementById('scienceTest1').value;
var scienceTest2 = document.getElementById('scienceTest2').value;
var scienceTest3 = document.getElementById('scienceTest3').value;
var physicsTest1 = document.getElementById('physicsTest1').value;
var physicsTest2 = document.getElementById('physicsTest2').value;
var physicsTest3 = document.getElementById('physicsTest3').value;
var historyTest1 = document.getElementById('historyTest1').value;
var historyTest2 = document.getElementById('historyTest2').value;
var historyTest3 = document.getElementById('historyTest3').value;
var scienceAverage = document.getElementById('scienceAverage');
var physicsAverage = document.getElementById('physicsAverage');
var historyAverage = document.getElementById('historyAverage');
var finalGrade = document.getElementById('finalGrade');
scienceAverage.value = (Number(scienceTest1) + Number(scienceTest2) + Number(scienceTest3)) / 3;
physicsAverage.value = (Number(physicsTest1) + Number(physicsTest2) + Number(physicsTest3)) / 3;
historyAverage.value = (Number(historyTest1) + Number(historyTest2) + Number(historyTest3)) / 3;
finalGrade.value = (scienceAverage.value * 5 + physicsAverage.value * 3 + historyAverage.value * 2) / 10;
});
<form>
Science: <input type="number" id="scienceTest1">
<input type="number" id="scienceTest2">
<input type="number" id="scienceTest3">
<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>
The problem is it only works if all the fields are edited. If the student doesn't take some tests, the average grades won't show the correct values. I know it's because of dividing by the fixed number 3 when it calculates the average grades:
scienceAverage.value = (Number(scienceTest1) + Number(scienceTest2) + Number(scienceTest3)) / 3;
physicsAverage.value = (Number(physicsTest1) + Number(physicsTest2) + Number(physicsTest3)) / 3;
historyAverage.value = (Number(historyTest1) + Number(historyTest2) + Number(historyTest3)) / 3;
Question
What is a simple approach to get the number of changed input fields in the following single row? I'll try to understand your method and then develop my form to multiple rows.
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');
average.value = (Number(test1) + Number(test2) + Number(test3)) / 3;
});
<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>
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>
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's a bit ugly but you can consider test scores as booleans: if there's any test score that's worth a 1, otherwise a 0.
Since input.value is of type string, converting it to a boolean would give as a result false when the input is empty ("") or true when there's any number in it.
Using OP's smaller snippet:
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 testCount = Boolean(test1) + Boolean(test2) + Boolean(test3);
// alternatively: var testCount = !!test1 + !!test2 + !!test3
var average = document.getElementById('average');
average.value = (Number(test1) + Number(test2) + Number(test3)) / testCount;
});
<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>
There are two major choke points in your code that you need to be aware of:
You are calculating the average of each subject regardless of their value. Technically, you only want to take into account the subject if it has a defined value. In this case, 0 will be counted, but an empty field will not (since a student can technically score a 0 on his/her test)
You are calculating the weighted average regardless of their value, too (see same logic as above).
Instead of trying to fix the code you have, I have actually refactored the logic so that all the calculations are abstracted into functions, based on the DRY (do not repeat yourself) principle. The functions are:
calculateSubjectAverage, which calculates the correct average of a given subject. It will take 0 into account, but ignore empty fields
setSubjectAverage, which will set the appropriate <output> element
Finally, instead of have to manually calculate the weighted average, you can easily store all that metadata in an array of objects, e.g.:
var subjects = [{
name: 'science',
weight: 5
}, {
name: 'physics',
weight: 3
}, {
name: 'history',
weight: 2
}];
This allows us to filter the subjects and calculate their correct weighted sum, and hence weighted average. Filtering is needed because there is a chance that an all-empty subject score will return undefined.
See proof-of-concept below:
function calculateSubjectAverage(className) {
var inputs = document.querySelectorAll('.' + className);
var scores = Array.prototype.map.call(inputs, function(input) {
if (input.value === '')
return;
return +input.value;
});
var count = 0;
var scoreSum = scores.reduce(function(acc, score) {
if (isNaN(score))
return acc;
count++;
return acc + score;
}, 0);
return scoreSum / count;
};
function setSubjectAverage(className, averageScore) {
if (isNaN(averageScore))
return;
document.getElementById(className + 'Average').value = averageScore;
}
document.getElementById('calcBtn').addEventListener('click', function() {
var subjects = [{
name: 'science',
weight: 5
}, {
name: 'physics',
weight: 3
}, {
name: 'history',
weight: 2
}];
var totalWeight = 0;
// Go through each subject and calculate & set average score
// Since we are iterating anyway, might want to calculate totalWeight, too
subjects.forEach(function(subject) {
var averageScore = calculateSubjectAverage(subject.name);
setSubjectAverage(subject.name, averageScore);
// Set average score to object
subject.average = averageScore;
if (!isNaN(averageScore))
totalWeight += subject.weight;
});
// Only compute weighted average from subject with valid averages
var weightedTotal = subjects.reduce(function(acc, subject) {
if (isNaN(subject.average))
return acc;
return acc + subject.average * subject.weight;
}, 0);
var weightedAverage = weightedTotal / totalWeight;
if (!isNaN(weightedTotal / totalWeight))
document.getElementById('finalGrade').value = weightedTotal / totalWeight;
});
<form>
Science: <input type="number" class="science">
<input type="number" class="science">
<input type="number" class="science">
<output id="scienceAverage"></output>
<br> Physics: <input type="number" class="physics">
<input type="number" class="physics">
<input type="number" class="physics">
<output id="physicsAverage"></output>
<br> History: <input type="number" class="history">
<input type="number" class="history">
<input type="number" class="history">
<output id="historyAverage"></output>
<br>
<input type="button" value="Calculate" id="calcBtn">
<output id="finalGrade"></output>
</form>
var tests = [
document.getElementById('test1').value || false,
document.getElementById('test2').value || false,
document.getElementById('test3').value || false
];
var average = 0,
length = 0;
for (var i = 0; i < tests.length; i++) {
if (tests[i] !== false) {
average += Number( tests[i] );
length ++;
}
}
average = average / length;
This is ES5 solution. You can do shorter but this in my opinion is intuitive.
You are dividing the value by 3 that's why its giving less result than expected.
Make the `html of the of your code dynamically.
Don't make too much variables(scienceTest1,scienceTest2.....) instead use loops are store the values in array
Writing like this Number(scienceTest1) + Number(scienceTest2) + Number(scienceTest3)) / 3 is bad because you can have more test and there are more chances of type error. Instead store the values in the array and at end use Array.prototype.reduce() to add them.
For the values array you need to check if the value !=='' before pushing it into array so it will get correct average.
The code is completely dynamic you can have any subjects and any
no of tests
//This is list of subjects. You can change it will work same
let subjects = ['science','physics','history'];
let noOfTests = 3;
//add <form> element to body
document.body.innerHTML = '<form></form>'
//getting that form as an element.
let form = document.querySelector('form')
//Creating the HTML dymamically
subjects.forEach(sub =>{
//setting the title of the subject
form.innerHTML += sub + ':' + '<br>';
for(let i = 0;i<noOfTests;i++){
//generating input feilds equal of 'noOfTests' for each subject
form.innerHTML += `<input type="number" id="${sub}Test${i+1}" /><br>`
}
//adding the output element to after addign all inputs.
form.innerHTML += `<output id="${sub}Average"></output><br>`
})
//Adding calculate button and finalOuput element.
form.innerHTML += `<br><input type="button" value="Calculate" id="calcBtn">
<output id="finalGrade"></output>`
document.getElementById('calcBtn').addEventListener('click', function() {
//'total' is array which will contain average of each subject
let total = [];
//looping thorugh each subject in 'subjects' array.
subjects.forEach(sub => {
//'vals' will store the values currect subject we are looping
let vals = []
for(let i = 0;i<noOfTests;i++){
//getting the value of each input feild of current subject
let val = document.getElementById(`${sub}Test${i+1}`).value;
//check if input have a value so we push it into the vals array.
if(val !== '') vals.push(val);
}
//getting average of all values using reduce
let result = vals.reduce((ac,a) => ac + Number(a),0)/vals.length;
//adding result(average) to the output of current subject.
document.getElementById(`${sub}Average`).innerHTML = result
//adding the average of current subject of the 'total' array.
total.push(result);
})
//At last find the average of total averages and add it to 'finalGrade'
total = total.filter(x => !isNaN(x));
document.getElementById('finalGrade').innerHTML = total.reduce((ac,a) => ac + a,0)/total.length;
});
input{
border-radius:5px;
padding:3px;
margin:5px;
font-size:20px;
}
form{
font-size:20px;
font-family:sans-serif;
text-transform:capitalize;
}
Introduce a counter to the function. After every click check if the input values is '' or not as it will be string before casting. If it is do nothing as the ternary operation will return false
test1!=''?num++:false;
If test1=='' then false else increment the counter. Before calculating the average check if the counter is 0, if it is set the counter to one. If it is zero the result of division by zero will be infinity and we will get NaN as the output, and counter set to 1 will give 0.
document.getElementById('calcBtn').addEventListener('click', function() {
let num=0;
var test1 = document.getElementById('test1').value;
test1!=''?num++:false;
var test2 = document.getElementById('test2').value;
test2!=''?num++:false;
var test3 = document.getElementById('test3').value;
test3!=''?num++:false;
var average = document.getElementById('average');
num==0?num++:false;
average.value = (Number(test1) + Number(test2) + Number(test3)) / num;
});
<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>
There are some solutions already. Here is mine.
There is something to optimize in your code and I think it is a good idea to create the HTML dynamically based on a simple configuration array like
const subjects = [{
name: 'science',
numberOfTests: 3
}, {
name: 'physics',
numberOfTests: 2
}, {
name: 'history',
numberOfTests: 3
}];
so if the subjects or the number of tests change, you don't need to change the code but only this configuration.
I tried to comment my code example to make understandable, what is done there. But that is not the important part. More important is
that you need to understand what's going on in the calculation part to be sure, the calculation is right. So I start with that part.
If you add name attributes to your test result inputs and give each input of the same subject the same name, you can easily retrieve a NodeList of that inputs, check the values for each Node and calculate based on it. So you know the values and the number of tests the student took for each subject.
Let's have a look
/* EventListener for the calculate button */
btn.addEventListener('click', function(e) {
e.preventDefault(); // don't submit the form
var totalAvSum = 0; // var for the total of all subject average totals
/* for all subjects in your configuration array */
subjects.forEach(function(subject) {
/* NodeList of all inputs with Name subject.name+'Test' */
let subjResInputs = document.getElementsByName(subject.name+'Test');
let testTotal = 0; // sum of test results
let testCnt = 0; // number of tests the student took
let tval; // value of input
/* for each input of the subject */
Array.prototype.map.call(subjResInputs, function(t){
tval = (t.value * 1); // make sure, value is treated as number
if (tval > 0) { // only if there is a value
testTotal += tval; // add test result
testCnt += 1; // increase test count
}
});
/* calculate average and show it in output */
totalAvSum += (testTotal/testCnt);
document.getElementById(subject.name+'Average').textContent = (testTotal/testCnt);
});
/* after calculating average per subject show total average */
document.getElementById('totalAverage').textContent = totalAvSum/subjects.length;
});
I use document.getElementsByName() here to get the NodeList of input elements for each subject. Then I use array.map() to walk through that list. To check, if an input has a value, I multiply the value by 1 (what gives a number) and calculate only, if the result is greater than 0.
The rest is the dynamical stuff for the HTML.
/* This is your configuration.
The form will be created based on that configuration.
So you don't need to change anything in the code if
subjects or number of tests change. */
const subjects = [{
name: 'science',
numberOfTests: 3
}, {
name: 'physics',
numberOfTests: 2
}, {
name: 'history',
numberOfTests: 3
}];
/* this functioncreates the form table */
function createFormTable() {
var tr, td, txt, outp, btn, frmTbl;
// frmTbl = document.getElementById('formTable');
/* create table */
frmTbl = document.createElement('table');
frmTbl.setAttribute('id', 'formTable'); // set id to 'formTable'
/* create table head */
tr = document.createElement('tr');
td = document.createElement('th');
txt = document.createTextNode('subject');
td.appendChild(txt);
tr.appendChild(td);
td = document.createElement('th');
txt = document.createTextNode('test results');
td.appendChild(txt);
tr.appendChild(td);
td = document.createElement('th');
txt = document.createTextNode('arith. mean');
td.appendChild(txt);
tr.appendChild(td);
/* add table head to table */
frmTbl.appendChild(tr);
/* create table row for each subject
the table row object is created in function createSubjectRow
and here added to the table */
subjects.forEach(function(subject) {
frmTbl.appendChild(createSubjectRow(subject)); // add tr to table
});
/* row with total average */
/* create tr element */
tr = document.createElement('tr');
td = document.createElement('th'); // td for text total
td.setAttribute('colspan', 2);
td.style.textAlign = 'right';
txt = document.createTextNode('total'); // textNode
td.appendChild(txt); // add textNode to td
tr.appendChild(td); // add td to tr
frmTbl.appendChild(tr); // add tr to table
td = document.createElement('td'); // td for total average output
outp = document.createElement('output'); // create output element
outp.setAttribute('id', 'totalAverage'); // set id
td.appendChild(outp); // add output to td
tr.appendChild(td); // add td to tr
frmTbl.appendChild(tr); // add tr to table
/* button */
btn = document.createElement('button');
btn.setAttribute('id', 'calcBtn');
txt = document.createTextNode('calculate');
btn.appendChild(txt);
// document.getElementById('gradesForm').appendChild(btn);
/* add button to last row in table */
tr = document.createElement('tr');
td = document.createElement('th'); // td for button
td.setAttribute('colspan', 3);
td.appendChild(btn); // add button to td
tr.appendChild(td); // add td to tr
frmTbl.appendChild(tr); // add tr to table
/* EventListener for the calculate button */
btn.addEventListener('click', function(e) {
e.preventDefault(); // don't submit the form
var totalAvSum = 0; // var for the total of all subject average totals
/* for all subjects in your configuration array */
subjects.forEach(function(subject) {
/* NodeList of all inputs with Name subject.name+'Test' */
let subjResInputs = document.getElementsByName(subject.name+'Test');
let testTotal = 0;
let testCnt = 0;
let tval;
/* for each input of the subject */
Array.prototype.map.call(subjResInputs, function(t){
tval = (t.value * 1); // make sure, value is treated as number
if (tval > 0) { // only if there is a value
testTotal += tval; // add test result
testCnt += 1; // increase test count
}
});
/* calculate average and show it in output */
totalAvSum += (testTotal/testCnt);
document.getElementById(subject.name+'Average').textContent = (testTotal/testCnt);
});
/* after calculating average per subject
show total average */
document.getElementById('totalAverage').textContent = totalAvSum/subjects.length;
});
return frmTbl;
}
function createSubjectRow(s) {
var tr, td, txt, inp, outp;
/* create tr element */
tr = document.createElement('tr');
/* create td elements for subject s */
td = document.createElement('td'); // td for subject name
txt = document.createTextNode(s.name); // textNode
td.appendChild(txt); // add textNode to td
tr.appendChild(td); // add td to tr
td = document.createElement('td'); // td for subject test results
for (var i = 0; i < s.numberOfTests; i += 1) {
inp = document.createElement('input'); // create input
inp.setAttribute('type', 'number'); // set input type
// inp.setAttribute('id', s.name + 'Test' + i); // set id
/* set name attribute of input to subject name + 'Test'
all test result inputs for the same subject will have the same name */
inp.setAttribute('name', s.name + 'Test');
inp.setAttribute('step', 0.1); // in case, you give grades like 3.5
inp.setAttribute('min', 1);
inp.setAttribute('max', 100);
td.appendChild(inp); // add input to td
}
tr.appendChild(td); // add td to tr
td = document.createElement('td'); // td for average output
outp = document.createElement('output'); // create output element
outp.setAttribute('id', s.name + 'Average'); // set id
td.appendChild(outp); // add output to td
tr.appendChild(td); // add td to tr
return tr; // return the resulting table row object
}
document.getElementById('gradesForm').appendChild(createFormTable());
#formTable td {
border: solid 1px #000;
padding: 6px;
border-spacing: 3px;
}
#formTable th {
border: none;
font-size:0.9em;
text-align: left;
}
input[type="number"] {
width: 4em;
border: solid 1px #999;
margin: 0 3px;
}
<form id="gradesForm">
</form>
Method
The issue you are having of dividing by the fixed number 3 can be solved by using the Array length property and make this value dynamic.
Step 1: Place all subjects into arrays and filter if the inputs have values.
Step 2: Get the average value by subject based.
Step 3: Calculate the final Score with weights.
There are two functions that are used multiple times.
isTruthy and average.
To slim down the average function I broke it up into sum and average
document.getElementById('calcBtn').addEventListener('click', function() {
// Helper Functions
function isTruthy (score) {
return !!score
}
function sum (scores) {
var total = 0;
for (var counter=0; counter<scores.length; counter++) {
total += (Number(scores[counter]) || 0);
}
return total
}
function average (scores) {
return (sum(scores) / scores.length) || 0
}
// Step 1
var scienceScores = [
document.getElementById('scienceTest1').value,
document.getElementById('scienceTest2').value,
document.getElementById('scienceTest3').value
].filter(isTruthy)
var physicsScores = [
document.getElementById('physicsTest1').value,
document.getElementById('physicsTest2').value,
document.getElementById('physicsTest3').value
].filter(isTruthy)
var historyScores = [
document.getElementById('historyTest1').value,
document.getElementById('historyTest2').value,
document.getElementById('historyTest3').value
].filter(isTruthy)
var scienceAverage = document.getElementById('scienceAverage');
var physicsAverage = document.getElementById('physicsAverage');
var historyAverage = document.getElementById('historyAverage');
var finalGrade = document.getElementById('finalGrade');
// Step 2
scienceAverage.value = average(scienceScores);
physicsAverage.value = average(physicsScores);
historyAverage.value = average(historyScores);
// Step 3
finalGrade.value = (scienceAverage.value * 5 + physicsAverage.value * 3 + historyAverage.value * 2) / 10;
});
<form>
Science: <input type="number" id="scienceTest1">
<input type="number" id="scienceTest2">
<input type="number" id="scienceTest3">
<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>
You can create an object called tests = {} and keep your tests as an array inside it, to give an example I have written some code for only one subject below which you can modify later.
Also, I have used classes instead of id's to reduce the complexity of code and repetitiveness.
Update: Updated the below code to be more dynamic and flexible to multiple subjects.
document.getElementById('calcBtn').addEventListener('click', function() {
// create a tests object and have subject specific test inside it
var tests = {
"Science": [],
"Physics": [],
"History": []
};
//looping over all subjects
for (var key in tests) {
// this can be looped as well if you have multiple subjects
var test = document.getElementsByClassName(key);
for (i = 0; i < test.length; i++) {
if (test[i].value != null || test[i].value != "") {
tests[key].push(Number(test[i].value));
}
}
// populate average by using reduce function
document.getElementById(key).value = tests[key].reduce((prev, curr) => prev + curr) / tests[key].length;
}
});
<form>
<fieldset>
<legend>Science</legend>
<input type="number" class="Science">
<input type="number" class="Science">
<input type="number" class="Science">
<output id="Science"></output>
</fieldset>
<fieldset>
<legend>Physics</legend>
<input type="number" class="Physics">
<input type="number" class="Physics">
<input type="number" class="Physics">
<output id="Physics"></output>
</fieldset>
<fieldset>
<legend>History</legend>
<input type="number" class="History">
<input type="number" class="History">
<input type="number" class="History">
<output id="History"></output>
</fieldset>
<br>
<input type="button" value="Calculate" id="calcBtn">
</form>
document.getElementById('calcBtn').addEventListener('click', function() {
var testcount = [];
var count = 0;
testcount = Array.prototype.slice.call(document.getElementsByClassName('test1'))
for(var i=0;i<testcount.length;i++)
{
if(Number(testcount[i].value) > 0)
{
count=count+1;
}
}
var test1 = document.getElementById('test1').value;
var test2 = document.getElementById('test2').value;
var test3 = document.getElementById('test3').value;
var average = document.getElementById('average');
average.value = (Number(test1) + Number(test2) + Number(test3)) / count;
});
<form>
<input type="number" class="test1" id="test1">
<input type="number" class="test1" id="test2">
<input type="number" class="test1" id="test3">
<output id="average"></output>
<br>
<input type="button" value="Calculate" id="calcBtn">
</form>
in the above, output is achieved by adding a class attribute in the input tag(give same name for same group of controls).
Second thing is looping through this class object for getting count of non-empty or non-zero valued.
My recommendation is, to avoid multiple input fields in order to read in multiple inputs. In my case, I am using semicolons to separate the individual values inside the input field. By doing so I am able to enter as many values as I want (at least one value). Therefore my form looks like the following:
<!-- form.html -->
<form>
Science: <input type="text" id="scienceTest">
<output id="scienceAverage"></output>
<br> Physics: <input type="text" id="physicsTest">
<output id="physicsAverage"></output>
<br> History: <input type="text" id="historyTest">
<output id="historyAverage"></output>
<br>
<input type="button" value="Calculate" id="calcBtn">
<output id="finalGrade"></output>
</form>
<script src="script.js"></script>
And my JavaScript looks like this:
// script.js
(function() {
var scienceTest = document.getElementById('scienceTest');
var physicsTest = document.getElementById('physicsTest');
var historyTest = document.getElementById('historyTest');
var scienceAverage = document.getElementById('scienceAverage');
var physicsAverage = document.getElementById('physicsAverage');
var historyAverage = document.getElementById('historyAverage');
var finalGrade = document.getElementById('finalGrade');
function sumArray(sum, item) {
return sum + item;
}
document.getElementById('calcBtn').addEventListener('click', function() {
// fetch the string of the input and split into its separate numbers
var scienceGradeStrings = scienceTest.value.split(";");
var physicsGradeStrings = physicsTest.value.split(";");
var historyGradeStrings = historyTest.value.split(";");
// calculate the averages
scienceAverage.value = scienceGradeStrings
// convert the grades from strings to numbers
.map(Number)
// sum all grades together
.reduce(sumArray, 0)
// calculate the average grade
/ scienceGradeStrings.length;
physicsAverage.value = physicsGradeStrings.map(Number).reduce(sumArray, 0) / physicsGradeStrings.length;
historyAverage.value = historyGradeStrings.map(Number).reduce(sumArray, 0) / historyGradeStrings.length;
finalGrade.value = (scienceAverage.value * 5 + physicsAverage.value * 3 + historyAverage.value * 2) / 10;
});
})();
What is a simple approach to get the number of changed input fields
We can assign each input a custom data attribute equal to the input value and then exclude the empty ones when counting them. The following generic script can be used for any number of courses:
var form = document.querySelector('form');
function calculateAverage(fieldset) {
var total = 0;
var inputs = fieldset.querySelectorAll('input');
for (var input of inputs) {
total += Number(input.value);
input.dataset.value = input.value;
}
return total / fieldset.querySelectorAll('input:not([data-value=""])').length;
}
function displayAverages() {
var fieldsets = form.querySelectorAll('fieldset');
for (var fieldset of fieldsets) {
var avg = calculateAverage(fieldset);
var output = fieldset.querySelector('output');
if (isNaN(avg)) {
output.value = 'Please enter a grade.';
} else {
output.value = 'Average: ' + avg.toFixed(1);
}
}
}
form.querySelector('button').addEventListener('click', displayAverages);
body {
display: flex;
}
fieldset {
margin: 0 0 16px;
}
input {
width: 4em;
}
output {
display: block;
height: 1em;
margin: 8px 0 0 2px;
}
<form>
<fieldset>
<legend>Physics</legend>
<input type="number">
<input type="number">
<input type="number">
<output></output>
</fieldset>
<fieldset>
<legend>History</legend>
<input type="number">
<input type="number">
<input type="number">
<output></output>
</fieldset>
<button type="button">Calculate</button>
</form>