Javascript: flatten multidimensional array into array of unique paths - javascript

Im trying to build a list of all the unique paths down a tree /
multidimensional array of objects.
Assuming this data...
const data = [
{
id: '1',
items: [
{
id: '1.1',
items: [ { id: '1.1.1' }, { id: '1.1.2' }, { id: '1.1.3' }, ]
}
]
},
{
id: '2',
items: [
{
id: '2.1',
items: [ { id: '2.1.1' }, { id: '2.1.2' }, { id: '2.1.3' }, ]
},
{
id: '2.2',
items: [ { id: '2.2.1' } ]
}
]
}
]
I need to end up with an array structure like this..
const result = [
['1', '1.1', '1.1.1'],
['1', '1.1', '1.1.2'],
['1', '1.1', '1.1.3'],
['2', '2.1', '2.1.1'],
['2', '2.1', '2.1.2'],
['2', '2.1', '2.1.3'],
['2', '2.2', '2.2.1']
];
Where each entry is an array of a unique path down the original tree structure.
I'm having real trouble getting each path as a separate entry. What I have so far returns them the path down to the lower level and appends the bottom level ids to the current path.
function flatten(items, path = []) {
let result = [];
items.forEach( item => {
path.push(item.id);
if (item.items && item.items.length) {
result.push(flatten(item.items, path.slice(0) )); //slice to clone the array
}
else {
result.push(...path);
}
});
return result;
}
Here is a JS fiddle...
https://jsfiddle.net/9ptdm1ve/

You can use reduce() method to create recursive function and return array as a result. You can use concat() method to create copy of prev array so that on each level of recursion you have new copy, because arrays are passed by reference and otherwise you would be changing original array.
const data = [{"id":"1","items":[{"id":"1.1","items":[{"id":"1.1.1"},{"id":"1.1.2"},{"id":"1.1.3"}]}]},{"id":"2","items":[{"id":"2.1","items":[{"id":"2.1.1"},{"id":"2.1.2"},{"id":"2.1.3"}]},{"id":"2.2","items":[{"id":"2.2.1"}]}]}]
function build(data, prev = []) {
return data.reduce(function(r, e) {
const copy = prev.concat(e.id)
if (e.items) r = r.concat(build(e.items, copy))
else r.push(copy)
return r;
}, [])
}
const result = build(data);
console.log(result)

The updated path in the foreach callback is shared. It should be local.
function flatten(items, path = []) {
let result = [];
items.forEach(item => {
let localPath = path.slice(0);
localPath.push(item.id);
if (item.items && item.items.length) {
result.push(flatten(item.items, localPath));
} else {
result.push(localPath);
}
});
return result;
}

You can use an inner method that will push all complete paths to the same result array:
const data = [{"id":"1","items":[{"id":"1.1","items":[{"id":"1.1.1"},{"id":"1.1.2"},{"id":"1.1.3"}]}]},{"id":"2","items":[{"id":"2.1","items":[{"id":"2.1.1"},{"id":"2.1.2"},{"id":"2.1.3"}]},{"id":"2.2","items":[{"id":"2.2.1"}]}]}]
function flatten(items) {
const result = [];
const iterateItems = (items, path = []) =>
items.forEach(({ id, items }) => {
const localPath = [...path, id];
if (items)
iterateItems(items, localPath);
else
result.push(localPath);
});
iterateItems(items);
return result;
}
console.log(flatten(data));
Another option is to use Array.map(), and flatten the results of each stage by spreading into Array.concat():
const data = [{"id":"1","items":[{"id":"1.1","items":[{"id":"1.1.1"},{"id":"1.1.2"},{"id":"1.1.3"}]}]},{"id":"2","items":[{"id":"2.1","items":[{"id":"2.1.1"},{"id":"2.1.2"},{"id":"2.1.3"}]},{"id":"2.2","items":[{"id":"2.2.1"}]}]}];
const flatten = (arr, path = []) =>
[].concat(...arr.map(({ id, items }) => items ?
flatten(items, [...path, id]) : [[...path, id]]
));
const result = flatten(data);
console.log(result);

