Can this algorithm be made more efficient? - javascript

Currently I have an algorithm that runs to compare to different arrays of objects.
const allGroups = [{ id: '12345', name: 'groupOne'}, {id: '23421', name: 'groupTwo'},
{id: '28182', name: 'groupThree'}]
const clientsGroups = [{ id: 'abcde', clientGroupID: '12345'}, {id: 'dfcdae', clientGroupID: '93282'},
{id: 'jakdab', clientGroupID: '28182'}, {id: 'oiewad', clientGroupID: '93482'}]
const updateClientGroups = (allGroups, clientsGroups) => {
let allGroupsCopy = [...allGroups];
for (let i = 0; i < allGroupsCopy.length; i++) {
const allGroupsId = allGroupsCopy[i].id;
for (let j = 0; j < clientsGroups.length; j++) {
if (allGroupsId === clientsGroups[j].clientGroupID) {
allGroupsCopy[i] = {
...allGroupsCopy[i],
inGroup: true,
clientGroupID: clientsGroups[j].id,
};
}
}
}
return allGroupsCopy;
};
I check two different arrays of objects, if the id of allGroups matches the clientGroupID of clientGroups, I mutate the 'allGroupsCopy' to have 'inGroup: true' and add in the id of the clientsGroups.
The problem with this algorithm is it runs in n^2 time. Is there a more efficient way to do this?

Without changing the original arrays, could this be the an optimization ?
const allGroups = [
{ id: "12345", name: "groupOne" },
{ id: "23421", name: "groupTwo" },
{ id: "28182", name: "groupThree" },
];
const clientsGroups = [
{ id: "abcde", clientGroupID: "12345" },
{ id: "dfcdae", clientGroupID: "93282" },
{ id: "jakdab", clientGroupID: "28182" },
{ id: "oiewad", clientGroupID: "93482" },
];
const updateClientGroups = (groups, clients) => {
return clients.reduce((acum, current) => {
const isInGroup = groups.find((group) => group.id === current.clientGroupID);
acum.push({
...current,
inGroup: Boolean(isInGroup),
});
return acum;
}, []);
};
updateClientGroups(allGroups, clientsGroups)

If you change allGroups structure from array to map, you can do the job in linear time.
Something like:
const allGroups = {
'12345': { id: '12345', name: 'groupOne'}
...
}
const updateClientGroups = (allGroups, clientsGroups) => {
const clientGroupsMap = {};
clientsGroups.forEach(({clientGroupID}) =>
if(allGroups[clientGroupID]) {
clientGroupsMap[clientGroupID] = {...allGroups[clientGroupID], inGroup: true};
}
);
return {...allGroups, ...clientGroupsMap};
};

Related

Compare two arrays of objects, and remove if object value is equal

