Javascript reduce taking too much time to convert array into object - javascript

I have 2 lists of array. One having 50 elements(users) and other with 20000 elements(allUsers). I have to filter users based on if they are present in allUsers.
For that, i am converting allUsers into object and using that object to filter out users.
allUsers is an array of object with 3 keys.
So, creating object from array is taking too much of time. How can i reduce the time of the overall operationn?
const usersMap = allUsers.reduce((aa, user) => {
acc = { ...aa, [user.id]: user }
return aa
}, {})
const matchedUsers = users.reduce((aa, user) => {
const mappedUser = usersMap[user.id]
if (mappedUser) {
aa.push(mappedUser)
}
return aa
}, [])

Your use of spread is definitlely going to slow this down. { ...aa, [user.id]: user } creates a new object every iteration rather than just adding a new property to it, and in so doing has to iterate every property of the spread object again making your reduce approach O(n^2) rather than being O(n).
You can start by making your reduce more efficient by removing the unnecessary spread and simply assigning a new property in the accumulator.
const usersMap = allUsers.reduce((aa, user) => {
aa[user.id] = user;
return aa;
}, {});
Or you can try using a Map
const usersMap = new Map(allUsers.map((user) => [user.id, user]));
const matchedUsers = users.reduce((aa, user) => {
if (usersMap.has(user.id)) {
aa.push(usersMap.get(user.id));
}
return aa;
}, []);

const lookup = new Set(users.map(x => x.id));
const matchedUsers = allUsers.filter(x => lookup.has(x.id));
Instead of
Building a map of 20k items
Go through 50 items trying to relate them back to the map
Return an array of all the map values that match
Flip the operation around and
Get a set of 50 IDs to match.
Extract the up to 50 items from allUsers that have a matching ID.
Since there is no object created from the 20k items, this reduces the time to create one to zero.

users.filter(user => allUsers.find(u => u.id === user.id))
Filter the users that aren't found in all the users.

Related

Filter JSON by key value with JavaScript

