Javascript Grouping by object property, when property is an array - javascript

I currently have an array of items that looks a bit like this: I want to group the items by category lookup, with the slight problem that category lookup is potentially an array, such that Parent Item 2 would be listed twice (once in My Cat) and once in something else) I tried using https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/groupBy but it doesn't seem to be able to handle this?
[
{
"tool_id": "4-19de-454673d9-9ef5-4545",
"somekey" : "Parent Item 2"
"categoryLookup": [
{
"category_name": "New Item",
}
]
},
{
"tool_id": "be7ea707-19de-43d9-9ef1-d4a3ff79f77a",
"somekey" : "Parent Item"
"categoryLookup": [
{
"category_name": "My Cat",
},
{
"category_name": "Something Else",
}
]
}
]
The final result would look something like:
[
{
New Item: [
{...contains 4-19de-454673d9-9ef5-4545 }
],
My Cat: [
{...contains be7ea707-19de-43d9-9ef1-d4a3ff79f77a}
],
Something Else: [
{... contain be7ea707-19de-43d9-9ef1-d4a3ff79f77a}
]
}
]

You can iterate over the original array and create the final one:
var data = [{
"tool_id": "4-19de-454673d9-9ef5-4545",
"somekey": "Parent Item 2",
"categoryLookup": [{
"category_name": "New Item",
}]
},
{
"tool_id": "be7ea707-19de-43d9-9ef1-d4a3ff79f77a",
"somekey": "Parent Item",
"categoryLookup": [{
"category_name": "My Cat",
},
{
"category_name": "Something Else",
}
]
}
];
function groupByCategory(data) {
const res = {};
data.forEach(item => {
item.categoryLookup.forEach(category => {
const name = category.category_name;
res[name] ??= [];
res[name].push({
item: item.tool_id //or all the properties you want
});
});
});
return res;
}
console.log( groupByCategory(data) );

Related

Javascript Lodash replace array with another [duplicate]

This question already has answers here:
How can I access and process nested objects, arrays, or JSON?
(31 answers)
Closed 10 months ago.
I'm trying to manipulate an array like this:
data = [
{
"id":"1",
"items":[
{
"title":"item 1"
},
{
"title":"item 2"
}
]
},
{
"id":"2",
"items":[
{
"title":"item2 1"
},
{
"title":"item2 2"
}
]
}
]
I need, for example, to push another array:
[
{
"title":"item new 1"
},
{
"title":"item new 2"
}
]
inside data[0].items and obtain:
data = [
{
"id":"1",
"items":[
{
"title":"item new 1"
},
{
"title":"item new 2"
}
]
},
{
"id":"2",
"items":[
{
"title":"item2 1"
},
{
"title":"item2 2"
}
]
}
]
...how can I do this maintaining immutability, for example with Lodash?
Not understand anding how to change only a specific sub object in a data structure.
Somebody have suggestions?
Thanks
Presented below is one possible way to immutably add given array into a particular index "items" prop.
Code Snippet
const immutableAdd = (aIdx, addThis, orig) => {
const newData = structuredClone(orig);
newData[aIdx].items = addThis;
return newData;
};
const data = [{
"id": "1",
"items": [{
"title": "item 1"
},
{
"title": "item 2"
}
]
},
{
"id": "2",
"items": [{
"title": "item2 1"
},
{
"title": "item2 2"
}
]
}
];
const addThisArr = [{
"title": "item new 1"
},
{
"title": "item new 2"
}
];
console.log('immutableAdd result: ', immutableAdd(0, addThisArr, data));
console.log('original data: ', data);
.as-console-wrapper { max-height: 100% !important; top: 0 }
Explanation
Use structuredClone() to deep-clone existing data array
Navigate to the aIdx of the cloned-array
Assign the given array (to be added) into aIdx's items prop
NOTE
This solution does not use lodash as it is not mandatory (to use lodash) to perform immutable operations.
If you want to maintain the immutability of original data, just map the content of original data to the new data as you want, and wrap your logic into a pure function to improve readability.
const dataOriginal = [{
"id": "1",
"items": [{
"title": "item 1"
},
{
"title": "item 2"
}
]
},
{
"id": "2",
"items": [{
"title": "item2 1"
},
{
"title": "item2 2"
}
]
}
]
const dataNew = createDataWithSomethingNew(dataOriginal, [{
"title": "item new 1"
},
{
"title": "item new 2"
}
])
function createDataWithSomethingNew(data, props) {
return data.map(function changeItemsOfId1ToProps(value) {
if (value.id === '1') {
return {
id: value.id,
items: props
}
} else {
return value
}
})
}
lodash has a method _.update can modify object with the correct path in string provided.
Another method _.cloneDeep can copy you object deeply. So that change in the pre-copied object will not affect the cloned object.
Finally use a deep freeze function to call Object.freeze recursively on the cloned object to make it immutable.
var data = [
{
"id":"1",
"items":[
{
"title":"item 1"
},
{
"title":"item 2"
}
]
},
{
"id":"2",
"items":[
{
"title":"item2 1"
},
{
"title":"item2 2"
}
]
}
]
var clonedData = _.cloneDeep(data) // copy the full object to avoid modify the source data
// update the data of that path '[0].items' in clonedData
_.update(clonedData, '[0].items', function(n) {
return [
{
"title":"item new 1"
},
{
"title":"item new 2"
}
]
})
// provide object immutability
const deepFreeze = (obj1) => {
_.keys(obj1).forEach((property) => {
if (
typeof obj1[property] === "object" &&
!Object.isFrozen(obj1[property])
)
deepFreeze(obj1[property])
});
Object.freeze(obj1)
};
deepFreeze(clonedData)
data[2] = {id: 3} // data will be changed
data[1].items[2] = {title: "3"} // and also this one
clonedData[2] = {id: 3} // nothing will be changed
clonedData[1].items[2] = {title: "3"} // and also this one
console.log(`data:`, data);
console.log(`clonedData:`, clonedData);
Runkit Example

