Collect unique objects in JavaScript array - javascript

Suppose I have following arrays of objects
var firstDataSet = [
{'id': 123, 'name': 'ABC'},
{'id': 456, 'name': 'DEF'},
{'id': 789, 'name': 'GHI'},
{'id': 101, 'name': 'JKL'}
];
var secondDataSet = [
{'id': 123, 'name': 'ABC', 'xProp': '1q'},
{'id': 156, 'name': 'MNO', 'xProp': '2w'},
{'id': 789, 'name': 'GHI', 'xProp': '3e'},
{'id': 111, 'name': 'PQR', 'xProp': '4r'}
];
Now I want to collect array with unique objects (matching id and name)i.e.
var firstDataSet = [
{'id': 123, 'name': 'ABC', 'xProp': '1q'},
{'id': 456, 'name': 'DEF'},
{'id': 789, 'name': 'GHI', 'xProp': '3e'},
{'id': 101, 'name': 'JKL'},
{'id': 156, 'name': 'MNO', 'xProp': '2w'},
{'id': 111, 'name': 'PQR', 'xProp': '4r'}
];
I am able to collect ALL with
Array.prototype.unshift.apply(firstDataSet , secondDataSet );
But not sure how I can filter out duplicates. Any suggestion?
Edit: My object on two different array are not same. At least based on number of properties.

To Remove Duplicates With All Identical Properties
This was the original question.
Use a Set:
The Set object lets you store unique values of any type, whether primitive values or object references.
You can also use object literals.
var list = [JSON.stringify({id: 123, 'name': 'ABC'}), JSON.stringify({id: 123, 'name': 'ABC'})];
var unique_list = new Set(list); // returns Set {"{'id': 123, 'name': 'ABC'}"}
var list = Array.from(unique_list); // converts back to an array, and you can unstringify the results accordingly.
For more ways to construct a set back to an array, you can follow instructions here.
If you can't use ES6 (which is what defines Set), there's a polyfill for older browsers.
To Remove Objects with Duplicate Subset of Properties
Unfortunately, these objects are no longer strictly duplicates and cannot be tackled in a friendly way using Set, for instance.
The easiest way to approach this type of problem is to iterate through the array of objects, identify those with repeated property values, and eliminate in place using splice, for example.

This can be achieved By extending Set class Like below
var firstDataSet = [
{'id': 123, 'name': 'ABC'},
{'id': 456, 'name': 'DEF'},
{'id': 789, 'name': 'GHI'},
{'id': 101, 'name': 'JKL'}
];
var secondDataSet = [
{'id': 123, 'name': 'ABC', 'xProp': '1q'},
{'id': 156, 'name': 'MNO', 'xProp': '2w'},
{'id': 789, 'name': 'GHI', 'xProp': '3e'},
{'id': 111, 'name': 'PQR', 'xProp': '4r'}
];
Array.prototype.unshift.apply(firstDataSet , secondDataSet );
//console.log(firstDataSet)
class UniqueSet extends Set {
constructor(values) {
super(values);
const data = [];
for (let value of this) {
if (data.includes(JSON.parse(value.id))) {
this.delete(value);
} else {
data.push(value.id);
}
}
}
}
console.log(new UniqueSet(firstDataSet))
Working link

We'll combine the two arrays using concat, then filter the resulting array using filter. For each element, we'll find the index of the first element with the same id and name, using findIndex. If that index is the same as the current index, it means this is the first occurrence of that id and name, so we just let it pass through. Otherwise, we'll add in new fields to the first occurrence, and filter it out.
function combine(a1, a2) {
function match(e1, e2) { return e1.id === e2.id && e1.name === e2.name); }
return a1.concat(a2) . filter((e1, i, a) => {
let firstIndex = a.findIndex(e2 => match(e1, e2));
if (i === firstIndex) return true; // this is the first occurrence
a[firstIndex].xProp = e2.xProp; // copy over property
return false; // filter out
});
}
If you want to handle arbitrary properties, instead of just xProp, then change the relevant line to something like
a[firstIndex] = Object.assign(e2, a[firstIndex]);
That will replace the first occurrence with the result of copying all its properties on top of the current occurrence including whatever additional properties it may have.
Mandatory disclaimer: As always, depending on your environment, you may not have arrow functions, or Array#findIndex, or Object.assign. In such cases, rewrite/polyfill/transpile as necessary.

