How to do two array operations parallelly in Javascript - javascript

I have two arrays of objects having structures like this:
let groups = [
{
word: 'ok',
id: 1,
},
{
word: 'hi',
id: 2,
}
]
let words = [
{
name: 'hello',
id: 1,
meaning: 'Grreeting !',
example: 'Hello how are you ?',
groupId: 1
},
{
name: 'morning',
id: 3,
meaning: 'First sign of the day !',
example: 'Good Morning ?',
groupId: 2
}
]
A group has many words. I have an empty array results[] and I will store all matching words there.
Now I want to search a keyword hi in both, groups and words array. The current approach I am following is :
First, I will map through groups array, and if I keyword hi matched with any group name,
then I will push all the words having that groupId into my results array.
Now I will map through the words array and if keyword hi matches with any word name then I will push that into results array.
Now, I want to do both these operations parallelly. Finding all the matches into words array and groups array parallelly and pushing data to results array.
Is there any way I can do this?

This can be done by first combining both arrays and then using the filter() array prototype function.
let groups = [
{
word: 'ok',
id: 1,
},
{
word: 'hi',
id: 2,
}
]
let words = [
{
name: 'hello',
id: 1,
meaning: 'Grreeting !',
example: 'Hello how are you ?',
groupId: 1
},
{
name: 'morning',
id: 3,
meaning: 'First sign of the day !',
example: 'Good Morning ?',
groupId: 2
}
];
let search = 'hi'; // get the search term from somewhere
let result = [ ...groups, ...words ] // combine arrays into one
.filter(({ word, name }) => ( // 'extract' word/name properties
word === search || name === search // return true when word or name matches searchterm
));
Little sidenote: in this solution searchterm can't be undefined, since that will always return true in the filter function.

Although you could technically do this with a single loop, doing so would be at the cost of clarity and simplicity. Basically you'd do a loop with the index going from 0 to Math.max(groups.length, words.length) and then do your check on each array:
for (let n = 0, len = Math.max(groups.length, words.length); n < len; ++n) {
if (n < groups.length) {
// Check `groups[i]`
}
if (n < words.length) {
// Check `words[i]`
}
}
That way, you have a single loop, which is about as close to "in parallel" as you're going to get without using a web worker (which would almost certainly be overkill). But again, it doesn't buy you much, and costs you clarity and simplicity.

Using async functions for the filter operations.
Not sure what you are doing that requires this, but note that they are not running multithreaded, and it's unlikely that this will be helpful or useful in solving the actual problem. Also, the order these execute in will be deterministic for your example.
const matches = async(list, key, match) => list.filter(x => x[key] === match);
let groups = [{
word: 'ok',
id: 1,
},
{
word: 'hi',
id: 2,
}
]
let words = [{
name: 'hello',
id: 1,
meaning: 'Grreeting !',
example: 'Hello how are you ?',
groupId: 1
},
{
name: 'morning',
id: 3,
meaning: 'First sign of the day !',
example: 'Good Morning ?',
groupId: 2
}
]
let matched = [];
Promise.all([
matches(groups, 'word', 'hi').then(m => {
matched = matched.concat(m);
console.log('groups matched:', matched);
}),
matches(words, 'name', 'hello').then(m => {
matched = matched.concat(m);
console.log('words matched:', matched);
})
]).then(res => {
console.log("matched:", matched);
console.log(".all results:", [...res[0], ...res[1]]);
})
console.log("matched immediate:", matched);
setTimeout(() => console.log("matched event loop delayed:", matched), 0);

Related

How can I .filter() through a .filter()? JavaScript

I'm trying to dynamically compare a searchTag field against an array student.tags , for every student which themselves all live in another array called students
So something like below
students = [
0: {
name: "Cheddar",
...,
tags: ["gravy","steak"]
},
...,
40: {
name: "Stacy",
...,
tags: ["gravy","grade"]
},
]
I'm failing bad with a filter inside a filter, and struggling returning the students who have that tag in their tag array
I can do
students.filter((student) => {
if ((student.tags.includes(searchTag))) {
return student;
} return '';
})
but this returns only on exact match and I need to find students dynamically, i.e. typing 'gra' returns both student 0 and 40, but 'grad' returns only 40
You can use a combination of filter and some to filter the array
const students = [
{
name: "Cheddar",
tags: ["gravy", "steak"],
},
{
name: "Stacy",
tags: ["gravy", "grade"],
},
];
let search = "gra";
const result = students.filter(({ tags }) => tags.some((tag) => tag.toLowerCase().includes(search.toLowerCase())));
console.log(result);

Find the difference between two arrays based on some the value inside a nested array