Merging an array of objects without overwriting

I am currently with some JSON, which has to be structured in a tree-like hierarchy. The depth of the hierarchy varies a lot, and is therefor unknown.
As it is right now, I have achieved to get an array of objects. Example is below.
[
{
"name": "level1",
"collapsed": true,
"children": [
{
"name": "Level 1 item here",
"id": 360082134191
}
]
},
{
"name": "level1",
"collapsed": true,
"children": [
{
"name": "level2",
"collapsed": true,
"children": [
{
"name": "Level 2 item here",
"id": 360082134751
}
]
}
]
},
{
"name": "level1",
"collapsed": true,
"children": [
{
"name": "Another level 1 item",
"id": 360082262772
}
]
}
]
What I want to achieve is these objects to be merged, without overwriting or replacing anything. Listed below is an example of how I want the data formatted:
[
{
"name": "level1",
"collapsed": true,
"children": [
{
"name": "level2",
"collapsed": true,
"children": [
{
"name": "Level 2 item here",
"id": 360082134751
}
]
},
{
"name": "Level 1 item here",
"id": 360082134191
},
{
"name": "Another level 1 item",
"id": 360082262772
}
]
}
]
How would I achieve this with JavaScript? No libraries is preferred, ES6 can be used though.
Edit:
It is important that the output is an array, since items without children can appear at the root.
I am assuming you need a little help on working with the data. There could be multiple ways to achieve this, here is how would I do.
// data => supplied data
const result = data.reduce ((acc, item) => {
// if acc array already contains an object with same name,
// as current element [item], merfe the children
let existingItem;
// Using a for loop here to create a reference to the
// existing item, so it'd update this item when childrens
// will be merged.
for (let index = 0; index < acc.length; index ++) {
if (acc[index].name === item.name) {
existingItem = acc[index];
break;
}
}
// if existingItem exists, merge children of
// existing item and current item.
// else push it into the accumulator
if (existingItem) {
existingItem.children = existingItem.children.concat(item.children);
} else {
acc.push (item);
}
return acc;
}, []);
I'm assuming you want to group based on the name property in the level 1 object. You could do a simple reduce and Object.values like this:
const input = [{"name":"level1","collapsed":true,"children":[{"name":"Level 1 item here","id":360082134191}]},{"name":"level1","collapsed":true,"children":[{"name":"level2","collapsed":true,"children":[{"name":"Level 2 item here","id":360082134751}]}]},{"name":"level1","collapsed":true,"children":[{"name":"Another level 1 item","id":360082262772}]}]
const merged = input.reduce((r,{name, collapsed, children}) =>{
r[name] = r[name] || {name, collapsed, children:[]};
r[name]["children"].push(...children)
return r;
}, {})
const final = Object.values(merged);
console.log(final)
You could do the whole thing in one line:
const input = [{"name":"level1","collapsed":true,"children":[{"name":"Level 1 item here","id":360082134191}]},{"name":"level1","collapsed":true,"children":[{"name":"level2","collapsed":true,"children":[{"name":"Level 2 item here","id":360082134751}]}]},{"name":"level1","collapsed":true,"children":[{"name":"Another level 1 item","id":360082262772}]}]
const output = Object.values(input.reduce((r,{name,collapsed,children}) => (
(r[name] = r[name] || {name,collapsed,children: []})["children"].push(...children), r), {}))
console.log(output)

