How to transfer MAP.values into Arrays efficiently? - javascript

In the example below, you see some noisy but straightforward implementation.
Initial Situation
We have an initial Array with repeating information.
const listArray = [
{ songBy: 'George Michael', uid: 'A', whatEver: 12},
{ songBy: 'George Michael', uid: 'A', whatEver: 13},
{ songBy: 'George Michael', uid: 'A', whatEver: 14},
{ songBy: 'Michael Jackson', uid: 'B', whatEver: 12},
{ songBy: 'Michael Jackson', uid: 'B', whatEver: 16},
]
STEP 1
We create a new Map because we distinctly want to save artist names, which means → no artist-name-repetitions (and we also want to get rid of the third column, by the way).
We need to use songBy as key, for some reason. We change it within the value into name.
const listMap = new Map();
listArray.forEach(
row => listMap.set(
row.songBy, {name: row.songBy, uid: row.uid}
)
);
STEP 2
Finally, we need an array with its values:
const distinctListArray: Array<any> = [];
listMap.forEach(value => distinctListArray.push(value));
So we achieve a result as an Array of distinct objects in the form of:
[
name: string
uid: string
]
Question:
To my mind, this implementation is too noisy and not so elegant. There are too many steps and too many variables. (This example here is a simplified version of a real code I cannot share).
Is there a way to simplify that code and make it more efficient?
EDIT: See online: TypeScript Playground

Convert the list to [key, value] pairs using Array.map(), and then create the map from the list. Convert the Map back to an array by applying Array.from() to the Map.values() iterator (TS playground):
const listArray = [{"songBy":"George Michael","uid":"A","whatEver":12},{"songBy":"George Michael","uid":"A","whatEver":13},{"songBy":"George Michael","uid":"A","whatEver":14},{"songBy":"Michael Jackson","uid":"B","whatEver":12},{"songBy":"Michael Jackson","uid":"B","whatEver":16}]
const distinctListArray = Array.from(
new Map(listArray.map(({ songBy, uid }) =>
[songBy, { name: songBy, uid }]
)).values()
)
console.log(distinctListArray)

Related

Lodash merging and unioning of nested array / object structure