This may not be the most efficient solution, but assuming that id is always unique, it should work.
var firstDataSet = [
{'id': 123, 'name': 'ABC'},
{'id': 456, 'name': 'DEF'},
{'id': 789, 'name': 'GHI'},
{'id': 101, 'name': 'JKL'}
];
var secondDataSet = [
{'id': 123, 'name': 'ABC', 'xProp': '1q'},
{'id': 156, 'name': 'MNO', 'xProp': '2w'},
{'id': 789, 'name': 'GHI', 'xProp': '3e'},
{'id': 111, 'name': 'PQR', 'xProp': '4r'}
];
Array.prototype.unique = function() {
var o = {}, i, l = this.length, r = [];
for(i=0; i<l;i+=1) o[this[i]] = this[i];
for(i in o) r.push(o[i]);
return r;
};
function concatUnique(a, b, property) {
var arr = a.concat(b);
arr.sort(function(a,b) {
return Object.keys(b).length - Object.keys(a).length;
});
var ids = arr.map(function(obj){ return obj[property] }).unique();
return arr.filter(function(obj) {
if(ids.indexOf(obj[property]) > -1) {
ids.splice( ids.indexOf(obj[property]) , 1);
return true;
} else {
return false
}
});
}
var newArray = concatUnique(firstDataSet, secondDataSet, 'id');

Related

How do you iterate over an array of objects to create a new object?

I'm attempting to iterate over a message (which is an array of objects) coming in from a WebSocket connection to create a new object. This is the original array:
[
{'id': 52, 'tag': 'RFS', 'price': 780},
{'id': 14, 'tag': 'XGH', 'price': 341},
{'id': 29, 'tag': 'LLP', 'price': 997},
]
I'm trying to use this array to create an object using the tag as the key:
{
'RFS': {'id': 52, 'price': 780},
'XGH': {'id': 14, 'price': 341},
'LLP': {'id': 29, 'price': 997},
}
Let this object = obj. In Python I'd be able to do something like:
>>> new_obj = {i['tag']: {'id': i['id'], 'price': i['price']} for i in obj}
>>> new_obj
{'RFS': {'id': 52, 'price': 780}, 'XGH': {'id': 14, 'price': 341}, 'LLP': {'id': 29, 'price': 997}}
How would I go about doing something like this in JS? I've tried experimenting with the map function but to no avail. I also attempted to use reduce:
var result = obj.reduce(function(new_obj, i) {
new_obj[i.tag] = {'id': i.id, 'price': i.price};
return new_obj;
}, {});
EDIT: The reduce method above was unsuccessful for me. I'm probably missing something silly, but console tells me Each child in a list should have a unique "key" prop., and it returns an undefined object.
Using .reduce and de-structuring
var data = [
{'id': 52, 'tag': 'RFS', 'price': 780},
{'id': 14, 'tag': 'XGH', 'price': 341},
{'id': 29, 'tag': 'LLP', 'price': 997},
];
var res= data.reduce((acc, {id, price, tag})=>{
acc[tag] = {id, price};
return acc
},{});
console.log(res)
Here's a solution with reduce:
const input = [
{'id': 52, 'tag': 'RFS', 'price': 780},
{'id': 14, 'tag': 'XGH', 'price': 341},
{'id': 29, 'tag': 'LLP', 'price': 997},
];
const output = input.reduce((acc, obj) => ({ ...acc, [obj.tag]: { id: obj.id, price: obj.price }}), {});
console.log(output);

Is there a lodash or other function to search an array of object arrays for a matching key/value?

I have an array of objects.
For each object, there's one key I need to use to search in an set of arrays of similar objects to see if they have a value for one key in my object, and if all those similar arrays have the SAME value, I want to set it as the value for the object wit
Is there a better way to find if all the searchable arrays. There's gotta be some awesome, elegant lodash function...right?
myValues[] = [{'id': 7, 'score': null}, {'id': 3, 'score': null}];
const searchme[] = /* data fetch*/;
// sample searchme[] =[
// [{'id': 1, 'score': 1}, {'id': 2, 'score': 1}, {'id': 3, 'score': 1}],
// [{'id': 1, 'score': 2}, {'id': 2, 'score': 1}, {'id': 4, 'score': 3}],
// [{'id': 1, 'score': 1}, {'id': 2, 'score': 1}, {'id': 3, 'score': 1}],
// ];
// searchme[] contains 1-5 arrays like myValues[];
// so searchme[0] = [{'id': 2, 'score': 2}, {'id': 7, 'score': 2}], and so on
// theoretically, each searchme[] array has all the ids all the others do
// - but a few might be missing
myValues.forEach(v => {
let score: number = null;
let started: boolean = false;
let fail: boolean = false;
searchme.forEach( anArray => {
found = anArray.find( search => search.id = v.id);
if (found) {
if (!started) { started = true; score = found.score; }
else { if (score != found.score) { fail = true; }
} else {
fail = true;
}
});
if (!fail) { v.score = score; }
});
This does work, but it seems super inefficient. Whaddaya think? Got some lodash chops to slap me with? :)
One idea is to flatten the searchme array, index it by ids.
(groupBy produces an object, keyed by the property name whose values are arrays of original objects with that key).
Not sure how to handle instances with the same id and different scores. For this, uniq() forces uniqueness on a given property.
let searchme = [
[{
'id': 1,
'score': 1
}, {
'id': 2,
'score': 1
}, {
'id': 3,
'score': 1
}],
[{
'id': 1,
'score': 2
}, {
'id': 2,
'score': 1
}, {
'id': 4,
'score': 3
}],
[{
'id': 1,
'score': 1
}, {
'id': 2,
'score': 1
}, {
'id': 3,
'score': 1
}],
];
let grouped = _.groupBy(_.flatten(searchme), 'id')
// now grouped is:
// { 1: [ {...} ], // the one object with id==1
// 2: [ {...}, {...} ], // the two objects with id==2
// etc
function scoresForId(id) {
let matches = grouped[id]
return _.uniq(matches, 'score')
}
console.log(scoresForId(2))
<script src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.9.1/underscore-min.js"></script>

