Find and group common elements across objects in an array - javascript

I am trying to figure out how to map a new array of objects that kind of creates teams by checking each array of users and, where there is a common users, moving that entire array into a new property that also features the notebookIds in common.
I have an array of objects structured like so:
const usersByNotebooks =
[
{
"notebookId": "abc",
"users": [1, 2, 3, 4]
},
{
"notebookId": "cde",
"users": [2, 3, 4]
},
{
"notebookId": "fgh",
"users": [3, 4, 5]
},
{
"notebookId": "qqq",
"users": [33, 16, 12]
},
]
So for the above data it would become something like this:
const teams =
[
{
"notebooksOnTeam": ["abc", "cde", "fgh"],
"usersOnTeam": [1, 2, 3, 4, 5]
},
{
"notebooksOnTeam": "qqq",
"usersOnTeam": [33, 16, 12]
},
]
I am using javascript and having trouble getting the logic down.

Loop over objects of array using reduce and check:
If the current notebook's users don't match any existing team with find method, so create a new team.
If the current notebook's users match an existing team, add the notebook to that team.
const usersByNotebooks = [{ "notebookId": "abc", "users": [1, 2, 3, 4] }, { "notebookId": "cde", "users": [2, 3, 4] }, { "notebookId": "fgh", "users": [3, 4, 5] }, { "notebookId": "qqq", "users": [33, 16, 12] }, ];
const teams = usersByNotebooks.reduce((result, current) => {
const teamFound = result.find((team) => team.usersOnTeam.some((user) => current.users.includes(user)));
if (!teamFound) {
result.push({
notebooksOnTeam: [current.notebookId],
usersOnTeam: current.users
});
} else {
teamFound.notebooksOnTeam.push(current.notebookId);
current.users.forEach((user) => {
if (!teamFound.usersOnTeam.includes(user)) {
teamFound.usersOnTeam.push(user);
}
});
}
return result;
}, []);
console.log(teams)

You could have a look to any objects of the result set and either get the first object of the same group and add all other found and finally add the actual value or later add a new object.
This approach works for unsorted and not connected items which gets later a joint.
const
addIfNotExist = (array, value) => array.includes(value) || array.push(value),
usersByNotebooks = [{ notebookId: "abc", users: [1, 2, 3, 4] }, { notebookId: "cde", users: [2, 3, 4] }, { notebookId: "fgh", users: [3, 4, 5] }, { notebookId: "qqq", users: [33, 16, 12] }, { notebookId: "xxx", users: [6, 7] }, { notebookId: "yyy", users: [5, 6] }],
result = usersByNotebooks.reduce(
(r, { notebookId, users }) => users.reduce((s, user) => {
const objects = [];
let first;
for (const o of s) {
if (!o.users.includes(user) && !o.notebooks.includes(notebookId)) {
objects.push(o);
continue;
}
if (!first) objects.push(first = o);
o.users.forEach(addIfNotExist.bind(null, first.users));
o.notebooks.forEach(addIfNotExist.bind(null, first.notebooks));
}
if (first) {
addIfNotExist(first.users, user);
addIfNotExist(first.notebooks, notebookId);
} else {
objects.push({ users: [user], notebooks: [notebookId] });
}
return objects;
}, r),
[]
);
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }
This is an abstract solution for any length of groups which are connected.
It works in three step:
Generate an array of pairs or more or less items in a tupel,
group connected items together in an aray of arrays and
map the items in the wanted format.
const
addIfNotExist = (array, value) => array.includes(value) || array.push(value),
groupConnectedParts = (r, a) => {
const objects = [];
let first;
for (const b of r) {
if (!a.some((v, i) => b[i].includes(v))) {
objects.push(b);
continue;
}
if (!first) objects.push(first = b);
b.forEach((group, i) => group.forEach(addIfNotExist.bind(null, first[i])));
}
if (first) a.forEach((v, i) => addIfNotExist(first[i], v));
else objects.push(a.map(v => [v]));
return objects;
},
usersByNotebooks = [{ notebookId: "abc", users: [1, 2, 3, 4] }, { notebookId: "cde", users: [2, 3, 4] }, { notebookId: "fgh", users: [3, 4, 5] }, { notebookId: "qqq", users: [33, 16, 12] }, { notebookId: "xxx", users: [6, 7] }, { notebookId: "yyy", users: [5, 6] }],
result = usersByNotebooks
.flatMap(({ notebookId, users }) => users.map(user => [notebookId, user]))
.reduce(groupConnectedParts, [])
.map(([notebooks, users]) => ({ notebooks, users }));
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }

