How to get unique combination in array of objects in javascript? [closed] - javascript

Closed. This question needs debugging details. It is not currently accepting answers.
Edit the question to include desired behavior, a specific problem or error, and the shortest code necessary to reproduce the problem. This will help others answer the question.
Closed 3 months ago.
Improve this question
I have array of objects like this,
let data = [
{ id: 1, name: 'a' },
{ id: 1, name: 'b'},
{ id: 1, name: 'a'},
{ id: 2, name: 'a'},
{ id: 2, name: 'b'},
{ id: 3, name: 'c'},
{ id: 3, name: 'c'}
]
I am trying to achieve unique combination of id and name, so expected output should be like,
output
[
{ id: 1, name: 'a'},
{ id: 1, name: 'b'},
{ id: 2, name: 'a'},
{ id: 2, name: 'b'},
{ id: 3, name: 'c'}
]
I have tried Set method but could not do it for key, value pair.
Please could someone help.
Thanks
Edit- 1
Most solutions have array of string, number or object with one key-value pair. I have two key-value pairs in object.

You can actually use Set for it, you just have to use combination of values that identifies if it is unique.
let data = [
{ id: 1, name: 'a' },
{ id: 1, name: 'b'},
{ id: 1, name: 'a'},
{ id: 2, name: 'a'},
{ id: 2, name: 'b'},
{ id: 3, name: 'c'},
{ id: 3, name: 'c'}
]
const nodup = new Set();
data.forEach(item => nodup.add(`${item.id}-${item.name}`));
console.log(Array.from(nodup))
let uniqueData = Array.from(nodup).map(item => {
const data = item.split('-')
return {id: data[0], name: data[1]};
});
console.log(uniqueData);
After this script, if you want to have array with objects with id and name again, you can simply create it from the result.

let data = [
{ id: 1, name: 'a' },
{ id: 1, name: 'b'},
{ id: 1, name: 'a'},
{ id: 2, name: 'a'},
{ id: 2, name: 'b'},
{ id: 3, name: 'c'},
{ id: 3, name: 'c'}
]
const unique_combos= (arr1) => {
const returned_array = []
arr1.map(element => {
if (!(returned_array.find(e=>e?.id === element?.id && e?.name === element.name)))
returned_array.push(element);
})
return (returned_array);
}
console.log(unique_combos(data))
I know it is not the best way to merge arrays but if that's the only case you want to handle. the above function will handle it for you.

If you want a new array of objects (rather than mutating/deleting objects from the array) you can dedupe the array in one iteration with reduce, and a Set (or an array, whatever takes your fancy).
const data=[{id:1,name:"a"},{id:1,name:"b"},{id:1,name:"a"},{id:2,name:"a"},{id:2,name:"b"},{id:3,name:"c"},{id:3,name:"c"}];
// Create a set to hold the keys
const keys = new Set();
// `reduce` over the data array, initialising the
// accumulator to an empty array.
const out = data.reduce((acc, obj) => {
// Destructure the id and name from each object
const { id, name } = obj;
// Create a key
const key = `${id}-${name}`;
// If the key exists in the set return the
// accumulator immediately
if (keys.has(key)) return acc;
// Otherwise add the key to the set,
// add the object to the accumulator, and
// return it for the next iteration
keys.add(key);
return [...acc, obj];
}, []);
console.log(out);
Additional documentation
Destructuring assignment
Template/string literals
If you want to remove objects from the array (mutation) you can use the same principle but just splice the objects from the array instead.
const data=[{id:1,name:"a"},{id:1,name:"b"},{id:1,name:"a"},{id:2,name:"a"},{id:2,name:"b"},{id:3,name:"c"},{id:3,name:"c"}];
const keys = new Set();
for (let i = 0; i < data.length; ++i) {
const { id, name } = data[i];
const key = `${id}-${name}`;
if (keys.has(key)) data.splice(i, 1);
keys.add(key);
}
console.log(data);

Here's a much cleaner solution for ES6 that I see isn't included here. It uses the Set and the spread operator: ...
var a = [1, 1, 2];
[... new Set(a)]
Which returns [1, 2]

Related

How to find names by id

