Intersection of multiple arrays base on properties - javascript

I want to find the common elements of multiple array of objects based on a common property. In addition, if an element appears more than once, I want the resulting array to reflect the number of times it occurs in all the arrays.
I tried the following:
var arr = [
[
{ name: 'kiwi', value: 12 },
{ name: 'apple', value: 5 },
{ name: 'apple', value: 12 },
{ name: 'pizza', value: 33 },
{ name: 'pizza', value: 24 },
{ name: 'fish', value: 5 },
{ name: 'milk', value: 5 },
{ name: 'banana', value: 7 },
{ name: 'orange', value: 11 },
],
[
{ name: 'taco', value: 23 },
{ name: 'pizza', value: 78 },
{ name: 'apple', value: 12 },
{ name: 'pizza', value: 33 },
{ name: 'pizza', value: 24 },
{ name: 'fish', value: 5 },
{ name: 'pie', value: 1 },
{ name: 'cake', value: 3 },
{ name: 'banana', value: 7 },
{ name: 'beef', value: 123 },
{ name: 'lime', value: 72 },
{ name: 'pizza', value: 34 },
],
[
{ name: 'apple', value: 12 },
{ name: 'pizza', value: 33 },
{ name: 'pizza', value: 24 },
{ name: 'pizza', value: 23 },
{ name: 'fish', value: 5 },
{ name: 'banana', value: 7 },
{ name: 'banana', value: 77 },
]
];
function findArraysWithCommonName(arr) {
let arrays = [...arr];
var result = arrays.shift().reduce(function(res, v) {
if (arrays.every(function(a) {
return (a.filter(function(e) {
return e.name === v.name
}).length > 0);
})) res.push(v);
return res;
}, []);
return result;
}
console.log(findArraysWithCommonName(arr))
The result I got is:
[
{name: "apple", value: 5},
{name: "apple", value: 12},
{name: "pizza", value: 33},
{name: "pizza", value: 24},
{name: "fish", value: 5},
{name: "banana", value: 7}
]
I expect the output to be:
[
{name: "apple", value: 12},
{name: "pizza", value: 33},
{name: "pizza", value: 24},
{name: "fish", value: 5},
{name: "banana", value: 7}
]
or
[
{name: "apple", value: 5},
{name: "pizza", value: 33},
{name: "pizza", value: 24},
{name: "fish", value: 5},
{name: "banana", value: 7}
]

One approach would be to build a map that relates an object to it's "count" in the array (ie the number of times that object occours in arr).
This can be done via .reduce() where you serialize each object to a string via JSON.stringify(obj) - this string is a unique encoding of the corresponding object shape and state which is used as the key to identify the objects of this form in the mapping. The key is used to query and update the "count" value of the mapping, for each object encountered in the arr.
Once the mapping has been build, filter mapping entries by those with a "count" value greater than one.
Finally for any filtered entries, deserialize the corresponding keys of those entries via .map() to obtain an array of objects that occoured more that one in the original arr.
This approach could be implemented as:
var arr=[[{name:'kiwi',value:12},{name:'apple',value:5},{name:'apple',value:12},{name:'pizza',value:33},{name:'pizza',value:24},{name:'fish',value:5},{name:'milk',value:5},{name:'banana',value:7},{name:'orange',value:11}],[{name:'taco',value:23},{name:'pizza',value:78},{name:'apple',value:12},{name:'pizza',value:33},{name:'pizza',value:24},{name:'fish',value:5},{name:'pie',value:1},{name:'cake',value:3},{name:'banana',value:7},{name:'beef',value:123},{name:'lime',value:72},{name:'pizza',value:34}],[{name:'apple',value:12},{name:'pizza',value:33},{name:'pizza',value:24},{name:'pizza',value:23},{name:'fish',value:5},{name:'banana',value:7},{name:'banana',value:77}]];
/* Flatten array heirachy */
const flatArr = arr.flat();
/* Obtain a count mapping for each object's occourance in flatArr */
const mapObjectToCount = flatArr.reduce((map, item) => {
const key = JSON.stringify(item);
const count = (map[key] ? map[key] : 0) + 1;
return { ...map, [ key ] : count };
}, {})
/* Get key/value pair of the prior mapping, filter the objects by
those that occour more that one time, and obtain the original object
by parsing the key */
const result = Object.entries(mapObjectToCount)
.filter(([json, count]) => count > 1)
.map(([json]) => JSON.parse(json));
console.log(result)