Related

Retain array structure when filtering nested array

My brain froze with this advanced filtering. This task has exceeded my basic knowledge of filter, map etc.
Here I have an array with nested objects with array:
const DATA = [
{
title: 'Spongebob',
data: [
{ id: 1, name: 'Mr Crabs' },
{ id: 2, name: 'Sandy' }
]
},
{
title: 'Dragon Balls Z',
data: [
{ id: 1, name: 'GoKu' },
{ id: 2, name: 'Zamasu' }
]
}
];
You may have seen this sort of style if you've worked with React Native (RN). This question is not for RN. I need to perform a filter on the name property in the nested array and when I get a match, I must return the format as the DATA variable.
const handleFiltering = (value) => {
const _value = value.toLowerCase();
const results = DATA.map(o => {
return o.data.filter(o => o.name.toLowerCase().indexOf(_value) != -1)
});
console.log(results);
};
My limited knowledge of deep filtering returns the basic filtering for the data array but need to retain the structure for DATA. The expected results I'd expect:
// I'm now querying for "ZAMASU"
const handleFiltering = (value='ZAMA') => {
const _value = value.toLowerCase();
const results = DATA.map(o => {
return o.data.filter(o => o.name.toLowerCase().indexOf(_value) != -1)
});
// console.log(results) should now be
// [
// {
// title: 'Dragon Balls Z',
// data: [
// { id: 2, name: 'Zamasu' }
// ]
// }
// ];
};
What comes to mind is the use of {...DATA, something-here } but my brain has frozen as I need to get back the title property. How to achieve this, please?
Another solution would be first use filter to find only objects containing the name in data passed through the argument, subsequently mapping data.
Here is your adjusted filter method
const handleFiltering = (value) => {
const _value = value.toLowerCase();
const results = DATA.filter((obj) =>
obj.data.some((character) => character.name.toLowerCase() === _value)
).map((obj) => ({
title: obj.title,
data: obj.data.filter(
(character) => character.name.toLowerCase() === _value
),
}));
console.log(results);
};
You can use reduce method of array. First find out the object inside data array and then add that to accumulator array as new entry by preserving the original structure.
const DATA = [
{
title: 'Spongebob',
data: [
{ id: 1, name: 'Mr Crabs', where: 'tv' },
{ id: 2, name: 'Sandy' }
]
},
{
title: 'Dragon Balls Z',
data: [
{ id: 1, name: 'GoKu' },
{ id: 2, name: 'Zamasu' }
]
}
];
let handleFiltering = (value='tv') => {
return DATA.reduce((acc,d) => {
let obj = d.data.find(a => a.name?.toLowerCase().includes(value.toLowerCase())
|| a.where?.toLowerCase().includes(value.toLowerCase()));
obj ? acc.push({...d, data:[obj]}) : null;
return acc;
}, []);
}
let result = handleFiltering();
console.log(result);

Using array values for nested filtering