I have two arrays that need merging in Javascript. They are arranged as follows:
arrayA = [town1A, town2A, town3A];
arrayB = [town3B, town5B];
Each town is an object with a townName: 'town1' (matching the object variable name). Each town also has an array of occupants: [{}, {}] which each have their own personName, and a status: 'dead' or 'alive'.
My goal, is that after merging, the new array will contain every unique town according to townName (town3B and town3A both have townName : 'town3').
arrayC = [town1, town2, town3, town5]
Any new towns in arrayB (i.e., town5) should be added directly to the list. Any towns with the same name (i.e., town3) should combine their lists of occupants, but remove any "dead" people. ArrayB has priority over ArrayA when determining status, as it is "overwriting" the old data. For example:
arrayA.town3.occupants = [{name: 'Bob', status: 'alive'}, {name: 'Joe', status: 'alive'}];
arrayB.town3.occupants = [{name: 'Bob', status: 'dead'}, {name: 'Alice', status: 'alive'}];
arrayC.town3.occupants = [{name: 'Joe', status: 'alive'}, {name: 'Alice', status: 'alive'}];
I'm just struggling with the logic sequence process here and need a nudge to figure out what tools to use. Currently I'm trying to work with Lodash's _.merge and _.union in some combination. It seems I can use _.mergeWith or _.unionBy to "nest" the merging steps without resorting to manually looping over the arrays, but their usage is going over my head. If a solution exists that uses one of those, I would like to see an example to learn better how they work.
Edit: I was asked for the entire contents of an example arrayA and arrayB:
arrayA = [
{
townName: 'town1',
occupants: [
{name: 'Charlie', status: 'alive'},
{name: 'Jim', status: 'dead'}
]
},
{
townName: 'town2',
occupants: [
{name: 'Rachel', status: 'alive'},
]
},
{
townName: 'town3',
occupants: [
{name: 'Bob', status: 'alive'},
{name: 'Joe', status: 'alive'}
]
}
];
arrayB = [
{
townName: 'town3',
occupants: [
{name: 'Bob', status: 'dead'},
{name: 'Alice', status: 'alive'}
]
},
{
townName: 'town5',
occupants: [
{name: 'Sam', status: 'dead'},
{name: 'Ray', status: 'alive'},
{name: 'Bob', status: 'alive'},
]
}
];
The output I expect is:
arrayC = [
{
townName: 'town1',
occupants: [
{name: 'Charlie', status: 'alive'},
]
},
{
townName: 'town2',
occupants: [
{name: 'Rachel', status: 'alive'},
]
},
{
townName: 'town3',
occupants: [
{name: 'Joe', status: 'alive'},
{name: 'Alice', status: 'alive'}
]
},
{
townName: 'town5',
occupants: [
{name: 'Ray', status: 'alive'},
{name: 'Bob', status: 'alive'},
]
}
];
I managed to find a consistent way to do this (thanks to #Enlico for some hints). Since _.mergeWith() is recursive, you can watch for a specific nested object property and handle each property differently if needed.
// Turn each array into an Object, using "townName" as the key
var objA = _.keyBy(arrayA, 'townName');
var objB = _.keyBy(arrayB, 'townName');
// Custom handler for _.merge()
function customizer(valueA, valueB, key) {
if(key == "occupants"){
//merge occupants by 'name'. _.union prioritizes first instance (so swap A and B)
return _.unionBy(valueB, valueA, 'name');
//Else, perform normal _.merge
}
}
// Merge arrays, then turn back into array
var merged = _.values(_.mergeWith(objA, objB, customizer));
// Remove dead bodies
var filtered = _.map(merged, town => {
town.occupants = _.filter(town.occupants, person => {return person.status == "alive";});
return town;
});
The complexity with this problem is that you want to merge on 2 different layers:
you want to merge two arrays of towns, so you need to decide what to do with towns common to the two arrays;
when handling two towns with common name, you want to merge their occupants.
Now, both _.merge and _.mergeWith are good candidates to accomplish the task, except that they are for operating on objects (or associative maps, if you like), whereas you have vectors of pairs (well, not really pairs, but objects with two elements with fixed keys; name/status and townName/occupants are fundamentally key/value) at both layers mentioned above.
One function that can be useful in this case is one that turns an array of pairs into an object. Here's such a utility:
arrOfPairs2Obj = (k, v) => (arr) => _.zipObject(..._.unzip(_.map(arr, _.over([k, v]))));
Try executing the following
townArr2townMap = arrOfPairs2Obj('townName', 'occupants');
mapA = townArr2townMap(arrayA);
mapB = townArr2townMap(arrayB);
to see what it does.
Now you can merge mapA and mapB more easily…
_.mergeWith(mapA, mapB, (a, b) => {
// … well, not that easily
})
Again, a and b are arrays of "pairs" name/status, so we can reuse the abstraction I showed above, defining
personArr2personMap = arrOfPairs2Obj('name', 'status');
and using it on a and b.
But still, there are some problems. I thought that the (a, b) => { … } I wrote above would be called by _.mergeWith only for elements which have the same key across mapA and mapB, but that doesn't seem to be the case, as you can verify by running this line
_.mergeWith({a: 1, b: 3}, {b:2, c:4, d: 6}, (x, y) => [x, y])
which results in
{
a: 1
b: [3, 2]
c: [undefined, 4]
d: [undefined, 6]
}
revealing that the working lambda is called for the "clashing" keys (in the case above just b), and also for the keys which are absent in the first object (in the case above c and d), but not for those absent in the second object (in the case above a).
This is a bit unfortunate, because, while you could filter dead people out of towns which are only in arrayB, and you could also filter out those people which are dead in arrayB while alive in arrayA, you'd still have no place to filter dead people out of towns which are only in arrayA.
But let's see how far we can get. _.merge doc reads
Source objects are applied from left to right. Subsequent sources overwrite property assignments of previous sources.
So we can at least handle the merging of towns common across the array in a more straightforward way. Using _.merge means that if a person is common in the two arrays, we'll always pick the one from arrayB, whether that's (still) alive or (just) dead.
Indeed, a strategy like this doesn't give you the precise solution you want, but not even one too far from it,
notSoGoodResult = _.mergeWith(mapA, mapB, (a, b) => {
return _.merge(personArr2personMap(a), personArr2personMap(b));
})
its result being the following
{
town1: [
{name: "Charlie", status: "alive"},
{name: "Jim", status: "dead"}
],
town2: [
{name: "Rachel", status: "alive"}
],
town3:
Alice: "alive",
Bob: "dead",
Joe: "alive"
},
town5: {
Bob: "alive",
Ray: "alive",
Sam: "dead"
}
}
As you can see
Bob in town3 is correctly dead,
we've not forgotten Alice in town3,
nor have we forogtten about Joe in town3.
What is left to do is
"reshaping" town3 and town5 to look like town1 and town2 (or alternatively doing the opposite),
filtering away all dead people (there's no more people appearing with both the dead and alive status, so you don't risk zombies).
Now I don't have time to finish up this, but I guess the above should help you in the right direction.
The bottom line, however, in my opinion, is that JavaScript, even with the power of Lodash, is not exactly the best tool for functional programming. _.mergeWith disappointed me, for the reason explained above.
Also, I want to mention that there a module named lodash/fp that
promotes a more functional programming (FP) friendly style by exporting an instance of lodash with its methods wrapped to produce immutable auto-curried iteratee-first data-last methods.
This shuould slightly help you be less verbose. With reference to your self answer, and assuming you wanted to write the lambda
person => {return person.status == "alive";}
in a more functional style, with "normal" Lodash you'd write
_.flowRight([_.curry(_.isEqual)('alive'), _.iteratee('status')])
whereas with lodash/fp you'd write
_.compose(_.isEqual('alive'), _.get('status'))
You can define a function for merging arrays with a mapper like this:
const union = (a1, a2, id, merge) => {
const dict = _.fromPairs(a1.map((v, p) => [id(v), p]))
return a2.reduce((a1, v) => {
const i = dict[id(v)]
if (i === undefined) return [...a1, v]
return Object.assign([...a1], { [i]: merge(a1[i], v) })
}, a1)
}
and use it like this:
union(
arrayA,
arrayB,
town => town.townName,
(town1, town2) => ({
...town1,
occupants: union(
town1.occupants,
town2.occupants,
occupant => occupant.name,
(occupant1, occupant2) => occupant1.status === 'alive' ? occupant1 : occupant2
).filter(occupant => occupant.status === 'alive')
})
)