Filtering nested arrays on multiple properties and multiple values

I want to create flexible table filtering on user-company-roles objects in JS array. User shall be able to provide filtering on multiple object properties, with multiple values, with use of AND (&&) operand when matching.
I would appreciate tip on how to proceed implementing the logic here. Of course I could loop through the array and have multiple nested if-statements, but maybe there are more clean and nice approach on this?
User-Company-Roles Array
const userArray = [
{
"id": "peterpan",
"name": "Peter pan",
"company": [
{
"id": "12345678",
"name": "Company A",
"roles": [
"Role A",
"Role B",
"Role C"
]
}
],
"systemRoles": [
{
"systemName": "System A",
"role": "Admin"
},
{
"systemName": "System B",
"role": "User"
}
]
},
{
"id": "robinhood",
"name": "Robin Hood",
"company": [
{
"id": "9876543",
"name": "Company B",
"roles": [
"Role A"
]
},
{
"id": "546372",
"name": "Company C",
"roles": [
"Role A"
]
}
],
"systemRoles": [
{
"systemName": "System B",
"role": "User"
}
]
},
{
"id": "biggiant",
"name": "Big Giant",
"company": [
{
"id": "546372",
"name": "Company C",
"roles": [
"Role A"
]
}
],
"systemRoles": [
{
"systemName": "System B",
"role": "User"
}
]
}
];
Filter object
const filter = {
includeUserIds: [], // filters 'user.id' that matches values in this array
includeCompanyNames: [], // filters 'user.company.name' that matches values in this array
includeSystemRoleNames: [], // filters 'user.systemRoles.role' that matches values in this array
includeCompanyRoles: [], // filters 'user.company.roles' that matches values in this array
excludeSystemRoleNames: [], // filters 'user.systemRoles.role' that **DOES NOT** match values in this array
excludeCompanyRoles: [] // filters 'user.company.roles' that **DOES NOT** match values in this array
}
Matching
When filtering the array I want the filter to match user-objects as this (pseudo code):
filter.includeUserIds && filter.includeCompanyNames
&& filter.includeSystemRoleNames && filter.includeCompanyRoles
&& !filter.excludeSystemRoleNames && !filter.excludeCompanyRoles
Example 1: Filter users by user id:
const filter = {
includeUserIds: ['peterpan', 'robinhood'],
includeCompanyNames: [],
includeSystemRoleNames: [],
includeCompanyRoles: [],
excludeSystemRoleNames: [],
excludeCompanyRoles: []
}
Would return array with Peter Pan and Robin Hood users
Example 2: Filter by company name
const filter = {
includeUserIds: [],
includeCompanyNames: ['Company C'],
includeSystemRoleNames: [],
includeCompanyRoles: [],
excludeSystemRoleNames: [],
excludeCompanyRoles: []
}
Would return array with Robin Hood and Big Giant users
Example 3: Filter by company roles and system roles
const filter = {
includeUserIds: [],
includeCompanyNames: [],
includeSystemRoleNames: ['User'],
includeCompanyRoles: ['Role A'],
excludeSystemRoleNames: ['Admin'],
excludeCompanyRoles: []
}
Would return array with Robin Hood and Big Giant users
I would create a decision function for each type of the filter and put it into object with same properties as filter is. Each function should accept 2 arguments: item of type any and filter of type string[]. So it will look like:
const deciders = {
includeUserIds: (item, filter) => { ... },
includeCompanyNames: (item, filter) => { ... },
includeSystemRoleNames: (item, filter) => { ... },
includeCompanyRoles: (item, filter) => { ... },
excludeSystemRoleNames: (item, filter) => { ... },
excludeCompanyRoles: (item, filter) => { ... },
};
Then filtering is a simple filter of original array like:
const filtered = userArray.filter(user => {
let include = true;
Object.keys(deciders).forEach(type => include &= deciders[type](user, filter[type]));
return include;
});

group by nested array property in JavaScript