I have a nested array object
const product = [{
ref_id: 'B123',
items: [
{
name: 'Brush',
ref_id: 'M1'
},
{
name: 'Paste',
ref_id: 'M2'
}
]
},
{
ref_id: 'B124',
items: [
{
name: 'Apple',
ref_id: 'M9'
},
{
name: 'Orange',
ref_id: 'M3'
}
]
},
{
ref_id: 'B113',
items: [
{
name: 'Maggi',
ref_id: 'M7'
},
{
name: 'Wai Wai',
ref_id: 'M12'
}
]
}
]
The outer ref_id is the category id and the inner one is the product id. Now, i have two array - one with values as category's ref_id and other with product's ref_id.
const filteredCategories = ['B123'];
const filteredItems = ['M2', 'M9', 'M7']
I want to filter out all items that fall under the filteredCategories ref_id and also under filteredItems ref_id which can be part of any other category. Also, the result should not contain duplicate items.
My Solution
let filteredProducts = [];
products.forEach(item => {
if(_.includes(filteredCategories, item.ref_id)) {
filteredProducts.push(...item.items);
} else {
const filterProductItems = _.filter(item.items, (productItem)=> _.includes(filteredItems, productItem.ref_id));
if(filterProductItems) {
filteredProducts.push(...filterProductItems);
}
}
});
Can this be done in a better and optimized way as items list can be really large?
#alia, I would suggest you use find method on product nested array object. Check for below code:
useEffect(() => {
getFilteredCategories()
}, []);
const filteredCategories = ['B123'];
let filteredProducts = [];
const getFilteredCategories = async () => {
filteredCategories.map((id, index) => {
const participants = product.find(o => o.ref_id === id);
filteredProducts.push(participants)
return filteredProducts;
});
console.log('filteredProducts', filteredProducts)
}

Json Array compare with different length in javascript

Below code which I am using for creating the new array if the id is the same in arr1 and arr2. But doesn't work since arr1 and arr2 are different. array 1 has index and arr2 is without index. screenshot for your reference. Can someone help?
Note: ID in arr1 is the same as EmpId in arr2
for(let i=0; i<arr1.length; i++) {
merged.push({
...arr1[i],
...(arr2.find((itmInner) => itmInner.id === arr1[i].id))}
);
}
console.log(merged);
Array1 looks like this :
[{"Active":1,"Id":1},
{"Active":1,"Id":3},
{"Active":1,"Id":2}]
Array2 looks something like this:
Below is the sample code on how I am framing array 2:
renderElement(activity){
var arr2 = [] ;
for(var i = 0; i < activity.length; i++) {
obj = activity[i];
if(obj.Id == 28){
fetch(geturl)
.then(function (response) {
return response.json();
})
.then(function (data) {
res = data;
arr2.push(res)
})
}
else{
// Do nothing
}
}
return arr2
}
Calling Render method like below:
outputarray = currentComponent.renderElement(activity);
console.log('output', outputarray)
Expected Output:
[{"Active":1,"Id":1,"Param1": true},
{"Active":1,"Id":3}, / Keep it as such if nothing exists in other array
{"Active":1,"Id":2, "Param2": false}]
You can try this approach instead:
Example #1
const arr1 = [
{ "Active":1, "Id":1 },
{ "Active":1, "Id":3 },
{ "Active":1, "Id":2 }
];
const arr2 = [
{
0: [
{
EmpId1: 1, Param1: true
}
]
},
{
1: [
{
EmpId2: 2,Param2: false
}
]
},
{
2: [
{
EmpId3: 2
}
]
},
];
const response = arr1
.reduce((acc, value) => {
const secondaryData = arr2.map((val, index) => {
const { [`EmpId${index + 1}`]: Id, ...others } = val[Object.keys(val)][0];
return { Id, ...others };
});
const match = secondaryData.findIndex(({ Id }) => Id === value.Id);
if (match >= 0) acc.push({...value, ...secondaryData[match]})
else acc.push(value);
return acc;
}, []);
console.log(response);
Example #2
const arr1 = [
{ "Active":1, "Id":1 },
{ "Active":1, "Id":3 },
{ "Active":1, "Id":2 }
];
const arr2 = [
[
{
EmpId1: 1,
Param1: true
}
],
[
{
EmpId2: 2,
Param2: false
}
],
[
{
EmpId3: 2
}
],
]
const response = arr1
.reduce((acc, value) => {
const secondaryData = arr2.map(([val], index) => {
const { [`EmpId${index + 1}`]: Id, ...others } = val;
return { Id, ...others };
});
const match = secondaryData.findIndex(({ Id }) => Id === value.Id);
if (match >= 0) acc.push({...value, ...secondaryData[match]})
else acc.push(value);
return acc;
}, []);
console.log(response);
Basically you can create a hash map by a object property and join on that property all the arrays, i.e. reduce an array of arrays into a result object, then convert the object's values back to an array. Since each array is reduced this means each array is only traversed once O(n) and the map object provides constant time O(1) lookup to match objects. This keeps the solution closer to O(n) rather than other solutions with a nested O(n) findIndex search, which yields a solution closer to O(n^2).
const mergeByField = (...arrays) => {
return Object.values(
arrays.reduce(
(result, { data, field }) => ({
...data.flat().reduce(
(obj, el) => ({
...obj,
[el[field]]: {
...obj[el[field]],
...el
}
}),
result
)
}),
{}
)
);
};
Load each array into a payload object that specifies the field key to match on. This will return all fields used to match by, but these can safely be ignored later, or removed, whatever you need. Example:
mergeByField(
{ data: arr1, field: "Id" },
{ data: arr2, field: "EmpId" },
);
const arr1 = [
{
Active: 1,
Id: 1
},
{
Active: 1,
Id: 2
},
{
Active: 1,
Id: 3
}
];
const arr2 = [[{ EmpId: 1, Param1: true }], [{ EmpId: 3, Param2: false }]];
const mergeByField = (...arrays) => {
return Object.values(
arrays.reduce(
(result, { data, field }) => ({
...data.flat().reduce(
(obj, el) => ({
...obj,
[el[field]]: {
...obj[el[field]],
...el
}
}),
result
)
}),
{}
)
);
};
console.log(
mergeByField({ data: arr1, field: "Id" }, { data: arr2, field: "EmpId" })
);