How to find names by id's ? I have two arrays and want to find names by their id's.
person: [
{id: 1, name: 'abc'},
{id: 2, name: 'xyz'},
{id: 3, name: 'pqr'},
]
data: [
{id: 1, personId: [1,2,3]},
{id: 2, personId: [1,3]},
{id: 3, personId: [1,2]},
]
Expected Output :
personId: [1,2,3] return // abc,xyz,pqr
personId: [1,3] return // abc,pqr
personId: [1,2] return // abc,xyz
I am using react-native. I have tried this :
for (let person of this.state.data) {
for (let personName of person['personId']){
let name = this.state.person.find(nme => nme['id'] === personName);
alert(name);
}
}
Any help would be greatly appreciated
You can use find
result = []
for (let i=0; i < personId.length; i++) {
result.push(person.find(data => data.id === personId[i]).name);
}
in order to be able to retrieve objects in your person array based on the id.
I guess that's JSON data stringify-ied or a JSON in general. So, you can first parse it to JS object to make it easier for you to use JS. But let me leave that part to you. I changed your data to array so that I can directly show you what I would do if it helps.
It's good to use methods that don't mutate original data. So I used array methods slice, forEach and map in this case. This way you can safely create a new data that is a replica of data (var data) but by including new property 'personNames', which is mapped from the other variable (person).
const person = [
{id: 1, name: 'abc'},
{id: 2, name: 'xyz'},
{id: 3, name: 'pqr'},
];
const data = [
{id: 1, personId: [1,2,3]},
{id: 2, personId: [1,3]},
{id: 3, personId: [1,2]},
];
// not to mutate your original data, just in case
const dataWithNames = data.slice();
// console.log(dataWithNames);
// modifying new data to include personNames property
dataWithNames.forEach( eachData => {
// let eachIdP = eachData.personId;
// creating new array by mapping person id to names from person data
let mappedNames = eachData.personId.map( pID => {
// goes to refer person data with same id
person.forEach( eachPerson => {
if ( eachPerson.id === pID ) {
pID = eachPerson.name;
}
} );
return pID;
} );
// console.log(mappedNames);
// adding new property 'personNames' and
// assign them to respective mappedNames
eachData.personNames = mappedNames;
} );
// console.log(dataWithNames);
This will get you the following.
/*
[
{
id: 1,
personId: [ 1, 2, 3 ],
personNames: [ 'abc', 'xyz', 'pqr' ]
},
{ id: 2, personId: [ 1, 3 ], personNames: [ 'abc', 'pqr' ] },
{ id: 3, personId: [ 1, 2 ], personNames: [ 'abc', 'xyz' ] }
]*/
I hope this will help you.

Delete multiple objects in an array by id

