Merge array of objects by key/value - javascript

I am trying to figure out the best way to map an objects id property that is within a multi-dimension array to object values that are within another array that share the same id.
As an example i have an array of genre_ids like so:
0: {id: 1, name: 'sci-fi'},
1: {id: 2, name 'comedy'},
2: {id: 3, name: 'action'}
And an array of tv_show_genre_ids which looks like:
0: {name: ..., genre_ids: [1, 4, 9]},
1: {name: ..., genre_ids: [2, 3, 4]},
I was trying to figure out the best way to retrieve a list of the genres name by its id.
I have managed to create a working solution so far but it feels incredibly dirty as i am performing multiple nested loops and i wasn't sure if there is a cleaner more declarative approach to my solution
Here is my approach which assumes i already have a list of genre ids and names (accessed within this.genres.
this.http.get('https://api.com/shows')
.subscribe((res: array <any> ) => {
this.shows = res.results;
this.shows.forEach(show => {
show.genre_names = '';
show.genre_ids.forEach(id => {
for (const [i, v] of this.genres.entries()) {
if (id == v.id) {
if (this.genres[i] && this.genres[i].name) {
if (show.genre_names === '') {
show.genre_names = this.genres[i].name
} else {
show.genre_names += `, ${this.genres[i].name}`;
}
}
}
}
})
});
});
Is there a better way of doing this as i seem to come across this type of problem quite often when trying to map ids from one object to another within multi-dimension arrays.
Any guidance would be greatly appreciated.
EDIT:
Here is an example of the Genre Data from the API af:
0: {id: 10759, name: "Action & Adventure"}
1: {id: 16, name: "Animation"}
And here is an example of the show data from the API:
0:
backdrop_path: "/ok1YiumqOCYzUmuTktnupOQOvV5.jpg"
first_air_date: "2004-05-10"
genre_ids: (2) [16, 35]
id: 100
name: "I Am Not an Animal"
origin_country: ["GB"]
original_language: "en"
original_name: "I Am Not an Animal"
overview: "I Am Not An Animal is an animated comedy series about the only six talking animals in the world, whose cosseted existence in a vivisection unit is turned upside down when they are liberated by animal rights activists."
popularity: 10.709
poster_path: "/nMhv6jG5dtLdW7rgguYWvpbk0YN.jpg"
vote_average: 9.5
vote_count: 341
I want to add a new property to the show object named genre_names which gets the genre name via the genre response.

Your best bet is to first convert your genres into a Map or an object to use as a lookup:
const genreLookup = new Map();
this.genres.forEach(genre => genreLookup.set(genre.id, genre));
Now when you process an array of shows, you don't have to loop through the genres multiple times:
this.shows.forEach(show => {
show.genre_names = show.genre_ids
.filter(id => genreLookup.has(id))
.map(id => genreLookup.get(id).name)
.join(', ');
});

Related

JavaScript Replacing Object in Array with ID Number

So I have a series of objects that are pulled from an API and inputted into an array, something like such:
array = [
{id: 0, name: "First", relationship: "Friend"},
{id: 1, name: "Second", relationship: "Friend"}
]
The user is allowed to add and remove objects to the list freely (they will appear within a Vue.JS DataTable), and said user is allowed a maximum of 4 objects within the array (lets say 4 "friends")
How should I go about implementing a function that searches the existing array (say, if its populated from the API), and inputs the new object with the corresponding ID that is missing (so if the user deletes the object with the id 2, and adds another, it will search said array with objects, find the missing id 2 slot in the array, and input the object in its place)?
Previously I have gone about it via implement array.find() with conditionals to see if the array contains or does not contain the certain id value, however, it searches through each entry and can end up inserting the same object multiple times. Another method I haven't attempted yet would be having a separate map that contains ids, and then when a user removes an object, having it correspond with the map, and vice versa when adding.
Any suggestions? Thanks
Instead of an array, I'd keep an object in data. Have it keyed by id, like this:
let objects = {
0: { id: 0, name: 'name0', relationship: 'relationship0' },
1: { id: 1, name: 'name1', relationship: 'relationship1' },
}
Integer keys in modern JS will preserve insertion order, so you can think of this object as ordered. The API probably returns an array, so do this...
// in the method that fetches from the api
let arrayFromApi = [...];
this.objects = array.reduce((acc, obj) => {
acc[obj.id] = obj; // insertion order will be preserved
return acc;
}, {});
Your UI probably wants an array, so do this (refer to "array" in the markup):
computed: {
array() {
return Object.values(this.objects);
},
To create a new object, insert it in order, minding the available keys. Note this is a linear search, but with small numbers of objects this will be plenty fast
methods: {
// assumes maxId is const like 4 (or 40, but maybe not 400)
createObject(name, relationship) {
let object = { name, relationship };
for (let i=0; i< maxId; i++) {
if (!this.objects[i]) {
object.id = i;
this.objects[i] = object;
break;
}
}
try this,
let array = [
{id: 0, name: "First", relationship: "Friend"},
{id: 4, name: "Second", relationship: "Friend"},
{id: 2, name: "Second", relationship: "Friend"},
]
const addItem = (item) => {
let prevId = -1
// this is unnecessary if your array is already sorted by id.
// in this example array ids are not sorted. e.g. 0, 4, 2
array.sort((a, b) => a.id - b.id)
//
array.forEach(ob => {
if(ob.id === prevId + 1) prevId++
else return;
})
item = {...item, id: prevId + 1 }
array.splice(prevId+1, 0, item)
}
addItem({name: "x", relationship: "y"})
addItem({name: "a", relationship: "b"})
addItem({name: "c", relationship: "d"})
console.log(array)
You can simply achieve this with the help of Array.find() method along with the Array.indexOf() and Array.splice().
Live Demo :
// Input array of objects (coming from API) and suppose user deleted 2nd id object from the array.
const arr = [
{id: 0, name: "First", relationship: "Friend" },
{id: 1, name: "Second", relationship: "Friend" },
{id: 3, name: "Fourth", relationship: "Friend" }
];
// find the objects next to missing object.
const res = arr.find((obj, index) => obj.id !== index);
// find the index where we have to input the new object.
const index = arr.indexOf(res);
// New object user want to insert
const newObj = {
id: index,
name: "Third",
relationship: "Friend"
}
// Insert the new object into an array at the missing position.
arr.splice(index, 0, newObj);
// Output
console.log(arr);

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')
})
)

Creating two arr with same intersect

Right now I need to merge the string into one.
This is my try ->
After this merge i got example of array ->
[
[{id: 1, name: "One"}],
[{id: 2, name : "two"}]
]
problem is newData because always print new array.
I need to data be like ->
[
{id: 1, name: "One"},
{id: 2, name : "two"}
]
What i am try, with foreEach ->
newState((oldData) => [...oldData, newData.forEach((new) => new)]);
No work.
Also what I am try
let filteredArray = newData.map(data => data);
Also no work, why?
Every time I get new information inside array newData....
I need solution to get only result inside array and print to
newState((oldData) => [...oldData, newResultWhichIsObject]);
Also some time my newData have few object inside array
The map method isn't the right method to use in your case. Map method will take as an entry an array of n elements and mutate it into an array of n element, on which an operation was applied. See MDN documentation
You should use the reduce method, which provides the ability to construct a brand new array from an empty one, here is the snippet :
const baseArray = [
[{id: 1, name: "One"}],
[{id: 2, name : "two"}]
];
const flattenedArray = baseArray.reduce((acc, curr) => ([...acc, ...curr]), []);
// For demo purpose, console.log
console.log(flattenedArray);
Reduce array method is a bit tricky, that is why I invite you to read the documentation carefully and play with it.
You can use .flat() method. Try this
const newData = [
[{id: 1, name: "One"}],
[{id: 2, name : "two"}]
];
console.log(newData.flat())

Create a subset of data selected on columns in 2d javascript array

Sorry for the basic question and bad lexicon, I am (very) new to javascript. I have an array of data and I would like to create a subset of that data, based on selected columns. The first few rows of my data, for example:
0: {ID: 3607, Name: 'Alamo', Funds: 52933955,
Revenues: 9160109, BAT: 5, …}
1: {ID: 3539, Name: 'Alvin', Funds: 6128147,
Revenues: 964083, BAT: 0, …}
2: {ID: 3540, Name: 'Amarillo', Funds: 12450969,
Revenues: 1716038, BAT: 0, …}
I want to create a new array from columns 0, 1, 2, and 4 (ID, Name, Funds, and BAT). In the code below, toolData is the array created from the original dataset (toolData.json), and tableData is the array I'm trying to create from the selected data. selections contains the column numbers I want to pull into the new array.
var getData = () => axios.get('toolData.json')
.then(res => res.data)
.then(data => {
var toolData = data;
console.log(toolData);
var tableData = [];
var selections = [0,1,2,4];
for (i=0; i < toolData.length; i++)
{
tableData[i] = toolData[i];
for (j=0; selections.length; j++)
{
k = selections[j],
tableData[i][j] = toolData[i][k]
}
}
console.log(tableData);
This particular code snippet doesn't work at all, I'm assuming I've created an infinite loop somehow. If I comment out tableData[i] = toolData[i]; then that problem resolves, but the code still doesn't work. console.log(toolData); gives me what I'm looking for (the full panel of data), but console.log(tableData); gives the error:
javascript.js:42 Uncaught (in promise) TypeError: Cannot set properties of undefined (setting '0')
at javascript.js:42
Ultimately I would like the user to be able to choose the columns they want to include in the new array, but before I can figure that puzzle out I need to solve this one.
Well, it seems from what you're saying is that every index in the array is an object.. arr[0][0]==undefined but arr[0]['ID']==3607
function newSubset(arr,dataToSelect){
//arr is the fetched array, dataToSelect is an array of the keys(like ID,Name...) that you want from the array
return arr.map(obj=>{
var toReturn={} //object that would give each desired key for each part in arr
dataToSelect.forEach(key=>toReturn[key]=obj[key]) //placing wanted keys in toReturn
return toReturn
})
}
//usage
var getData = () => axios.get('toolData.json')
.then(res => res.data)
.then(data => {
var wantedKeys=["ID","Name","Funds","BAT"]
console.log(newSubset(data,wantedKeys))
//rest of your code here
LIVE EXAMPLE
var dataArray=[{ID: 3607, Name: 'Alamo', Funds: 52933955, Revenues: 9160109, BAT: 5}, {ID: 3539, Name: 'Alvin', Funds: 6128147, Revenues: 964083, BAT: 0}, {ID: 3540, Name: 'Amarillo', Funds: 12450969, Revenues: 1716038, BAT: 0}]
function newSubset(arr,dataToSelect){
//arr is the fetched array, dataToSelect is an array of the keys(like ID,Name...) that you want from the array
return arr.map(obj=>{
var toReturn={} //object that would give each desired key for each part in arr
dataToSelect.forEach(key=>toReturn[key]=obj[key]) //placing wanted keys in toReturn
return toReturn
})
}
console.log(newSubset(dataArray,["ID","Name","Funds","BAT"]))
The data is a JSON object. It is not indexed by numbers but rather by names.
It's also recommend to use the built-in map function for this.
const tableData = toolData.map(row => ({
ID: row.ID,
Name: row.Name,
Funds: row.Funds,
BAT: row.BAT
}));
If you want the new toolData array to contain arrays instead of objects, you can instead do:
const tableData = toolData.map(row => [
row.ID,
row.Name,
row.Funds,
row.BAT
]);

How to filter multiple properties of one array of object from the other array of object?

I am trying to solve the problem in which i have to apply multiple filters to the array of object. Let suppose I am having a larger array of object which contains the configuration property which is further an object. On other side i have small object which are the ones the user chooses to filter(based on the checkboxes). i want to compare objects made with the parent array of objects by selecting multiple values.
So in the image the user chooses multiple values(using check boxes) and based on that he needs to filter the main array of objects.So after checking the checkboxes i get childObject and i have to filter parentArray on the basis of that..... please help me with this:
childobject =
{'Bathroom': '[2,1]',
'Bedroom': '[3,2]',
'halfBathroom':'0',
'name':'[2BD-2BA,2BD-2BA-1]'}
parentArray = [
0:{},
1:{},
2:{
'property1':'____',
'property2':'_____',
'configuration':'{
bathroom: 2
bedroom: 2
created_at: "2019-03-08 20:52:52"
created_by: 264
half_bathroom: 1
id: 26
is_selected: 0
name: "2BD-2BA-1/2BA"
name_en: "2BD-2BA-1/2BA"
name_es: "2RE-2BA-1/2BA"
status: 1
updated_at: "2019-08-23 05:39:44"
}'
}
3: {},
4:{}
]
I had to update the datastructure at some points:
You had different key in child and parent (upper/lowercase + camelcase/_ writing)
Some Missing } in the parent.
In child quotationmarks for integer deleted.Missing , added.
Changing some values in cruiteria, so that there is a result.
In parent delting of 0:, 1:, 2:, 3:, 4: to get a valid array.
childArray = {
'bathroom': [2,1],
'bedroom': [3,2],
'half_bathroom':1,
'name':['2BD-2BA', '2BD-2BA-1/2BA']
};
parentArray = [
{},
{},
{
'property1':'____',
'property2':'_____',
'configuration':{
bathroom: 2,
bedroom: 2,
created_at: "2019-03-08 20:52:52",
created_by: 264,
half_bathroom: 1,
id: 26,
is_selected: 0,
name: "2BD-2BA-1/2BA",
name_en: "2BD-2BA-1/2BA",
name_es: "2RE-2BA-1/2BA",
status: 1,
updated_at: "2019-08-23 05:39:44"
},
},
{},
{}
]
let res = parentArray.filter(elem => Object.entries(childArray).every(([key,val]) => {
let conf = elem.configuration;
if (conf===undefined) return false;
if (typeof(val) === 'object') {
return val.some(crit => crit===conf[key]);
} else {
return val===conf[key];
}
}));
console.log(res);

Categories