I have two arrays
arrayOfItems: [
{
id: '4321-3321-4423',
value: 'some text'
},
{
id: '4322-4654-9875',
value: 'some text again'
}
]
Then the second array
itemX: [
{
id: '3214-6543-4321',
nestedArrayOfItems:[
{id: '4321-3321-4423'}
{id: '3455-8765-7764'}
]
}
]
I need to create a new array based on arrayOfItems that doesn't include any of the id's in the itemX.nestedArrayOfItems
Because of it being a nested Array I'm drawing a blank on what I need to do... I'm searching through Lodash to see if there is something that doesn't involve me using a bunch of for loops.
You can use Array.prototype.filter() and then check if the id exists with
Array.prototype.some() like so:
const arrayOfItems = [
{
id: '4321-3321-4423',
value: 'some text'
},
{
id: '4322-4654-9875',
value: 'some text again'
}
]
const itemX = [
{
id: '3214-6543-4321',
nestedArrayOfItems: [
{id: '4321-3321-4423'},
{id: '3455-8765-7764'}
]
}
]
const res = arrayOfItems.filter(item => !itemX[0].nestedArrayOfItems.some(({ id }) => id === item.id))
console.log(res);
how about this :
let difference = arrayOfItems.filter(x => ! itemX.nestedArrayOfItems.includes(x));
PS : ids must be string

How to perform array join in Node.js efficiently/fast similar to MongoDB $lookup?

I want to perform a $lookup in Node.js similar to $lookup aggreation from MongoDB.
I have a solution but I'm not sure how fast it performs with more objects in each of the two arrays or with bigger objects.
let users = [
{userId: 1, name: 'Mike'},
{userId: 2, name: 'John'}
]
let comments = [
{userId: 1, text: 'Hello'},
{userId: 1, text: 'Hi'},
{userId: 2, text: 'Hello'}
]
let commentsUsers = [
{userId: 1, text: 'Hello', user: {userId: 1, name: 'Mike'}},
{userId: 1, text: 'Hi', user: {userId: 1, name: 'Mike'}},
{userId: 2, text: 'Hello', user: {userId: 2, name: 'John'}}
] //Desired result
I know this can be done easily with ECMA6 arrays. For example:
let commentsUsers = comments.map(comment => {comment, users.find(user => user.userId === comment.userId)} )
I that an effective way to do this for a large number of users eg. 1M users. How does lodash compare to this or any other more specialized library? Are there better ways to do this with vanilla JS eg. with Array.prototype.reduce()? Can indexing be used in any way to improve the performance of the join?
Edit:
My ideal solution
let users = [{userId:1,name:'Mike'},{userId:2,name:'John'}]
let comments = [{userId:1,text:'Hello'},{userId:1,text:'Hi'},{userId:2,text:'Hello'}];
let usersMap = new Map(users.map(user => [user.userId, user]))
let commentsUsers = comments.map(comment => ({...comment, user: usersMap.get(comment.userId)}))
console.log(commentsUsers)
Thanks for the feedback!
Your desired result is not a proper data structure. You are missing a key to your object of e.g. {userId: 1, name: 'Mike'}. I added user as the key value for a indexing solution.
First I create a Map where the userId will be our loop-up value. Afterwards I just iterate over the comments with map, transforming each object to a new one that contains all the comment information plus a new k-v pair of user. For that pair we don't need to use find anymore instead we have a simple HashMap get call.
Time-complexity-wise this changes the code from O(n^2) to O(n).
let users = [{userId:1,name:'Mike'},{userId:2,name:'John'}],
comments = [{userId:1,text:'Hello'},{userId:1,text:'Hi'},{userId:2,text:'Hello'}];
function mergeCommentUser(users, comments) {
let map = new Map(users.map(v => [v.userId, v]));
return comments.map(o => ({...o, user: map.get(o.userId)}));
}
console.log(JSON.stringify(mergeCommentUser(users,comments)))
Depending on what you want (and to save on redundancy), you could also change the following line:
let map = new Map(users.map(v => [v.userId, v]));
to the following instead:
let map = new Map(users.map(v => [v.userId, v.name]));
By that your result would look like:
[
{"userId":1,"text":"Hello","user":"Mike"},
{"userId":1,"text":"Hi","user":"Mike"},
{"userId":2,"text":"Hello","user":"Paul"}
]
Otherwise, you could omit the comment.userId and instead add the full user to the object for another way to avoid redundancy.
Currently, the code example you provide is O(n * m), or, O(n2). You could create a map of each of the userId's and their respective indexes in the users array, and then rather than find the user, you can directly access it by index. This will reduce the time to O(n + m), that is, O(n).
The code would look something like this:
const users = [{ userId: 1, name: "Mike" }, { userId: 2, name: "John" }];
const comments = [
{ userId: 1, text: "Hello" },
{ userId: 1, text: "Hi" },
{ userId: 2, text: "Hello" }
];
const map = new Map(users.map((o, i) => [o.userId, i]));
console.log(
comments.map(o => {
const index = map.get(o.userId);
return index !== undefined
? {
comment: o.text,
user: users[index]
}
: o;
})
);
Obviously, you can modify the end result, but this approach would be much more efficient than the one you proposed.

How to prevent lodash mapKeys from reordering my array?