given an array of objects in users.json:
{"key":"userSubscriptions","value":
[{"channel":"Netflix","user":"Bobby","region":"NA"},
[{"channel":"Netflix","user":"Bobby","region":"EU"},
[{"channel":"Netflix","user":"Jamie","region":"SEA"},
[{"channel":"Prime Video","user":"Bobby","region":"NA"}]}
How would one filter such that the end result will be
console.log(result); // Bobby, your region subscriptions are: Netflix: (NA, EU), Prime Video: (NA)
Thank you!
EDIT: i tried using .filter() & .map() method but no luck so far
you have to use lodash to group data, then map data
var data = {
"key":"userSubscriptions",
"value":
[{"channel":"Netflix","user":"Bobby","region":"NA"},
{"channel":"Netflix","user":"Bobby","region":"EU"},
{"channel":"Netflix","user":"Jamie","region":"SEA"},
{"channel":"Prime Video","user":"Bobby","region":"NA"}]
}
var users = _.chain(data.value)
.groupBy("user").map((value, key) => ({ user: key, data: value })).value();
users.forEach(element => {
console.log(element)
console.log(`${element.user}, your region subscriptions are: Netflix: ${element.data.map(c=>c.channel).join(',')}, Prime Video: ${element.data.map(c=>c.region).join(',')}`)
});
Your JSON data is invalid. However, I think I adapted it to what you intended.
This breaks the data down into a by-user format, and supplies a function to output the Subscriptions and Regions for any named user.
const jsonString = `{"key":"userSubscriptions","value":[{"channel":"Netflix","user":"Bobby","region":"NA"},{"channel":"Netflix","user":"Bobby","region":"EU"},{"channel":"Netflix","user":"Jamie","region":"SEA"},{"channel":"Prime Video","user":"Bobby","region":"NA"}]}`;
const obj = JSON.parse(jsonString);                                  // parse the data from a string into an object
const byUser = obj.value.reduce((acc, o) => {                        // reduce the "value" array to a single object
if (!acc[o.user]) acc[o.user] = {};                                // create acc.userName if it does not exist
if (acc[o.user][o.channel]) acc[o.user][o.channel].push(o.region); // if acc.userName.channelName has an array, add this region to it
else acc[o.user][o.channel] = [o.region];                          // else create an array with this region in it
return acc;                                                        // keep the created object for the next iteration
}, {});
function printForUser(userName) {                                    // create a function that takes a user's name
const user = byUser[userName];                                     // take the user object from the byUser object
return Object.keys(user).reduce((acc, k) => {                      // reduce the keys of the user object to one string
return acc + ` ${k}: (${user[k].join(", ")}),`;                  // add on each subscription and regions
}, `${userName}, your region subscriptions are:`).slice(0, -1);    // initialize the string and remove the last comma
}
console.log( printForUser("Bobby") );

How to change a property of an object in Vue?

I try to modify data in Vue for chartjs.
The basic datas are purchase orders with supplier_id and supplier_name
I like to know, how many orders each supplier has:
methods: {
render() {
let suppliers = []
_.each(this.orders, function (value, key) {
if(!_.find(suppliers, {name: value.supplier_name})) {
suppliers.push({
name: value.supplier_name,
num: 1
});
}
else {
let supplier = _.find(suppliers, {name: value.supplier_name})
let data = {
name: value.supplier_name,
num: supplier.num + 1
}
Vue.set(suppliers, suppliers.findIndex(suppliers => suppliers.name === value.supplier_name), data)
}
})
console.log(suppliers)
}
},
I read many posts and I created the solution above. But is it the best practice to do it?
Just to explain Dan's answer a bit as judging from the code in the original question, it might not make a lot of sense when you first look at it.
Create an empty object. This will be the "dictionary", which we will build in the next step. The idea is that we can fill this Object with keys/values for easy access. The keys will be the supplier_names and the values will be how many orders each supplier_name has.
render() {
const hash = {};
Build the dictionary. Since this.orders is an array, you can use forEach() to loop through its values. Take each order's supplier name (o.supplier_name) and use it as a key to look for a value in hash. If you find one, add 1 to it and store it back to the same location in hash. If you don't find one, store the value 1 at that location (|| 1). Again, forEach() will do this for each order in this.orders. When it finishes, you should have a complete dictionary of supplier_names along with how many orders each one has.
this.orders.forEach(o => {
hash[o.supplier_name] = hash[o.supplier_name] + 1 || 1;
})
Transform the object "dictionary" into an array. Since hash is an Object, we can't use forEach() to iterate over them like we did previously. However we can get an array containing just the supplier_names using Object.keys(). Then we can use map() to iterate over them and return a transformed result for each. Each result looks like this: { name: <supplier name>, num: <number of orders> }
const suppliers = Object.keys(hash).map(name => ({ name: name, num: hash[name] }))
console.log(suppliers);
}
While Lodash is a great library, it's really not needed in this case. A basic understanding of the built-in functions in JavaScript goes a long way.
You can make this simpler and without using the lodash library:
render() {
const hash = {};
this.orders.forEach(o => {
hash[o.supplier_name] = hash[o.supplier_name] + 1 || 1;
})
const suppliers = Object.keys(hash).map(name => ({ name: name, num: hash[name] }))
console.log(suppliers);
}
This uses an object hash (dictionary) without lodash to store the supplier / count key-value pair, and removes the excess finds / program logic / lodash.

Fetch pokemon API forEach issue

I need help with the forEach method. I am trying to mess around with the Pokemon API, but I can't run a forEach method without it returning undefined.
When I tried to use forEach instead of the map (I noticed map returned one big array) it still returned undefined.
fetch('https://pokeapi.co/api/v2/pokemon?limit=151')
.then(res => res.json())
.then(data => fetchPokemon(data))
const fetchPokemon = (res) => {
console.log(res)
// This turns the object api into an array
const arr = [res];
console.log(arr)
// This creates a variable to map correctly
const firstArr = arr[0].results
// This creates a new array with pokemon name
const array = firstArr.map(pokemon => pokemon.name)
console.log(array);
const html =
`
<div>${array}</div>
`
const pokemon = document.querySelector('.pokemon');
pokemon.innerHTML = html;
}
So there's a few things going on:
You don't need ".then(res => res.json())" because the response is already in json. You'll also want to extract the data property specifically like:
axios('https://pokeapi.co/api/v2/pokemon?limit=151')
.then(({data}) => fetchPokemon(data))
"const arr = [res];" does not turn the object into an array but rather places it into an array. Also, there's a further result layer you need to extra, so you'd want to instead do:
const fetchPokemon = (res) => {
//extract the results property into its own variable
const {results} = res;
//create an array of just object.name properties
const names = results.map( pokemon => pokemon.name)
}
Finally you can use the map property to create an array of just the pokemon names.
The forEach() method calls a function once for each element in an array, in order.
forEach: This iterates over a list and applies some operation with
side effects to each list member (example: saving every list item to
the database)
map: This iterates over a list, transforms each member of that list,
and returns another list of the same size with the transformed members
(example: transforming list of strings to uppercase)
from here
Meaning, if you want to do something for every element of the array, foreach is the correct function.
Example of Array.ForEach
var a = ["a", "b", "c"];
a.forEach(function(entry) {
console.log(entry);
});
Now, you are trying to create a new array with all the pokemon names, therefore map is correct - you are mapping the array into a new array.
However, if you want to, say, make an li element in html for every name, then it would be correct to use ForEach.
Example:
fetch('https://pokeapi.co/api/v2/pokemon?limit=151')
.then(res => res.json())
.then(data => fetchPokemon(data))
const fetchPokemon = (res) => {
const firstArr = [res][0].results
var ul = document.getElementById("pokemon");
firstArr.forEach(function (entry) {
var li = document.createElement('li');
li.appendChild(document.createTextNode(entry.name));
ul.appendChild(li);
});
}
<ul id="pokemon"></ul>
The forEach method is supposed to return undefined.

Remove Strings From Array

I'm trying to make an array of objects from the browsing history that can become links in a React list.
What this does is takes the history element and pushes it to the user object, then I take the name and ID, and splice it back into the array. I then filter it down to distinct values.
What I am left with, is an array that looks like this:
[{id1,name1},{id2,name2},{id3,name3},"id1","id2","id3"]
which is almost exactly what I want, except if I'm going to map that to create links, I need to get rid of the string values.
It occurred to me to try skipping the history element and just make an array of matches, and any number of other things.
pop and shift don't work to isolate the first object because then the whole function continually returns a single item.
This is the only way I have gotten a result close to what I want, I just need a simple way of filtering out string values from an array after it's created, or maybe before it's mapped out.
const getLead = async _id => {
const res = await axios.get(`/api/leads/${_id}`);
dispatch({
type: GET_LEAD,
payload: res.data
});
const { name } = res.data
const match = { name, _id }
const setPrevious = () => {
const prevLeads = user.prevLeads
const history = createBrowserHistory(getLead);
const prevLead = history.location.pathname.slice(6);
prevLeads.push(prevLead);
return prevLeads;
}
const prevLeads = setPrevious();
const [...spliceLeads] = prevLeads.splice(0,1,match,match);
const distinct = (value, index, self) => {
return self.indexOf(value) === index;
}
const recentLeads = prevLeads.filter(distinct) ;
console.log(spliceLeads)
}
You just check the type in your .filter logic:
const array = [
{ test: 'ing' },
1,
new Date(),
'this should be removed'
];
const result = array.filter(e => typeof e !== 'string');
console.log(result);
The solution what has been provided already works like charm, that should be used for your solution, below I'm providing a bit different way to figure out if the element is a string or not.
I guess filter() is a good way to test each elements on an array and return only the passing once as a result. If you call on each item Object.prototype.toString.call(e) then it will return as a text if you have [object String] or something else. This can be also used to filter out your string values from your array.
See the example below:
const prevLeads = [{id:'id1',name: 'name1'},{id: 'id2',name:'name2'},{id:'id3',name:'name3'},"id1","id2","id3"];
const filter = e => Object.prototype.toString.call(e) !== '[object String]';
const recentLeads = prevLeads.filter(filter);
console.log(recentLeads);
I hope that helps!

Filter an array and create a new array

I have a very simple requirement for filter some objects from a larger array and create a sub-array. Right now, I'm doing it as follows, but I'm wondering whether I could do inline and avoid the for-each.
var branches = $filter('filter')(vm.locations, { 'fkLocationTypeId': 5 });
vm.branchList = [];
angular.forEach(branches, function (obj) {
vm.branchList.push({ id: obj.Id, label: obj.locationDisplayName });
});
Since you want to both filter the array and modify the retained items, you can use filter() in combination with map(), both native array methods
vm.branchList = vm.locations
.filter(function(location) {
return location.fkLocationTypeId === 5;
})
.map(function(location) {
return {
id: location.Id,
label: location.locationDisplayName
};
});
If you want to avoid iterating over the array twice you can use the reduce() method to perform both the filtering and mapping at the same time
vm.branchList = vm.locations
.reduce(function(builtList, location) {
if (location.fkLocationTypeId === 5) {
return builtList.concat({
id: location.Id,
label: location.locationDisplayName
});
}
return builtList;
}, []);
I don't think there's much wrong with your use of forEach, but you could replace it by a map operation on the filtered set.
Personally, I'd use reduce to combine both filter and map operations in one loop, but you'd probably only start seeing a performance increase when you're filtering very large sets of data.
To combine the two in one reducer:
const locToObj = loc =>
({ id: loc.Id, label: loc.locationDisplayName });
const branchReducer = (acc, loc) =>
loc.fkLocationTypeId === 5
? (acc.push(locToObj(loc)), acc)
: acc
const branches = vm.locations.reduce(branchReducer, []);

Categories

Resources