I'd first transform each subarray into an object indexed by the number of occurences of each name. Then, iterate through each of those sub-objects created, creating a new object whose values are the minimum of the values found on the combined object, for every key.
Lastly, return a .filter of the first array, checking whether the occurence count of the name being iterated over on that object is greater than 0, reducing that count by one when found:
function findArraysWithCommonName(arr) {
const [oneArr, ...rest] = arr;
/* Transform each subarray into, eg:
{
"taco": 1,
"pizza": 4,
"apple": 1,
"fish": 1,
"pie": 1,
...
*/
const countsByName = rest.map(
subarr => subarr.reduce((a, { name }) => {
a[name] = (a[name] || 0) + 1;
return a;
}, {})
);
/* Combine the objects into one that contains only the minimum value for each property, eg:
{
"apple": 1,
"pizza": 3,
"fish": 1,
"banana": 1
}
*/
const combinedCountsByName = countsByName.reduce((a, countObj) => {
Object.entries(countObj).forEach(([key, val]) => {
countObj[key] = Math.min(a[key], val) || 0;
});
return countObj;
});
console.log(combinedCountsByName);
return oneArr.filter(({ name }) => {
if (combinedCountsByName[name] > 0) {
combinedCountsByName[name]--;
return true;
}
});
}
var arr = [
[
{ name: 'kiwi', value: 12 },
{ name: 'apple', value: 5 },
{ name: 'apple', value: 12 },
{ name: 'pizza', value: 33 },
{ name: 'pizza', value: 24 },
{ name: 'fish', value: 5 },
{ name: 'milk', value: 5 },
{ name: 'banana', value: 7 },
{ name: 'orange', value: 11 },
],
[
{ name: 'taco', value: 23 },
{ name: 'pizza', value: 78 },
{ name: 'apple', value: 12 },
{ name: 'pizza', value: 33 },
{ name: 'pizza', value: 24 },
{ name: 'fish', value: 5 },
{ name: 'pie', value: 1 },
{ name: 'cake', value: 3 },
{ name: 'banana', value: 7 },
{ name: 'beef', value: 123 },
{ name: 'lime', value: 72 },
{ name: 'pizza', value: 34 },
],
[
{ name: 'apple', value: 12 },
{ name: 'pizza', value: 33 },
{ name: 'pizza', value: 24 },
{ name: 'pizza', value: 23 },
{ name: 'fish', value: 5 },
{ name: 'banana', value: 7 },
{ name: 'banana', value: 77 },
]
];
console.log(findArraysWithCommonName(arr));

Related

How to get the sum of specific values in an array contained within another array

