'Get occurrences of students from array of objects
I have this array of students:
var studentsArray = [{
"studentId": "1",
"courses": ["A", "A", "B", "D", "A"],
"classPeriods": ["Chemistry", "Chemistry", "Maths"]
},
{
"studentId": "1",
"courses": ["E", "F", "E"],
"classPeriods": ["Biology", "Chemistry", "Biology"]
},
{
"studentId": "1",
"courses": ["A", "D", "D"],
"classPeriods": ["Computer Science", "Computer Scienc", "Biology", "Biology"]
},
{
"studentId": "1",
"courses": ["G", "K"],
"classPeriods": ["Computer Science", "Chemistry", "Biology", "Chemistry"]
},
{
"studentId": "1",
"courses": ["A", "B", "D"],
"classPeriods": ["Maths", "Maths", "Biology", "Chemistry"]
},
{
"studentId": "2",
"courses": ["A", "C", "F"],
"classPeriods": ["Arts", "Maths", "Arts", "Maths"]
},
{
"studentId": "2",
"courses": ["B", "B", "D"],
"classPeriods": ["Arts", "Arts", "Maths", "Maths"]
},
{
"studentId": "3",
"courses": ["H", "B", "R"],
"classPeriods": ["Biology", "Biology", "Maths", "Maths"]
}
]
Here I need total occurrences of students with their courses and classPeriods merged.
E.g:
A single object of Student 2 should be:
{
"studentId": "2",
"occurrence": 2,
"courses": ["A", "C", "F", "B", "B", "D"],
"classPeriods": ["Arts", "Maths", "Arts", "Chemistry", "Chemistry", "Arts", "Arts", "Maths", "Maths"]
}
What I have tried so far is:
function getOccurrences(arr, key) {
let arr2 = [];
arr.forEach((x) => {
if (arr2.some((val) => {
return val[key] == x[key]
})) {
arr2.forEach((k) => {
if (k[key] === x[key]) {
k["occurrence"]++
}
})
} else {
let a = {}
a[key] = x[key]
a["occurrence"] = 1
arr2.push(a);
}
})
return arr2
}
var studentsArray = [{
"studentId": "1",
"courses": ["A", "A", "B", "D", "A"],
"classPeriods": ["Chemistry", "Chemistry", "Maths"]
},
{
"studentId": "1",
"courses": ["E", "F", "E"],
"classPeriods": ["Biology", "Chemistry", "Biology"]
},
{
"studentId": "1",
"courses": ["A", "D", "D"],
"classPeriods": ["Computer Science", "Computer Scienc", "Biology", "Biology"]
},
{
"studentId": "1",
"courses": ["G", "K"],
"classPeriods": ["Computer Science", "Chemistry", "Biology", "Chemistry"]
},
{
"studentId": "1",
"courses": ["A", "B", "D"],
"classPeriods": ["Maths", "Maths", "Biology", "Chemistry"]
},
{
"studentId": "2",
"courses": ["A", "C", "F"],
"classPeriods": ["Arts", "Maths", "Arts", "Maths"]
},
{
"studentId": "2",
"courses": ["B", "B", "D"],
"classPeriods": ["Arts", "Arts", "Maths", "Maths"]
},
{
"studentId": "3",
"courses": ["H", "B", "R"],
"classPeriods": ["Biology", "Biology", "Maths", "Maths"]
}
]
getOccurrences(studentsArray, "studentId");
Now the issue I am facing here is that it is not returning all the properties of object and also not merging the arrays of course and classPeriods.
Can I get some help with this?
Solution 1:[1]
No need to create a object... you can just append occurrence to x before pushing it.
function getOccurrences(arr, key) {
let arr2 = [];
arr.forEach((x) => {
if (arr2.some((val) => {
return val[key] == x[key]
})) {
arr2.forEach((k) => {
if (k[key] === x[key]) {
k["occurrence"]++
}
})
} else {
// let a = {}
// a[key] = x[key]
// a["occurrence"] = 1
// arr2.push(a);
x["occurrence"] = 1; // <-----
arr2.push(x); // <-----
}
})
return arr2
}
var studentsArray = [{
"studentId": "1",
"courses": ["A", "A", "B", "D", "A"],
"classPeriods": ["Chemistry", "Chemistry", "Maths"]
},
{
"studentId": "1",
"courses": ["E", "F", "E"],
"classPeriods": ["Biology", "Chemistry", "Biology"]
},
{
"studentId": "1",
"courses": ["A", "D", "D"],
"classPeriods": ["Computer Science", "Computer Scienc", "Biology", "Biology"]
},
{
"studentId": "1",
"courses": ["G", "K"],
"classPeriods": ["Computer Science", "Chemistry", "Biology", "Chemistry"]
},
{
"studentId": "1",
"courses": ["A", "B", "D"],
"classPeriods": ["Maths", "Maths", "Biology", "Chemistry"]
},
{
"studentId": "2",
"courses": ["A", "C", "F"],
"classPeriods": ["Arts", "Maths", "Arts", "Maths"]
},
{
"studentId": "2",
"courses": ["B", "B", "D"],
"classPeriods": ["Arts", "Arts", "Maths", "Maths"]
},
{
"studentId": "3",
"courses": ["H", "B", "R"],
"classPeriods": ["Biology", "Biology", "Maths", "Maths"]
}
]
let result = getOccurrences(studentsArray, "studentId");
console.log(result);
Solution 2:[2]
My approach is as below:
// this function is written in Arrow syntax, and declared as a const (constant)
// since I don't expect it to change during the lifetime of the document;
// the function takes two arguments, the array through which it must search
// and the key it's looking for:
const getOccurrences = (arr, key) => {
// our first action is to filter the supplied array in order to discard
// unnecessary information, using Array.prototype.filter(), this iterates
// over each Array-element, and discards any element(s) that don't return
// true/truthy for the supplied assessment:
return arr.filter(
// we use destructuring assignment to retrieve the value of the
// studentId property from the current Object of the Array of
// Objects:
({
studentId
// here we check that the current studentId, parsed as a number
// (in base 10) is exactly equal to the similarly parsed value
// of the key passed into the function:
}) => parseInt(studentId, 10) === parseInt(key, 10)
// we then pass over to Array.prototype.reduce, to reduce the Array to
// to another data-structure, here an Object:
// we have three arguments passed in:
// acc: the accumulator, the Object that we're creating as we iterate,
// the property-values of the named properties 'courses' and 'classPeriods'
// which are retrieved from the current Array-element of the Objects that
// remain in the Array, and then
// index: the index of the current Array-element in the remaining Array:
).reduce((acc, {
courses,
classPeriods
}, index) => {
// if the 'courses' property doesn't exist on the accumulator:
if (!acc.courses) {
// we define that property, and set it's initial value to be
// equal to the courses property-value of the current Object:
acc.courses = courses;
} else {
// otherwise we set the value to a new Array comprised of the
// existing values, with the new values from the current Object:
acc.courses = [...acc.courses, ...courses];
}
// this next step is exactly the same as above:
if (!acc.classPeriods) {
acc.classPeriods = classPeriods;
} else {
acc.classPeriods = [...acc.classPeriods, ...classPeriods];
}
// here we set the occurrences value to the index of the
// current Array-element, and increment by 1 (since at
// index 0, the first Array-element, the occurrence is
// 1, not zero):
acc.occurences = index + 1;
// returning the accumulator Object for the next iteration:
return acc;
// defining the accumulator Object, and setting its studentId
// property to be equal to the key we're looking for:
}, {
studentId: key,
});
};
// your existing Array of Objects:
const studentsArray = [{
"studentId": "1",
"courses": ["A", "A", "B", "D", "A"],
"classPeriods": ["Chemistry", "Chemistry", "Maths"]
},
{
"studentId": "1",
"courses": ["E", "F", "E"],
"classPeriods": ["Biology", "Chemistry", "Biology"]
},
{
"studentId": "1",
"courses": ["A", "D", "D"],
"classPeriods": ["Computer Science", "Computer Scienc", "Biology", "Biology"]
},
{
"studentId": "1",
"courses": ["G", "K"],
"classPeriods": ["Computer Science", "Chemistry", "Biology", "Chemistry"]
},
{
"studentId": "1",
"courses": ["A", "B", "D"],
"classPeriods": ["Maths", "Maths", "Biology", "Chemistry"]
},
{
"studentId": "2",
"courses": ["A", "C", "F"],
"classPeriods": ["Arts", "Maths", "Arts", "Maths"]
},
{
"studentId": "2",
"courses": ["B", "B", "D"],
"classPeriods": ["Arts", "Arts", "Maths", "Maths"]
},
{
"studentId": "3",
"courses": ["H", "B", "R"],
"classPeriods": ["Biology", "Biology", "Maths", "Maths"]
}
],
// a simple function to display the results on screen, this is again an Arrow
// function, and a constant:
displayResults = (results) => {
// we get the class-names we're looking for from the passed-in results Object's
// property-keys, using Object.keys() to retrieve those keys as an Array:
let classes = Object.keys(results);
// iterating over the Array of class-names, using Array.prototype.forEach():
classes.forEach(
// c is the current key, which is the class-name we're looking for, using
// a template-literal and document.querySelector, and then we're updating
// the text-content of that element:
(c) => document.querySelector(`.${c}`).textContent =
// if the current property-value of the results Object is an Array
// (Array.isArray() returns a Boolean, true or false) we join the
// Array-elements together using Array.prototype.join() to spread
// out the elements in the string. Otherwise, we simply return the value:
Array.isArray(results[c]) ? results[c].join(', ') : results[c]
)
}
// retrieving the <select> element via its id of 'studentNumber', and binding the
// anonymous Arrow function as the event-handler for the 'change' event:
document.querySelector('#studentNumber').addEventListener('change', (evt) => {
// the anonymous function calls the displayResults() function, which
displayResults(
// receives the result from the getOccurrences(), function,
// passing in the studentsArray, and the value of the <select> element:
getOccurrences(studentsArray, evt.currentTarget.value)
);
});
*,
::before,
::after {
box-sizing: border-box;
font-family: Montserrat, sans-serif;
margin: 0;
padding: 0;
}
form {
width: clamp(30em, 70vw, 1200px);
margin-block: 1em;
margin-inline: auto;
}
ul,
li {
list-style-type: none;
}
ul {
margin-block-start: 1em;
margin-inline-start: 1em;
}
li {
color: #696;
display: flex;
gap: 0.5em;
justify-content: start;
min-height: 2em;
}
li::before {
color: #363;
font-weight: bold;
content: attr(class)": ";
text-transform: capitalize;
}
<form action="">
<fieldset>
<legend>Student details</legend>
<label>
<span>Details for Student: </span>
<select id="studentNumber">
<option value="-1" disabled selected>Please choose</option>
<option value="1">Student 1</option>
<option value="2">Student 2</option>
<option value="3">Student 3</option>
</select>
</label>
<div class="results">
<ul>
<li class="studentId"></li>
<li class="occurences"></li>
<li class="courses"></li>
<li class="classPeriods"></li>
</ul>
</div>
</fieldset>
</form>
Solution 3:[3]
You can use Array#reduce and Array#map as in the following demo:
NOTE: This combines data for all studentId values; I misunderstood the question. It's now clear to me that you want the function to return the result for one ID. See next demo.
const
studentsArray = [{ "studentId": "1", "courses": ["A", "A", "B", "D", "A"], "classPeriods": ["Chemistry", "Chemistry", "Maths"] }, { "studentId": "1", "courses": ["E", "F", "E"], "classPeriods": ["Biology", "Chemistry", "Biology"] }, { "studentId": "1", "courses": ["A", "D", "D"], "classPeriods": ["Computer Science", "Computer Scienc", "Biology", "Biology"] }, { "studentId": "1", "courses": ["G", "K"], "classPeriods": ["Computer Science", "Chemistry", "Biology", "Chemistry"] }, { "studentId": "1", "courses": ["A", "B", "D"], "classPeriods": ["Maths", "Maths", "Biology", "Chemistry"] }, { "studentId": "2", "courses": ["A", "C", "F"], "classPeriods": ["Arts", "Maths", "Arts", "Maths"] }, { "studentId": "2", "courses": ["B", "B", "D"], "classPeriods": ["Arts", "Arts", "Maths", "Maths"] }, { "studentId": "3", "courses": ["H", "B", "R"], "classPeriods": ["Biology", "Biology", "Matchs", "Maths"] } ],
getOccurrences = (arr, key) => Object.entries(
arr.reduce((acc,cur) =>
acc[cur[key]] ?
({...acc, [cur[key]]: {
occurrence: acc[cur[key]].occurrence + 1,
courses: acc[cur[key]].courses.concat(cur.courses),
classPeriods: acc[cur[key]].classPeriods.concat(cur.classPeriods)
}}) :
({...acc, [cur[key]]: {
occurrence: 1,
courses:cur.courses,
classPeriods:cur.classPeriods
}}), {}
)
)
.map(
([studentId,courseInfo]) =>
({studentId, ...courseInfo})
);
console.log( getOccurrences(studentsArray,"studentId") );
Single ID
The function in this demo returns results for one value of studentId supplied as key as you intended:
const
studentsArray = [{ "studentId": "1", "courses": ["A", "A", "B", "D", "A"], "classPeriods": ["Chemistry", "Chemistry", "Maths"] }, { "studentId": "1", "courses": ["E", "F", "E"], "classPeriods": ["Biology", "Chemistry", "Biology"] }, { "studentId": "1", "courses": ["A", "D", "D"], "classPeriods": ["Computer Science", "Computer Scienc", "Biology", "Biology"] }, { "studentId": "1", "courses": ["G", "K"], "classPeriods": ["Computer Science", "Chemistry", "Biology", "Chemistry"] }, { "studentId": "1", "courses": ["A", "B", "D"], "classPeriods": ["Maths", "Maths", "Biology", "Chemistry"] }, { "studentId": "2", "courses": ["A", "C", "F"], "classPeriods": ["Arts", "Maths", "Arts", "Maths"] }, { "studentId": "2", "courses": ["B", "B", "D"], "classPeriods": ["Arts", "Arts", "Maths", "Maths"] }, { "studentId": "3", "courses": ["H", "B", "R"], "classPeriods": ["Biology", "Biology", "Matchs", "Maths"] } ],
getOccurrences = (arr, key) => {
let d = arr.filter(({studentId}) => studentId === key);
return {
studentId: key,
occurrence: d.length,
courses: d.flatMap(({courses}) => courses),
classPeriods: d.flatMap(({classPeriods}) => classPeriods)
}
};
console.log( getOccurrences(studentsArray,"1") );
Solution 4:[4]
This feels like a textbook example for reduction, but you could use any kind of looping, and preferably a Map to track id-aggregateddata pairs. Then its values() can get the actual result:
var studentsArray = [{
"studentId": "1",
"courses": ["A", "A", "B", "D", "A"],
"classPeriods": ["Chemistry", "Chemistry", "Maths"]
},
{
"studentId": "1",
"courses": ["E", "F", "E"],
"classPeriods": ["Biology", "Chemistry", "Biology"]
},
{
"studentId": "1",
"courses": ["A", "D", "D"],
"classPeriods": ["Computer Science", "Computer Scienc", "Biology", "Biology"]
},
{
"studentId": "1",
"courses": ["G", "K"],
"classPeriods": ["Computer Science", "Chemistry", "Biology", "Chemistry"]
},
{
"studentId": "1",
"courses": ["A", "B", "D"],
"classPeriods": ["Maths", "Maths", "Biology", "Chemistry"]
},
{
"studentId": "2",
"courses": ["A", "C", "F"],
"classPeriods": ["Arts", "Maths", "Arts", "Maths"]
},
{
"studentId": "2",
"courses": ["B", "B", "D"],
"classPeriods": ["Arts", "Arts", "Maths", "Maths"]
},
{
"studentId": "3",
"courses": ["H", "B", "R"],
"classPeriods": ["Biology", "Biology", "Maths", "Maths"]
}
];
let stuff=[...studentsArray.reduce((res,student)=>{
if(!res.has(student.studentId))
res.set(student.studentId,{...student,occurrences:1});
else{
let s=res.get(student.studentId);
s.courses=s.courses.concat(student.courses);
s.classPeriods=s.classPeriods.concat(student.classPeriods);
s.occurrences++;
}
return res;
},new Map()).values()];
console.log(stuff);
With optional/unknown content: for...in can loop over the (enumerable) properties of an object, Array.isArray() can tell if something is an array, and then we can act on an in check and either add it as a new property (if it doesn't exist yet), or concate to an existing one:
var studentsArray = [{
"studentId": "1",
"courses": ["A", "A", "B", "D", "A"],
"classPeriods": ["Chemistry", "Chemistry", "Maths"]
},
{
"studentId": "1",
"courses": ["E", "F", "E"],
"classPeriods": ["Biology", "Chemistry", "Biology"]
},
{
"studentId": "1",
"courses": ["A", "D", "D"],
"classPeriods": ["Computer Science", "Computer Scienc", "Biology", "Biology"]
},
{
"studentId": "1",
"courses": ["G", "K"],
"classPeriods": ["Computer Science", "Chemistry", "Biology", "Chemistry"]
},
{
"studentId": "1",
"courses": ["A", "B", "D"],
"classPeriods": ["Maths", "Maths", "Biology", "Chemistry"]
},
{
"studentId": "2",
"courses": ["A", "C", "F"],
"classPeriods": ["Arts", "Maths", "Arts", "Maths"]
},
{
"studentId": "2",
"courses": ["B", "B", "D"],
"classPeriods": ["Arts", "Arts", "Maths", "Maths"]
},
{
"studentId": "3",
"courses": ["H", "B", "R"],
"classPeriods": ["Biology", "Biology", "Maths", "Maths"]
},
{
"studentId": "4",
"courses": ["H", "B", "R"],
},
{
"studentId": "4",
"classPeriods": ["Biology", "Biology", "Maths", "Maths"]
}
];
let stuff=[...studentsArray.reduce((res,student)=>{
if(!res.has(student.studentId))
res.set(student.studentId,{...student,occurrences:1});
else{
let s=res.get(student.studentId);
for(let prop in student)
if(Array.isArray(student[prop]))
s[prop]=prop in s?s[prop].concat(student[prop]):student[prop];
s.occurrences++;
}
return res;
},new Map()).values()];
console.log(stuff);
See how 4 has the same array content as 3, but the arrays come from 2 different occurrences (and occurrences is 2 of course).
Solution 5:[5]
If you are concerned in performance, this approach would do a good job. Look that instead of using the concat method, which returns a new array, or using the array findIndex method (which involves iteration through the array), I'm using a subIndex array to save already loaded subObjects indexes. Hope it helps!
const studentsArray = [{ "studentId": "1", "courses": ["A", "A", "B", "D", "A"], "classPeriods":["Chemistry","Chemistry","Maths"] }, {"studentId": "1", "courses": ["E", "F", "E"], "classPeriods": ["Biology", "Chemistry", "Biology"] }, {"studentId": "1", "courses": ["A", "D", "D"], "classPeriods": ["Computer Science", "Computer Scienc", "Biology", "Biology"] }, {"studentId": "1", "courses": ["G", "K"], "classPeriods": ["Computer Science", "Chemistry", "Biology", "Chemistry"] }, {"studentId": "1", "courses": ["A", "B", "D"], "classPeriods": ["Maths", "Maths", "Biology", "Chemistry"] }, {"studentId": "2", "courses": ["A", "C", "F"], "classPeriods": ["Arts", "Maths", "Arts", "Maths"] }, {"studentId": "2", "courses": ["B", "B", "D"], "classPeriods": ["Arts", "Arts", "Maths", "Maths"] }, {"studentId": "3", "courses": ["H", "B", "R"], "classPeriods": ["Biology", "Biology", "Maths", "Maths"] } ];
// Array where loaded subindexes would be saved
// Example:
// (StudentId: 1) would be in the index '0' of the 'result' array
// (StudentId: 2) would be in the index '1' of the 'result' array
// (StudentId: 3) would be in the index '2' of the 'result' array
// So, 'subIndexes' would be equal to { '1': 0, '2': 1, '3': 2}
const subIndexes = {};
// Reduce the 'studentsArray', with an array accumulator '[]'
const result = studentsArray.reduce((acc, item) => {
// Let's check if the subIndex 'item.studentId' was loaded.
if (subIndexes[item.studentId] === undefined) {
// If the subIndex is not loaded (undefined), it means that
// we need to push the new object into the accumulator.
// Also, remember that the 'subIndexes' array at key 'item.studentId'
// must have the index of the new subObject. Remember: `{ '1': 0, '2': 1, '3': 2}`
subIndexes[item.studentId] =
// Push the new object
acc.push({
...item,
occurrence: 1
// Subtract 1, because the 'push' method returns the new lenght
// of the array, and we need the index
}) - 1;
// In case the subIndex was already loaded
} else {
// Just use the subIndex value to modify the occurences number,
// and push the new values of 'courses' and 'classPeriods'
// Look that I'm using the spread operator to break
// the reference to the original array,. I you don't break it,
// it may cause unexpected results later.
++acc[subIndexes[item.studentId]].occurrence;
acc[subIndexes[item.studentId]].courses.push(...item.courses);
acc[subIndexes[item.studentId]].classPeriods.push(...item.classPeriods);
}
return acc;
// initial value of the accumlator, empty array
}, []);
console.log(result);
Inline
const studentsArray = [{ "studentId": "1", "courses": ["A", "A", "B", "D", "A"], "classPeriods":["Chemistry","Chemistry","Maths"] }, {"studentId": "1", "courses": ["E", "F", "E"], "classPeriods": ["Biology", "Chemistry", "Biology"] }, {"studentId": "1", "courses": ["A", "D", "D"], "classPeriods": ["Computer Science", "Computer Scienc", "Biology", "Biology"] }, {"studentId": "1", "courses": ["G", "K"], "classPeriods": ["Computer Science", "Chemistry", "Biology", "Chemistry"] }, {"studentId": "1", "courses": ["A", "B", "D"], "classPeriods": ["Maths", "Maths", "Biology", "Chemistry"] }, {"studentId": "2", "courses": ["A", "C", "F"], "classPeriods": ["Arts", "Maths", "Arts", "Maths"] }, {"studentId": "2", "courses": ["B", "B", "D"], "classPeriods": ["Arts", "Arts", "Maths", "Maths"] }, {"studentId": "3", "courses": ["H", "B", "R"], "classPeriods": ["Biology", "Biology", "Maths", "Maths"] } ];
const getOcurrences = (b, a = {}) => b.reduce((s,e)=>(void 0===a[e.studentId]?a[e.studentId]=s.push({...e,occurrence:1})-1:(++s[a[e.studentId]].occurrence,s[a[e.studentId]].courses.push(...e.courses),s[a[e.studentId]].classPeriods.push(...e.classPeriods)),s),[]);
console.log(getOcurrences(studentsArray));
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | Ahmed Hafez |
| Solution 2 | David Thomas |
| Solution 3 | |
| Solution 4 | |
| Solution 5 |
