Creating a reduced set and nested array within an array of objects - javascript

I'm trying to wrap my head around transforming an array of "flat" objects, into a condensed but nested version:
const startingArray = [
{ name: 'one', id: 100, thing: 1 },
{ name: 'one', id: 100, thing: 2 },
{ name: 'one', id: 100, thing: 4 },
{ name: 'two', id: 200, thing: 5 }
];
/*
desiredResult = [
{name: 'one', id:100, things: [
{thing: 1}, {thing: 2}, {thing:4}
]},
{name: 'two', id:200, things: [
{thing: 5}
]}
]
*/
// THIS DOES NOT WORK
const result = startingArray.reduce((acc, curr) => {
if (acc.name) {
acc.things.push(curr.thing)
}
return { name: curr.name, id: curr.id, things: [{thing: curr.thing}] };
}, {});
What am I not understanding?!

In your reduce callback, acc is not "each element of the array" (that's what curr is) or "the matching element in the results" (you have to determine this yourself), it's the accumulated object being transformed with each call to the function.
That is to say, when you return { name: curr.name, id: curr.id, things: [{thing: curr.thing}] };, it sets acc to that object for the next iteration, discarding whatever data was contained in it before - acc.name will only ever hold the last name iterated over and the results will never accumulate into anything meaningful.
What you want to do is accumulate the results in an array (because it's your desired output), making sure to return that array each iteration:
const startingArray = [
{ name: 'one', id: 100, thing: 1 },
{ name: 'one', id: 100, thing: 2 },
{ name: 'one', id: 100, thing: 4 },
{ name: 'two', id: 200, thing: 5 }
];
const result = startingArray.reduce((acc, curr) => {
let existing = acc.find(o => o.id == curr.id);
if(!existing) acc.push(existing = { name: curr.name, id: curr.id, things: [] });
existing.things.push({thing: curr.thing});
return acc;
}, []);
console.log(result);
Because of your desired result format this involves quite a few acc.find() calls, which is expensive - you can get around this in a concise way with this trick, using the first element of the accumulator array as a mapping of ids to references (also featuring ES6 destructuring):
const startingArray = [
{ name: 'one', id: 100, thing: 1 },
{ name: 'one', id: 100, thing: 2 },
{ name: 'one', id: 100, thing: 4 },
{ name: 'two', id: 200, thing: 5 }
];
const result = startingArray.reduce((acc, {name, id, thing}) => {
if(!acc[0][id])
acc.push(acc[0][id] = { name, id, things: [] });
acc[0][id].things.push({thing});
return acc;
}, [{}]).slice(1); //slice off the mapping as it's no longer needed
console.log(result);

Ok, providing alternative way. Key point is that you accumulate an Object and later take only the values, so get an Array:
const startingArray = [
{ name: 'one', id: 100, thing: 1 },
{ name: 'one', id: 100, thing: 2 },
{ name: 'one', id: 100, thing: 4 },
{ name: 'two', id: 200, thing: 5 }
];
const res = startingArray.reduce((acc, curr) => {
if (acc[curr.name]) {
acc[curr.name].things.push({thing: curr.thing})
} else {
acc[curr.name] = {
name: curr.name,
id: curr.id,
things: [{thing: curr.thing}]
}
}
return acc
}, {})
console.log(Object.values(res))

Related

How group and count elements by lodash?

My data:
[{
id: 1,
name: 'foo'
}, {
id: 2,
name: 'bar'
}, {
id: 1,
name: 'foo'
}
];
I want to do something like this:
[
{
name: "foo",
id: 1,
count: 2
},
{
name: "bar",
id: 2,
count: 1
}
]
Now i'm grouping the elements with groupBy by name.
Thanks for the help!
var source = [{
id: 1,
name: 'foo'
}, {
id: 2,
name: 'bar'
}, {
id: 1,
name: 'foo'
}
];
var result = _(source)
.groupBy('name')
.map(function(items, name) {
return {
name: name,
count: items.length
}
}).value();
console.log(result);
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.4/lodash.js"></script>
It's not exactly what you asked for, but here is a simple method without using lodash. Here I'm using the ID of each element, but you can use anything that uniquely identifies it.
const objArray = [{
id: 1,
name: 'foo'
}, {
id: 2,
name: 'bar'
}, {
id: 1,
name: 'foo'
}
];
const reduceAndCount = (arr) => {
const newMap = {};
arr.forEach(el => {
if (!newMap[el.id]) {
el.count = 1
return newMap[el.id] = el
}
newMap[el.id].count = newMap[el.id].count + 1
})
return Object.values(newMap)
}
const result = reduceAndCount(objArray)
console.log(result)
With deference to the answer provided here which uses 'lodash', as requested in the above question, the below points are observed:
the 'groupBy' is fixed (ie, the 'name' field/column/prop) is used
the result given includes the name & count, but the expected result needs the 'id' as well.
EDIT:
Adding solution using lodash,
const arrayRaw = [{
id: 1,
name: 'foo'
}, {
id: 2,
name: 'bar'
}, {
id: 1,
name: 'foo'
}];
const groupBy = (col = 'name', arr = arrayRaw) => (
_(arr).groupBy(col).map(it => ({
...it[0],
count: it.length
})).value()
);
console.log(groupBy());
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/1.2.1/lodash.min.js"></script>
In case, someone requires a solution without 'lodash', the below should be one possible implementation:
const arrayRaw = [{
id: 1,
name: 'foo'
}, {
id: 2,
name: 'bar'
}, {
id: 1,
name: 'foo'
}];
const groupBy = (col = 'name', arr = arrayRaw) => (
Object.entries(arr.reduce((fin, itm, idx) => ({
...fin,
[itm[col]]: (fin[itm[col]] || []).concat([idx])
}), {})).map(([k, v]) => ({
...arr[v[0]],
count: v.length
}))
);
console.log(groupBy());
Approach / Explanation
It is self-explanatory, however, should anyone require one - please comment and I shall add.

Common values in array of arrays - lodash

I have an array that looks like this:
const myArray = [
[
{id: 1, name: 'Liam'},
{id: 2, name: 'Oliver'},
{id: 3, name: 'Jake'},
],
[
{id: 1, name: 'Liam'},
{id: 2, name: 'Oliver'},
{id: 4, name: 'Joe'},
],
]
I need to find common elements by id, and return them in an array that would look something like this:
[
{id: 1, name: 'Liam'},
{id: 2, name: 'Oliver'},
]
If there isn't any way to do it with lodash, just JS could work too.
Note that I do not know how many of these arrays I will have inside, so it should work for any number.
You can use lodash's _.intersectionBy(). You'll need to spread myArray because _intersectionBy() expect arrays as arguments, and not a single array of array:
const myArray = [[{"id":1,"name":"Liam"},{"id":2,"name":"Oliver"},{"id":3,"name":"Jake"}],[{"id":1,"name":"Liam"},{"id":2,"name":"Oliver"},{"id":4,"name":"Joe"}]]
const result = _.intersectionBy(...myArray, 'id')
console.log(result)
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.20/lodash.min.js" integrity="sha512-90vH1Z83AJY9DmlWa8WkjkV79yfS2n2Oxhsi2dZbIv0nC4E6m5AbH8Nh156kkM7JePmqD6tcZsfad1ueoaovww==" crossorigin="anonymous"></script>
A vanilla solution can be as simple as a filter() call on the first element of the array checking to see that every() subsequent element contains some() elements that match.
const [srcElement, ...compArray] = [...myArray];
const intersection = srcElement.filter(o => (
compArray.every(arr => arr.some(p => p.id === o.id)))
);
console.log(intersection)
.as-console-wrapper { max-height: 100% !important; top: 0; }
<script>
const myArray = [
[{ id: 1, name: 'Liam' }, { id: 2, name: 'Oliver' }, { id: 3, name: 'Jake' }],
[{ id: 1, name: 'Liam' }, { id: 2, name: 'Oliver' }, { id: 4, name: 'Joe' }],
[{ id: 1, name: 'Liam' }, { id: 2, name: 'Oliver' }, { id: 5, name: 'Dean' }, { id: 6, name: 'Mara' }]
]
</script>
Use nested forEach loops and Set. Go over each sub-array and find out the common items so far.
const intersection = ([firstArr, ...restArr]) => {
let common = new Set(firstArr.map(({ id }) => id));
restArr.forEach((arr) => {
const newCommon = new Set();
arr.forEach(({ id }) => common.has(id) && newCommon.add(id));
common = newCommon;
});
return firstArr.filter(({ id }) => common.has(id));
};
const myArray = [
[
{ id: 1, name: "Liam" },
{ id: 2, name: "Oliver" },
{ id: 3, name: "Jake" },
],
[
{ id: 1, name: "Liam" },
{ id: 2, name: "Oliver" },
{ id: 4, name: "Joe" },
],
[
{ id: 2, name: "Oliver" },
{ id: 4, name: "Joe" },
],
];
console.log(intersection(myArray));
Nowadays vanilla ES is pretty powerful to work with collections in a functional way even without the help of utility libraries.
You can use regular Array's methods to get a pure JS solution.
I've created two examples with pure JS.
Of course, there could be more approaches as well. And if you already use Lodash in your application, probably it would be better to just use its high-level implementation in form of _.intersectionBy() proposed above to reduce the code complexity.
const myArray = [
[
{id: 1, name: 'Liam'},
{id: 2, name: 'Oliver'},
{id: 3, name: 'Jake'},
],
[
{id: 1, name: 'Liam'},
{id: 2, name: 'Oliver'},
{id: 4, name: 'Joe'},
],
];
// Regular functional filter-reduce
const reducer = (accum, x) => {
return accum.findIndex(y => x.id == y.id) < 0
? [...accum, x]
: accum;
};
const resultFilterReduce = myArray
.flat()
.filter(x => myArray.every(y => y.findIndex(obj => obj.id === x.id) > -1))
.reduce(reducer, []);
console.log(resultFilterReduce);
// Filter-reduce with using of "HashMap" to remove duplicates
const resultWithHashMap = Object.values(
myArray
.flat()
.filter(x => myArray.every(y => y.findIndex(obj => obj.id === x.id) > -1))
.reduce((accum, x) => {
accum[x.id] = x;
return accum;
}, {})
);
console.log(resultWithHashMap);

Remove duplication of Id?

Why spread is not removing the duplication from the Items?
I am using spread to merge two variables resultA and resultB. There are two duplicated Id (222) in DataA and DataB which should be removed when using spread. Why it did not happen and how to solve this?
const DataA = [
{ Id: 111, Name: 'A' },
{ Id: 222, Name: 'B' },
{ Id: 333, Name: 'C' }
]
const DataB = [
{ Id: 999, Name: 'A' },
{ Id: 222, Name: 'B' },
{ Id: 444, Name: 'C' }
]
let resultA = (DataA || []).map(item => item.Id);
let resultB = (DataB || []).map(item => item.Id);
const items = [
...resultA,
...resultB,
]
const mapping = {
Total: items.length,
Items: items.map(item => { return { Id: item }})
}
console.log(mapping);
Expected Result:
{ Total: 5,
Items:
[ { Id: 111 },
{ Id: 222 },
{ Id: 333 },
{ Id: 999 },
{ Id: 444 } ]
}
The spread syntax on arrays doesn't remove duplicates. To get the result you wanted, Add a line
items = [...new Set(items)] after items
const DataA = [
{ Id: 111, Name: 'A' },
{ Id: 222, Name: 'B' },
{ Id: 333, Name: 'C' }
]
const DataB = [
{ Id: 999, Name: 'A' },
{ Id: 222, Name: 'B' },
{ Id: 444, Name: 'C' }
]
let resultA = (DataA || []).map(item => item.Id);
let resultB = (DataB || []).map(item => item.Id);
let items = [
...resultA,
...resultB,
]
items = [...new Set(items)] // this will remove duplicates
const mapping = {
Total: items.length,
Items: items.map(item => { return { Id: item }})
}
console.log(mapping);
I don't know where you've read that spread removes duplicates, but it does not. Luckily, there are a number of simple ways to achieve this - below is one option that involves very little modification to your code. Simply wrap the array containing items from the 2 spread arrays in a Set constructor (since I see you're already coding to the ES6 spec).
From MDN:
Set objects are collections of values. You can iterate through the
elements of a set in insertion order. A value in the Set may only
occur once; it is unique in the Set's collection.
Finally, you can simply wrap the Set in Array.from to get back to an array (among other ways to do this).
const DataA = [{
Id: 111,
Name: 'A'
},
{
Id: 222,
Name: 'B'
},
{
Id: 333,
Name: 'C'
}
]
const DataB = [{
Id: 999,
Name: 'A'
},
{
Id: 222,
Name: 'B'
},
{
Id: 444,
Name: 'C'
}
]
let resultA = (DataA || []).map(item => item.Id);
let resultB = (DataB || []).map(item => item.Id);
const items = Array.from(new Set([
...resultA,
...resultB,
]));
const mapping = {
Total: items.length,
Items: items.map(item => {
return {
Id: item
}
})
}
console.log(mapping);

Find Duplicate Object in List and Add Parameters

I am trying to find duplicate objects in a list of objects and add new parameters to the duplicate one.
Below snipped code is what I implemented so far. The problem is that it adds desired parameters to every object in the list.
const list = [{
id: 1,
name: 'test1'
},
{
id: 2,
name: 'test2'
},
{
id: 3,
name: 'test3'
},
{
id: 2,
name: 'test2'
}
];
const newList = list.reduce(
(unique, item) => (unique.includes(item) ? unique : [...unique, {
...item,
duplicated: true,
name: `${item.name}_${item.id}`
}]), []
);
console.log(newList);
Since there are two duplicate objects by id, the duplicated one should have duplicated and new name parameters. What part is wrong in my implementation?
By using findIndex method:
const list = [{
id: 1,
name: 'test1'
},
{
id: 2,
name: 'test2'
},
{
id: 3,
name: 'test3'
},
{
id: 2,
name: 'test2'
}
];
const newList = list.reduce(
(unique, item) => (unique.findIndex(x => x.id === item.id) > -1 ? [...unique, {
...item,
duplicated: true,
name: `${item.name}_${item.id}`
}] : [...unique, item]), []);
console.log(newList);
It can be written simply:
const
list = [
{ id: 1, name: 'test1' },
{ id: 2, name: 'test2' },
{ id: 3, name: 'test3' },
{ id: 2, name: 'test2' }
],
uniqueList = list.reduce((arr, { id, name }) =>
arr.concat({
id,
name,
...arr.some(item => id === item.id) && { duplicate: true, name: `${name}_${id}` }
}), []);
console.log(uniqueList);
The problem was that when you called includes you were actually looking for an object whose pointer exists in the array.
In order to find an object which has property are the same as a requested property, you have no choice but to use functions such as some or every that is different from includes - you can send them a callback and not just an object.

How to groupBy an object key inside nested array of objects?

I have a nested array of objects and I want to groupBy id and form a new array. Here's my array:
mainArray = [
{ name: 'a',age: 10, company: [ { desc: 'test1' , id: 6 }, { desc: 'testa' , id: 10 }] },
{ name: 'b',age: 20, company: [ { desc: 'test2' , id: 30 }] },
{ name: 'c',age: 40, company: [ { desc: 'test3' , id: 10 }, { desc: 'testc' , id: 30 }] }
]
I can flatten the entire array but it doesn't seem like the right way to do it.
My new array should look like something like this:
result = [
comapny_6: [
{
name: 'a',
age: 10,
desc: 'test1'
},
],
comapny_10: [
{
name: 'a',
age: 10,
desc: 'testa'
},
{
name: 'c',
age: 40,
desc: 'test3'
}
],
company_30 :[
{
name: 'b',
age: 20,
desc: 'test2'
},
{
name: 'c',
age: 40,
desc: 'testc'
}
]
]
I am open to suggestions on how the final data structure should look like. The bottom line is I want groupBy id so that I have information about each company separated out.
You can use reduce to loop thru the array and construct the desired object output. Use forEach to loop thru company
var mainArray = [{"name":"a","age":10,"company":[{"desc":"test1","id":6},{"desc":"testa","id":10}]},{"name":"b","age":20,"company":[{"desc":"test2","id":30}]},{"name":"c","age":40,"company":[{"desc":"test3","id":10},{"desc":"testc","id":30}]}];
var result = mainArray.reduce((c, {name,age,company}) => {
company.forEach(({id,desc}) => (c["company_" + id] = c["company_" + id] || []).push({name,age,desc}));
return c;
}, {});
console.log(result);
You can first create a 1D array using flatMap() and then use reduce() to group
const mainArray = [
{ name: 'a',age: 10, company: [ { desc: 'test1' , id: 6 }, { desc: 'testa' , id: 10 }] },
{ name: 'b',age: 20, company: [ { desc: 'test2' , id: 30 }] },
{ name: 'c',age: 40, company: [ { desc: 'test3' , id: 10 }, { desc: 'testc' , id: 30 }] }
]
const flat = mainArray.flatMap(({company,...rest}) => company.map(a => ({...rest,...a})));
const res = flat.reduce((ac,{id,...rest}) => ((ac[`company_${id}`] || (ac[`company_${id}`] = [])).push(rest),ac),{})
console.log(res)
Explanation
reduce() is method with returns a single value after iterating through all the array. The accumulator i.e ac in above case is set to empty object {}(which is the second argument passed to function)
In each iteration we return the updated accumulator which becomes ac for next iteration. So what we return from function is following expression
((ac[`company_${id}`] || (ac[`company_${id}`] = [])).push(rest),ac)
ac[company_${id}] is using Bracket Notation which takes an expression company_${id}. It is same as
ac["company_" + id]
The above line checks if ac[company_${id}] exists in the ac then push() rest to the it.
If ac[company_${id}] is not created yet they set it to empty array [] then push() the rest to it.
The last part uses comma operator
((ac[`company_${id}`] || (ac[`company_${id}`] = [])).push(rest),ac)
The above whole expression will evaluate to the last value separated by comma , which is ac. So in each iteration we are pushing rest to the respective array and returning ac it the end. The code is equivalent to
const res = flat.reduce((ac,{id,...rest}) => {
//check if company id doesnot exist as key in ac then set it empty array
if(!ac[`company_${id}`]) ac[`company_${id}`] = [];
//push rest(which will be an object with all the keys expect id)
ac[`company_${id}`].push(rest)
//at last return ac
return ac;
})
You can achieve this with Array.reduce and inside it with an Array.forEach over the array of companies like this:
let data = [ { name: 'a',age: 10, company: [ { desc: 'test1' , id: 6 }, { desc: 'testa' , id: 10 }] }, { name: 'b',age: 20, company: [ { desc: 'test2' , id: 30 }] }, { name: 'c',age: 40, company: [ { desc: 'test3' , id: 10 }, { desc: 'testc' , id: 30 }] } ]
let result = data.reduce((r,{ name, age, company }) => {
company.forEach(({ id, desc }) =>
r[`company_${id}`] = (r[`company_${id}`] || []).concat({ name, age, desc }))
return r
}, {})
console.log(result)

Categories

Resources