I have an object with an array which contains another array. I need to add up the values from these child arrays where the name matches each other.
let arr = {
expenses: [
{
id: 11,
freqs: [
{ name: "day", value: 100 },
{ name: "week", value: 200 },
{ name: "month", value: 300 },
],
},
{
id: 12,
freqs: [
{ name: "day", value: 100 },
{ name: "week", value: 200 },
{ name: "month", value: 300 },
],
},
{
id: 13,
freqs: [
{ name: "day", value: 100 },
{ name: "week", value: 200 },
{ name: "month", value: 300 },
],
},
],
};
In this example, I would need the results:
let result = [
{ name: "day", value: 300 },
{ name: "week", value: 600 },
{ name: "month", value: 900 },
];
I've been trying for ages with a combination of filter() and reduce() methods (unsure if these are the right way), but I just can't get it - it's really a headscratcher for me!
Thank you
This combines all the freqs into one array then sums their values into an object and then reformats that object to be an array of objects with the name and value keys.
const arr = {"expenses":[{"id":11,"freqs":[{"name":"day","value":100},{"name":"week","value":200},{"name":"month","value":300}]},{"id":12,"freqs":[{"name":"day","value":100},{"name":"week","value":200},{"name":"month","value":300}]},{"id":13,"freqs":[{"name":"day","value":100},{"name":"week","value":200},{"name":"month","value":300}]}]};
const res = Object.entries(
arr.expenses
.flatMap(({ freqs }) => freqs)
.reduce(
(acc, { name, value }) => Object.assign(acc, { [name]: (acc[name] ?? 0) + value }),
{}
)
).map(([name, value]) => ({ name, value }));
console.log(res);
If I didn't know the structure then this could be difficult. However, given your input, I think this should solve your problem.
// Your input
let arr = {
expenses: [
{
id: 11,
freqs: [
{ name: "day", value: 100 },
{ name: "week", value: 200 },
{ name: "month", value: 300 },
],
},
{
id: 12,
freqs: [
{ name: "day", value: 100 },
{ name: "week", value: 200 },
{ name: "month", value: 300 },
],
},
{
id: 13,
freqs: [
{ name: "day", value: 100 },
{ name: "week", value: 200 },
{ name: "month", value: 300 },
],
},
],
};
// My code
let result = new Array();
// Create each object structure and push into empty result array
arr.expenses[0].freqs.forEach((item)=>result.push({name: item.name, value: 0}));
// Map through each object in arr.expenses
arr.expenses.map((object)=>{
// Loop through each object in freqs
object.freqs.forEach((item)=>{
result.filter(eachItem=>{
// Check if result objs' name matches the freqs objs' name
if(eachItem.name==item.name){
eachItem.value+=item.value; // Add the values
}
})
});
});
// Check the output
console.log(result);
We want to reduce the frequences to a single element, so we can do:
let arr = { expenses: [ { id: 11, freqs: [ {name: "day", value: 100}, {name: "week", value: 200}, {name: "month", value: 300}, ], }, { id: 12, freqs: [ {name: "day", value: 100}, {name: "week", value: 200}, {name: "month", value: 300}, ], }, { id: 13, freqs: [ {name: "day", value: 100}, {name: "week", value: 200}, {name: "month", value: 300}, ], }, ], };
let result = arr.expenses.reduce((total, curr) => {
total[0].value += curr.freqs[0].value
total[1].value += curr.freqs[1].value
total[2].value += curr.freqs[2].value
return total
}, [{name: "day", value: 0}, {name: "week", value: 0}, {name: "month", value: 0}])
console.log(result)

How to flat array of objects which can contain nested objects in my case

I have a data with structure like this:
const arr1 = [
{
name: 'a',
subObjects: [
{ name: 'a1', age: 10 },
{ name: 'a2', age: 12 },
],
},
{ name: 'b', age: 23 },
{
name: 'c',
subObjects: [
{ name: 'c1', age: 30 },
{ name: 'c2', age: 32 },
],
},
...
];
So, the array contains an array of objects, some objects also contain nested level object subObjects which contains the same structure as parent. Overall some 1st level object in array can have maximum two levels of nest (like above example shows).
Now, I need to have an array that gather all names of objects from above array, something like:
[
{ name: 'a' },
{ name: 'a1' },
{ name: 'a2' },
{ name: 'b' },
{ name: 'c' },
{ name: 'c1' },
{ name: 'c2' },
];
This is what I tried:
const arr1 = [
{
name: 'a',
subObjects: [
{ name: 'a1', age: 10 },
{ name: 'a2', age: 12 },
],
},
{ name: 'b', age: 23 },
{
name: 'c',
subObjects: [
{ name: 'c1', age: 30 },
{ name: 'c2', age: 32 },
],
},
];
const arr2 = arr1.map((obj) => {
return obj.subObjects ? obj.subObjects.flat() : obj.name;
});
console.log(arr2.flat());
But the output lost the 1st level object names for those who has nested objects. So, what is the best way to achieve what I need?
You could use a recursive flatMap to do it (with a little help from the spread oparator!):
const arr1 = [{name: 'a', subObjects:[{name: 'a1', age: 10}, {name: 'a2', age: 12},]}, {name: 'b', age: 23}, {name: 'c', subObjects:[{name: 'c1', age: 30}, {name: 'c2', age: 32},]}];
const recursiveFlat = (arr) => arr.flatMap(
a => a.subObjects
? [{name: a.name}, ...recursiveFlat(a.subObjects)]
: {name: a.name});
console.log(recursiveFlat(arr1));
This will work with any depth of nesting.

How to combine duplicate object value in the array then output a list?