I have a main array of objects with each object having some key/values as well as a "id" key with 1,2,3,4,5, etc
Now I have another array representing just id's (like [2,3])
I want to use this array to delete objects from the main array...so in this case, objects from the main array having id's 2 & 3 should be deleted
While I am aware of findBy(id), I am not sure if that can be used to delete multiple objects at once.
You can use filter. In the filter callback function check if the id is also there in id array by using includes
let idArr = [1, 2]
let obj = [{
id: 1,
name: 'abc'
},
{
id: 2,
name: 'abc'
},
{
id: 3,
name: 'abc'
},
{
id: 4,
name: 'abc'
}
];
let data = obj.filter(item => !idArr.includes(item.id));
console.log(data);
console.log(obj)
using filter might work well here. you could write something like:
var newArray = oldArray.filter(object => !ids.includes(object.id))
You can do it, like this:
[2,3].forEach(key => {
delete object[key];
})
You can use filter method for this.
Ex:
let id = 2;
let list = [{
Id: 1,
Name: 'a'
}, {
Id: 2,
Name: 'b'
}, {
Id: 3,
Name: 'c'
}];
let lists = list.filter(x => {
return x.Id != id;
})
console.log(lists);
Assuming you want to delete items from the original array by entirely removing the element from the array (and you don't want to get a new array), you can take advantage of
Array.splice
let idArr = [1, 2];
let obj = [{
id: 1
},
{
id: 2
},
{
id: 3
},
{
id: 4
}
];
for (let id of idArr) {
// look for the element by its id.
const objIdRef = obj.find(i => i.id === id);
// if it actually exists, splice it.
objIdRef && obj.splice(obj.indexOf(objIdRef), 1);
}
console.log(obj);
If the obj array is big, you might want to make a map from it before processing the id array, so that the complexing is reduced to O(1) when the delete process begins.
Perhaps This is what you want:
var arr= [{id:1, name: "foo"}, {id:2, name: "bar"}, {id:3, name:"not to be deleted"}];
var idsToDelete = [1, 2];
var res = arr.map((i, idx)=>{
return arr[idx] = idsToDelete.includes(i.id)? undefined : arr[idx]
}).filter(i=>i)
console.log(res)
You can try Lodash.js functions _.forEach() and _.remove()
let valuesArr = [
{id: 1, name: "dog"},
{id: 2, name: "cat"},
{id: 3, name: "rat"},
{id: 4, name: "bat"},
{id: 5, name: "pig"},
];
let removeValFromIndex = [
{id: 2, name: "cat"},
{id: 5, name: "pig"},
];
_.forEach(removeValFromIndex, (indi) => {
_.remove(valuesArr, (item) => {
return item.id === indi.id;
});
})
console.log(valuesArr)
/*[
{id: 1, name: "dog"},
{id: 3, name: "rat"},
{id: 4, name: "bat"},
]; */
Don't forget to clone (_.clone(valuesArr) or [...valuesArr]) before mutate your array

JavaScript Array push() method use to the id as the index value?

I want to use id as the index value and then generate a new array.
What better way do you have?
This is the result I want
Arr:Array[2]
3:"a"
8:"b"
Before processing
Arr:Array[2]
0:"a"
1:"b"
My code
var data = [{
id: 3,
name: 'a'
},
{
id: 8,
name: 'b',
}
]
var arr = []
const p = data.map(item => {
arr[item.id].push(item.name)
})
console.log(p)
You could use reduce, initialised with an empty array, set each index with id, and each value with name:
var data = [{
id: 3,
name: 'a'
},
{
id: 8,
name: 'b',
}
]
console.log(data.reduce((a, {id, name}) => (a[id] = name, a), []))
NOTE, you cannot have an array without indexes between values. Javascript will automatically fill these with undefined
If this doesn't fit your needs, then the only other option is to use an object (or a map but that's more complicated :P), which can still act like an array in a sense:
var data = [{
id: 3,
name: 'a'
},
{
id: 8,
name: 'b',
}
]
const obj = data.reduce((a, {id, name}) => (a[id] = name, a), {})
console.log(obj)
console.log(obj[3]) // a
console.log(obj[8]) // b

How to perform a nested update using Ramda in the given object structure?

