Average Score of a Student

Among the myriad of Youtube channels I’m subscribed to, I saw Erik Hanchett’s video the other day. The one where he talked about his interview experience and the question he had worked on during his interview. Here is my take on that problem.

A Classic Trio

This question is a classic example of the trifecta methods: filter, map, reduce. I already have another article about the brotherhood of FMR. Once you master the use of each method, combining them in a meaningful order really helps to transform the data structure you are working with. Without further ado, what’s his interview question? He was given an array of objects that had an id and score as properties. For the sake of simplicity, the id values are only 1 and 2, representing only 2 students. The goal is to calculate the average score of each student by taking into account the highest 5 scores. Consider the following as an example.

const scores = [
  { id: 1, score: 76 },
  { id: 2, score: 99 },
  { id: 1, score: 100 },
  { id: 2, score: 56 },
  { id: 2, score: 12 },
  { id: 2, score: 87 },
  { id: 2, score: 100 },
  { id: 2, score: 18 },
  { id: 1, score: 86 },
  { id: 1, score: 81 },
  { id: 1, score: 83 },
  { id: 2, score: 37 },
  { id: 1, score: 65 }
];

As you can see, there are more than 5 objects for each student so we’ll need to find the ones that have the highest score. However, we need something more essential to start with. We should probably write a function that accepts an id parameter so we can filter on that scores variable.

function studentAverage(id){
  return scores.filter(score=>score.id===id)
}

A simple filter that only returns the objects for a particular student will do it. Although id attribute was essential to curb the scores array, it’s now time we switch our attention to the score property in those objects.

function studentAverage(id) {
  return (
    scores
      .filter(score => score.id === id)
      .map(score => score.score)
  );
}

This should return only the score part of each object but we have all the scores, not just 5 so it’s time to slice it up. However, if you take the 5 elements from that intermediary array, the scores are not guaranteed to be sorted. You could have arranged the original scores array but you may not always have the luxury of doing that so we should now run a basic sort.

function studentAverage(id) {
  return (
    scores
      .filter(score => score.id === id)
      .map(score => score.score)
      .sort((a, b) => b - a)
  );
}

Sort me Out

What’s happening in that sort? Well, if you went ahead and used the default sort(), the result would put 100 as the first element because, by default, the sort method sorts elements alphabetically. Check out that first paragraph at MDN web docs. So, basically, your numbers will be treated as strings. Therefore, 100 will come before 65 because, you know, 1<6.

All is not lost though. If you read the rest of MDN web docs, you’ll see that sort takes a compareFunction which is what you can use to compare the elements of your array. The logic is simple, each consecutive element is compared and the function returns either 1, 0 or -1 depending on how you interpret the sorting should work. In most simple cases, you can actually cheat and you don’t have to write these 3 pieces. Say, for example, our example is a trivial number comparison. We can simply look at the difference between two consecutive numbers such as a-b. That would actually sort the array in an ascending order but we would probably want the highest 5 as the first items so we instead of sorting and reversing the array, we sort it in a descending order.

So far, if you used our function for student 1, you’d be left with an array of [100, 86, 83, 81, 76, 65]. It’s time slice things up.

function studentAverage(id) {
  return (
    scores
      .filter(score => score.id === id)
      .map(score => score.score)
      .sort((a, b) => b - a)
      .slice(0, 5)
  );
}

Now you see the benefit of sorting in reverse order. If you look at the scores array for the student with id 2, you’ll see 7 results. If you had sorted your results in an ascending order, you could have still used negative indices in slice backward to get the last 5 results from an array 6 or 7 elements but dealing with positive numbers is easier to wrap your head around than counting backwards. In other words, compare it to the code below which gives the same result.

function studentAverage(id) {
  return (
    scores
      .filter(score => score.id === id)
      .map(score => score.score)
      .sort((a, b) =>  a - b)
      .slice(-5)
  );
}

Almost There

All we have to do now is to take the average of 5 numbers we have filtered out and sorted. They are in an array so just sum them up and divide by 5. We’ll use reduce for this instead of a while or for loop.

function studentAverage(id) {
  return (
    scores
      .filter(score => score.id === id)
      .map(score => score.score)
      .sort((a, b) => b - a)
      .slice(0, 5)
      .reduce((sum, next) => sum + next) / 5
  );
}

The reduce code here is the most generic code example you’ll see all around the Internet. Sum being 0 as a newly defined variable gets added the value of each next item in the array so the end result is the sum of all elements. You can make the studentAverage function better by adding a second argument, that is the number of results to use to calculate the average. If the teacher decides to take the highest 4 scores to calculate, instead of hardcoding it, you can change that 5 into an argument the function uses. Maybe, the default should be 5 but you want to have the luxury of passing a different value for it. Then,

function studentAverage(id, count = 5) {
  return (
    scores
      .filter(score => score.id === id)
      .map(score => score.score)
      .sort((a, b) => b - a)
      .slice(0, count)
      .reduce((sum, next) => sum + next) / count
  );
}

Conclusion

I believe the gist of that interview question was to use the array functions I have presented so far. You can, of course, right your calculation logic in a way that checks whether 5 scores are available or whether the score property is even a number. However, like I said, I think the interviewers were first looking for the most common way of solving this exercise. In an interview like that, if you have time left, you can emphasize these details about data clean up or integrity. You can even bring up the issue with our function call that it directly works on scores array. Maybe, that should be an argument passed around too there are fewer coupled things. Keep in mind though, write something that works first, then you can optimize.


Recent posts