I've tried modifying some of the similar solutions on here but I keep getting stuck, I believe I have part of this figured out however, the main caveat is that:
Some of the objects have extra keys, which renders my object comparison logic useless.
I am trying to compare two arrays of objects. One array is the original array, and the other array contains the items I want deleted from the original array. However there's one extra issue in that the second array contains extra keys, so my comparison logic doesn't work.
An example would make this easier, let's say I have the following two arrays:
const originalArray = [{id: 1, name: "darnell"}, {id: 2, name: "funboi"},
{id: 3, name: "jackson5"}, {id: 4, name: "zelensky"}];
const itemsToBeRemoved = [{id: 2, name: "funboi", extraProperty: "something"},
{id: 4, name: "zelensky", extraProperty: "somethingelse"}];
after running the logic, my final output should be this array:
[{id: 1, name: "darnell"}, {id: 3, name: "jackson5"}]
And here's the current code / logic that I have, which compares but doesn't handle the extra keys. How should I handle this? Thank you in advance.
const prepareArray = (arr) => {
return arr.map((el) => {
if (typeof el === "object" && el !== null) {
return JSON.stringify(el);
} else {
return el;
}
});
};
const convertJSON = (arr) => {
return arr.map((el) => {
return JSON.parse(el);
});
};
const compareArrays = (arr1, arr2) => {
const currentArray = [...prepareArray(arr1)];
const deletedItems = [...prepareArray(arr2)];
const compared = currentArray.filter((el) => deletedItems.indexOf(el) === -1);
return convertJSON(compared);
};
How about using filter and some? You can extend the filter condition on select properties using &&.
const originalArray = [
{ id: 1, name: 'darnell' },
{ id: 2, name: 'funboi' },
{ id: 3, name: 'jackson5' },
{ id: 4, name: 'zelensky' },
];
const itemsToBeRemoved = [
{ id: 2, name: 'funboi', extraProperty: 'something' },
{ id: 4, name: 'zelensky', extraProperty: 'somethingelse' },
];
console.log(
originalArray.filter(item => !itemsToBeRemoved.some(itemToBeRemoved => itemToBeRemoved.id === item.id))
)
Or you can generalise it as well.
const originalArray = [
{ id: 1, name: 'darnell' },
{ id: 2, name: 'funboi' },
{ id: 3, name: 'jackson5' },
{ id: 4, name: 'zelensky' },
];
const itemsToBeRemoved = [
{ id: 2, name: 'funboi', extraProperty: 'something' },
{ id: 4, name: 'zelensky', extraProperty: 'somethingelse' },
];
function filterIfSubset(originalArray, itemsToBeRemoved) {
const filteredArray = [];
for (let i = 0; i < originalArray.length; i++) {
let isSubset = false;
for (let j = 0; j < itemsToBeRemoved.length; j++) {
// check if whole object is a subset of the object in itemsToBeRemoved
if (Object.keys(originalArray[i]).every(key => originalArray[i][key] === itemsToBeRemoved[j][key])) {
isSubset = true;
}
}
if (!isSubset) {
filteredArray.push(originalArray[i]);
}
}
return filteredArray;
}
console.log(filterIfSubset(originalArray, itemsToBeRemoved));
Another simpler variation of the second approach:
const originalArray = [
{ id: 1, name: 'darnell' },
{ id: 2, name: 'funboi' },
{ id: 3, name: 'jackson5' },
{ id: 4, name: 'zelensky' },
];
const itemsToBeRemoved = [
{ id: 2, name: 'funboi', extraProperty: 'something' },
{ id: 4, name: 'zelensky', extraProperty: 'somethingelse' },
];
const removeSubsetObjectsIfExists = (originalArray, itemsToBeRemoved) => {
return originalArray.filter(item => {
const isSubset = itemsToBeRemoved.some(itemToBeRemoved => {
return Object.keys(item).every(key => {
return item[key] === itemToBeRemoved[key];
});
});
return !isSubset;
});
}
console.log(removeSubsetObjectsIfExists(originalArray, itemsToBeRemoved));
The example below is a reusable function, the third parameter is the key to which you compare values from both arrays.
Details are commented in example
const arr=[{id:1,name:"darnell"},{id:2,name:"funboi"},{id:3,name:"jackson5"},{id:4,name:"zelensky"}],del=[{id:2,name:"funboi",extraProperty:"something"},{id:4,name:"zelensky",extraProperty:"somethingelse"}];
/** Compare arrayA vs. delArray by a given key's value.
--- ex. key = 'id'
**/
function deleteByKey(arrayA, delArray, key) {
/* Get an array of only the values of the given key from delArray
--- ex. delList = [1, 2, 3, 4]
*/
const delList = delArray.map(obj => obj[key]);
/* On every object of arrayA compare delList values vs
current object's key's value
--- ex. current obj[id] = 2
--- [1, 2, 3, 4].includes(obj[id])
Any match returns an empty array and non-matches are returned
in it's own array.
--- ex. ? [] : [obj]
The final return is a flattened array of the non-matching objects
*/
return arrayA.flatMap(obj => delList.includes(obj[key]) ? [] : [obj]);
};
console.log(deleteByKey(arr, del, 'id'));
let ff = [{ id: 1, name: 'darnell' }, { id: 2, name: 'funboi' },
{ id: 3, name: 'jackson5' },
{ id: 4, name: 'zelensky' }]
let cc = [{ id: 2, name: 'funboi', extraProperty: 'something' },
{ id: 4, name: 'zelensky', extraProperty: 'somethingelse' }]
let ar = []
let out = []
const result = ff.filter(function(i){
ar.push(i.id)
cc.forEach(function(k){
out.push(k.id)
})
if(!out.includes(i.id)){
// console.log(i.id, i)
return i
}
})
console.log(result)