Assuming the follow object how is it possible to use Ramda to perform a nested update in a criteria given an application, criteria ID and data?
const application = {
id: 'a1',
features: [
{
id: 'f1',
criterias: [
{ id: 'c1' }
]
},
{
id: 'f2',
criterias: [
{ id: 'c2' },
{ id: 'c3' }
]
}
]
}
The function would look something like this:
const updateCriteria = (application, criteriaId, data) => // magic...
updateCriteria(application, 'c2', { name: 'foo' })
// output: {
// id: 'a1',
// features: [
// {
// id: 'f1',
// criterias: [
// { id: 'c1' }
// ]
// },
// {
// id: 'f2',
// criterias: [
// { id: 'c2', name: 'foo' },
// { id: 'c3' }
// ]
// }
// ]
// }
Lenses are probably your best bet for this. Ramda has a generic lens function, and specific ones for an object property (lensProp), for an array index(lensIndex), and for a deeper path(lensPath), but it does not include one to find a matching value in an array by id. It's not hard to make our own, though.
A lens is made by passing two functions to lens: a getter which takes the object and returns the corresponding value, and a setter which takes the new value and the object and returns an updated version of the object.
Here we write lensMatch which find or sets the value in the array where a given property name matches the supplied value. And lensId simply passes 'id' to lensMatch to get back a function which will take an id value and return a lens.
Using any lens, we have the view, set, and over functions which, respectively, get, set, and update the value.
We could use idLens like this:
const data = [{id: 'a'}, {id: 'b'}, {id: 'c'}]
view (idLens ('b'), data)
//=> {id: 'b'}
set (idLens ('b'), 'foo', data)
//=> [ {id: 'a'}, 'foo', {id: 'c'} ]
over (idLens ('b'), merge ({name: 'foo'}), data)
//=> [ {id: 'a'}, {id: 'b', name: 'foo}, {id: 'c'} ]
So for your problem, we could write something like this:
const lensMatch = (propName) => (key) => lens
( find ( propEq (propName, key) )
, (val, arr, idx = findIndex (propEq (propName, key), arr)) =>
update (idx > -1 ? idx : length (arr), val, arr)
)
const lensId = lensMatch ('id')
const updateCriteria = (featureId, criteriaId, data, application) => over
( compose
( lensProp ('features')
, lensId (featureId)
, lensProp ('criterias')
, lensId (criteriaId)
)
, merge (data)
, application
)
const application = {id: 'a1', features: [{id: 'f1', criterias: [{ id: 'c1' }]}, {id: 'f2', criterias: [{ id: 'c2' }, { id: 'c3' }]}]}
const newApp = updateCriteria ('f2', 'c2', {name: 'foo'}, application)
console.log(newApp)
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.js"></script>
<script>
const {lens, find, propEq, findIndex, update, length, view, set, over, compose, lensProp, merge} = R
</script>
But this presupposes that you know the featureId. If you need to find both the featureId and the nested object with your internal id, you could write a more complex lens for that, but it will be much more heavyweight.
A minor note: 'criteria' is already plural, so 'criterias' is odd. The singular is 'criterion'.

How can I perform an inner join with two object arrays in JavaScript?

