Remove duplicates from nested array in array of objects - javascript

I have this array of objects:
const arrayOfObjects = [{
id: 10,
children: [1000]
},
{
id: 10,
children: [2000]
},
{
id: 20,
children: [1000]
},
{
id: 20,
children: [1000, 2000]
},
{
id: 20,
children: [2000]
},
];
I want to remove duplicates using this code:
const arrayHashMap = arrayOfObjects.reduce((obj, item) => {
if (obj[item.id]) {
// obj[item.id].children.push(...item.children);
const temporaryArray = [...obj[item.id].children, ...item.children];
obj[item.id].children = [...new Set(temporaryArray)];
} else {
obj[item.id] = {
...item
};
}
return obj;
}, {});
const result = Object.values(arrayHashMap);
In this code I commented part where I push values to array. I tried to use "new Set" to remove duplicates from final array, but I am always assigning the value to "obj[item.id].children". Is this OK or is there a better way to write this?
Expected result:
[{
id: 10,
children: [1000, 2000]
}, {
id: 20,
children: [1000, 2000]
}]
Thanks

You could group by id and check the array if the value not exists, then push the value.
const
data = [{ id: 10, children: [1000] }, { id: 10, children: [2000] }, { id: 20, children: [1000] }, { id: 20, children: [1000, 2000] }, { id: 20, children: [2000] }],
result = Object.values(data.reduce((r, { id, children }) => {
r[id] ??= { id, children: [] };
children.forEach(v => {
if (!r[id].children.includes(v)) r[id].children.push(v);
})
return r;
}, {}));
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }

Use Array#prototype#reduce to reduce over the array and initialize a set over the children property and keep on adding to the set and lastly map the set back to an array.
const arrayOfObjects = [{
id: 10,
children: [1000]
},
{
id: 10,
children: [2000]
},
{
id: 20,
children: [1000]
},
{
id: 20,
children: [1000, 2000]
},
{
id: 20,
children: [2000]
},
];
const result = Object.values(
arrayOfObjects.reduce((r, c) => {
r[c.id] = r[c.id] || {
id: c.id,
children: new Set()
};
c.children.forEach((item) => r[c.id].children.add(item));
return r;
}, Object.create(null))
)
.map((x) => ({
id: x.id,
children: [...x.children]
}));
console.log(result);

const arr = [
{
id: 10,
children: [1000],
},
{
id: 10,
children: [2000],
},
{
id: 20,
children: [1000],
},
{
id: 20,
children: [1000, 2000],
},
{
id: 20,
children: [2000],
},
];
let result = arr.reduce((acc, i) => {
let obj = acc.find((a) => a.id === i.id);
obj ? (obj.children = [...new Set(obj.children.concat(i.children))]): acc.push(i);
return acc;
}, []);
console.log(result);

you can try this fiddle : https://jsfiddle.net/d0kboywv/2/
const arrayOfObjects = [
{
id: 10,
children: [1000]
},
{
id: 10,
children: [2000]
},
{
id: 20,
children: [1000]
},
{
id: 20,
children: [1000, 2000, 3000]
},
{
id: 20,
children: [2000, 4000]
},
];
let mappedArray = new Map(arrayOfObjects.map(o => [o.id, {}] ));
for (let obj of arrayOfObjects) {
let child = mappedArray.get(obj.id);
for (let [key, val] of Object.entries(obj.children)) {
child[key] = (child[key] || new Set).add(val);
}
}
let result = Array.from(mappedArray.entries(), ([id, child]) => ({
id,
children: [...new Set(Object.entries(child).map(([k, v]) =>
[...v]
).reduce((a, b) => a.concat(b), []))].sort()
}));
console.log(result);
It do the job for me !