Related

Filtering out users that appear in an array of strings

I am trying to filter out any user whose id appears in an array of strings. I am trying to use a filter() method to do this but strugglign with implementing the logic.
const x = [
{
notebookId: "abc",
notebookUsers: [1, 2, 3, 4],
},
{
notebookId: "cde",
notebookUsers: [2, "foo", 4, 3],
},
{
notebookId: "fgh",
notebookUsers: ["bla", 4, 5, "123"],
},
{
notebookId: "qqq",
notebookUsers: [33, 16, 12],
},
{
notebookId: "ab",
notebookUsers: ["abc", 23213, 2131, 33],
},
];
const y = ["abc", "123", "bla", "foo"];
const filteredUsersArray = x.filter((nb) => {
const filteredUsers = nb.notebookUsers.filter(
(user) => !y.includes(user)
);
return (
nb.notebookId !== "welcome" &&
nb.notebookId !== "null" &&
nb.notebookId !== "1234" &&
filteredNotebookUsers.length > 0
);
});
console.log(filteredUsersByNotebookArray);
Result:
[
{
notebookId: "abc",
notebookUsers: [1, 2, 3, 4, 2, "foo", 4, 3]
},
{
notebookId: "cde",
notebookUsers: [2, "foo", 4, 3]
},
{
notebookId: "fgh",
notebookUsers: ["bla", 4, 5, "123"]
},
{
notebookId: "qqq",
notebookUsers: [33, 16, 12, "abc", 23213, 2131, 33]
},
{
notebookId: "ab",
notebookUsers: ["abc", 23213, 2131, 33]
}
]
This doesn't appear to remove the forbidden ids. Not sure where i am wrong.
you are returning a check on a variable called filteredNotebookUsers which does not exist, update the return statement to check the length of the filteredUsers array
const x = [
{
notebookId: "abc",
notebookUsers: [1, 2, 3, 4],
},
{
notebookId: "cde",
notebookUsers: [2, "foo", 4, 3],
},
{
notebookId: "fgh",
notebookUsers: ["bla", 4, 5, "123"],
},
{
notebookId: "qqq",
notebookUsers: [33, 16, 12],
},
{
notebookId: "ab",
notebookUsers: ["abc", 23213, 2131, 33],
},
];
const y = ["abc", "123", "bla", "foo"];
const filteredUsersArray = x.filter((nb) => {
const filteredUsers = nb.notebookUsers.filter(
(user) => !y.includes(user)
);
return (
nb.notebookId !== "welcome" &&
nb.notebookId !== "null" &&
nb.notebookId !== "1234" &&
filteredUsers.length > 0
);
});
console.log(filteredUsersArray);
It seems that filter doesn't do quite what you expect.
Consider this small example:
const userIds = [1,2,3,4];
const filteredUserIds = userIds.filter(id => id >= 3);
console.log(userIds); // [1,2,3,4]
console.log(filteredUserIds); // [3,4]
filter does not change the array that it is filtering
It looks like what you want to do is to map the notebooks so that the users are filtered, and then filter the mapped notebooks:
const filteredNotebooks =
x.map((nb) => {
const filteredUsers = nb.notebookUsers.filter(
(user) => !y.includes(user)
);
return { ...nb, notebookUsers: filteredUsers };
}).filter((nb) => {
return (
nb.notebookId !== "welcome" &&
nb.notebookId !== "null" &&
nb.notebookId !== "1234" &&
nb.notebookUsers.length > 0
);
});
I'm not sure if i understood what you are trying to achieve.
Maybe it is the following:
'Before' [
{
notebookId: 'abc',
notebookUsers: [ 1, 2, 3, 4 ]
},
{
notebookId: 'cde',
notebookUsers: [ 2, 'foo', 4, 3 ]
},
{
notebookId: 'fgh',
notebookUsers: [ 'bla', 4, 5, '123' ]
},
{
notebookId: 'qqq',
notebookUsers: [ 33, 16, 12 ]
},
{
notebookId: 'ab',
notebookUsers: [ 'abc', 23213, 2131, 33 ]
}
]
'After' [
{
notebookId: 'abc',
notebookUsers: [ 1, 2, 3, 4 ]
},
{ notebookId: 'cde', notebookUsers: [ 2, 4, 3 ] },
{ notebookId: 'fgh', notebookUsers: [ 4, 5 ] },
{
notebookId: 'qqq',
notebookUsers: [ 33, 16, 12 ]
},
{
notebookId: 'ab',
notebookUsers: [ 23213, 2131, 33 ]
}
]
Here is the code to return the same Array but without the forbiddenIDs
const forbiddenIDs = ["abc", "123", "bla", "foo"];
const filteredUsersArray = notebooksArray.map((nb) => {
const filteredUsers = nb.notebookUsers.filter(
(user) => !forbiddenIDs.includes(user)
);
return (
nb.notebookId !== "welcome" &&
nb.notebookId !== "null" &&
nb.notebookId !== "1234" &&
filteredUsers.length > 0
) && { notebookId: nb.notebookId, notebookUsers: filteredUsers};
});
console.log(filteredUsersArray);
.map returns a new Array that will be stored in filteredUsersArray.
With && { notebookId: nb.notebookId, notebookUsers: filteredUsers}; we mean that if the previous condition is true we return an object like we had before but with the filtered users. So we add the same notebookId that we had before, and the notebookUsers array will now contain the filtered users.
I don't know exactly what you are wanting. But you can try in this way -
const x = [
{
notebookId: "abc",
notebookUsers: [1, 2, 3, 4],
},
{
notebookId: "cde",
notebookUsers: [2, "foo", 4, 3],
},
{
notebookId: "fgh",
notebookUsers: ["bla", 4, 5, "123"],
},
{
notebookId: "qqq",
notebookUsers: [33, 16, 12],
},
{
notebookId: "ab",
notebookUsers: ["abc", 23213, 2131, 33],
},
];
const y = ["abc", "123", "bla", "foo"];
const filteredUsersArray = x.filter((nb) => {
return nb.notebookUsers.filter(
(user) => !y.includes(user)
)
});
console.log(filteredUsersArray);

