How to make this function more reusable/specific/better design? - javascript

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

Related

Why is my element counter not properly functioning?

So I'm working on a project that compares different arrays. It's almost completely working, however I have one problem. Say that in this situation, a and b are two different fetches that hold array data. The data is mostly the same however one is slightly more updated than the other (the non-updated one is a subset essentially). The code I have down here is this:
async function dataDetect() {
let fetchA = await fetch('https://a/data');
let fetchB = await fetch('https://b/data');
let dataA = await fetchA.json();
let dataB = await fetchB.json();
let differenceDetector = (dataA, dataB) => {
let compareA = new Set(dataA);
for (const x of new Set(dataB)) {
if (compareA.has(x)) {
compareA.delete(x);
} else {
compareA.add(x);
}
}
return Array.from(compareA);
};
let detected = differenceDetector(dataA, dataB);
console.log('All differences script detected: ',{detected});
}
dataDetect();
And almost everything is working. However, I'm having a huge problem. For some reason whenever I run this, the total array is actually both of the arrays combined, and it never removed the common elements. I'm sure there is a way to fix this but I've tried multiple combinations as to how and none of them have fully worked. My problem is kinda like this(these aren't the actual arrays in my thing):
dataA=['Violet','Orange','Plumage','Crimson']
and
dataB=['Violet','Orange','Plumage','Crimson','Maroon'].
The console logs this: ['Violet','Orange','Plumage','Crimson','Violet','Orange','Plumage','Crimson','Maroon'].
My final log is literally just both of the arrays stacked. This works with normal arrays but with fetches it doesn't. Why does this happen and can someone explain how to fix this?
let differenceDetector = (dataA, dataB) => {
let compareA = new Set(dataA);
for (const x of new Set(dataB)) {
if (compareA.has(x)) {
compareA.delete(x);
} else {
compareA.add(x);
}
}
return Array.from(compareA);
};
const dataB = ['Violet', 'Orange', 'Plumage', 'Crimson', 'Maroon'];
const dataA = ['Violet', 'Orange', 'Plumage', 'Crimson'];
console.log(differenceDetector(dataA, dataB));
Also, I see some people wanted to know what the actual array data was.
I don't think you guys need to know the data but one guy said that if it was objects it wouldn't work. That's what the array is made of. Objects. So since there all objects how can I fix it?
The distinction between arrays of objects vs. strings has to do with how equality is tested. The Set has() method wraps an equality test (like ===) which works for immutable types but requires generalization to compare objects, such as those the OP might get from an API...
This intersection function allows the caller to pass in an arbitrary predicate. The caller can use it to perform an equivalence test on objects.
function differenceDetector(arrayA, arrayB, test) {
test = test || ((a, b) => a === b); // default to deep equality
return arrayA.filter(a => !arrayB.find(b => test(a,b)));
}
// this works as expected for strings...
const a = ['Violet', 'Orange', 'Plumage', 'Crimson', 'Maroon'];
const b = ['Violet', 'Orange', 'Plumage', 'Crimson'];
console.log(differenceDetector(a, b));
// and it also works for objects...
const c = [{ id: 1 }, { id: 2}, { id: 3 }, { id: 4 }];
const d = [{ id: 1 }, { id: 2}, { id: 3 }];
const test = (a, b) => a.id === b.id;
// note that we pass an equivalence predicate "test"
// without it, the default "===" test will give the OP's undesired result
console.log(differenceDetector(c, d, test));
Note that this implementation, like the OP's, is not commutative. The difference found is elements of the first array that are not in the second, according to a given predicate.
Also note, comparing whole objects for equivalence (all keys and values are equivalent) is a tricky subject. A cheap way to code -- though maybe not so cheap at run time -- is to compare JSON encodings...
const wholeObjectTest = (a, b) => JSON.stringify(a) === JSON.stringify(b);

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.

Extracting unique values from an array of objects where value is a nested object in JavaScript

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

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, []);

BackboneJS: iterate on model attribute and change value

I want to make a function with functionality like toJSON()'s functionality which returns and edits model.
My question is how to iterate on model's attribute and edit the specific value of the attribute you selected.
If have a model e.g:
Item = Backbone.Model.extend({
defaults: {
name: '',
amount: 0.00
},
toHTML: function(){
// i think this is the place where
// where i can do that function?
//
console.log(this.attribute)
}
});
var item = new Item;
item.set({name: 'Pencil', amount: 5}):
item.toJSON();
-> {name: 'Pencil', amount: 5}
// this is the function
item.toHTML();
-> {name: 'Pencil', amount: 5.00}
You can iterate over an object using a for ... in loop and then use toFixed to format the number:
toHTML: function() {
var attrs = { }, k;
for(k in this.attributes) {
attrs[k] = this.attributes[k];
if(k === 'amount')
attrs[k] = attrs[k].toFixed(2);
}
return attrs;
}
Note that amount will come out as a string but that's the only way to get 5.00 rather than 5 to come out. I'd probably leave the formatting up to the template and not bother with this toHTML implementation.
Demo: http://jsfiddle.net/ambiguous/ELTe5/
If you want to iterate over a model's attributes, use the attributes hash:
// Inside your model's method
for(attr in this.attributes){
console.log(attr, this.attributes[attr]);
}
Here's a jsFiddle using your example code.
Though the answers provided here are correct and would do what you want. But I think the better way would be to use the underscore functions for this purpose.
for simple looping you can use
_.each(list, iteratee, [context])
_.each(model.attributes, function(item, index, items){
console.log(item);
console.log(index);
})
you can also use specialized functions as per your specific need. Like if you are want to have a new result array as a result of applying some function on every element of your list, map can be the best option for you.
_.map(list, iteratee, [context])
var newList = _.map(model.attributes, function(item, index, list){
return item * 5;
})
I would recommend you to go through the documentation of underscore and backbone for the best function for your need.

Categories

Resources