I've been working on this couple hours and google around, see lots of example for remove duplicate, but not combined the value. so I hope someone can help me out here.
I want to check the item.name is the same, then add the price together then push to new list array.
const items = [
{ name: 'apple', price: '10' },
{ name: 'banana', price: '1' },
{ name: 'orange', price: '2' },
{ name: 'apple', price: '5' },
{ name: 'orange', price: '2.5' },
{ name: 'banana', price: '3' },
{ name: 'strawberry', price: '7' },
{ name: 'apple', price: '12' }
]
let newItem = []
const checkItem = items.map((prev, next) => {
if (prev.name === next.name) {
return newItem.push = {
name: next.name,
value: parseInt(prev.price) + parseInt(next.price)
}
}
});
console.log(newItem)
Big thanks for the help!
This will work.You can use reduce with Find.
const items = [{
name: 'apple',
price: '10'
},
{
name: 'banana',
price: '1'
},
{
name: 'orange',
price: '2'
},
{
name: 'apple',
price: '5'
},
{
name: 'orange',
price: '2.5'
},
{
name: 'banana',
price: '3'
},
{
name: 'strawberry',
price: '7'
},
{
name: 'apple',
price: '12'
}
]
let result = items.reduce((acc, el) => {
if (acc.filter(ele => ele.name == el.name).length == 0) {
acc.push(el);
} else {
let filtered = acc.find(ele => ele.name == el.name)
filtered.price = parseFloat(filtered.price) + parseFloat(el.price);
}
return acc;
}, [])
console.log(result)
var new_array = arr.map(function callback(currentValue[, index[, array]]) {
// Return element for new_array
}[, thisArg])
The Array​.prototype​.map()'s callback functions first two arguments are currentValue i.e item of the array and second value is it's index, & not prev and next elements.
What you are looking for is something like this.
const items = [
{ name: "apple", price: "10" },
{ name: "banana", price: "1" },
{ name: "orange", price: "2" },
{ name: "apple", price: "5" },
{ name: "orange", price: "2.5" },
{ name: "banana", price: "3" },
{ name: "strawberry", price: "7" },
{ name: "apple", price: "12" }
];
const combine = items.reduce((acc, item) => {
if (acc[item.name] !== undefined) {
acc[item.name] += Number(item.price);
} else acc[item.name] = Number(item.price);
return acc;
}, {});
const fruitKeys = Object.keys(combine);
newItem = fruitKeys.map(item => ({ name: item, price: combine[item] }));
console.log(newItem);
I have split the solution into two steps, namely combine and reconstruction of the object so that you can clearly see what's happening.
I highly recommend you to refer the documentation for reduce method to understand its working

How to sort array of objects and shift some type of strings to end of the list? [duplicate]

This question already has answers here:
Sort array of objects by string property value
(57 answers)
Closed 3 years ago.
I'm trying to sort array of objects by "name" key. Here some values starts with 'summer', after sorting need to place those objects to be end of the list. Have any idea look this sorting once.
This is my list:
var list = [
{ name: 'Summer_Mango', id: 20055 },
{ name: 'Orange', id: 20053 },
{ name: 'Apple', id: 45652 },
{ name: 'Grape', id: 20066 },
{ name: 'Summer_Watermelon', id: 20073 },
{ name: 'Banana', id: 20010 }
];
After sorting my list need to come like this
var list = [
{ name: 'Apple', id: 45652 },
{ name: 'Banana', id: 20010 }
{ name: 'Grape', id: 20066 },
{ name: 'Orange', id: 20053 },
{ name: 'Summer_Mango', id: 20055 },
{ name: 'Summer_Watermelon', id: 20073 },
];
You can customize sort condition for your requirement.
list = list.sort((a, b) => {
if (a.name.startsWith('Summer')) {return 1; }
if (b.name.startsWith('Summer')) {return -1;}
return a.name.localeCompare(b.name)
});
var list = [
{ name: 'Summer_Mango', id: 20055 },
{ name: 'Orange', id: 20053 },
{ name: 'Apple', id: 45652 },
{ name: 'Grape', id: 20066 },
{ name: 'Summer_Watermelon', id: 20073 },
{ name: 'Banana', id: 20010 }
];
list = list.sort((a, b) => {
if (a.name.startsWith('Summer')) {return 1; }
if (b.name.startsWith('Summer')) {return -1;}
return a.name.localeCompare(b.name)
});
console.log(list)

Reduce array by its items key in ES6

