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.
Related
I want to create a cache where I have O(1) lookups to its contents, and I lookup keys by value, not by reference. What data structure in JS, if any, would let me accomplish this?
Requirements:
The keys represent an array of valid JS variables (primitives or objects).
Comparison is handled by value, not by reference. (If it was reference, we could just use a single Map)
What I've tried:
I was thinking of nested Maps following this structure:
const resultKey = new Symbol('result'); // Create a unique result key, so we don't accidentally return if a key happens to be called 'result'.
// Cache is nested maps, not objects.
const cache = {
[key1]: {
[key2]: {
[key3]: {
[resultKey]: 1234
}
}
}
}
const foo = function cachedFunc(key1, key2, key3); // If these keys match values in the cache, just return the cache value.
And this would work fine for O(1) lookups by reference, but for value I would still need to iterate the keys at each level, and do a deep equality check.
Any ideas how I can get an O(1) lookup by value?
Would you like to try serialization with hash alongside with your value?
I mean, something like:
const cache = {
[key1]: {
[key2]: {
[key3]: {
[resultValue]: { a: 5, b: 6 },
[resultHash]: md5(JSON.stringify(val))
}
}
}
}
Browsers doesn't have any built-in hash functions API. So, get it from npmjs.org
UPDATE:
I may have misunderstood your question. What about this implementation?
const cache = new Map()
const hash = data => btoa(JSON.stringify(data))
const hashKeys = (...keys) => keys.map(key => hash(key)).join('-')
const store = (data, ...keys) => cache[hashKeys(...keys)] = data
const load = (...keys) => cache[hashKeys(...keys)]
seems the most suitable data structures for your task are HashMap and Set (based on HashMap). The average time for insert and getting is O(1)
https://adrianmejia.com/data-structures-time-complexity-for-beginners-arrays-hashmaps-linked-lists-stacks-queues-tutorial
Let's say I've got the following array of objects in JavaScript:
const requests = [
{
id: 1,
person: {
id: 1
}
},
{
id: 2,
person: {
id: 1
}
},
{
id: 3,
person: {
id: 2
}
},
{
id: 4,
person: {
id: 3
}
},
{
id: 5,
person: {
id: 2
}
}
]
And what I've written below will go over each item in the array, and then create a new array containing just the person object.
const requestsPeopleIds = []
for (const request of requests) {
requestsPeopleIds.push(request.person.id)
}
I then take that new array and create another new array using Set to remove the duplicate ids:
const uniquePeopleIds = Array.from(new Set(requestsPeopleIds))
The final result is as I'd expect:
console.log(uniquePeopleIds) // [1, 2, 3]
where these are the unique ids of the people who made a request. So out of the 5 requests, these were made by 3 people.
There must be a more efficient way of doing this, so I'm reaching out to you stack overflow JS gurus.
Thanks in advance.
I think you got the basics. Here's a way to tighten the code:
var ids = new Set;
requests.forEach(i => ids.add(i.person.id));
You could also do this with map method and spread syntax ....
const requests = [{"id":1,"person":{"id":1}},{"id":2,"person":{"id":1}},{"id":3,"person":{"id":2}},{"id":4,"person":{"id":3}},{"id":5,"person":{"id":2}}]
const result = [...new Set(requests.map(({ person: { id }}) => id))]
console.log(result)
You can do it by making an object by the person's id as a key and get the keys of the object.
const requests = [{"id":1,"person":{"id":1}},{"id":2,"person":{"id":1}},{"id":3,"person":{"id":2}},{"id":4,"person":{"id":3}},{"id":5,"person":{"id":2}}]
// Take an empty object
const uniques = {};
// Iterate through the requests array and make person's id as a
// key of the object and put any value at this index (here I put 1).
requests.forEach(request => (uniques[request.person.id] = 1));
// Finally get the keys of the unique object.
console.log(Object.keys(uniques));
I've done some research and have inferred some interesting facts:
It looks like when we have very various data and larger array, then Set collection shows not best results. Set is very optimized collection, however, in my view, it should always check whether element is already added into Set. And this checking will take O(n) complexity. But we can use simple JavaScript object. Checking whether object contains key is O(1). So object will have huge advantage over Set.
foreach arrow function is very convenient, however, simple for loop is faster.
Adding console.log makes Set the most fastest solution, however, without console.log, the most fastest solution is combination of for loop and object.
So the most performant code without console.log() looks like this:
const hashMap = {};
const uniques = [];
for (let index = 0; index < requests.length; index++) {
if (!hashMap.hasOwnProperty(requests[index].person.id)){
hashMap[requests[index].person.id] = 1;
uniques.push(requests[index].person.id);
}
}
However, the most performant code with console.log() looks like this(I cannot understand the reason why it happens. It would be really great to know why it happens):
var ids = new Set;
requests.forEach(i => ids.add(i.person.id));
console.log(ids)
Tests:
with console.log
without console.log
I wrote this function below which transforms the passed array of products by product type and currency type
function getProductsByCurrency(products, type, exchangeRate = 1) {
var productsRetrieved = products.map(item => ({id: item.id,
name: item.name,
price: (item.price * exchangeRate).toFixed(2),
type: type}));
return productsRetrieved;
}
Is it possible to break down the function to be more specific? or design it in a better way?
For example by naming it getProductsByCurrency it doesn't look right because if I am using it with default rate, i can pass books array to retrieve products with 'books' type independent of exchange rate.
Perhaps there is a way to use partial functions(FP)?
Edit:
Adding more context to what I am trying to achieve.
Lets say I have three categories of products(phones, cosmetics, books)
coming from three resources. I need to create three consolidated arrays of all the products by different currencies(productsinUSD, productsinAUD, productsinPounds)
Also using below function to consolidate the arrays
function concatProducts(arr) {
return [].concat.apply([], arr);
}
So I am calling getProductsByCurrency three times to transform them by product type and currency(exchange rate) and passing those values as an array to concat them to achieve productsinUSD. And repeat to get productsinAUD, productsinPounds.
Also type is string value(ex: 'mobiles')
First there's nothing really wrong with the function you posted. There's a few things I'd do differently, but I'm not going to pretend this isn't splitting hairs a bit.
const processItem = (type, exchangeRate = 1) => ({
id,
price,
name,
}) => ({
id,
name,
type,
price: (price * exchangeRate).toFixed(2),
});
We have a function that takes a type and an optional exchangeRate and returns a function that converts a single item to the form you want. This is what bob was talking about in the comments. I've also used object destructuring on the item and property shorthand on the result to make the code cleaner. Now we can map it over various categories of stuff:
const results = [
...mobilePhones.map(processItem('phone')),
...cosmetics.map(processItem('cosmetics')),
...books.map(processItem('book')),
];
If you need the interim results for some other purpose, just stuff them in vars but I've spread them directly into the results array for simplicity.
While this is shorter, clearer, and more flexible than the one you posted for the code quality hat trick, I'd like to reiterate that I've seen way, way worse than the function you posted.
There is nothing wrong with what you've done, but you could also create 3 more functions to call that in turn call getProductsByCurrency with the respective type.
var example = JSON.parse(`[{"id":1,"name":"1","price":5},{"id":2,"name":"2","price":15},{"id":3,"name":"3","price":20}]`);
function getProductsByCurrency(type, products, exchangeRate = 1) {
return products.map(item => ({
id: item.id,
name: item.name,
price: (item.price * exchangeRate).toFixed(2),
type: type
}));
}
function getPhonesByCurrency() {
return getProductsByCurrency("phone", ...arguments);
}
function getCosmeticsByCurrency() {
return getProductsByCurrency("cosmetic", ...arguments);
}
function getBooksByCurrency() {
return getProductsByCurrency("book", ...arguments);
}
console.log([].concat(getPhonesByCurrency(example), getCosmeticsByCurrency(example, 0.5), getCosmeticsByCurrency(example, 2)));
You might also prefer wrapping those 3 functions in an object (more tidy and helps IDE with autocomplete)
var example = JSON.parse(`[{"id":1,"name":"1","price":5},{"id":2,"name":"2","price":15},{"id":3,"name":"3","price":20}]`);
function getProductsByCurrency(type, products, exchangeRate = 1) {
return products.map(item => ({
id: item.id,
name: item.name,
price: (item.price * exchangeRate).toFixed(2),
type: type
}));
}
const getByCurrency = {
phones: function() {
return getProductsByCurrency("phone", ...arguments);
},
cosmetics: function() {
return getProductsByCurrency("cosmetic", ...arguments);
},
books: function() {
return getProductsByCurrency("book", ...arguments);
}
};
console.log([].concat(getByCurrency.phones(example), getByCurrency.cosmetics(example, 0.5), getByCurrency.books(example, 2)));
that depends on the kind of data you're feeding into these functions. if you'll pass different arrays of objects (that all have a type property) than I reckon you'd have a function that filters arrays by type ( or any other property and condition that is common between different sets of data). You could than chain your filter function with your mapping function. Your mapping function seems that it needs to be specific to the currenncy as you're plucking certain props from the object, than computing some of the values before you return it.
Hope this helps
In the following function I push and object to the accountsToDelete array, I need to then remove the matching object from the accountsToAdd array. I am guessing I would have to use a combination of IndexOf, Filter, Reduce but I am still a little rough in understanding how to accomplish this. This is the current function:
accountDelete(id, name) {
const accountsToAdd = this.userForm.value.accountsToAdd;
const accountsToDelete = this.userForm.value.accountsToDelete;
this.userForm.value.accountsToDelete.push(
{
id: id,
name: name
}
);
}
You can simply use the filter function. By this you can say, that in the accountToAdd all entries should be filtered, which id fits the to deleted account.
An example:
// Initialize both lists.
let accountsToAdd = []
let accountsToDelete = []
// Preparation by adding a account to the first list.
const account = { id: 1, name: 'Max' }
accountsToAdd.push(account)
// Mark account to be removed.
accountsToDelete.push(account)
accountsToAdd = accountsToAdd.filter(acc => acc.id !== account.id)
// Verify result.
console.log(accountsToAdd)
console.log(accountsToDelete)
Note:
Your both lists are defined as constant. By this you can't use the reassignment.
I am running eslint and it is recommended to return a value whenever an arrow function(lambda function) is used. Well that makes sense. However, I come across a case that is hard to walk around.
Dict = {}
Instances = [/* an array of items where items is a dictionary that contains data */]
Instances.map((item) => {
Dict[item.name] = item.url;
});
My goal is to get the data from the Instances array and fill the dictionary Dict with it. I am using the array function to assign key value pair to the dictionary, but that violates the rule of the arrow function.
Is there any iteratools or functions other than map that would help me to achieve the goal, and avoid the rule violation?
Edit: This does not adhere to Airbnb's ES6 Style Guide.
My goal is to get the data from the Instances array and fill the dictionary with it.
Use .reduce
.. and just pass an empty object as the accumulator, filling it up as you iterate through your array.
const instances = [
{ name: 'foo', url: 'https://google.com' },
{ name: 'bar', url: 'https://stackoverflow.com' }
]
const result = instances.reduce((dict, item) => {
dict[item.name] = item.url
return dict
}, {})
console.log(result)
Why not .map?
Array.map always returns a new Array and is meant for mapping each array element to another format.
If your resulting data structure shouldn't be an Array, with the same length as the Array you are operating on, you should avoid using it.
Why .reduce instead of .forEach?
I use forEach only for doing "work" rather than transforming data. Transforming data is almost always achievable with just map and/or reduce.
Here's what I mean by "work":
const users = [userInstance, userInstance, userInstance]
users.forEach(user => user.sendEmail('Hello World'))
Use forEach instead of map.
The point of map is to modify each item in an array and put the modified versions in a new array.
forEach just runs a function on each item.
If you are looking for ES6 solution to fill dictionary object this could help and should pass ESLint also:-
const dict = Instances.reduce((map, obj) => (map[obj.name] = obj.url, map), {});
update
const dict = Instances.reduce((map, obj) => {
let mapClone = {};
mapClone = Object.assign({}, map);
mapClone[obj.name] = obj.url;
return mapClone;
}, {});