JavaScript: Sort array of objects by computed property or by an existing property if equal

[
{ name: 'Joe', scores: [1, 2, 3] },
{ name: 'Jane', scores: [1, 2, 3] },
{ name: 'John', scores: [1, 2, 3] }
]
how do I make a function that sorts the elements first by the sum in scores and later by name?
Using Array#sort, sort the array by the sum of scores, or name as fallback (using String#localeCompare)
const arr = [ { name: 'Joe', scores: [1] }, { name: 'Jane', scores: [1, 2] }, { name: 'John', scores: [1, 2] } ];
const sum = (arr = []) => arr.reduce((total, num) => total + num, 0);
const sorted = arr.sort((a, b) =>
sum(b.scores) - sum(a.scores) || a.name.localeCompare(b.name)
);
console.log(sorted);
You could take a Map for all sums and the object as key without mutating data.
const
data = [{ name: 'Joe', scores: [1, 2, 3] }, { name: 'Jane', scores: [1, 4, 3] }, { name: 'John', scores: [1, 2, 1] }],
add = (a, b) => a + b,
sums = new Map(data.map(o => [o, o.scores.reduce(add, 0)]));
data.sort((a, b) => sums.get(b) - sums.get(a) || a.name.localeCompare(b.name));
console.log(data);
.as-console-wrapper { max-height: 100% !important; top: 0; }
The lodash package could be handy
const lodash = require('lodash')
const list = [
{ name: 'Joe', scores: [1, 2, 4] },
{ name: 'Jane', scores: [1, 2, 3] },
{ name: 'John', scores: [1, 2, 4] }
]
const listWithSum = list.map(item => {
return {
...item,
sum: lodash.sum(item.scores)
}
})
const sortedList = lodash.orderBy(listWithSum, ['score', 'name'], ['desc', 'desc'])
console.log(sortedList)

