ajonno
ajonno

Reputation: 2240

How can I remove an object from array of objects based on max value in javascript

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);

Upvotes: 9

Views: 842

Answers (8)

Sampson Crowley
Sampson Crowley

Reputation: 1253

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))

Upvotes: 2

Ultimater
Ultimater

Reputation: 4738

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>

Upvotes: 3

Ori Drori
Ori Drori

Reputation: 191976

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);

Upvotes: 1

Mark
Mark

Reputation: 92440

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))

Upvotes: 5

Guzz
Guzz

Reputation: 66

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>

Upvotes: 0

Eddie
Eddie

Reputation: 26844

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);

Upvotes: 4

Josh
Josh

Reputation: 18690

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.

Upvotes: 0

lastr2d2
lastr2d2

Reputation: 3968

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;
}

Upvotes: 3

Related Questions