I've got array of fruits:
const fruits = [
{ name: 'apple', count: 2 },
{ name: 'apple', count: 4 },
{ name: 'banana', count: 5 },
{ name: 'apple', count: 3 },
{ name: 'apple', count: 4 },
{ name: 'apple', count: 1 },
{ name: 'banana', count: 3 },
{ name: 'apple', count: 1 },
{ name: 'orange', count: 2 }
];
What I try to achieve is to reduce this array so it look like this:
let fruitsReduces = [
{ name: 'apple', count: 15 },
{ name: 'banana', count: 8 },
{ name: 'orange', count: 2 }
];
I've done this using very ugly for loop:
for (let i = 0; i < fruits.length; i++) {
fruitItem = {};
fruitItem.name = fruits[i].name;
fruitItem.count = fruits[i].count;
const fruitAlreadyInTheArray = fruitsReduced.find(fruit => fruit.name === fruitItem.name);
if (!fruitAlreadyInTheArray) {
fruitsReduced.push(fruitItem);
} else {
fruitAlreadyInTheArray.count += fruitItem.count;
}
}
But I'm sure that same thing can be done somehow by using ES6 array.reduce. Can you help me?
I looked at JS (ES6): Reduce array based on object attribute but I can't figure it out.
You can us reduce() the array into an object. Use Object.values() to convert the object into an array.
const fruits = [{"name":"apple","count":2},{"name":"apple","count":4},{"name":"banana","count":5},{"name":"apple","count":3},{"name":"apple","count":4},{"name":"apple","count":1},{"name":"banana","count":3},{"name":"apple","count":1},{"name":"orange","count":2}];
var result = Object.values(fruits.reduce((c, {name,count}) => {
c[name] = c[name] || {name,count: 0};
c[name].count += count;
return c;
}, {}));
console.log(result);
You could use an array as accumulator and seach for an inserted object with a wanted name for adding the actual count. If not found add a new object.
const
fruits = [{ name: 'apple', count: 2 }, { name: 'apple', count: 4 }, { name: 'banana', count: 5 }, { name: 'apple', count: 3 }, { name: 'apple', count: 4 }, { name: 'apple', count: 1 }, { name: 'banana', count: 3 }, { name: 'apple', count: 1 }, { name: 'orange', count: 2 }],
result = fruits.reduce((r, { name, count }) => {
var item = r.find(o => o.name === name);
if (!item) {
item = { name, count: 0 };
r.push(item);
}
item.count += count;
return r;
}, []);
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }
A different approach by collecting all counts first with a Map and render the wanted array with Array.from and a mapping function which builds new objects.
const
fruits = [{ name: 'apple', count: 2 }, { name: 'apple', count: 4 }, { name: 'banana', count: 5 }, { name: 'apple', count: 3 }, { name: 'apple', count: 4 }, { name: 'apple', count: 1 }, { name: 'banana', count: 3 }, { name: 'apple', count: 1 }, { name: 'orange', count: 2 }],
result = Array.from(
fruits.reduce(
(m, { name, count }) => m.set(name, (m.get(name) || 0) + count),
new Map
),
([name, count]) => ({ name, count })
);
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }
You can try to reduce() like
const fruits = [
{ name: 'apple', count: 2 },
{ name: 'apple', count: 4 },
{ name: 'banana', count: 5 },
{ name: 'apple', count: 3 },
{ name: 'apple', count: 4 },
{ name: 'apple', count: 1 },
{ name: 'banana', count: 3 },
{ name: 'apple', count: 1 },
{ name: 'orange', count: 2 }
];
function getReduced(total, {name,count}) {
total[name] = total[name] || {name,count: 0};
total[name].count += count;
return total;
}
let fruitsReduces = Object.values(fruits.reduce(getReduced,{}));
console.log(fruitsReduces);
Yet another (slightly different) solution
const fruits = [
{ name: 'apple', count: 2 },
{ name: 'apple', count: 4 },
{ name: 'banana', count: 5 },
{ name: 'apple', count: 3 },
{ name: 'apple', count: 4 },
{ name: 'apple', count: 1 },
{ name: 'banana', count: 3 },
{ name: 'apple', count: 1 },
{ name: 'orange', count: 2 } ];
const reduced = Object.entries (
fruits.reduce( (p, o) =>
p[o.name] ? (p[o.name] += o.count, p) : {...p, ...{[o.name]: o.count}}, {} ) )
.map(([key, value]) => ({name: key, count: value}));
console.log(reduced);
You might use a hashtable to build up the resultig array:
const result = [], hash = {};
for(const {name, count} of fruits) {
if(hash[name]) {
hash[name].count += count;
} else {
result.push(hash[name] = {name, count});
}
}
You can use a simple forEach method like this:
const res = {};
fruits.forEach((e) => res[e.name] = (res[e.name] || 0) + e.count);
res will contain your expected result.

Categories