I have a json object as below in my web application. It's an array of product objects and each product object has a category property which contains an array of categories that the product belongs to.
var products = [
{
"id":1,
"name":"Product 1",
"price":10,
"category":[
{
"id":10,
"name":"Category 1"
},
{
"id":20,
"name":"Category 2"
}
]
},
{
"id":2,
"name":"Product 2",
"price":20,
"category":[
{
"id":20,
"name":"Category 2"
},
{
"id":30,
"name":"Category 3"
}
]
}
]
So now I want to display them grouped by categories so the end result will look like below. I am already using Underscore.js in my project so it will be good if I can use it to achieve this.
var categories = [
{
"id":10,
"name":"Category 1",
"products":[
{
"id":1,
"name":"Product 1",
"price":10
}
]
},
{
"id":20,
"name":"Category 2",
"products":[
{
"id":1,
"name":"Product 1",
"price":10
},
{
"id":2,
"name":"Product 2",
"price":20,
}
]
},
{
"id":30,
"name":"Category 3",
"products":[
{
"id":2,
"name":"Product 2",
"price":20,
}
]
}
]
I'm not entirely sure whether there is an out-of-the-box solution to this problem with underscore, however solving this by hand shouldn't be too hard, either:
var categoriesIndexed = {};
var categories = [];
products.forEach(function(product) {
product.category.forEach(function(category) {
// create a new category if it does not exist yet
if(!categoriesIndexed[category.id]) {
categoriesIndexed[category.id] = {
id: category.id,
name: category.name,
products: []
};
categories.push(categoriesIndexed[category.id]);
}
// add the product to the category
categoriesIndexed[category.id].products.push({
id: product.id,
name: product.name,
price: product.price
});
});
});
here is what I would do
var categories = [];
var cat = new Map();
var addUniqueCategory(category) { /* determine if category is already in list of categories, if not add it to categories */ };
products.each (function (item) {
item.categories.each(function (c) {
if (!cat.has(c.name)) cat.set(c.name, []);
var list = cat.get(c.name);
list.push( { id: item.id, name: item.name, price: item.price });
addUniqueCategory(c);
});
});
categories.each( function (c) {
var list = cat.get(c.name);
if (!c.products) c.products = [];
c.products.splice( c.length, 0, list);
});
roughly, I'm on a phone

Manipulating javascript object with underscore

I have a Javascript object with a format like below
"items":
{
"Groups":[
{
"title":"group 1",
"SubGroups":[
{
"title":"sub1",
"id" : "1",
"items":[
{
"title":"Ajax request 1",
},
{
"title":"Ajax request 2",
}
]
},
{
"title":"sub2",
"id" : "2",
"items":[
{
"title":"Ajax request 3",
},
{
"title":"Ajax request 4",
}
]
}
]
}
]
There are n 'Groups', n 'subGroups' and n 'items'.
What I want to do firstly is get all the items from a particular group based on id. This is achieved using:
_.each(items.Groups, function(o) {
result = _.where(o.SubGroups, {
'id': '1'
});
});
which returns
"items":[{"title":"Ajax request 1",},{"title":"Ajax request 2",}]
Then I want to get the rest of the data, excluding the items and parent group I have just retrieved.
I tried this:
_.each(items.Groups, function(o) {
arr = _.without(o.SubGroups, _.findWhere(o.SubGroups, {id: '2'}));
});
But this only returns me the items like this:
{
"title":"sub2",
"id" : "2",
"items":[{"title":"Ajax request 3"},{"title":"Ajax request 4",}]
}
whereas what I need is this:
"items":
{
"Groups":[
{
"title":"group 1",
"SubGroups":[
{
"title":"sub2",
"id" : "2",
"items":[
{
"title":"Ajax request 3",
},
{
"title":"Ajax request 4",
}
]
}
]
}
]
Just try this:
_.each(items.Groups, function(o) {
arr = _.without(o, _.findWhere(o.SubGroups, {id: '2'}));
});
o should be enough => you want to get Groups and not SubGroups.
Following is a pure JS implementation:
JSFiddle.
var data = {
"Groups": [{
"title": "group 1",
"SubGroups": [{
"title": "sub1",
"id": "1",
"items": [{
"title": "Ajax request 1",
}, {
"title": "Ajax request 2",
}]
}, {
"title": "sub2",
"id": "2",
"items": [{
"title": "Ajax request 3",
}, {
"title": "Ajax request 4",
}]
}]
}]
}
var items = [];
var group = [];
data.Groups.forEach(function(o) {
var _tmp = JSON.parse(JSON.stringify(o));
_tmp.SubGroups = [];
o.SubGroups.forEach(function(s) {
if (s.id == "1") {
items.push(s.items);
} else {
_tmp.SubGroups.push(s);
group.push(_tmp)
}
});
});
function printObj(label, obj) {
document.write(label + "<pre>" + JSON.stringify(obj, 0, 4) + "</pre>")
}
printObj("group", group);
printObj("items", items);
Using underscore and using your logic to filter all subgroups:
//array to store subgroup with ID 1
var results = [];
var d = _.each(data.items.Groups, function(o) {
result = _.where(o.SubGroups, {
'id': '1'
});
//add to results array
results.push(result);
});
//make a clone of the earlier object so that you get the parent structure.
var data1 = _.clone(data);
//set the filtered results to the group
data1.items.Groups = results;
//your data as you want
console.log(data1)
Working code here

Categories

Resources