9

I've got an array of objects that looks like this:

[ { person: 'Fred', scoreTotal: 29 },
{ person: 'Alice', scoreTotal: 34 },
{ person: 'Alice', scoreTotal: 22 },
{ person: 'Mary', scoreTotal: 14 },
{ person: 'Bob', scoreTotal: 33 },
{ person: 'Bob', scoreTotal: 13 },
{ person: 'Bob', scoreTotal: 22 },
{ person: 'Joe', scoreTotal: 28 }]

and where there are multiple objects for a given person -> I want to keep the top "X". For example:

a. top 1 result for a person Result would look like this:

    [ { person: 'Fred', scoreTotal: 29 },
    { person: 'Alice', scoreTotal: 34 },
    { person: 'Mary', scoreTotal: 14 },
    { person: 'Bob', scoreTotal: 33 },
    { person: 'Joe', scoreTotal: 28 }]

b. top 2 results for a person Result would look like this:

   [ { person: 'Fred', scoreTotal: 29 },
  { person: 'Alice', scoreTotal: 34 },
   { person: 'Alice', scoreTotal: 22 },
   { person: 'Mary', scoreTotal: 14 },
   { person: 'Bob', scoreTotal: 33 },
   { person: 'Bob', scoreTotal: 22 },
   { person: 'Joe', scoreTotal: 28 }]

Is there a way to achieve this using something like Lodash?

I think I'm heading in the right direction with this but not quite there yet:

for (var i = 0; i < t.length - 1; i++) {
if (
  t[i].golfer === t[i + 1].golfer &&
  t[i].scoreTotal < t[i + 1].scoreTotal
) {
  delete t[i];
}

}

// remove the "undefined entries"

t = t.filter(function(el) {
    return typeof el !== "undefined";
});
console.log(t);
0

8 Answers 8

5

You can do this pretty simply without Underscore/Lodash. Sort the array highest to lowest by score. Then use Array.filter. As filter goes through the array keep track of how many times you see each person and start return false after the top number you want has been reached for a person.

let arr = [ { person: 'Fred', scoreTotal: 29 },{ person: 'Alice', scoreTotal: 34 },{ person: 'Alice', scoreTotal: 22 },{ person: 'Mary', scoreTotal: 14 },{ person: 'Bob', scoreTotal: 33 },{ person: 'Bob', scoreTotal: 13 },{ person: 'Bob', scoreTotal: 22 },{ person: 'Joe', scoreTotal: 28 }]

function filterTop(arr, top) {
    let counts = {}
    return [...arr].sort((a, b) => b.scoreTotal - a.scoreTotal)
    .filter(score => (counts[score.person] = (counts[score.person] || 0) +1 ) <= top)
}


console.log(filterTop(arr, 1))
console.log(filterTop(arr, 2))

Sign up to request clarification or add additional context in comments.

8 Comments

I don't understand why people run to libraries for map, reduce and filter operations
@AnilRedshift I think it's not library ! If you don't understand , read docs and practice . It will reduce your time from making nested loop
@AnilRedshift I really respect the work and solid coding that went into Lodash and underscore, but I agree, it seems like a lot people use them for tasks that have become pretty convenient since ES6.
Not stupid at all @Shanoor. sort() will sort an array in place, which mean it alters the array that passed into the function. This can be considered a bad practice. In this case the function would still work, but the original array would be changed.
Ah, thanks, forgot about pass by reference parameters. It's been a while since I wrote any "real" code, the sysops got me ^^
|
4

Using only ES6, you can use 2 reduces. First is to group the arrays. the second is to get the number of top per group.

let arr = [{"person":"Fred","scoreTotal":29},{"person":"Alice","scoreTotal":34},{"person":"Alice","scoreTotal":22},{"person":"Mary","scoreTotal":14},{"person":"Bob","scoreTotal":33},{"person":"Bob","scoreTotal":13},{"person":"Bob","scoreTotal":22},{"person":"Joe","scoreTotal":28}];

let getTop = (a, t) => {                           //Parameters a = array. t = top
  return Object.values(a.reduce((c, v) => {        //Group the array using the person property
    c[v.person] = c[v.person] || [];
    c[v.person].push(v);
    return c;
  }, {})).reduce((c, v) => {
    v.sort((a, b) => b.scoreTotal - a.scoreTotal); //Sort the sub array
    c = c.concat(v.slice(0, t));                   //Add the top to accumulator
    return c;
  }, []);
}


let result1 = getTop(arr, 1); //Get top 1
let result2 = getTop(arr, 2); //Get top 2

console.log('Top 1', result1);
console.log('Top 2', result2);

1 Comment

Not gonna get much more elegant than two reduces
3

This does exactly what you want in the exact order you want using the exact library you're trying to use (lodash). I broke the process down into the following steps:

Step 1: group the data. There should be a an array containing all "Bob" records, and another array containing all "Joe" records, etc. I decided to go a step further and only store the scoreTotal rather than the entire record.

Step 2: loop through each of these arrays, sort them in descending order, and slice to the desired "top" results.

Step 3: filter the original data, using our previous findings and only include a record in the results if we have an exact scoreTotal match for a specific person, and only once per such score, removing it from our temporary array along the way so we don't get duplicate records and return more records than the "top" records desired.