JS array of arrays to object

I have a JS array (shown 4 examples actual has 66 )
[["A","Example1"],["A","Example2"],["B","Example3"],["B","Example4"]]
that I am trying to get into an object for a multi select drop down menu:
var opt = [{
label: 'A', children:[
{"label":"Example1","value":"Example1","selected":"TRUE"},
{"label":"Example2","value":"Example2","selected":"TRUE"}
]
},
{
label: 'B', children:[
{"label":"Example3","value":"Example3","selected":"TRUE"},
{"label":"Example4","value":"Example4","selected":"TRUE"}
]
}
]
Is there a easy way to do this ?
Updated:
Using reduce() and filter() to get expected results.
const result = [['A', 'Example1'], ['A', 'Example2'], ['B', 'Example3'], ['B', 'Example4']].reduce((acc, cur) => {
const objFromAccumulator = acc.filter((row) => row.label === cur[0]);
const newChild = {label: cur[1], value: cur[1], selected: 'TRUE'};
if (objFromAccumulator.length) {
objFromAccumulator[0].children.push(newChild);
} else {
acc.push({label: cur[0], children: [newChild]});
}
return acc;
}, []);
console.log(result);
Something like this should work:
const raw = [["A","Example1"],["A","Example2"],["B","Example3"],["B","Example4"]];
const seen = new Map();
const processed = raw.reduce((arr, [key, label]) => {
if (!seen.has(key)) {
const item = {
label: key,
children: []
};
seen.set(key, item);
arr.push(item);
}
seen.get(key).children.push({
label,
value: label,
selected: "TRUE"
})
return arr;
}, []);
console.log(processed);
Here's a rather efficient and concise take on the problem using an object as a map:
const data = [["A","Example1"],["A","Example2"],["B","Example3"],["B","Example4"]];
const opt = data.reduce((results,[key,val]) => {
if(!results[0][key]) //first element of results is lookup map of other elements
results.push(results[0][key] = { label: key, children: [] });
results[0][key].children.push({ label: val, value: val, selected:"TRUE" });
return results;
}, [{}]).slice(1); //slice off map as it's no longer needed
console.log(opt);

Get value from child Array with a given ID