I'm using lodash mapKeys to take my array of objects and convert it to a mapped object using the id property. That's simple enough, but the problem is that it's sorting the new object by id.
For example if I had three objects in my array:
let myArray = [
{
id: 3,
name: 'Number Three'
},
{
id: 1,
name: 'Number One'
},
{
id: 2,
name: 'Number Two'
}
];
Then I map the keys by id:
_.mapKeys(myArray, 'id')
It returns the following:
{
1: {
id: 1,
name: 'Number One'
},
2: {
id: 2,
name: 'Number Two'
},
3: {
id: 3,
name: 'Number Three'
}
}
My server returns the array in a specific order, so I would like the objects to remain the same, so that when I loop over the object properties, they are in the correct order.
Is that possible with this method? If not, is there a possible alternative to achieve the results?
Use a Map because each item has a custom key (like objects), but the order of insertion will be the order of iteration (like arrays):
const myArray = [
{
id: 3,
name: 'Number Three'
},
{
id: 1,
name: 'Number One'
},
{
id: 2,
name: 'Number Two'
}
];
const map = myArray.reduce((map, item) => map.set(item.id, item), new Map());
map.forEach((item) => console.log(item));
As pointed out in the comments, looping over an object doesn't guarantee order. If you want an ordered list, you need an array.
However, you could apply the iterator pattern. In this pattern, it's up to you to decide what “next” element is. So, you could have a set with the objects (in order to get them in constant time) and an array to store the order. To iterate, you'd use the iterator.
This code could be used as example.
Hope it helps.
let myArray = [{
id: 3,
name: 'Number Three'
}, {
id: 1,
name: 'Number One'
}, {
id: 2,
name: 'Number Two'
}];
let myIterator = ((arr) => {
let mySet = _.mapKeys(arr, 'id'),
index = 0,
myOrder = _.map(arr, _.property('id'));
return {
getObjById: (id) => mySet[id],
next: () => mySet[myOrder[index++]],
hasNext: () => index < myOrder.length
};
})(myArray);
// Access elements by id in constant time.
console.log(myIterator.getObjById(1));
// Preserve the order that you got from your server.
while (myIterator.hasNext()) {
console.log(myIterator.next());
}
<script src="https://cdn.jsdelivr.net/lodash/4.16.6/lodash.min.js"></script>
Like mentioned in the comments, the best would be to keep the object references both in an array to keep the order and in a hash to ease updating.
Backbone's collection (source) works like this. It keeps objects in an array (models), but automatically updates a hash (_byId) when adding and removing models (objects) or when a model's id changes.
Here's a simple implementation of the concept. You could make your own implementation or check for a collection lib.
// a little setup
var array = [];
var hash = {};
var addObject = function addObject(obj) {
hash[obj.id] = obj;
array.push(obj);
}
// Create/insert the objects once
addObject({ id: 3, name: 'Number Three' });
addObject({ id: 1, name: 'Number One' });
addObject({ id: 2, name: 'Number Two' });
// Easy access by id
console.log("by id with hash", hash['1']);
// updating is persistent with the object in the array
hash['1'].name += " test";
// keeps the original ordering
for (var i = 0; i < 3; ++i) {
console.log("iterating", i, array[i]);
}

Counting and storing distinct elements and their counts from an array of object

I have an array of object and I want to count the number of distinct elements and counts of those objects.
[ { name: 'Suman',
game: '5A'
},
{ name: 'Suman',
game: '5A'
},
{ name: 'Namus',
game: '5A'
},
{ name: 'Namus',
game: '5A'
}
]
I want to count the number of distinct names and store them in an object. I have tried it by 1# pushing all the names in an array,
2# then sorting them,
3# then calculating the number of distinct names and
4# finally pushing them to the object.
This process is too long. Is there a shorter way to do this. I am using Nodejs
Thanks in advance
You will create a new object, where the key is the name and the value the count:
var youArr = [
{ name: 'Suman',
game: '5A'
},
{ name: 'Suman',
game: '5A'
},
{ name: 'Namus',
game: '5A'
},
{ name: 'Namus',
game: '5A'
}
];
var count = {}
for(var i=0; i < youArr.length; i++){
count[youArr[i].name] = count[youArr[i].name] || 0;
count[youArr[i].name]++;
}
alert(count['Namus']); // 2
This is a great place to use the reduce function:
The reduce() method applies a function against an accumulator and each
value of the array (from left-to-right) has to reduce it to a single
value.
...
reduce executes the callback function once for each element present in
the array, excluding holes in the array, receiving four arguments: the
initial value (or value from the previous callback call), the value of
the current element, the current index, and the array over which
iteration is occurring.
It would look something like this:
var arr = [ { name: 'Suman',
game: '5A'
},
{ name: 'Suman',
game: '5A'
},
{ name: 'Namus',
game: '5A'
},
{ name: 'Namus',
game: '5A'
}
]
var counts = arr.reduce(function(counts, item) {
counts[item.name] = (counts[item.name] || 0) + 1;
return counts;
}, {});
counts is then:
{ Suman: 2, Namus: 2 }
Asked in the comments:
what if i want the count as well as name in an array of object like
[{name: 'Suman', count:'2'}, {name:'Namus', count:'2'}]
If you already have counts from the reduce call above, then you can map its keys to the format you want:
var countsArray = Object.keys(counts).map(function(name) {
return {name: name, count: counts[name]};
});
countsArray is then:
[ { name: 'Suman', count: 2 },
{ name: 'Namus', count: 2 } ]

Categories