Javascript: Merge Two Arrays of Objects, Only If Not Duplicate (Based on Specified Object Key)

Background
Say I have an initial array of objects:
var initialData = [
{
'ID': 1,
'FirstName': 'Sally'
},
{
'ID': 2,
'FirstName': 'Jim'
},
{
'ID': 3,
'FirstName': 'Bob'
}
];
I then get new data (another array of objects):
var newData = [
{
'ID': 2,
'FirstName': 'Jim'
},
{
'ID': 4,
'FirstName': 'Tom'
},
{
'ID': 5,
'FirstName': 'George'
}
];
Goal
I want to merge the new data into initial data. However, I don't want to overwrite any objects in the initial data array. I just want to add in objects that weren't already there.
I know the objects are duplicates based on their 'ID' key.
What I've Tried
I know I can do this by looping through the new data, checking to see if it exists in the initial data, and if not, pushing into initial data.
for ( var i = 0, l = newData.length; i < l; i++ ) {
if ( ! key_exists( newData[i].key, initialData ) ) { // key_exists() is a function that uses .filter() to test.
initialData.push( newData[i] );
}
}
I'm concerned about performance, though. I know there are lots of new ES6 ways of manipulating arrays, so I'm hoping someone has a better idea.
Question
What is the best way (best as in best performance) of merging the new data into the initial data, while ignoring duplicates in new data?
You can create a set of IDs from initialData and this will make "check if ID is already in initial data" faster - O(1):
var initialData = [{
'ID': 1,
'FirstName': 'Sally'
},
{
'ID': 2,
'FirstName': 'Jim'
},
{
'ID': 3,
'FirstName': 'Bob'
}
];
var newData = [{
'ID': 2,
'FirstName': 'Jim'
},
{
'ID': 4,
'FirstName': 'Tom'
},
{
'ID': 5,
'FirstName': 'George'
}
];
var ids = new Set(initialData.map(d => d.ID));
var merged = [...initialData, ...newData.filter(d => !ids.has(d.ID))];
console.log(merged);
The final runtime of this approach is O(n + m).
If you want to be slightly more efficient, you can consider looping through newData and pushing any new elements to the final result array manually (instead of using filter and the spread operator).
Actually, if you are interested on performance, you could think on changing your initialData structure to something like this:
var initialData = {
"1": {'FirstName': 'Sally'},
"2": {'FirstName': 'Jim'},
"3": {'FirstName': 'Bob'}
};
In other words, we use the IDs as the keys of an object, this will give you O(1) on access the data, and O(1) in the exists test. You can get this structure using the next approach with reduce():
var initialData = [
{'ID': 1, 'FirstName': 'Sally'},
{'ID': 2, 'FirstName': 'Jim'},
{'ID': 3, 'FirstName': 'Bob'}
];
let newInitialData = initialData.reduce((res, {ID, FirstName}) =>
{
res[ID] = {FirstName : FirstName};
return res;
}, {});
console.log(newInitialData);
Using this new structure, you can make a O(n) algorithm to insert the new data that is not already there:
var initialData = {
"1": {'FirstName': 'Sally'},
"2": {'FirstName': 'Jim'},
"3": {'FirstName': 'Bob'}
};
var newData = [
{'ID': 2, 'FirstName': 'Jim'},
{'ID': 4, 'FirstName': 'Tom'},
{'ID': 5, 'FirstName': 'George'}
];
newData.forEach(({ID, FirstName}) =>
{
initialData[ID] = initialData[ID] || {FirstName: FirstName};
});
console.log(initialData);
I think the alternative solution proposed by #slider in the accepted answer, would be something like this:
const oldData = [
{ id: 1, name: 'John' },
{ id: 2, name: 'Jane' },
{ id: 3, name: 'Mike' },
];
const newData = [
{ id: 2, name: 'Jane' },
{ id: 4, name: 'Rick' },
{ id: 5, name: 'Jim' },
];
const usersMap = new Map();
oldData.forEach(user => usersMap.set(user.id, user));
newData.forEach(user => {
const exists = usersMap.has(user.id);
if (!exists) {
usersMap.set(user.id, user);
}
})
usersMap.forEach(user => console.log(user))