I have the following Array and I'm trying to print the child array value using .find by providing an ID
connections = [
{
group: 'a',
items: [
{
id: '1',
name: 'Andre'
},
{
id: '2',
name: 'David'
}
]
},
{
group: 'b',
items: [
{
id: '3',
name: 'Brandon'
}
]
},
]
I have tried the following in my Angular app,
getUser(id) {
this.activeItem = this.connections.items.find(data => data.id === id);
console.log(this.activeItem);
}
I'm providing the correct ID but I'm getting an error saying,
error TS2339: Property 'items' does not exist on type....
Thank you.
You can use filter and some methods. This approach will filter array and your array will contain only desired items:
let connections = [
{
group: 'a',
items: [
{
id: '1', name: 'Andre'
},
{
id: '2', name: 'David'
}
]
},
{
group: 'b',
items: [
{
id: '3', name: 'Brandon'
}
]
},
]
let id = 3;
// ONE WAY
const result = connections.filter(f=> f.items.some(s=> s.id == id))
.flatMap(fm => fm.items);
console.log(`result: `, result);
// OR ANOTHER WAY:
const resultWithGroup = connections.filter(f=> f.items.some(s=> s.id == id));
const resultItem = Object.assign({}, ...resultWithGroup).items.find(f => f.id == id);
console.log(`resultItem: `, resultItem);
console.log(`resultItem as an array: `, [resultItem]);
In addition, it is possible to use flatMap method. By using this approach you are getting all items with desired id and then find the first element with id == 3:
let connections = [
{
group: 'a',
items: [
{
id: '1', name: 'Andre'
},
{
id: '2', name: 'David'
}
]
},
{
group: 'b',
items: [
{
id: '3', name: 'Brandon'
}
]
},
]
const result = connections.flatMap(f => f.items).find(f => f.id == id);
console.log(`result as array`, [result]);
You can use flatMap
Try like this:
getUser(id) {
this.activeItem = this.connections.flatMap(x => x.items).find(data => data.id === id);
console.log(this.activeItem);
}
Working Demo
As i can see from the Json object, the items arrays are grouped inside their parent objects. So first you would have to flatten the grouped array:
let items = []
connections.forEach(obj => obj.items.forEach( item => items.push(item)))
Now the items array would only be item objects so it will be easier to do a find:
items.find(item => item.id == 3)
You are trying to use an array without specifying the element of the array
-----------------\/ here
this.connections[0].items.find(data => data.id === id);
The reason your "this.connections.items.find" is not working is that connections variable here represents an array of objects, you cannot directly access a key that is inside an objects contained in an array of objects.
Use this code instead:
this.activeItem = this.connections.filter(obj => obj.items.find(val => val.id == id));
console.log(this.activeItem.items);
connections variable is an array and you are trying to access it as an object. PFB the below code it should be working fine for you.
getUser(id) {
this.activeItem = connections.find(function(element) { return element.items.find(function(el){return el.id==id;}); });
console.log(this.activeItem);
}
Try this. (You made mistake with connections.items)
getUser(id) {
let activeItemIndex = -1;
this.connections.forEach((c) => {
activeItemIndex = c.items.findIndex(item => item.id === id);
if (activeItemIndex > -1) {
this.activeItem = c.items[activeItemIndex];
}
});
}
It is normal that you get this error: "error TS2339: Property 'items' does not exist on type....".
Actually, 'connections' is an array and does not have any prperty 'items'.
'items' is an attribut of the elements contained in 'connections' array.
You can try something like:
getUser(id) {
for (const element of this.connections) {
this.activeItem = element.items.find(data => data.id === id);
if (this.activeItem) {
break;
}
}
console.log(this.activeItem);
}
Once 'this.activeItem' is found we exit the loop with the 'break;' statement.
You have to specify index of connections.
But this is the better solution, Because you have all the users in one place:
getUser(id) {
users = connections.reduce((users, item)=>{
users.push(...item.items);
return users;
}, []);
this.activeItem = users.find(data => data.id === id);
console.log(this.activeItem);
}

Categories