Return similar values ​from multiple array of objects in Javascript

Guys I made a simple example to illustrate my problem. I have 3 object arrays, datasOne, datasTwo and datasThree and what I want is to return a new array only with the objects that are in the 3 arrays. For example, if there is only Gustavo in the 3 arrays, then he will be returned. But there is a detail that if the datasThree is an empty array, then it will bring the data in common only from datasOne and datasTwo and if only the datasTwo which has data and the other two arrays have empty, then it will return data only from datasTwo. In other words it is to return similar data only from arrays that have data. I managed to do this algorithm and it works the way I want, but I would like to know another way to make it less verbose and maybe simpler and also work in case I add more arrays to compare like a dataFour for example. I appreciate anyone who can help me.
My code below:
let datasOne = [
{ id: 1, name: 'Gustavo' },
{ id: 2, name: 'Ana' },
{ id: 3, name: 'Luiz' },
{ id: 8, name: 'Alice' }
]
let datasTwo = [
{ id: 1, name: 'Gustavo' },
{ id: 3, name: 'Luiz' },
{ id: 8, name: 'Alice' }
]
let datasThree = [
{ id: 1, name: 'Gustavo' },
{ id: 3, name: 'Luiz' },
{ id: 2, name: 'Ana' },
{ id: 5, name: 'Kelly' },
{ id: 4, name: 'David' }
]
let filtered
if (datasOne.length > 0 && datasTwo.length > 0 && datasThree.length > 0) {
filtered = datasOne.filter(firstData => {
let f1 = datasThree.filter(
secondData => firstData.id === secondData.id
).length
let f2 = datasTwo.filter(
secondData => firstData.id === secondData.id
).length
if (f1 && f2) {
return true
}
})
} else if (datasOne.length > 0 && datasTwo.length > 0) {
filtered = datasOne.filter(firstData => {
return datasTwo.filter(secondData => firstData.id === secondData.id).length
})
} else if (datasOne.length > 0 && datasThree.length > 0) {
filtered = datasOne.filter(firstData => {
return datasThree.filter(secondData => firstData.id === secondData.id)
.length
})
} else if (datasTwo.length > 0 && datasThree.length > 0) {
filtered = datasTwo.filter(firstData => {
return datasThree.filter(secondData => firstData.id === secondData.id)
.length
})
} else if (datasThree.length > 0) {
filtered = datasThree
} else if (datasTwo.length > 0) {
filtered = datasTwo
} else if (datasOne.length) {
filtered = datasOne
}
console.log(filtered)
1) You can first filter the array which is not empty in arrs.
const arrs = [datasOne, datasTwo, datasThree].filter((a) => a.length);
2) Flatten the arrs array using flat().
arrs.flat()
3) Loop over the flatten array and count the occurrence of all objects using Map
const map = new Map();
for (let o of arrs.flat()) {
map.has(o.id)
? (map.get(o.id).count += 1)
: map.set(o.id, { ...o, count: 1 });
}
4) Loop over the map and collect the result only if it is equal to arrs.length
if (count === arrs.length) result.push(rest);
let datasOne = [
{ id: 1, name: "Gustavo" },
{ id: 2, name: "Ana" },
{ id: 3, name: "Luiz" },
{ id: 8, name: "Alice" },
];
let datasTwo = [
{ id: 1, name: "Gustavo" },
{ id: 3, name: "Luiz" },
{ id: 8, name: "Alice" },
];
let datasThree = [
{ id: 1, name: "Gustavo" },
{ id: 3, name: "Luiz" },
{ id: 2, name: "Ana" },
{ id: 5, name: "Kelly" },
{ id: 4, name: "David" },
];
const arrs = [datasOne, datasTwo, datasThree].filter((a) => a.length);
const map = new Map();
for (let o of arrs.flat()) {
map.has(o.id)
? (map.get(o.id).count += 1)
: map.set(o.id, { ...o, count: 1 });
}
const result = [];
for (let [, obj] of map) {
const { count, ...rest } = obj;
if (count === arrs.length) result.push(rest);
}
console.log(result);
/* This is not a part of answer. It is just to give the output fill height. So IGNORE IT */
.as-console-wrapper { max-height: 100% !important; top: 0; }
Not 100% sure it cover all edge cases, but this might get you on the right track:
function filterArrays(...args) {
const arraysWithData = args.filter((array) => array.length > 0);
const [firstArray, ...otherArrays] = arraysWithData;
return firstArray.filter((item) => {
for (const array of otherArrays) {
if (!array.some((itemTwo) => itemTwo.id === item.id)) {
return false;
}
}
return true;
});
}
Usage:
const filtered = filterArrays(datasOne, datasTwo, datasThree);
console.log(filtered)
I believe the code is fairly readable, but if something is not clear I'm glad to clarify.
function merge(arr){
arr = arr.filter(item=>item.length>0)
const map = {};
arr.forEach(item=>{
item.forEach(obj=>{
if(!map[obj.id]){
map[obj.id]=[0,obj];
}
map[obj.id][0]++;
})
})
const len = arr.length;
const ret = [];
Object.keys(map).forEach(item=>{
if(map[item][0]===len){
ret.push(map[item][1])
}
})
return ret;
}
merge([datasOne,datasTwo,datasThree])

