This question already has answers here:
Group objects by multiple properties in array then sum up their values
(16 answers)
Closed 3 years ago.
Given an array of objects like this:
[
{
playerId: 1,
playerName: "Cooper",
assists: 1,
points: 5,
},
{
playerId: 1,
playerName: "Cooper",
assists: 3,
points: 2,
},
{
playerId: 2,
playerName: "Tom",
assists: 1,
points: 3,
},
{
playerId: 2,
playerName: "Tom",
assists: 3,
points: 1,
},
{
playerId: 3,
playerName: "Shelby",
assists: 2,
points: 7,
},
{
playerId: 3,
playerName: "Shelby",
assists: 1,
points: 2,
},
]
Is there a way using ES6 to find matching object keys (playerId in this example) in an array of of objects, and then combine other object keys (assists and points) for those matches so the final array looks something like this:
[
{
playerId: 1,
playerName: "Cooper",
assists: 4,
points: 7,
},
{
playerId: 2,
playerName: "Tom",
assists: 4,
points: 4,
},
{
playerId: 3,
playerName: "Shelby",
assists: 3,
points: 9,
},
]
You can use Array.prototype.reduce() to accumulate the properties, keeping track by reducing to an object that has the playerId as keys first, then using Object.values() to get back an array as intended.
let arrIn = [{"playerId":1,"playerName":"Cooper","assists":1,"points":5},{"playerId":1,"playerName":"Cooper","assists":3,"points":2},{"playerId":2,"playerName":"Tom","assists":1,"points":3},{"playerId":2,"playerName":"Tom","assists":3,"points":1},{"playerId":3,"playerName":"Shelby","assists":2,"points":7},{"playerId":3,"playerName":"Shelby","assists":1,"points":2}];
let arrOut = Object.values(arrIn.reduce((acc, current) => {
let base = acc[current.playerId];
if (!base) {
// if we have not processed an object with this playerId yet,
// simply copy the current object
acc[current.playerId] = current;
} else {
// otherwise, increase the properties
base.assists += current.assists;
base.points += current.points;
}
return acc;
}, {}));
console.log(arrOut);
Related
This question builds on many similar ones like Construct hierarchy tree from flat list with parent field?
However the twist is that there is no parent id.
e.g.
[
{id: 1, depth: 1, ...},
{id: 2, depth: 2, ...},
{id: 3, depth: 3, ...},
{id: 4, depth: 2, ...},
{id: 5, depth: 1, ...},
{id: 6, depth: 2, ...},
{id: 7, depth: 2, ...},
{id: 8, depth: 1, ...},
{id: 9, depth: 2, ...},
{id: 10, depth: 3, ...},
{id: 11, depth: 3, ...},
]
What is a performant way to construct the following tree?
Note that the children always come after the parent i.e. one can see the tree from the depth value. For example, id 2 is a child of id 1 since its depth is 2 and id 1 has a depth of 1. id 3 is a child of id 2 since id 3 has a depth of 3. id 4 is a child of id 1 not id 3 because id 4 has a depth of 2 (a step up) from id 3's depth of 3
\\tree digram
1
2
3
4
5
6
7
8
9
10
11
Should have values like
[
{id:1, depth:1, children: [
{id: 2, depth: 2, children: [...]},
...
]},
{id:5, depth:1, children: [...]},
{id:6, depth:1, children: [...]},
]
You can use an array for this that has an index for each depth. At every moment, it will represent a path from the (virtual) root to the current node. When dealing with a node, its parent will sit at index depth-1, where it can be inserted in that parent's children property, and the node itself will be placed at index depth:
function createForest(flatdata) {
const path = [{ children: [] }];
for (const obj of flatdata) {
path[obj.depth - 1].children.push(path[obj.depth] = { ...obj, children: [] });
}
return path[0].children;
}
// demo
const flatdata = [{id: 1, depth: 1},{id: 2, depth: 2},{id: 3, depth: 3},{id: 4, depth: 2},{id: 5, depth: 1},{id: 6, depth: 2},{id: 7, depth: 2},{id: 8, depth: 1},{id: 9, depth: 2},{id: 10, depth: 3},{id: 11, depth: 3}];
const roots = createForest(flatdata);
console.log(roots);
Irregular depths
If the depth values do not correspond to the actual depth of the nodes, but leave gaps, then use a "dictionary" (a plain object) to record the mapping of the depth property values with which real depth they correspond with:
function createForest(flatdata) {
const path = [{ children: [] }];
const depthMap = { 0: 0 };
for (const obj of flatdata) {
path[(depthMap[obj.depth] ??= path.length) - 1].children.push(
path[depthMap[obj.depth]] = { ...obj, children: []}
);
}
return path[0].children;
}
// demo
const flatdata = [{id: 1, depth: 10},{id: 2, depth: 20},{id: 3, depth: 30},{id: 4, depth: 20},{id: 5, depth: 10},{id: 6, depth: 20},{id: 7, depth: 20},{id: 8, depth: 10},{id: 9, depth: 20},{id: 10, depth: 30},{id: 11, depth: 30}];
const roots = createForest(flatdata);
console.log(roots);
If however, the only irregularity is that the depth does not always start at 1, but sometimes at 2, it will be more efficient to prefix the input data with a dummy depth-one node, use the first function, and then remove the dummy "root" (with depth 1) from the result.
Go through the array and add each item to the tree as well as to a trail of breadcrumbs. Each next item either goes as a child to the last one or you backtrack through the breadcrumb trail to the correct depth where it needs to be inserted:
const peek = arr =>
arr[arr.length-1];
function toTree(arr) {
const tree = [];
const trail = [];
for (const item of arr) {
while ((peek(trail)?.depth ?? 0) >= item.depth) {
trail.pop();
}
const current = peek(trail)?.children ?? tree;
const treeNode = {...item, children: []};
current.push(treeNode);
trail.push(treeNode);
}
return tree;
}
const array = [
{id: 1, depth: 1, },
{id: 2, depth: 2, },
{id: 3, depth: 3, },
{id: 4, depth: 2, },
{id: 5, depth: 1, },
{id: 6, depth: 2, },
{id: 7, depth: 2, },
{id: 8, depth: 1, },
{id: 9, depth: 2, },
{id: 10, depth: 3 },
{id: 11, depth: 3 },
]
console.log(toTree(array));
This solution clones each item, in order to add the .children property. If no cloning is necessary, item can be directly mutated.
You could take an array of the last inserted objects.
const
data = [{ id: 1, depth: 1 }, { id: 2, depth: 2 }, { id: 3, depth: 3 }, { id: 4, depth: 2 }, { id: 5, depth: 1 }, { id: 6, depth: 2 }, { id: 7, depth: 2 }, { id: 8, depth: 1 }, { id: 9, depth: 2 }, { id: 10, depth: 3 }, { id: 11, depth: 3 }],
result = data.reduce((r, { depth, ...o }) => {
r[depth - 1].push({ ...o, children: r[depth] = [] });
return r;
}, [[]])[0];
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }
I am currently trying to filter available products based on their selected options.
const products = [{
id: 1,
name: 'Safari',
horsepowers: 30,
doors: 4,
gear_type: 'automatic',
wheels: 6
},
{
id: 2,
name: 'Jungle',
horsepowers: 50,
doors: 3,
gear_type: 'automatic',
wheels: 5
},
{
id: 3,
name: 'Moon',
horsepowers: 30,
doors: 4,
gear_type: 'manual',
wheels: 4
}
]
const selectedOptions =
{
horsepowers: 50,
doors: 3,
gear_type: null,
wheels: null
}
Typically I would do something like
const availableProducts = products.filter((product) =>
product.horsepowers === selectedOptions.horsepowers &&
product.doors === selectedOptions.doors .... etc
however, how do I skip null values, empty arrays, and undefined values if the user has not yet selected all possible options yet?
The next provided approach takes advantage of the 2nd thisArg argument of almost every available prototypal array method.
Thus one can write a generic filter function which compares any item's property values to the related ones configured by the selectedOptions object which will be passed alongside the filter function as filter's 2nd argument and as the filter function's this context ...
const selectedOptions = {
horsepowers: 50,
doors: 3,
gear_type: null,
wheels: null,
};
const products = [{
id: 1,
name: 'Safari',
horsepowers: 30,
doors: 4,
gear_type: 'automatic',
wheels: 6,
}, {
id: 2,
name: 'Jungle',
horsepowers: 50,
doors: 3,
gear_type: 'automatic',
wheels: 5,
}, {
id: 3,
name: 'Moon',
horsepowers: 30,
doors: 4,
gear_type: 'manual',
wheels: 4,
}];
function doItemPropertiesEqualEveryBoundSelectedOption(item) {
return Object
// create key value pairs from the `this` bound selected options.
.entries(this)
// skip/ignore selected option entries where `value` equals `null`.
.filter(([key, value]) => value !== null)
// execute item specific selected option validation via `every`.
.every(([key, value]) => item[key] === value);
}
console.log(
products
.filter(
doItemPropertiesEqualEveryBoundSelectedOption,
selectedOptions,
)
);
.as-console-wrapper { min-height: 100%!important; top: 0; }
In order to answer another of the OP's questions ...
"however, how do I skip null values, empty arrays, and undefined values if the user has not yet selected all possible options yet?"
... and also provide a generic solution to it, the above approach can be changed to a thisArg object which not only features the selected options but also the condition of not to be validated (invalid) selectedOption properties ...
const products = [{
id: 1,
name: 'Safari',
horsepowers: 30,
doors: 4,
gear_type: 'automatic',
wheels: 6,
}, {
id: 2,
name: 'Jungle',
horsepowers: 50,
doors: 3,
gear_type: 'automatic',
wheels: 5,
}, {
id: 3,
name: 'Moon',
horsepowers: 30,
doors: 4,
gear_type: 'manual',
wheels: 4,
}];
const selectedOptions = {
horsepowers: 50,
doors: 3,
gear_type: null,
wheels: null,
};
const isInvalidValue = (value) => {
return Array.isArray(value)
// empty array validation.
? (value.length === 0)
// undefined and null value validation.
: (value == null)
}
function doItemPropertiesEqualEveryBoundValidOption(item) {
const { options, isInvalidValue } = this;
return Object
.entries(options)
.filter(([key, value]) => !isInvalidValue(value))
.every(([key, value]) => item[key] === value);
}
console.log(
products
.filter(
doItemPropertiesEqualEveryBoundValidOption,
{ options: selectedOptions, isInvalidValue },
)
);
.as-console-wrapper { min-height: 100%!important; top: 0; }
selectedOptions.horsepowers == null ? true : product.horsepowers === selectedOptions.horsepowers &&
product.doors == null ? true : product.doors === selectedOptions.doors
If you want to keep it in line, you can use a ternary operator to check if it's null before comparing.
You could preprocess selectedOptions first and then comparing:
const applicableOptions = Object.entries(selectedOptions).filter(
([_, value]) => value !== null && value !== undefined
);
const availableProducts = products.filter((product) =>
applicableOptions.every(([optKey, optValue]) => product[optKey] === optValue)
);
to compare using array, you would need to update your example as there's no array property
Rather than typing in each option by hand, you could just iterate over selectedOptions. Then it's as simple as checking if the value of each option is null before comparing.
let filtered = products.filter(e => {
for(let [key, value] of Object.entries(selectedOptions))
{
if(value != null && e[key] != value)
return false;
}
return true;
});
const products = [{
id: 1,
name: 'Safari',
horsepowers: 30,
doors: 4,
gear_type: 'automatic',
wheels: 6
},
{
id: 2,
name: 'Jungle',
horsepowers: 50,
doors: 3,
gear_type: 'automatic',
wheels: 5
},
{
id: 3,
name: 'Moon',
horsepowers: 30,
doors: 4,
gear_type: 'manual',
wheels: 4
}
]
const selectedOptions =
{
horsepowers: 50,
doors: 3,
gear_type: null,
wheels: null
}
let filtered = products.filter(e => {
for(let [key, value] of Object.entries(selectedOptions))
{
if(value != null && e[key] != value)
return false;
}
return true;
});
console.log(filtered);
However, if you really want to write it out, I'd just check if the option is set with a simple boolean check. !(null) returns true, so this would work.
return (!selectedOptions.horsepowers || selectedOptions.horsepowers == product.horsepowers) && ...
You could generate a filter array from selectedOptions and filter the entries and use this for filtering the data, later.
const
products = [{ id: 1, name: 'Safari', horsepowers: 30, doors: 4, gear_type: 'automatic', wheels: 6 }, { id: 2, name: 'Jungle', horsepowers: 50, doors: 3, gear_type: 'automatic', wheels: 5 }, { id: 3, name: 'Moon', horsepowers: 30, doors: 4, gear_type: 'manual', wheels: 4 }],
selectedOptions = { horsepowers: 50, doors: 3, gear_type: null, wheels: null },
filter = Object
.entries(selectedOptions)
.filter(([, v]) => v !== null),
result = products.filter(o => filter.every(([k, v]) => o[k] === v));
console.log(result);
Using Object#entries and Array#filter, get the pairs with selected values from selectedOptions to use for filtering the products list
Using Array#filter and Array#every, filter the list to make sure that resulting products match the above pairs
const
products = [ { id: 1, name: 'Safari', horsepowers: 30, doors: 4, gear_type: 'automatic', wheels: 6 }, { id: 2, name: 'Jungle', horsepowers: 50, doors: 3, gear_type: 'automatic', wheels: 5 }, { id: 3, name: 'Moon', horsepowers: 30, doors: 4, gear_type: 'manual', wheels: 4 } ],
selectedOptions = { horsepowers: 50, doors: 3, gear_type: null, wheels: null };
const filterOptions =
Object.entries(selectedOptions).filter(([_, value]) => value !== null);
const selectedProducts =
products.filter(product =>
filterOptions.every(([key, value]) => product[key] === value)
);
console.log(selectedProducts);
The data structure that I am trying to achieve would look as so :
I would like the list_id to become a key in a object, and hold all the id's of the items that have the matching list id.
var lists = { (list_id)1 : [1, 2, 3]
(list_id)2 : [4, 5, 6]
(list_id)3 : [7, 8, 9]
(list_id)4 : [10, 11, 12] };
this object is created from a json data structure that looks like this:
let json = [{ id: 1, list_id: 1 }, { id: 2, list_id: 1 },
{id: 3, list_id: 1 }, {id: 4, list_id: 2 },
{id: 5, list_id: 2 }, {id: 6, list_id: 2 },
{id: 7, list_id: 3 }, {id: 8, list_id: 3 },
{id: 9, list_id: 3 }, {id: 10, list_id: 4 },
{id: 11, list_id: 4 }, {id: 12, list_id: 4 }]
I can make an object that holds all the list_id's as keys but am getting stumped on pushing the actions_id into the value pair array with the matching list id.
let listAll = {};
json.forEach(function(lista, index, listb) {
listAll[lista.list_id] = [];
if ( listAll[lista.list_id] === lista.list_id){
listAll[lista.list_id].push(lista.id)
} else {
listAll[lista.list_id] = [lista.id];
}
});
My goal is to have and object that contains a key for every list_id currently avaliable from the actions.
Then add every action that contains the matching list_id into a value pair array.
the current output of this code is
{ '1': [ 3 ], '2': [ 6 ], '3': [ 9 ], '4': [ 12 ] }
which does not contain all numbers, each array should contain 3 numbers.
An alternative is using the function reduce to group the objects by a specific key = ['list_id', list_id].join('').
let json = [{ id: 1, list_id: 1 }, { id: 2, list_id: 1 }, {id: 3, list_id: 1 }, {id: 4, list_id: 2 }, {id: 5, list_id: 2 }, {id: 6, list_id: 2 }, {id: 7, list_id: 3 }, {id: 8, list_id: 3 }, {id: 9, list_id: 3 }, {id: 10, list_id: 4 }, {id: 11, list_id: 4 }, {id: 12, list_id: 4 }],
result = json.reduce((a, {id, list_id}) => {
let key = ['list_id', list_id].join(''); // For example: this is creating ['list_id', 1] to list_id1
(a[key] || (a[key] = [])).push(id);
return a;
}, Object.create(null)/*This is only to create an object without prototype -> {}*/);
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }
Why don't you try hasOwnProperty instead?
var listAll = {};
json.forEach(function(list, index) {
if (listAll.hasOwnProperty(list.list_id)) {
listAll[list.list_id].push(list.id);
}else {
listAll[list.list_id] = [list.id];
}
});
console.log(listAll);
Say I have a filter:
filter = [ {key: "pl", value: 3}, {key: "sh", value: 2} ]
I want to filter the following javascript object with the above filter conditions:
var data = [
{title: "The Uncertainty of the Poet ",
pl: 3,
si: 2,
va: 3,
te: 0,
co: 0,
or: 4,
sh: 2,
po: 0,
li: 0,
ar: 5
},
{
title: "Direction",
pl: 4,
si: 3,
va: 1,
te: 3,
co: 0,
or: 3,
sh: 2,
po: 0,
li: 0,
ar: 5
}
...
]
I tried the following with no luck:
var result = data.filter(function(d){
for (item in filter) {
return d.key==d.value;
}
Just another potential option to check if the object meets all the criteria:
data.filter(function(obj) {
return filter.reduce(function(a, f) {
return a && (obj[f.key] === f.value);
}, true);
});
That will work without having to check for hasOwnProperty because of the use of reduce. If you wanted to check for if any of the filter conditions are true, you would change it to
data.filter(function(obj) {
return filter.reduce(function(a, f) {
return a || (obj[f.key] === f.value);
}, false);
});
You can do this way as well:
var filters = [{key: "pl", value: 3}, {key: "sh", value: 2}]
var data = [
{
title: "The Uncertainty of the Poet ",
pl: 2,
si: 2,
va: 3,
te: 0,
co: 0,
or: 4,
sh: 3,
po: 0,
li: 0,
ar: 5
},
{
title: "Direction",
pl: 3,
si: 3,
va: 1,
te: 3,
co: 0,
or: 3,
sh: 2,
po: 0,
li: 0,
ar: 5
}
]
var result = data.filter((item) => {
for(let i = 0; i < filters.length; ++i) {
let filter = filters[i];
if(item[filter.key] && item[filter.key] === filter.value) {
return true;
}
}
return false;
});
If you want it to match one or the other values, this will work:
match = [ {key: "pl", value: 3}, {key: "sh", value: 2} ]
var result = data.filter(function(d) {
return d.pl === match[0]['value'] || d.sh === match[1]['value']
})
I've changed the name of the array to match to avoid confusion.
You aren't going deep enough with your for in. It is looping over the array and not working with each object in the array
Can use Array#every() to make sure every object in filter array has match in the data object
// filter main data
var result = data.filter(function(dataObj){
// check if all proprties within filter array are a match
return filter.every(function(filterObj){
//compare value of property found in filterObject with value
return dataObj[filterObj.key] === filterObj.value
})
})
console.log(result)
<script>
var filter = [ {key: "pl", value: 2}, {key: "sh", value: 3} ]
var data = [{
title: "The Uncertainty of the Poet ",
pl: 2,
si: 2,
va: 3,
te: 0,
co: 0,
or: 4,
sh: 3,
po: 0,
li: 0,
ar: 5
},
{
title: "Direction",
pl: 4,
si: 3,
va: 1,
te: 3,
co: 0,
or: 3,
sh: 2,
po: 0,
li: 0,
ar: 5
}
]
</script>
I've got an array like that:
const arr = [
[{rivals: ['player1','player2'], winner: "player1", player1Scored: 2, player2Scored: 1}],
[{rivals: ['player1','player3'], winner: "none", player1Scored: 2, player3Scored: 2}],
[{rivals: ['player2','player3'], winner: "player3", player2Scored: 1, player3Scored: 3}],
[{rivals: ['player1','player4'], winner: "none", player1Scored: 1, player4Scored: 1}]
]
I need to count scored points of every player, so it'll look like that:
{player1Scored: 5, player2Scored: 2, player3Scored: 5, player4Scored:1}
I tried this:
let scoreResult = arr.reduce((result, {0: obj}) => {
obj.rivals.forEach(rival => result[`${rival}Scored`] = result[`${rival}Scored`] || 0);
obj.rivals.forEach(rival => result[`${rival}Scored`] += obj.player1Scored)
return result;
}, {});
My mistake is that I'm asigning points of one player to two of them but cannot solve that.
Thank you for your help
To answer the problem, you have nested arrays with a single object. You could take a destructuring with the array and take the firts item as object.
Then take the names from rivals array and add the scores.
const
array = [[{ rivals: ['player1', 'player2'], winner: "player1", player1Scored: 2, player2Scored: 1 }], [{ rivals: ['player1', 'player3'], winner: "none", player1Scored: 2, player3Scored: 2 }], [{ rivals: ['player2', 'player3'], winner: "player3", player2Scored: 1, player3Scored: 3 }], [{ rivals: ['player1', 'player4'], winner: "none", player1Scored: 1, player4Scored: 1 }]],
result = array.reduce((r, [o]) => {
o.rivals.forEach(k => r[k + 'Scored'] = (r[k + 'Scored'] || 0) + o[k + 'Scored']);
return r;
}, Object.create(null));
console.log(result);
try this
scores = {}
arr.map(s => s[0]).forEach(s => {
s.rivals.forEach(r => {
scores[r] = scores[r] || 0;
scores[r] += s[`${r}Scored`]
})
})