Remove array of objects from another array of objects

Assume we have the following arrays of objects to be compared based on property id:
a = [{'id':'1', 'name':'a1'}, {'id':'2', 'name':'a2'}, {'id':'3', 'name':'a3'}]
and
b = [[{'id':'2', 'name':'a2'}, ]
How can I subtract b from a? So that we have c = a - b which should be equal to [ {'id':'1', 'name':'a1'}, {'id':'3', 'name':'a3'}].
I have tried using this:
var c= a.filter(function(item) {
return !b.includes(item.id);
});
but still not working.
How about this solution? It assumes that 'b' is also an array so for each element of 'a' you check if there is a matching object in 'b'. If there is a matching object then return a false in the filter function so that that element is discarded.
var a = [{
'id': '1',
'name': 'a1'
}, {
'id': '2',
'name': 'a2'
}, {
'id': '3',
'name': 'a3'
}]
var b = [{
'id': '2',
'name': 'a2'
}]
var c = a.filter(function(objFromA) {
return !b.find(function(objFromB) {
return objFromA.id === objFromB.id
})
})
console.log(c)
Here is a nice one line answer :)
Basically, you can filter, as you were trying to do already. Then you can also filter b for each a element and if the length of the filtered b is zero, then you return true because that means the a element is unique to a.
var a = [{
'id': '1',
'name': 'a1'
}, {
'id': '2',
'name': 'a2'
}, {
'id': '3',
'name': 'a3'
}];
var b = [{
'id': '2',
'name': 'a2'
}];
c = a.filter( x => !b.filter( y => y.id === x.id).length);
console.log(c);
Easy with new ES6 Syntax
Second and Third way are more performant i guess....
a.filter(i => !b.filter(y => y.id === i.id).length); // One Way
a.filter(i => !b.find(f => f.id === i.id)); // Second Way
a.filter(i => b.findIndex(f => f.id === i.id)) // Third Way
First, you build just a map of the ids you want to delete.
Then, you filter your first array with it, like that:
var a = [{
'id': '1',
'name': 'a1'
}, {
'id': '2',
'name': 'a2'
}, {
'id': '3',
'name': 'a3'
}];
var b = [{
'id': '2',
'name': 'a2'
}];
var idsToDelete = b.map(function(elt) {return elt.id;});
var result = a.filter(function(elt) {return idsToDelete.indexOf(elt.id) === -1;});
console.log(result)

Sort array of objects

I have an array of object literals like this:
var myArr = [];
myArr[0] = {
'score': 4,
'name': 'foo'
}
myArr[1] = {
'score': 1,
'name': 'bar'
}
myArr[2] = {
'score': 3,
'name': 'foobar'
}
How would I sort the array so it ascends by the 'score' parameter such that it would change to:
myArr[0] = {
'score': 1,
'name': 'bar'
}
myArr[1] = {
'score': 3,
'name': 'foobar'
}
myArr[2] = {
'score': 4,
'name': 'foo'
}
Thanks in advance.
Try myArr.sort(function (a, b) {return a.score - b.score});
The way the array elements are sorted depends on what number the function passed in returns:
< 0 (negative number): a goes ahead of b
> 0 (positive number): b goes ahead of a
0: In this cases the two numbers will be adjacent in the sorted list. However, the sort is not guaranteed to be stable: the order of a and b relative to each other may change.
You could have a look at the Array.sort documentation on MDN. Specifically at the documentation about providing a custom compareFunction
const myArray = [
{
'score': 4,
'name': 'foo'
},{
'score': 1,
'name': 'bar'
},{
'score': 3,
'name': 'foobar'
}
]
const myOrderedArray = _.sortBy(myArray, o => o.name);
console.log(myOrderedArray);
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.5/lodash.js"></script>
lodash sortBy

Categories