How can I structure a .find() function that takes an array of values and returns an Object of key : values

I would like to know if there is a better way to retrieve multiple key value pairs in an array of objects from MongoDB.
My goal is to create a function that receives an array of values that I want to search for (i.e. an _id) and return an Object of key value pairs where the key is the original search term.
The data could be structured as seen below:
'users': [
{
_id: '123',
displayName: 'John Doe',
timezone: 'America/New_York',
},
{
_id: '456',
displayName: 'Jane Doe',
timezone: 'America/New_York',
},
{
_id: '789',
displayName: 'Ken Watanabe',
timezone: 'America/New_York',
}
]
The input could look like this: ['123','789']
And the preferred output would look like this:
{
'123':{
_id: '123',
displayName: 'John Doe',
timezone: 'America/New_York',
},
'789':{
_id: '789',
displayName: 'Ken Watanabe',
timezone: 'America/New_York',
}
}
The data that matches the search parameters returns and the value type of that was searched for is the new corresponding key.
So far, I am using the following:
let data = await db.collection(collection).find( { _id : { $in : _ids } }).toArray();
However, this only stores the data in an array of Objects:
[
{
_id: '123',
displayName: 'John Doe',
timezone: 'America/New_York',
},
{
_id: '789',
displayName: 'Ken Watanabe',
timezone: 'America/New_York',
}
]
This can be parsed using Object.entries(), but perhaps there is a better way to retrieve the data.
Edit. Marcus has already provided an excellent answer below in case you are looking to solve this problem server side. Be sure to check it out.
To clarify, I am looking for a solution that retrieves the desired output from the database without needing to modify the data after arrival.
Sergio has suggested .agregate() which I will look into now. If I find a solution before someone else, I will be sure to update with an answer.
This should do the trick:
const result = {};
users.map((item) =>result[item._id] = item)
You can use reduce() to iterate through and the spread {...} syntax to accumulate the new array of objects
let users = data.users.reduce((b, a) =>
(search.includes(a._id) ? // if our user id is a match
{ ...b, ...{[a._id]: a}} : b) // return the current object with the new user object added in
, {})
let data = {
users: [{ _id: '123',displayName: 'John Doe',timezone: 'America/New_York'},
{_id: '456',displayName: 'Jane Doe',timezone: 'America/New_York'},
{_id: '789',displayName: 'Ken Watanabe',timezone: 'America/New_York'}]}
let search = ['123', '789'];
let users = data.users.reduce((b, a) =>
(search.includes(a._id) ?
{ ...b, ...{[a._id]: a}} : b)
, {})
console.log(users)