Can searching for overlapping elements from one array in a two-dimensional array be simplified to avoid nested for loops?

I have an array of items that I would like to remove from within a nested object array, if present.
var itemsToRemove = [1, 2, 3];
var data = [
{ id: 'a', list: [1, 3, 4, 5] },
{ id: 'b', list: [2, 6, 7] }
];
should update to
data = [
{ id: 'a', list: [4, 5] },
{ id: 'b', list: [6, 7] }
];
I am able to cut it down to two loops (below) instead of three, but I'm wondering if there's any way to simplify it to one loop/unnested loops.
data.forEach(obj => {
var map = {};
obj.list.forEach(el => map[el] = true);
itemsToRemove.forEach(el => if(map[el] { delete map[el] }));
obj.list = Object.keys(map);
});
You could take Array#filter with Array#includes.
const
itemsToRemove = [1, 2, 3],
data = [
{ id: 'a', list: [1, 3, 4, 5] },
{ id: 'b', list: [2, 6, 7] }
];
data.forEach(o => o.list = o.list.filter(v => !itemsToRemove.includes(v)));
console.log(data);
.as-console-wrapper { max-height: 100% !important; top: 0; }
If you need a more faster approach, you could take a Set.
const
itemsToRemove = [1, 2, 3],
data = [
{ id: 'a', list: [1, 3, 4, 5] },
{ id: 'b', list: [2, 6, 7] }
],
remove = new Set(itemsToRemove);
data.forEach(o => o.list = o.list.filter(v => !remove.has(v)));
console.log(data);
.as-console-wrapper { max-height: 100% !important; top: 0; }

How to reduce array of objects based on common key values?

Let's say, I have an Array of object which looks like:
var jsonData = [
{"DS01":123,"DS02":88888,"DS03":1,"DS04":2,"DS05":3,"DS06":666},
{"DS01":123,"DS02":88888,"DS03":2,"DS04":3,"DS05":4,"DS06":666},
{"DS01":124,"DS02":99999,"DS03":3,"DS04":4,"DS05":5,"DS06":333},
{"DS01":124,"DS02":99999,"DS03":5,"DS04":6,"DS05":7,"DS06":333}
];
You can see there are some common key fields which are DS01, DS02 and DS06. Firstly, I want to find which are common group of keys.
For first 2 Objects : DS01 = 123, DS02 = 88888, DS06 = 666
For last 2 Objects : DS01 = 124, DS02 = 99999, DS06 = 333
I want to convert this array of objects to a format like this:
var jsonDataReduced =
[{
"DS01": 123,
"DS02": 88888,
"DS03": [1, 2],
"DS04": [2, 3],
"DS05": [3, 4],
"DS06": 666
},
{
"DS01": 124,
"DS02": 99999,
"DS03": [3, 5],
"DS04": [4, 6],
"DS05": [5, 7],
"DS06": 333
}
];
Let's say, I have another array of objects.
var jsonData2 = [{
"Mass": 3,
"Force": 3.1,
"Acceleration": 4
}, {
"Mass": 3,
"Force": 4.1,
"Acceleration": 4
}];
So after reducing it should be:
var jsonData2 = [{
"Mass": 3,
"Force": [3.1, 4.1],
"Acceleration": 4
}];
I have been trying to do these by using Array.reduce() but not getting an idea on how to do this job efficiently.
Is it possible to
making a single function
passing these kinds of array of objects as a parameter
and finally getting the reduced dataset
What I have tried :
var jsonData2 = [{
"Mass": 3,
"Force": 3.1,
"Acceleration": 4
}, {
"Mass": 3,
"Force": 4.1,
"Acceleration": 4
}];
const reduced = jsonData2.reduce((r, e, i, a) => {
if (i % 2 == 0) {
const next = a[i + 1];
const obj = { ...e, Force: [e.Force] }
if (next) obj.Force.push(next.Force);
r.push(obj)
}
return r;
}, []);
console.log(reduced);
You could get common keys and group by them.
var data = [{ DS01: 123, DS02: 88888, DS03: 1, DS04: 2, DS05: 3, DS06: 666 }, { DS01: 123, DS02: 88888, DS03: 2, DS04: 3, DS05: 4, DS06: 666 }, { DS01: 124, DS02: 99999, DS03: 3, DS04: 4, DS05: 5, DS06: 333 }, { DS01: 124, DS02: 99999, DS03: 5, DS04: 6, DS05: 7, DS06: 333 }],
common,
temp = data.reduce((r, o, i) => {
Object.entries(o).forEach(([k, v]) => {
r[k] = r[k] || [];
r[k][i] = v;
});
return r;
}, {}),
min = Infinity,
result;
Object.entries(temp).forEach(([k, a]) => {
var s = new Set;
temp[k] = a.map(v => s.add(v).size);
min = Math.min(min, s.size);
});
common = Object.keys(temp).filter(k => temp[k][temp[k].length - 1] === min);
result = data.reduce((r, o) => {
var temp = r.find(q => common.every(k => q[k] === o[k]));
if (!temp) {
r.push({ ...o });
} else {
Object.keys(o).filter(k => !common.includes(k)).forEach(k => Array.isArray(temp[k]) ? temp[k].push(o[k]) : (temp[k] = [temp[k], o[k]]));
}
return r;
}, []);
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }

