I started experimenting with functional programming lately and I'm trying to convert an old module I have written using imperative programming.
Let's say I have two arrays of objects i.e
orders: [
{
idOrder: 1,
amount: 100,
customerId: 25,
},
{
idOrder: 2,
amount: 200,
customerId: 20,
}
]
customers: [
{
customerId: 20,
name: "John Doe",
orders: []
},
{
customerId: 25,
name: "Mary Jane",
orders: []
}
]
I want to push all the orders to their respective customer. Is there a clean way of doing it?
I have tried this , but obviously it doesn't work that way :
customers.orders = orders.filter((x) => {
if (x.customerId === customerId) {
customer.orders.push(x);
}
});
Thanks
You could use a Map and get all customers first and then push the orders to the customers.
var object = { orders: [{ idOrder: 1, amount: 100, customerId: 25 }, { idOrder: 2, amount: 200, customerId: 20 }], customers: [{ customerId: 20, name: "John Doe", orders: [] }, { customerId: 25, name: "Mary Jane", orders: [] }] },
map = object.customers.reduce((m, a) => m.set(a.customerId, a), new Map);
object.orders.forEach(a => map.get(a.customerId).orders.push(a));
console.log(object.customers);
Possible solution:
for (c of customers){
c.orders.push(orders.filter( function(o){ return o.customerId === c.customerId} ));
}
If you think of customers as your accumulator you can Reduce orders with customers as your initial value.
NOTE: this does mutate customers if you do not want this as a side-effect you would have to clone customers. Also there is not error handling for customerId not found.
var orders = [{ idOrder: 1, amount: 100, customerId: 25 }, { idOrder: 2, amount: 200, customerId: 20}];
var customers = [{ customerId: 20, name: "John Doe", orders: [] }, { customerId: 25, name: "Mary Jane", orders: [] } ];
var customers_orders = orders.reduce(
(accum, v) =>
{ accum.find(
c => c.customerId == v.customerId).orders.push(v);
return accum;
}, customers);
console.log(customers_orders);
You can write a function and pass it to reduce method and compose it with map
Just one things: once it's created, it may never change. You can user Object.assign and concat.
var customersWithOrders = customers.map(function(customer) {
var relatedOrders = orders.filter(function(order) { return order.customerId === customer.customerId })
return Object.assign(
customer,
{
orders: customer.orders.concat(relatedOrders)
}
)
})
Related
I am trying to improve the time complexity and quality of the code snippet below.
I am iterating through one array to check if the element this array exists in the object, should this be true it should return the name matching the element id in the object.
how can I do this without having a nested loop?
Can someone tell me what I can do to make this algo better, please?
Thank you all in advance.
let genres = [28, 12, 878];
data = {
genres: [
{
id: 28,
name: 'Action',
},
{
id: 12,
name: 'Adventure',
},
{
id: 16,
name: 'Animation',
},
{
id: 35,
name: 'Comedy',
},
{
id: 80,
name: 'Crime',
},
{
id: 99,
name: 'Documentary',
},
{
id: 18,
name: 'Drama',
},
{
id: 10751,
name: 'Family',
},
{
id: 14,
name: 'Fantasy',
},
{
id: 36,
name: 'History',
},
{
id: 27,
name: 'Horror',
},
{
id: 10402,
name: 'Music',
},
{
id: 9648,
name: 'Mystery',
},
{
id: 10749,
name: 'Romance',
},
{
id: 878,
name: 'Science Fiction',
},
{
id: 10770,
name: 'TV Movie',
},
{
id: 53,
name: 'Thriller',
},
{
id: 10752,
name: 'War',
},
{
id: 37,
name: 'Western',
},
],
};
const getGenreName = () => {
let result = [];
for (let genre of data.genres) {
//console.log("genre", genre.name)
for (let id of genres) {
//console.log('id',genres[i])
if (id === genre.id) result.push(genre.name);
}
}
console.log(result);
};
getGenreName();
You can use reduce and includes as others have already shown. This will make the code a bit cleaner, but not change the overall runtime complexity. To improve runtime complexity you may need to use a different data structure.
For instance instead of
let genres = [1,2,3,4];
as a simple array, you could use a Set, which has a better lookup performance.
let genres = new Set([1,2,3,4]);
Then you can use this as follows
let result = data.genres
.filter(g => genres.has(g.id))
.map(g => g.name);
and won't need any explict for loops
The simplest improvement would probably be converting genres to a Set https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set
and use the has method to check if each id in the data is a member of the set of chosen genres.
You can also convert the data to a map with the ids as the keys in order to look up by id quickly instead of looping, but that is only faster if the data is reused many times.
JavaScript #reduce in the example outlined below would have O(n) time complexity. This only loops through the array once. We could use filter, and map but it would result in us having to loop through the array twice.
const getGenreName = () => {
const genreSet = new Set(genres);
return data.genres.reduce((accumulator, { id, name }) => {
if (genreSet.has(id)) accumulator.push(name);
return accumulator;
}, []);
};
console.log(getGenreName()); // [ 'Action', 'Adventure', 'Science Fiction' ]
We are initializing the reducer to start with the array [], or an empty array, and then checking to see if the genre property of the object is included in the genres array, if it isn't, return the accumulator, if it is, append it to the end of the accumulator and return it.
You wanted this in one loop, so here it is:
let result = [];
data.genres.forEach(function (e) {
if (genres.includes(e.id)) result.push(e.name);
});
console.log(result);
In case you were wondering about forEach, here's a very good reference: https://www.w3schools.com/jsref/jsref_foreach.asp
The current time complexity is O(MN) where M is the length of data.genres and N is the length of genres.
Time complexity in JavaScript depends on which engine you use, but in most cases you can use a Map to reduce this time complexity to O(max{N,M}):
const getGenreName = () => {
const dataGenresMap = new Map( // O(M)
data.genres.map(({id,...params}) => [id,params]) // O(M)
)
let result = []
for (let id of genres) { // O(N)
if (dataGenresMap.has(id)) result.push(dataGenresMap.get(id).name) // O(1)
}
console.log(result)
}
If you might be doing this more than once then I'd recommend using a Map. By creating a hash map, retrieving genre names per id is much more performant.
let genres = [28, 12, 878];
data = {
genres: [
{
id: 28,
name: 'Action',
},
{
id: 12,
name: 'Adventure',
},
{
id: 16,
name: 'Animation',
},
{
id: 35,
name: 'Comedy',
},
{
id: 80,
name: 'Crime',
},
{
id: 99,
name: 'Documentary',
},
{
id: 18,
name: 'Drama',
},
{
id: 10751,
name: 'Family',
},
{
id: 14,
name: 'Fantasy',
},
{
id: 36,
name: 'History',
},
{
id: 27,
name: 'Horror',
},
{
id: 10402,
name: 'Music',
},
{
id: 9648,
name: 'Mystery',
},
{
id: 10749,
name: 'Romance',
},
{
id: 878,
name: 'Science Fiction',
},
{
id: 10770,
name: 'TV Movie',
},
{
id: 53,
name: 'Thriller',
},
{
id: 10752,
name: 'War',
},
{
id: 37,
name: 'Western',
},
],
};
const genreById = new Map ();
data.genres.forEach(({id, name}) => genreById.set(id, name));
const pushMapValueIfTruthy = map => array => key => {
const val = map.get(key);
if (val) {
array.push(val);
}
};
/** function that takes an array, then id, and pushes corresponding name (if exists) into the array. */
const pushGenreNaneIfExists = pushMapValueIfTruthy(genreById);
const getGenreNames = (ids) => {
result = [];
ids.forEach(pushGenreNaneIfExists(result));
return result;
};
console.log(getGenreNames(genres));
Say I have 2 arrays users and userCity. I want to map through users array and return updated user object with merged city data from userCity array based on related userId
I get the error:
> *TypeError: Cannot read property 'city' of undefined
> at user.map.u (eval at <anonymous> (:7:47),*
const users = [
{ userId: 1, name: "Jim", age: 25 },
{ userId: 2, name: "Rens", age: 15 },
{ userId: 3, name: "Ed", age: 5 }
];
const userCity = [{ userId: 1, city: "TX" }, { userId: 3, city: "NY", age: 5 }];
const info = users.map(u => {
return {
...u,
city: userCity.find(uc => {
return uc.userId === u.userId;
}).city
};
});
console.log(info);
Note:
I read somewhere that higher-order functions are synchronous therefore I expect the map function to return the values and assign them to info variable.
So I expect the console.log output to be an array with merged user and city info based on userId
[{ userId: 1, name: "Jim", age: 25, city: "TX" },
{ userId: 3, name: "Ed", age: 5, city: "NY" }]
Instead of iterating through users, you will want to iterate through userCity, since you only want to merge data into that array of objects (not the other way round).
With that in mind, when you iterate through userCity, you simply fetch the matching user from the users array by using Array.prototype.find(), using a predicate/callback that enforce a userID match:
const users = [
{ userId: 1, name: "Jim", age: 25 },
{ userId: 2, name: "Rens", age: 15 },
{ userId: 3, name: "Ed", age: 5 }
];
const userCity = [{ userId: 1, city: "TX" }, { userId: 3, city: "NY", age: 5 }];
const info = userCity.map(c => {
const user = users.find(u => u.userId = c.userId);
return {...c, ...user};
});
console.log(info);
You need to guard yourself against city info not being found.
const users = [{
userId: 1,
name: "Jim",
age: 25
},
{
userId: 2,
name: "Rens",
age: 15
},
{
userId: 3,
name: "Ed",
age: 5
}
];
const userCity = [{
userId: 1,
city: "TX"
}, {
userId: 3,
city: "NY",
age: 5
}];
const info = users.map(u => {
const foundObj = userCity.find(uc => uc.userId === u.userId);
return foundObj ? {
...u,
city: foundObj.city
} : u;
});
console.log(info);
If rxjs and observables are an option, to provide some modern-ish stream style, you could go this way:
const users = [
{ userId: 1, name: 'Jim', age: 25 },
{ userId: 2, name: 'Rens', age: 15 },
{ userId: 3, name: 'Ed', age: 5 }
];
const userCity = [
{ userId: 1, city: 'TX' },
{ userId: 3, city: 'NY', age: 5 }
];
const users$ = from ( users );
const user_city$ = from ( userCity );
users$.pipe (
switchMap ( x => user_city$.pipe (
filter ( y => x.userId === y.userId ),
map ( y => ( { ...x, ...y } ) )
) ),
scan ( ( acc, curr ) => acc.concat ( curr ), [] ),
last ()
).subscribe ( x => console.log ( x ) );
This should trace out a single array with two merged objects. This sort of pattern is pretty de rigueur (and very useful) these days.
Note you're iterating through users as the top level, though I don't think it'd matter either way.
Okay it was a bit hard to explain what I want. Let me explain in more detail. I have an array of objects. Because the "only" identifier of the arrays elements are their indexes, if we want to change an element we need to know which is the target index. But even if we have the index, I don't want to change the whole object, just assign the new one and "merge" them together.
I have a solution, which is ugly, but at least makes more sense what I want:
const users = [
{
id: 0,
name: "John",
hobby: "soccer"
},
{
id: 1,
name: "Alice",
hobby: "squash"
},
{
id: 2,
name: "Greg",
hobby: "guitar"
}
]
const newUsers = [
{
id: 0,
work: "developer"
},
{
id: 2,
work: "musician"
},
{
id: 3,
name: "Roger",
work: "accountant"
}
]
const concatArray = (newArray, oldArray) => {
const objectFromArray = array => array.reduce((object, user) => {
object[user.id] = user;
return object
}, {});
const objectOfOldArray = objectFromArray(oldArray);
const objectOfNewArray = objectFromArray(newArray);
const allIds = Object.keys({...objectOfOldArray, ...objectOfNewArray})
return allIds.map(id => {
const oldProps = objectOfOldArray[id] || {};
const newProps = objectOfNewArray[id] || {};
return {id, ...oldProps, ...newProps}
})
}
console.log(concatArray(newUsers, users))
It works fine, but there should be a more sufficient solution for this. I mean it's a very small operation, adding some properties to the specified objects, but my solution is too over-complicated to earn this. There should be an easier way to earn this.
You can try below approach of Array.forEach
const users = [
{
id: 0,
name: "John",
hobby: "soccer"
},
{
id: 1,
name: "Alice",
hobby: "squash"
},
{
id: 2,
name: "Greg",
hobby: "guitar"
}
]
const newUsers = [
{
id: 0,
work: "developer"
},
{
id: 2,
work: "musician"
},
{
id: 3,
name: "Roger",
work: "accountant"
}
]
let updatedUsers = {};
[...users, ...newUsers].forEach(d => updatedUsers[d.id] = { ...(updatedUsers[d.id] || {}), ...d })
console.log(Object.values(updatedUsers))
I have two arrays that I would like to join
const users = [{ user_id: 100, name: 'Bob' }, { user_id: 101, name: 'Joe' }]
const departments [{ id: 900, manager: 100 }, { id: 901, manager: 101 }]
I want to create a new departments array that contains the user's name by matching the user_id property to the department's manager property.
Is there a simple way to achieve this in lodash (or plain Javascript) ?
The new array would look like this
[{ id: 900, manager: 100, name: 'Bob' }, { id: 900, manager: 101, name: 'Joe' }];
Any help is appreciated!
You can use a Map for faster lookup, and then make the mapping:
const users = [{ user_id: 100, name: 'Bob' }, { user_id: 101, name: 'Joe' }],
departments = [{ id: 900, manager: 100 }, { id: 901, manager: 101 }];
const names = new Map(users.map( user => [user.user_id, user.name] )),
res = departments.map( dep => Object.assign({ name: names.get(dep.manager) }, dep) );
console.log(res);
.as-console-wrapper { max-height: 100% !important; top: 0; }
Note that the extra step of creating a Map will lead to O(n) time efficiency as opposed to O(n²) when using an array searching method in each iteration. This is not relevant for small user arrays, but will be when working with larger array sizes.
You can easily do it with just JavaScript using map and find:
const users = [{ user_id: 100, name: 'Bob' }, { user_id: 101, name: 'Joe' }]
const departments = [{ id: 900, manager: 100 }, { id: 901, manager: 101 }]
const result = departments
.map(d => ({
...d,
name: users.find(u =>
u.user_id === d.manager).name
}));
console.log(result);
I have an array like this:
[{
id: 1,
amount: 10,
date: '21/01/2017'
},{
id: 1,
amount: 10,
date: '21/01/2017'
},{
id: 1,
amount: 30,
date: '22/01/2017'
},{
id: 2,
amount: 10,
date: '21/01/2017'
},]
And I would like this to output like:
{
'21/01/2017': {
1: {
amount: 20
},
2: {
amount: 10
}
},
'22/01/2017': {
1: {
amount: 30
}
}
}
Essentially grouping by data, nested grouping by id, whilst summing the relevant amounts.
So far I have tried using reduce, and have looked at lodash functions but have been unsuccessful. Reduce makes it easy to group by and sum by one prop, but I cannot find an elegant way to create the second level. I have created the a sandbox to demonstrate:
https://codesandbox.io/s/woj9xz6nq5
const dataToConsolidate = [{
id: 1,
amount: 10,
date: '21/01/2017'
}, {
id: 1,
amount: 10,
date: '21/01/2017'
}, {
id: 1,
amount: 30,
date: '22/01/2017'
}, {
id: 2,
amount: 10,
date: '21/01/2017'
},]
export const consolidate = (data) => {
const result = data.reduce(function (res, obj) {
(!(obj.date in res)) ?
res.__array.push(res[obj.date] = obj) :
res[obj.date].amount += obj.amount;
return res;
}, { __array: [] });
return result;
};
console.log(consolidate(dataToConsolidate))
You could take the object as hash table and add the properties with a default style.
var array = [{ id: 1, amount: 10, date: '21/01/2017' }, { id: 1, amount: 10, date: '21/01/2017' }, { id: 1, amount: 30, date: '22/01/2017' }, { id: 2, amount: 10, date: '21/01/2017' }],
result = {};
array.forEach(function (o) {
result[o.date] = result[o.date] || {};
result[o.date][o.id] = result[o.date][o.id] || { amount: 0 };
result[o.date][o.id].amount += o.amount;
});
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }
Here's a relatively ES6-y solution using reduce:
const initial = [{ id: 1, amount: 10, date: '21/01/2017' }, { id: 1, amount: 10, date: '21/01/2017' }, { id: 1, amount: 30, date: '22/01/2017' }, { id: 2, amount: 10, date: '21/01/2017' }];
const parse = obj => obj.reduce((final, { date, id, amount }) => {
let group = final[date] = final[date] || {};
group[id] = group[id] || { amount: 0 };
group[id].amount += amount;
return final;
}, {});
const final = parse(initial);
console.log(final);
You'll want to add appropriate logic to handle missing/erroneous date, id, or amount keys.
As a fan of the library Ramda (disclaimer: I'm one of the authors), I might well use it to make everything a bit more explicit:
const {pipe, groupBy, prop, map, pluck, sum} = R
const data = [
{"amount": 10, "date": "21/01/2017", "id": 1},
{"amount": 10, "date": "21/01/2017", "id": 1},
{"amount": 30, "date": "22/01/2017", "id": 1},
{"amount": 10, "date": "21/01/2017", "id": 2}
]
const consolidate = pipe(
groupBy(prop('date')),
map(groupBy(prop('id'))),
map(map(pluck('amount'))),
map(map(sum))
)
console.log(consolidate(data))
.as-console-wrapper { max-height: 100% !important; top: 0; }
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.25.0/ramda.min.js"></script>
Of course using a library just to solve this problem is simply overkill, when something like the solution from #NinaSholz does just fine. But if you find several places where it could help, it might be worth looking into something like that.
you can do it thru _.groupBy:
const res = _.chain(data)
.groupBy('date')
.mapValues(dateObj => _.chain(dateObj)
.groupBy('id')
.mapValues(idObj => ({ amount: _.sumBy(idObj, 'amount') }))
.value()
)
.value();