What is the easiest way to match/consolidate two arrays of data that have relationships? [duplicate]

This question already has answers here:
Merge 2 arrays of objects
(46 answers)
Closed 1 year ago.
Say I have two data arrays for a ticketed event. One is attendees:
[
{name: 'Jack', ticket_code: 'iGh4rT'},
{name: 'Lisa', ticket_code: 'it1ErB'}
]
The other is tickets:
[
{code: 'iGh4rT', name: 'General Admission'},
{code: 'it1ErB', name: 'VIP'}
]
Now say I want to display a table like this:
Name
Ticket Name
Jack
General Admission
Lisa
VIP
I am struggling with doing this efficiently. I can display a table with one array no problem like something like this:
for (let i = 0; i < attendees.length; i++){
const row = `<tr>
<td>${attendees[i].name}</td>
<td>${attendees[i].ticket_code}</td>
</tr>`
document.getElementById('TableBody').innerHTML += row
I need to somehow 'query' the tickets array with the code from the attendees array for that particular person, get the name of the ticket, and supplant the ticket name instead of the code.
With SQL something like this is easy, but is one able to "query" an array and get a specific property? Should I construct a whole new array with the needed info? What is the best way to do this that would work for large unordered datasets?
You could take one of your array as an object with code as key for the object and map the other array with wanted data and the previous stored data from the object.
const
attendees = [{ name: 'Jack', ticket_code: 'iGh4rT' }, { name: 'Lisa', ticket_code: 'it1ErB' }],
tickets = [{ code: 'iGh4rT', name: 'General Admission' }, { code: 'it1ErB', name: 'VIP' }],
ticketsByCode = Object.fromEntries(tickets.map(o => [o.code, o])),
table = attendees.map(({ name, ticket_code }) => [name, ticketsByCode [ticket_code].name]);
console.log(table);
try this:
let a = [
{name: 'Jack', ticket_code: 'iGh4rT'},
{name: 'Lisa', ticket_code: 'it1ErB'}
];
let b = [
{code: 'iGh4rT', name: 'General Admission'},
{code: 'it1ErB', name: 'VIP'}
];
let c = b.map(item => {
return {
tiketName: item.name,
...a.find(itemA => itemA.ticket_code == item.code)
}
});
console.log(c);

Javascript combine two array of objects

I would like to combine elements of 2 arrays based on the name. For example:
Array1 = [
{name: "name1", language: "lang1"},
{name: "name2", language: "lang2"},
{name: "name3", language: "lang3"}]
Array2 = [
{name: "name1", subject: "sub1"},
{name: "name2", subject: "sub2"},
{name: "name3", subject: "sub3"}]
I need to generate the following array:
Array3 = [
{language: "lang1", subject: "sub1"},
{language: "lang2", subject: "sub2"},
{language: "lang3", subject: "sub3"}]
The logic I could think of was to write an explicit for loop to compare every element of first array with every element of second array and check if name matches as shown below.
let Array3 = []
for(let i=0;i<Array1.length;i++)
{
let elem = Array1[i];
for(let j=0;j<Array2.length;j++)
{
if(Array2[j].name===elem.name)
{
Array3.append({language: elem.language, subject: Array2[j].subject})
break;
}
}
}
However, my actual dataset is quite large and this seems inefficient. How can this can be achieved in a more efficient manner (like using higher order functions or something)?
Using a Map for O(1) lookup of one of the arrays using name as key lets you iterate each array only once.
const Array1=[{name:"name1",language:"lang1"},{name:"name2",language:"lang2"},{name:"name3",language:"lang3"}],Array2=[{name:"name1",subject:"sub1"},{name:"name2",subject:"sub2"},{name:"name3",subject:"sub3"}];
const a1Map = new Map(Array1.map(({name, ...r})=> [name, {...r}]));
const res = Array2.map(({name, ...r}) => ({...r, ...a1Map.get(name)}))
console.log(res)
You need to iterate over the two arrays and group the generated object in a map having the name as the key:
let Array1 = [
{name: "name1", language: "lang1"},
{name: "name2", language: "lang2"},
{name: "name3", language: "lang3"}
];
let Array2 = [
{name: "name1", subject: "sub1"},
{name: "name2", subject: "sub2"},
{name: "name3", subject: "sub3"}
];
let map = new Map();
Array1.forEach(e => map.set(e.name, {language: e.language}));
Array2.forEach(e => {
if(map.has(e.name))
map.set(e.name, {...map.get(e.name), subject: e.subject});
});
let Array3 = [...map.values()].filter(e => e.language && e.subject);
console.log(Array3);
Yes you are thinking in right order , you need to use the sort algorithm logics , I will say nested for loops will be just as good. With larger dataset , since you need to extract the values from two different array you can use the nested for loops.
for(int i=0;i>array1.length();i++){
This can be use for first array
Define String x=",";
For second
for(int j=0;j>array2.length();j++)
{
Check if ( (","+j+",").contains(x)) then break;
If array1 name found in array 2, store array3 as you want
Also Store value of j in x
Like x=x +j+",";
}}
This way your nested for loop will skip the comparison code.
Above algo is raw but will reduce the complexity a significant bit.

Populating a New Array from Corresponding Index of Elements from 3 Arrays

I have been trying to resolve an issue where my conditional logic doesn't work when I have the same string value in two elements of an array. I have been trying it with for-loops, but without success.
What occurs to me after thinking about it is that the best way to handle this is to take my three arrays - of which, in my use case, there will ALWAYS be an equal number of elements, and mash them together into a new array of objects -- taking the corresponding value from each array element to popular the new array of objects.
Imagine data like this:
const goalScorers = ['John Smith', 'Dave Jones', 'Rob Porter'];
const goalTimes = [4, 23, 56];
const goalTypes = ['penalty', 'breakaway', 'header'];
How best should I handle this to end up with array like this:
const combinedArr = [
{ scorer: 'John Smith', time: 4, type: 'penalty' },
{ scorer: 'Dave Jones', time: 23, type: 'breakaway' },
{ scorer: 'Rob Porter', time: 56, type: 'header' }
]
Map over one of the arrays. The callback function receives the array index, it can use that to access the corresponding elements of the other arrays.
const combinedArr = goalScorers.map((scorer, i) => ({
scorer: scorer, time: goalTimes[i], type: goalTypes[i]
}));
If all the arrays are sorted correctly, so each index is the same instance in every array than you can parse one with map and use index to populate other fields
const goalScorers = ['John Smith', 'Dave Jones', 'Rob Porter'];
const goalTimes = [4, 23, 56];
const goalTypes = ['penalty', 'breakaway', 'header'];
const newArray = goalScorers.map((element, index) => {
return {
scorer: element,
time: goalTimes[index],
type: goalTypes[index],
}
})
console.log(newArray)

Categories