// The magical function
function showTopResults(input, topResults)
{

	// Step one: group like data together
	var groupedData = _.reduce(input, function(result, value, key) {
	    if(typeof result[value.person] == 'undefined'){ result[value.person] = []; }
	    result[value.person].push(value.scoreTotal);
	    return result;
	}, {});
	
	// Step two: loop through person keys, sort it, then grab only the first "x" elements
	_.forEach(groupedData, function(value, key) {
	   value = value.sort(function(a,b){ var n = b - a; return n ? n < 0 ? -1 : 1 : 0}); // first element is largest
	   groupedData[key] = value.slice(0, topResults); // we only want first x results, so we get largest only
	});
	
	// Step three: filter our elements only where we have a match
	var filterResults = _.filter(input,function(o){
		var idx = _.indexOf(groupedData[o.person],o.scoreTotal);
		if( idx > -1)
		{
			groupedData[o.person].splice(idx, 1); // remove element so we don't get multiple elements of equal value
			return true; // We have a match
		} else {
			return false; // not a match
		}
	});

	return filterResults;

}

// Our input
var input = [
	{ person: 'Fred', scoreTotal: 29 },
	{ person: 'Alice', scoreTotal: 34 },
	{ person: 'Alice', scoreTotal: 22 },
	{ person: 'Mary', scoreTotal: 14 },
	{ person: 'Bob', scoreTotal: 33 },
	{ person: 'Bob', scoreTotal: 13 },
	{ person: 'Bob', scoreTotal: 22 },
	{ person: 'Joe', scoreTotal: 28 }
];

// Tests using our magical function
console.log(showTopResults(input, 1));
console.log(showTopResults(input, 2));
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.10/lodash.min.js"></script>

Comments

3

Here is a solution with lodash...

function pickTopX(data, x) {
  let grouped = _.groupBy(data, 'person');
  let sorted = _.sortBy(grouped, 'scoreTotal');
  let sliced = _.map(sorted, function(pair) {
    return _.take(pair, x)
  });
  let result = _.flatten(sliced);
  return result;
}

1 Comment

Lot more elegant than my lodash solution. Have an upvote.
2

Not exactly what you asked but worth considering:

The best way to handle this would be to restructure your data

var people = {},
        personArray = [
          { person: 'Fred', scoreTotal: 29 },
          { person: 'Alice', scoreTotal: 34 },
          { person: 'Alice', scoreTotal: 22 },
          { person: 'Mary', scoreTotal: 14 },
          { person: 'Bob', scoreTotal: 33 },
          { person: 'Bob', scoreTotal: 13 },
          { person: 'Bob', scoreTotal: 22 },
          { person: 'Joe', scoreTotal: 28 }
        ];
        
    //loop person Array to group array by person
    //and sort their top scores from best to worst
    for(var i = 0; i < personArray.length; i++){
      var person = personArray[i].person,
          score = personArray[i].scoreTotal,
          scores = people[person] || [],
          pushed = false;

      if(scores.length){
        for(var n = 0; n < scores.length; n++){
          if(score > scores[n]){
            pushed = true;
            scores.splice(n, 0, score);
            break;
          }
        }
       
      }
      if(!pushed) scores.push(score);
      
      people[person] = scores;
    }
    
    console.log(people);
    
    //return top `n` scores for each person from best to worst
    function topScores(nScores){
      var result = [];
      for(var name in people){
        if(people.hasOwnProperty(name)){
          for(var r = 0; ((r < people[name].length) && (r < nScores)); r++){
            result.push({person: name, scoreTotal: people[name][r]});
          }
        }
      }
      return result
    }
    
    console.log(topScores(2))
    console.log(topScores(3))
    console.log(topScores(1))

Comments

1

Reduce the array to an object, using the person prop as key. For each person add to object, if key doesn't exists or if scoreTotal is greater than current scoreTotal. Convert back to array using Object.values():

const data = [{"person":"Fred","scoreTotal":29},{"person":"Alice","scoreTotal":34},{"person":"Alice","scoreTotal":22},{"person":"Mary","scoreTotal":14},{"person":"Bob","scoreTotal":33},{"person":"Bob","scoreTotal":13},{"person":"Bob","scoreTotal":22},{"person":"Joe","scoreTotal":28}];

const result = Object.values(data.reduce((r, p) => {
  if(!r[p.person] || r[p.person].scoreTotal < p.scoreTotal) {
    r[p.person] = p;
  }
  
  return r;
}, {}));

console.log(result);

Comments

0

This isn't a full answer, just a tip. Don't use delete array[index], use array.splice. See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/splice for more info.

The next suggestion is to clarify whether you actually need to modify the array, or just get a new array without some of the original array's data. If you do not need to modify the original, it is far better to just use a function like reduce, or use a for loop to selectively copy items into the new array.

Comments

0

Using underscore: https://underscorejs.org/

const db = [ 
{ person: 'Fred', scoreTotal: 29 },
{ person: 'Fred', scoreTotal: 10 },
{ person: 'Fred', scoreTotal: 2 },
{ person: 'Alice', scoreTotal: 34 },
{ person: 'Alice', scoreTotal: 5 },
{ person: 'Alice', scoreTotal: 15 },
{ person: 'Alice', scoreTotal: 40 },
{ person: 'Mary', scoreTotal: 23 },
{ person: 'Mary', scoreTotal: 32 },
{ person: 'Mary', scoreTotal: 98 },
{ person: 'Mary', scoreTotal: 4 },
{ person: 'Bob', scoreTotal: 70 },
{ person: 'Bob', scoreTotal: 65 },
{ person: 'Bob', scoreTotal: 35 },
{ person: 'Bob', scoreTotal: 5 },
{ person: 'Joe', scoreTotal: 28 }];
const nOfItens = 2;
const persons = _.map(db, item => item.person);
const names = _.uniq(persons);
let newList = [];

for (var i = 0; names.length > i; i++) {
  let filter = _.filter(db, person => person.person === names[i]);
  let sort = _.sortBy(filter, num => -num.scoreTotal);
  let items = sort.splice(0, nOfItens)
  newList = [...newList, ...items]
}

console.log(newList);
<script src="https://underscorejs.org/underscore-min.js"></script>

Comments

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.