Getting occurrences of different values on nested object

I've an array of objects like this:
arrObj = [{
id: 1
data: {
info: {
name: 'jhon'
}
}
},{
id: 1
data: {
info: {
name: 'jane'
}
}
},{
id: 1
data: {
info: {
name: 'jhon'
}
}
}]
And I needs get a summary of occurrences for different values, like this:
{ jane: 1, jhon: 2 }
The big problem is that I need pass the nested prop dynamically:
getSummary('data.info.name',obj) //--> { jane: 1, jhon: 2 }
Any ideas?
You can use the below code, this is just hint. you need to do error handling if some input is not having correct nested keys.
let arrObj = [{
id: 1,
data: {
info: {
name: 'jhon'
}
}
},{
id: 1,
data: {
info: {
name: 'jane'
}
}
},{
id: 1,
data: {
info: {
name: 'jhon'
}
}
}]
const getSummary = (dynamicKeys,obj) => {
const list = dynamicKeys.split('.');
const op = {};
for (let i = 0; i < obj.length; i++) {
let n = 1, key = obj[i][list[0]];
while (list.length > n) {
key = key[list[n]];
n++;
}
op[key] = op[key] ? op[key] + 1 : 1;
}
return op;
}
const test = getSummary('data.info.name', arrObj);
console.log(test)
A possible solution could be as below. Here at first given prop is found out from each element of arrayObj. If the finding isn't successful, the element is skipped and move to next. When the finding is successful, append the finding value to summary if it does not exist in summary or increment the existing value. You can change the code as your requirements.
const arrObj = [{
id: 1,
data: {
info: {
name: 'jhon'
}
}
}, {
id: 1,
data: {
info: {
name: 'jane'
}
}
}, {
id: 1,
data: {
info: {
name: 'jhon'
}
}
}];
const getSummary = (prop, arr) => {
const keys = prop.split('.');
const findPropValue = (elem) =>
keys.reduce((val, key, index) => {
if (index === 0) return elem[key];
return (val && val[key]) || val
}, null);
return arr.reduce((sum, curr) => {
const key = findPropValue(curr);
if (!key) return sum;
sum[key] = (sum[key] && sum[key] + 1) || 1;
return sum;
}, {});
};
console.log(getSummary('data.info.name', arrObj));
Go over elements using forEach. For each object, access the value and build a res object with keys as value (eg jane) and object values are aggregated.
[Access the value, by split the path, access object nested using reduce)
const getSummary = (path, items) => {
const paths = path.split(".");
const res = {};
items.forEach((item) => {
const value = paths.reduce((acc, cur) => acc[cur], item);
res[value] = (res[value] ?? 0) + 1;
});
return res;
};
arrObj = [
{
id: 1,
data: {
info: {
name: "jhon",
},
},
},
{
id: 1,
data: {
info: {
name: "jane",
},
},
},
{
id: 1,
data: {
info: {
name: "jhon",
},
},
},
];
const output = getSummary("data.info.name", arrObj);
console.log(output);