I have two object arrays:
var a = [
{id: 4, name: 'Greg'},
{id: 1, name: 'David'},
{id: 2, name: 'John'},
{id: 3, name: 'Matt'},
]
var b = [
{id: 5, name: 'Mathew', position: '1'},
{id: 6, name: 'Gracia', position: '2'},
{id: 2, name: 'John', position: '2'},
{id: 3, name: 'Matt', position: '2'},
]
I want to do an inner join for these two arrays a and b, and create a third array like this (if the position property is not present, then it becomes null):
var result = [{
{id: 4, name: 'Greg', position: null},
{id: 1, name: 'David', position: null},
{id: 5, name: 'Mathew', position: '1'},
{id: 6, name: 'Gracia', position: '2'},
{id: 2, name: 'John', position: '2'},
{id: 3, name: 'Matt', position: '2'},
}]
My approach:
function innerJoinAB(a,b) {
a.forEach(function(obj, index) {
// Search through objects in first loop
b.forEach(function(obj2,i2){
// Find objects in 2nd loop
// if obj1 is present in obj2 then push to result.
});
});
}
But the time complexity is O(N^2). How can I do it in O(N)? My friend told me that we can use reducers and Object.assign.
I'm not able to figure this out. Please help.
I don't know how reduce would help here, but you could use a Map to
accomplish the same task in O(n):
const a = [
{id: 4, name: 'Greg'},
{id: 1, name: 'David'},
{id: 2, name: 'John'},
{id: 3, name: 'Matt'}];
const b = [
{id: 5, name: 'Mathew', position: '1'},
{id: 6, name: 'Gracia', position: '2'},
{id: 2, name: 'John', position: '2'},
{id: 3, name: 'Matt', position: '2'}];
var m = new Map();
// Insert all entries keyed by ID into the Map, filling in placeholder
// 'position' since the Array 'a' lacks 'position' entirely:
a.forEach(function(x) { x.position = null; m.set(x.id, x); });
// For values in 'b', insert them if missing, otherwise, update existing values:
b.forEach(function(x) {
var existing = m.get(x.id);
if (existing === undefined)
m.set(x.id, x);
else
Object.assign(existing, x);
});
// Extract resulting combined objects from the Map as an Array
var result = Array.from(m.values());
console.log(JSON.stringify(result));
.as-console-wrapper { max-height: 100% !important; top: 0; }
Because Map accesses and updates are O(1) (on average - because of hash
collisions and rehashing, it can be longer), this makes O(n+m) (where n
and m are the lengths of a and b respectively; the naive solution you
gave would be O(n*m) using the same meaning for n and m).
One of the ways how to solve it.
const a = [
{id: 4, name: 'Greg'},
{id: 1, name: 'David'},
{id: 2, name: 'John'},
{id: 3, name: 'Matt'},
];
const b = [
{id: 5, name: 'Mathew', position: '1'},
{id: 6, name: 'Gracia', position: '2'},
{id: 2, name: 'John', position: '2'},
{id: 3, name: 'Matt', position: '2'},
];
const r = a.filter(({ id: idv }) => b.every(({ id: idc }) => idv !== idc));
const newArr = b.concat(r).map((v) => v.position ? v : { ...v, position: null });
console.log(JSON.stringify(newArr));
.as-console-wrapper { max-height: 100% !important; top: 0; }
If you drop the null criteria (many in the community are saying using null is bad) then there's a very simple solution
let a = [1, 2, 3];
let b = [2, 3, 4];
a.filter(x => b.includes(x))
// [2, 3]
To reduce the time complexity, it is inevitable to use more memory.
var a = [
{id: 4, name: 'Greg'},
{id: 1, name: 'David'},
{id: 2, name: 'John'},
{id: 3, name: 'Matt'},
]
var b = [
{id: 5, name: 'Mathew', position: '1'},
{id: 6, name: 'Gracia', position: '2'},
{id: 2, name: 'John', position: '2'},
{id: 3, name: 'Matt', position: '2'},
]
var s = new Set();
var result = [];
b.forEach(function(e) {
result.push(Object.assign({}, e));
s.add(e.id);
});
a.forEach(function(e) {
if (!s.has(e.id)) {
var temp = Object.assign({}, e);
temp.position = null;
result.push(temp);
}
});
console.log(result);
update
As #Blindman67 mentioned:"You do not reduce the problems complexity by moving a search into the native code." I've consulted the ECMAScript® 2016 Language Specification about the internal procedure of Set.prototype.has() and Map.prototype.get(), unfortunately, it seemed that they both iterate through all the elements they have.
Set.prototype.has ( value )#
The following steps are taken:
Let S be the this value.
If Type(S) is not Object, throw a TypeError exception.
If S does not have a [[SetData]] internal slot, throw a TypeError exception.
Let entries be the List that is the value of S's [[SetData]] internal slot.
Repeat for each e that is an element of entries,
If e is not empty and SameValueZero(e, value) is true, return true.
Return false.
http://www.ecma-international.org/ecma-262/7.0/#sec-set.prototype.has
Map.prototype.get ( key )#
The following steps are taken:
Let M be the this value.
If Type(M) is not Object, throw a TypeError exception.
If M does not have a [[MapData]] internal slot, throw a TypeError exception.
Let entries be the List that is the value of M's [[MapData]] internal slot.
Repeat for each Record {[[Key]], [[Value]]} p that is an element of entries,
If p.[[Key]] is not empty and SameValueZero(p.[[Key]], key) is true, return p.[[Value]].
Return undefined.
http://www.ecma-international.org/ecma-262/7.0/#sec-map.prototype.get
Perhaps, we can use the Object which can directly access its properties by their names, like the hash table or associative array, for example:
var a = [
{id: 4, name: 'Greg'},
{id: 1, name: 'David'},
{id: 2, name: 'John'},
{id: 3, name: 'Matt'},
]
var b = [
{id: 5, name: 'Mathew', position: '1'},
{id: 6, name: 'Gracia', position: '2'},
{id: 2, name: 'John', position: '2'},
{id: 3, name: 'Matt', position: '2'},
]
var s = {};
var result = [];
b.forEach(function(e) {
result.push(Object.assign({}, e));
s[e.id] = true;
});
a.forEach(function(e) {
if (!s[e.id]) {
var temp = Object.assign({}, e);
temp.position = null;
result.push(temp);
}
});
console.log(result);
You do not reduce the problems complexity by moving a search into the native code. The search must still be done.
Also the addition of the need to null a undefined property is one of the many reasons I dislike using null.
So without the null the solution would look like
var a = [
{id: 4, name: 'Greg',position: '7'},
{id: 1, name: 'David'},
{id: 2, name: 'John'},
{id: 3, name: 'Matt'},
]
var b = [
{id: 5, name: 'Mathew', position: '1'},
{id: 6, name: 'Gracia', position: '2'},
{id: 2, name: 'John', position: '2'},
{id: 3, name: 'Matt', position: '2'},
]
function join (indexName, ...arrays) {
const map = new Map();
arrays.forEach((array) => {
array.forEach((item) => {
map.set(
item[indexName],
Object.assign(item, map.get(item[indexName]))
);
})
})
return [...map.values()];
}
And is called with
const joinedArray = join("id", a, b);
To join with a default is a little more complex but should prove handy as it can join any number of arrays and automatically set missing properties to a provided default.
Testing for the defaults is done after the join to save a little time.
function join (indexName, defaults, ...arrays) {
const map = new Map();
arrays.forEach((array) => {
array.forEach((item) => {
map.set(
item[indexName],
Object.assign(
item,
map.get(item[indexName])
)
);
})
})
return [...map.values()].map(item => Object.assign({}, defaults, item));
}
To use
const joinedArray = join("id", {position : null}, a, b);
You could add...
arrays.shift().forEach((item) => { // first array is a special case.
map.set(item[indexName], item);
});
...at the start of the function to save a little time, but I feel it's more elegant without the extra code.
Here is an attempt at a more generic version of a join which accepts N objects and merges them based on a primary id key.
If performance is critical, you are better off using a specific version like the one provided by ShadowRanger which doesn't need to dynamically build a list of all property keys.
This implementation assumes that any missing properties should be set to null and that every object in each input array has the same properties (though properties can differ between arrays)
var a = [
{id: 4, name: 'Greg'},
{id: 1, name: 'David'},
{id: 2, name: 'John'},
{id: 3, name: 'Matt'},
];
var b = [
{id: 5, name: 'Mathew', position: '1'},
{id: 600, name: 'Gracia', position: '2'},
{id: 2, name: 'John', position: '2'},
{id: 3, name: 'Matt', position: '2'},
];
console.log(genericJoin(a, b));
function genericJoin(...input) {
//Get all possible keys
let template = new Set();
input.forEach(arr => {
if (arr.length) {
Object.keys(arr[0]).forEach(key => {
template.add(key);
});
}
});
// Merge arrays
input = input.reduce((a, b) => a.concat(b));
// Merge items with duplicate ids
let result = new Map();
input.forEach(item => {
result.set(item.id, Object.assign((result.get(item.id) || {}), item));
});
// Convert the map back to an array of objects
// and set any missing properties to null
return Array.from(result.values(), item => {
template.forEach(key => {
item[key] = item[key] || null;
});
return item;
});
}
Here's a generic O(n*m) solution, where n is the number of records and m is the number of keys. This will only work for valid object keys. You can convert any value to base64 and use that if you need to.
const join = ( keys, ...lists ) =>
lists.reduce(
( res, list ) => {
list.forEach( ( record ) => {
let hasNode = keys.reduce(
( idx, key ) => idx && idx[ record[ key ] ],
res[ 0 ].tree
)
if( hasNode ) {
const i = hasNode.i
Object.assign( res[ i ].value, record )
res[ i ].found++
} else {
let node = keys.reduce( ( idx, key ) => {
if( idx[ record[ key ] ] )
return idx[ record[ key ] ]
else
idx[ record[ key ] ] = {}
return idx[ record[ key ] ]
}, res[ 0 ].tree )
node.i = res[ 0 ].i++
res[ node.i ] = {
found: 1,
value: record
}
}
} )
return res
},
[ { i: 1, tree: {} } ]
)
.slice( 1 )
.filter( node => node.found === lists.length )
.map( n => n.value )
join( [ 'id', 'name' ], a, b )
This is essentially the same as Blindman67's answer, except that it adds an index object to identify records to join. The records are stored in an array and the index stores the position of the record for the given key set and the number of lists it's been found in.
Each time the same key set is encountered, the node is found in the tree, the element at it's index is updated, and the number of times it's been found is incremented.
finally, the idx object is removed from the array with the slice, any elements that weren't found in each set are removed. This makes it an inner join, you could remove this filter and have a full outer join.
finally each element is mapped to it's value, and you have the merged array.

Categories

Resources