Reputation: 2240
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
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
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
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
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
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
Reputation: 26844
Using only ES6, you can use 2 reduce
s. 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
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
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