you can temporary transform data structure to more simple
const objectOfArray = {};
your id is key, your children is value
I use name initialData for refer to your array
const objectOfArray = {};
initialData.forEach(e => {
if (objectOfArray[e.id] {
objectOfArray[e.id].push(...e.children);
} else {
objectOfArray[e.id] = [...e.children];
}
});
const result = Object.entries(objectOfArray).map(([id, children]) => {
return {
id,
children: children.filter((e, i) => i === chilren.indexOf(i)),
}
});

You can also achieve expected output by running the below code
makeMapping = {};
for (let obj of arrayOfObjects) {
makeMapping[obj.id] = {...obj, children: [...new Set([...obj.children, ...(makeMapping[obj.id]?.children || [])])]};
}
console.log(Object.values(makeMapping));

i dont know about "better", but perhaps terser:
const arrayOfObjects = [{
id: 10,
children: [1000]
},
{
id: 10,
children: [2000]
},
{
id: 20,
children: [1000]
},
{
id: 20,
children: [1000, 2000]
},
{
id: 20,
children: [2000]
},
];
const arrayHashmap = arrayOfObjects.reduce((obj, {
id,
children
}) => ({
...obj,
[id]: {
id,
children: [...new Set([
...obj[id]?.children ?? [],
...children
])]
}
}), {})
const result = Object.values(arrayHashmap);
console.log(result)
edit: whoops, the "tidy" button changed semantics. fixed.

Related

Advanced filtering nested elements in TypeScript/JavaScript

Given the following structure and data:
interface GrandChild {
id: number,
values: Array<string>,
}
interface Child {
id: number,
subItems: Array<GrandChild>
}
interface Foo {
items: Array<Child>
}
const data: Foo = {
items: [
{ id: 1, subItems: [ { id: 10, values: ['10', '100'] }, { id: 11, values: ['11', '110', '1100'] } ] },
{ id: 2, subItems: [ { id: 20, values: ['REMOVE', 'REMOVE'] }, { id: 21, values: ['REMOVE'] } ] },
{ id: 3, subItems: [ { id: 30, values: ['REMOVE'] }, { id: 31, values: ['REMOVE'] }, { id: 32, values: ['REMOVE', '32'] } ] },
]
};
How can I use the Array's methods (filter, map, some, etc.) to achieve the following result?
const expected: Foo = {
items: [
{ id: 1, subItems: [ { id: 10, values: ['10', '100'] }, { id: 11, values: ['11', '110', '1100'] } ] },
{ id: 3, subItems: [ { id: 32, values: ['32'] } ] },
]
}
So far, I filtered the resulting data, removing the undesired elements, as following:
const filteredData: Foo = {
...data,
items: data.items.map(item => ({
...item,
subItems: item.subItems.map(subItem => ({
...subItem,
values: subItem.values.filter(value => value !== 'REMOVE')
}))
}))
}
Resulting:
{
items: [
{ id: 1, subItems: [ { id: 10, values: ['10', '100'] }, { id: 11, values: ['11', '110', '1100'] } ] },
{ id: 2, subItems: [ { id: 20, values: [] }, { id: 21, values: [] } ] },
{ id: 3, subItems: [ { id: 30, values: [] }, { id: 31, values: [] }, { id: 32, values: ['32'] } ] },
]
};
But, I cannot figure a way out to remove the empty subItems elements without looping through the result.
You can check online the above code here.
If you really want to do it just with filter and map, add a filter after each of your maps to remove subItems that have an empty values array and to remove items that have an empty subItems array:
const filteredData = {
...data,
items: data.items
.map((item) => ({
...item,
subItems: item.subItems
.map((subItem) => ({
...subItem,
values: subItem.values.filter((value) => value !== "REMOVE"),
}))
.filter(({ values }) => values.length > 0), // ***
}))
.filter(({subItems}) => subItems.length > 0), // ***
};
But:
When I have map followed by filter, I always ask myself if the data is large enough that I should avoid making multiple passes through it.
When I'm doing lots of nesting of map calls and such, I always ask myself if it would be clearer when reading the code later to use simpler, smaller loops.
Here's what you might do if answering "yes" to either or both of those questions:
const filteredData: Foo = {
...data,
items: [],
};
for (const item of data.items) {
const subItems: Array<GrandChild> = [];
for (const subItem of item.subItems) {
const values = subItem.values.filter((value) => value !== "REMOVE");
if (values.length) {
subItems.push({
...subItem,
values,
});
}
}
if (subItems.length > 0) {
filteredData.items.push({
...item,
subItems,
});
}
}

How to sort array with selected list in javascript?

I have a list look like:
const initArray = [
{
id: 0,
},
{
id: 1,
},
{
id: 2,
},
{
id: 3,
},
];
A selected list look like:
const selectedList = [
{
id: 2,
},
];
And the desired data has been sorted:
const outPut= [
{
id: 2,
},
{
id: 0,
},
{
id: 1,
},
{
id: 3,
},
];
I'm in trouble right now, so I can't figure it out yet.
Can you share some solutions?
You could take an object which keeps the order of the first objects and sort the rest after.
const
data = [{ id: 0 }, { id: 1 }, { id: 2 }, { id: 3 }],
selectedList = [{ id: 2 }],
order = Object.fromEntries(selectedList.map(({ id }, i) => [id, i + 1]));
data.sort((a, b) => (order[a.id] || Number.MAX_VALUE) - (order[b.id] || Number.MAX_VALUE));
console.log(data);
Using Set and Array#map, get set of ids to prioritize
Using Array#sort, sort the items using the above set
const _sort = (arr = [], selected = []) => {
const priority = new Set( selected.map(({ id }) => id) );
return [...arr].sort(({ id: a }, { id: b }) => priority.has(b) - priority.has(a));
}
const
initArray = [ { id: 0 }, { id: 1 }, { id: 2 }, { id: 3 } ],
selectedList = [ { id: 2 } ];
console.log( _sort(initArray, selectedList) );

How to locate and delete an item into array of objects

I have an array as:
const arr=[{
id: 9,
name: 'Patrimônios',
children: [],
},
{
id: 10,
name: 'Despesas',
children: [
{ id: 16, name: 'Manutenção' },
{ id: 17, name: 'Despesa' },
{ id: 18, name: 'Impostos' },
{ id: 19, name: 'Gráfica' },
],
},
....]
I´d like a function to delete a item by id. As we can see the item can be into 'children' or not. If the item is in the "children" I´d like to delete it from children array only.
I´d like something as:
findById(tree, nodeId) {
for (const node of tree) {
if (node.id === nodeId) return node
if (node.children && node.children.length) {
const desiredNode = this.findById(node.children, nodeId)
if (desiredNode) return desiredNode
}
}
return false
},
function deleteItemById(arr,id){
item=findById(arr,id)
if (item){
/// here How I can delete it?
}
}
How could I do that?
Here is a recursive function that matches your code's terminology to filter an object by id:
const removeObjectByIdRecursively = (list, id) => list.reduce((acc, item) => {
if (item.id === id) return acc;
if (item.children) {
return [
...acc,
{
...item,
children: removeObjectByIdRecursively(item.children, id)
}
];
}
return [
...acc,
item
];
}, []);
Super basic, untested and targeted directly at the shape of your data, but this shows how you can use Array.filter() to return a new array that doesn't contain the elements you want to remove by id.
const testArray = [
{ id: 0, value: "zero" },
{ id: 1, value: "one", children: [{ id: 3 }] },
{ id: 2, value: "two" },
{ id: 3, value: "three" },
{ id: 4, value: "four" },
];
const deleteObjectById = ({ ary, id }) => ary.filter((item) => {
const child = (item.children || []).find((c) => c.id === id);
return item.id !== id && !(!!child);
});
const filteredArray = deleteObjectById({
ary: testArray,
id: 3,
});
console.log(testArray);
console.log(filteredArray);

Better way to find data across multiple arrays

getComponentById: (state) => (componentId) => {
return state.articles
.filter(article => Object.keys(article).some(key => {
return ['maps', 'charts', 'tables'].includes(key);
}))
.reduce((acc, article) => {
acc = article.components?.find(c => c.id == componentId);
if (acc) return acc;
acc = article.maps?.find(c => c.id == componentId);
if (acc) return acc;
acc = article.charts?.find(c => c.id == componentId);
if (acc) return acc;
acc = article.tables?.find(c => c.id == componentId);
if (acc) return acc;
})
}
Wonder if there's a better way to rewrite this because the list of components might grow so it feels wrong to just keep adding the lines.
If the id is unique can you just look into every key on every article?
If my guess at your data structure is close you should be able to do something like this
let articles = [
{
maps: [{ id: 1, name: 'map1' }, { id: 2, name: 'map2' }],
charts: [{ id: 3, name: 'charts1' }, { id: 4, name: 'charts2' }],
tables: [{ id: 5, name: 'tables1' }, { id: 6, name: 'tables2' }]
},
{
maps: [{ id: 7, name: 'map3' }, { id: 8, name: 'map4' }],
charts: [{ id: 9, name: 'charts3' }, { id: 10, name: 'charts4' }],
tables: [{ id: 11, name: 'tables3' }, { id: 12, name: 'tables4' }]
}
]
let getComponentById = (componentId) => {
let result = null;
articles.forEach(article => {
Object.keys(article).forEach(key => {
let component = article[key].find(x=> x.id == componentId);
if(component) {
result = component;
}
});
});
return result;
}
console.log(getComponentById(3));
console.log(getComponentById(12));
Credit to #IrKenInvader's answer, I copy data from him.
I use for loop because once you find a component, you can early return and no need to check the rest of the data.
let state = {
articles: [
{
maps: [
{ id: 1, name: "map1" },
{ id: 2, name: "map2" },
],
charts: [
{ id: 3, name: "charts1" },
{ id: 4, name: "charts2" },
],
tables: [
{ id: 5, name: "tables1" },
{ id: 6, name: "tables2" },
],
},
{
maps: [
{ id: 7, name: "map3" },
{ id: 8, name: "map4" },
],
charts: [
{ id: 9, name: "charts3" },
{ id: 10, name: "charts4" },
],
tables: [
{ id: 11, name: "tables3" },
{ id: 12, name: "tables4" },
],
},
],
};
const getComponentById = state => componentId => {
for (let i = 0; i < state.articles.length; i++) {
const filteredKey = Object.keys(state.articles[i]).filter(key =>
["maps", "charts", "tables"].includes(key)
);
for (let j = 0; j < filteredKey.length; j++) {
const foundComponent = state.articles[i][filteredKey[j]].find(
a => a.id == componentId
);
if (foundComponent) return foundComponent;
}
}
return null;
};
const output = getComponentById(state)(12);
console.log(output);

Javascript find node from tree with recursive function

I have JavaScript tree data like this.
const tree = {
children:[
{id: 10, children: [{id: 34, children:[]}, {id: 35, children:[]}, {id: 36, children:[]}]},
{id: 10,
children: [
{id: 34, children:[
{id: 345, children:[]}
]},
{id: 35, children:[]},
{id: 36, children:[]}
]
},
{id: 11, children: [{id: 30, children:[]}, {id: 33, children:[]}, {id: 3109, children:[]}]}
],
id: 45
}
const getByID = (tree, id) => {
let result = null
if (id === tree.id) {
return tree
} else {
if(tree.children){
tree.children.forEach( node=> {
result = getByID(node, id)
})
}
return result
}
}
const find345 = getByID(tree, 345)
console.log(find345)
I was try to find item by its id from this tree. im using recursive function to iterate the tree and its children, but it wont find the item as i expected.
its always return null. expected to return {id: 345, children:[]}
You need to exit the loop by using a method which allows a short circuit on find.
The problem with visiting nodes but already found the node, is the replacement of the result with a later wrong result. You need to exit early with a found node.
Array#some allows to iterate and to exit the loop if a truty value is returned. In this case the result is truthy on find.
const tree = { children: [{ id: 10, children: [{ id: 34, children: [] }, { id: 35, children: [] }, { id: 36, children: [] }] }, { id: 10, children: [{ id: 34, children: [{ id: 345, children: [] }] }, { id: 35, children: [] }, { id: 36, children: [] }] }, { id: 11, children: [{ id: 30, children: [] }, { id: 33, children: [] }, { id: 3109, children: [] }] }], id: 45 };
const getByID = (tree, id) => {
let result = null
if (id === tree.id) {
return tree
} else {
if(tree.children){
tree.children.some(node => result = getByID(node, id));
// ^^^^ exit if found
// ^^^^^^^^^^^^^^^^^^^^^^^^^^ return & assign
}
return result;
}
}
const find345 = getByID(tree, 345)
console.log(find345)
A bit shorter
var tree = { children: [{ id: 10, children: [{ id: 34, children: [] }, { id: 35, children: [] }, { id: 36, children: [] }] }, { id: 10, children: [{ id: 34, children: [{ id: 345, children: [] }] }, { id: 35, children: [] }, { id: 36, children: [] }] }, { id: 11, children: [{ id: 30, children: [] }, { id: 33, children: [] }, { id: 3109, children: [] }] }], id: 45 },
getByID = (tree, id) => {
var temp;
return tree.id === id
? tree
: (tree.children || []).some(o => temp = getByID(o, id)) && temp;
};
console.log(getByID(tree, 345));
We can also use reduce method for recursive function
function findAllByKey(obj, keyToFind) {
return Object.entries(obj)
.reduce((acc, [key, value]) => (key === keyToFind)
? acc.concat(value)
: (typeof value === 'object' && value)
? acc.concat(findAllByKey(value, keyToFind))
: acc
, []) || [];
}

Categories