Convert multidimensional array to one-dimensional array

I have a multidimensional array. I want to group the values in this and know how many.
I've created a new array. I've looped a multidimensional array. If the current value does not exist in the new array, I add this value into the array. But I couldn't do it dynamically, they were all added to the bottom. I couldn't add it to "subCategories".
In this way I have a multidimensional array.
currentArray = [
[1, 2, 3, 5],
[1, 2, 3, 4],
[1, 2, 3, 4],
[1, 2, 3, 4],
[1, 2, 3, 4],
[1, 2, 3, 4],
[1, 2, 3, 4]
]
I used a loop like this.
newArray= [];
for (let i = 0; i < currentArray.length; i++) {
for (let k = 0; k < currentArray[i].length; k++) {
let obj = { id: currentArray[i][k], subCategories: [] };
let index = newCategories.findIndex(x => x.id === obj.id);
if (index === -1) {
if (k === 0) {
newCategories.push(obj);
}
} else {
newCategories[index].subCategories.push(obj);
}
}
}
I used a loop like this but I did not get a successful result. Logic error in the current code and I couldn't figure it out.
I want the same elements in the array to be added to the new array only once. And I want to get "count" in the last elements.
So the output I want to achieve is as follows.
{
"id": 1,
"subCategories": [
{
"id": 2,
"subCategories": [
{
"id": 3,
"subCategories": [
{
"id": 5,
"count": 1,
"subCategories": []
},
{
"id": 4,
"count": 6,
"subCategories": []
}
]
}
]
}
]
}
You could reduce the array by reduceing the inner array and look for the wanted id.
var array = [[1, 2, 3, 5], [1, 2, 3, 4], [1, 2, 3, 4], [1, 2, 3, 4], [1, 2, 3, 4], [1, 2, 3, 4], [1, 2, 3, 4]],
result = array
.reduce((r, a) => {
var o = a.reduce((p, id) => {
var temp = p.subCategories.find(q => q.id === id);
if (!temp) {
p.subCategories.push(temp = { id, subCategories: [] });
}
return temp;
}, r);
o.count = (o.count || 0) + 1;
return r;
}, { subCategories: [] })
.subCategories;
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }
This is in the same style as you had, by using a starting object which matches the inner format and a search for the items for returning this object for next level.
var currentArray = [[1, 2, 3, 5], [1, 2, 3, 4], [1, 2, 3, 4], [1, 2, 3, 4], [1, 2, 3, 4], [1, 2, 3, 4], [1, 2, 3, 4]],
newArray = [],
temp,
item;
for (let i = 0; i < currentArray.length; i++) {
temp = { subCategories: newArray };
for (let k = 0; k < currentArray[i].length; k++) {
item = temp.subCategories.find(x => x.id === currentArray[i][k]);
if (!item) {
temp.subCategories.push(item = { id: currentArray[i][k], subCategories: [] });
}
temp = item;
}
temp.count = (item.count || 0) + 1;
}
console.log(newArray);
.as-console-wrapper { max-height: 100% !important; top: 0; }

Categories