Most efficient way to attach properties from one object to another if certain key:values match?

Let's say I have two data sources that are fairly large, 3000+ entries each. They might look something like this...
const arOfObj1 = [
{ type: 'Something', properties: { name: 'ABC' } },
{ type: 'Something', properties: { name: 'DEF' } },
{ type: 'Something', properties: { name: 'GHI' } },
...and so on...
];
const arOfObj2 = [
{ name: 'ABC', stats: { age: 1, other: "Something" } },
{ name: 'DEF', stats: { age: 2, isEnrolled: true } },
{ name: 'GHI', stats: { age: 3 } },
...and so on...
];
What would be the most efficient way of finding the name property that matches in each object, and appending the stats{...} (or other properties if they exist) from arOfObj2 to arOfObj1? so I would end up with something like this
const newArOfObj1 = [
{ type: 'Something', properties: { name: 'ABC', stats: { age: 1, other: "Something" } },
{ type: 'Something', properties: { name: 'DEF', stats: { age: 2, isEnrolled: true } },
{ type: 'Something', properties: { name: 'GHI', stats: { age: 3 } },
...and so on...
]
My initial thought was to do something like this...
arOfObj1.forEach(obj1 => {
arOfObj2.forEach(obj2 => {
if (obj1.properties.name === obj2.name) {
obj1.stats = obj2.stats
}
})
});
Just not sure if there's a better way than to loop through arOfObj2 for each entry in arOfObj1
I don't know what the most efficient way is, and I don't really know how the memory works in javascript, but I have a way that works.
const arOfObj1 = [
{ type: 'Something', properties: { name: 'ABC' }},
{ type: 'Something', properties: { name: 'DEF' }},
{ type: 'Something', properties: { name: 'GHI' }}
];
const arOfObj2 = [
{ name: 'ABC', stats: { age: 1, other: "Something" } },
{ name: 'DEF', stats: { age: 2, isEnrolled: true } },
{ name: 'GHI', stats: { age: 3 } }
];
var names = new Map();
for(var i = 0, len = arOfObj2.length; i < len; i++){
var obj = arOfObj2[i];
names.set(obj.name, obj.stats);
}
for(var i = 0, len = arOfObj1.length; i < len; i++){
var properties = arOfObj1[i].properties;
properties.stats = names.get(properties.name);
}
console.log(arOfObj1);
What it does is loop through each object, saving the name and stats as the key and value in a Map. Then it loops through the first array of objects, adding the stats property that it gets from Map.get.
Memory is usually not the issue in the JavaScript applications and 3000+ array of objects is not that big for a modern machine. I'll have to assume that you're looking for a speed increase.
In case I am wrong and you're looking for something that would be more memory efficient then ignore my response and look in to batching with something like js batch to avoid loading up memory all at once and instead spreading the memory load across batches.
Assuming we're looking for a fastest way to complete the task.
Code:
map/find
arOfObj1.map(o => ({
...o,
properties: {
...o.properties,
stats: arOfObj2.find(a => a.name === o.properties.name).stats
}
}));
Simple double for loop
let result = [];
for (let i = 0; i < arOfObj1.length; i++) {
let o = arOfObj1[i];
let stats;
for (let j = 0; j < arOfObj2.length; j++) {
if (stats) break;
let a = arOfObj2[j];
if (a.name === o.properties.name) {
stats = a.stats;
}
}
result.push({
...o, properties: {...o.properties, stats}
});
}
Map set - while I was at it #programmerRaj posted his answer with this solution so look at his response, but I'll include it in my speed test below.
So now we can test these for speed:
Filling both arrays with 5000 items and running all solutions while measuring time.
let arOfObj1 = [];
let arOfObj2 = [];
for (let i = 0; i < 5000; i++) {
arOfObj1.push({type: 'Something', properties: { name: `name${i}` }});
arOfObj2.push({name: `name${i}`, stats: { age: i, other: "Something" }});
}
console.time('map/find');
arOfObj1.map(o => ({
...o,
properties: {
...o.properties,
stats: arOfObj2.find(a => a.name === o.properties.name).stats
}
}));
console.timeEnd('map/find');
console.time('for loop');
let result = [];
for (let i = 0; i < arOfObj1.length; i++) {
let o = arOfObj1[i];
let stats;
for (let j = 0; j < arOfObj2.length; j++) {
if (stats) break;
let a = arOfObj2[j];
if (a.name === o.properties.name) {
stats = a.stats;
}
}
result.push({
...o, properties: {...o.properties, stats}
});
}
console.timeEnd('for loop');
console.time('programmerRaj\'s solution');
var names = new Map();
for(var i = 0, len = arOfObj2.length; i < len; i++){
var obj = arOfObj2[i];
names.set(obj.name, obj.stats);
}
for(var i = 0, len = arOfObj1.length; i < len; i++){
var properties = arOfObj1[i].properties;
properties.stats = names.get(properties.name);
}
console.timeEnd('programmerRaj\'s solution');
Running test with Node lts (12) I get the following results:
map/find: 189.902ms
for loop: 188.912ms
programmerRaj's solution: 3.236ms

Javascript - How to combine all combinations into an array of objects

I have the following array:
[{
name: 'foo',
values: '10,12'
},
{
name: 'bar',
values: 'red,blue'
}]
Using some javascript logic I would like to output the following array:
[{
option1: 10,
option2: 'red'
},
{
option1: 10,
option2: 'blue'
},
{
option1: 12,
option2: 'red'
},
{
option1: 12,
option2: 'blue'
}]
What is the best and correct way to achieve this using javascript?
Lets say your first array is named arr.
var arr = [{
name: 'foo',
values: '10,12'
},
{
name: 'bar',
values: 'red,blue'
}];
var v1 = arr[0].values.split(',');
var v2 = arr[1].values.split(',');
var res = new Array();
for(i in v1){
for(j in v2){
res.push({'option1':v1[i],'option2':v2[j]});
}
}
console.log(res);
Here's an approach that can handle an arbitrary number of objects.
function valuesCrossProduct(input) {
return input.flatMap((current, index, array) => {
let result = [];
let values = current.values.split(',');
for (let v of values) {
for (let i = 0; i < array.length; i++) {
if (i <= index) {
// Skip creating cross products with self (i.e. == index)
// and with previously visited objects (i.e. < index).
continue;
}
let iValues = array[i].values.split(',');
let currentKey = `option${index}`;
let iKey = `option${i}`;
for (let iv of iValues) {
result.push({
[currentKey]: v,
[iKey]: iv,
});
}
}
}
return result;
});
}
let twoElementArray = [{
name: 'foo',
values: '10,12'
},
{
name: 'bar',
values: 'red,blue',
}];
let threeElementArray = [{
name: 'foo',
values: '10,12'
},
{
name: 'bar',
values: 'red,blue',
},
{
name: 'baz',
values: 'wham,bam',
}];
console.log(valuesCrossProduct(twoElementArray));
console.log(valuesCrossProduct(threeElementArray));
Functional for the win.
Note: as it is, this only works for an array of two objects, with any number of values in each, where the first set of values are numbers and the second set are strings, which is what you described above.
const arr = [{
name: 'foo',
values: '10,12'
},
{
name: 'bar',
values: 'red,blue'
}];
const values = arr
.map(o => o.values.split(','))
.reduce((cur, next) => {
return cur.map(c => {
return next.map(n => {
return {
option1: parseInt(c),
option2: n
};
});
}).flat();
});
console.log(values);
If you need generic approach to get possible options from various values.
const options = data => {
let sets = [[]];
data.forEach(({ values }, i) => {
const new_set = [];
values.split(",").forEach(value => {
new_set.push(
Array.from(sets, set => [...set, [`option${i + 1}`, value]])
);
});
sets = new_set.flatMap(set => set);
});
return sets.map(set => Object.fromEntries(set));
};
const data = [
{
name: "foo",
values: "10,12"
},
{
name: "bar",
values: "red,blue,green"
},
{
name: "test",
values: "top,bottom"
}
];
console.log(options(data));

Categories