compare two arrays and remove duplicates [duplicate] - javascript

I created an array of objects like so:
[
{
"lat": 12.123,
"lng": 13.213,
"city": "New York"
},
{
"lat": 3.123,
"lng": 2.213,
"city": "New York"
},
{
"lat": 1.513,
"lng": 1.113,
"city": "London"
}
]
I'm trying to create a new array that filters the places to only contains objects that don't have the same city property (lat/lng duplicates are ok). Is there a built in JS or Jquery function to achieve this?

I'd probably use a flags object during the filtering (edit: I wouldn't anymore, see the note at the end of the answer about ES2015's Set), like this:
var flags = {};
var newPlaces = places.filter(function(entry) {
if (flags[entry.city]) {
return false;
}
flags[entry.city] = true;
return true;
});
That uses Array#filter from ECMAScript5 (ES5), which is one of the ES5 additions that can be shimmed (search for "es5 shim" for several options).
You can do it without filter, of course, it's just a bit more verbose:
var flags = {};
var newPlaces = [];
var index;
for (index = 0; index < places.length; ++index) {
if (!flags[entry.city]) {
flags[entry.city] = true;
newPlaces.push(entry);
}
});
Both of the above assume the first object with a given city should be kept, and all other discarded.
Note: As user2736012 points out below, my test if (flags[entry.city]) will be true for cities with names that happen to be the same as properties that exist on Object.prototype such as toString. Very unlikely in this case, but there are four ways to avoid the possibility:
(My usual preferred solution) Create the object without a prototype: var flags = Object.create(null);. This is a feature of ES5. Note that this cannot be shimmed for obsolete browsers like IE8 (the single-argument version of Object.create can be except when that argument's value is null).
Use hasOwnProperty for the test, e.g. if (flags.hasOwnProperty(entry.city))
Put a prefix on that you know doesn't exist for any Object.prototype property, such as xx:
var key = "xx" + entry.city;
if (flags[key]) {
// ...
}
flags[key] = true;
As of ES2015, you could use a Set instead:
const flags = new Set();
const newPlaces = places.filter(entry => {
if (flags.has(entry.city)) {
return false;
}
flags.add(entry.city);
return true;
});

Shortest, but not best performance (see update bellow) solution for es6 :
function unique(array, propertyName) {
return array.filter((e, i) => array.findIndex(a => a[propertyName] === e[propertyName]) === i);
}
performance: https://jsperf.com/compare-unique-array-by-property

You can filter using a Set by only including elements with a property value that has not yet been added to the Set (after which it should be added to the Set). This can be accomplished in one line using the logical and operator (&&). Using this data structure has the advantage of sublinear lookup times (often O(1)).
Below is a general function to obtain a unique array of objects based on a specific property (prop) from an array of objects (arr). Note that in the case of duplicates, only the first object with the property value will be retained.
const getUniqueBy = (arr, prop) => {
const set = new Set;
return arr.filter(o => !set.has(o[prop]) && set.add(o[prop]));
};
Demo:
var places = [{
lat: 12.123,
lng: 13.213,
city: 'New York'
}, {
lat: 3.123,
lng: 2.213,
city: 'New York'
}, {
lat: 3.123,
lng: 4.123,
city: 'Some City'
}];
const getUniqueBy = (arr, prop) => {
const set = new Set;
return arr.filter(o => !set.has(o[prop]) && set.add(o[prop]));
};
console.log(getUniqueBy(places, 'city'));

https://lodash.com/docs#uniqBy
https://github.com/lodash/lodash/blob/4.13.1/lodash.js#L7711
/**
* This method is like `_.uniq` except that it accepts `iteratee` which is
* invoked for each element in `array` to generate the criterion by which
* uniqueness is computed. The iteratee is invoked with one argument: (value).
*
* #static
* #memberOf _
* #since 4.0.0
* #category Array
* #param {Array} array The array to inspect.
* #param {Array|Function|Object|string} [iteratee=_.identity]
* The iteratee invoked per element.
* #returns {Array} Returns the new duplicate free array.
* #example
*
* _.uniqBy([2.1, 1.2, 2.3], Math.floor);
* // => [2.1, 1.2]
*
* // The `_.property` iteratee shorthand.
* _.uniqBy([{ 'x': 1 }, { 'x': 2 }, { 'x': 1 }], 'x');
* // => [{ 'x': 1 }, { 'x': 2 }]
*/

I expanded a bit on #IgorL solution, but extended prototype and gave it a selector function instead of a property to make it a little more flexible:
Array.prototype.unique = function(selector) {
return this.filter((e, i) => this.findIndex((a) => {
if (selector) {
return selector(a) === selector(e);
}
return a === e;
}) === i);
};
Usage:
// with no param it uses strict equals (===) against the object
let primArr = ['one','one','two','three','one']
primArr.unique() // ['one','two','three']
let a = {foo:123}
let b = {foo:123}
let fooArr = [a,a,b]
fooArr.unique() //[a,b]
// alternatively, you can pass a selector function
fooArr.unique(item=>item.foo) //[{foo:123}] (first "unique" item returned)
Definitely NOT the most performant way to do this but as long as the selector is simple and the array isn't massive, it should work fine.
In Typescript
Array.prototype.unique = function<T>(this: T[], selector?: (item: T) => object): T[] {
return this.filter((e, i) => this.findIndex((a) => {
if (selector) {
return selector(a) === selector(e);
}
return a === e;
}) === i);
};

My suggestion :
Array.prototype.uniqueCity = function() {
var processed = [];
for (var i=this.length-1; i>=0; i--){
if (processed.indexOf(this[i].city)<0) {
processed.push(this[i].city);
} else {
this.splice(i, 1);
}
}
}
in use :
places.uniqueCity();
or
Array.prototype.uniqueObjectArray = function(field) {
var processed = [];
for (var i=this.length-1; i>=0; i--) {
if (this[i].hasOwnProperty(field)) {
if (processed.indexOf(this[i][field])<0) {
processed.push(this[i][field]);
} else {
this.splice(i, 1);
}
}
}
}
places.uniqueObjectArray('city');
With the above you can sort the array by any of the fields in the objects, even if they are not present for some of the objects.
or
function uniqueCity(array) {
var processed = [];
for (var i=array.length-1; i>=0; i--){
if (processed.indexOf(array[i].city)<0) {
processed.push(array[i].city);
} else {
array.splice(i, 1);
}
}
return array;
}
places = uniqueCity(places);

You could use a Map so the entries with the same key property (in your case 'city') only appear once
module.exports = (array, prop) => {
const keyValueArray = array.map(entry => [entry[prop], entry]);
const map = new Map(keyValueArray);
return Array.from(map.values());
};
More info about Map and array objects here
Basic example on Codepen

Another option:
const uniqueBy = prop => list => {
const uniques = {}
return list.reduce(
(result, item) => {
if (uniques[item[prop]]) return result
uniques[item[prop]] = item
return [...result, item]
},
[],
)
}
const uniqueById = uniqueBy('id')
uniqueById([
{ id: 1, name: 'one' },
{ id: 2, name: 'two' },
{ id: 1, name: 'one' },
{ id: 3, name: 'three' }
])
You can paste it on your console to see it working.
It should work for the scenario presented and a few others.

We can create the list of unique objects by any property using JavaScript Map.
For example :
var places = [{ 'lat': 12.123, 'lng': 13.213, 'city': "New York"},
{ 'lat': 3.123, 'lng': 2.213, 'city': "New York"},
{ 'lat': 43.123, 'lng': 12.213, 'city': "London"}];
var cityMap = new Map();
places.forEach(p=> cityMap.set(p.city, p));
console.log([...cityMap.values()]);
Execute code snippet to see the result.

As pointed out in the comments, you could use an object as a map, which will allow you to avoid duplicates, you can then enumerate the properties of the object.
working fiddle: http://jsfiddle.net/gPRPQ/1/
var places = [];
var a = {};
a.lat = 12.123;
a.lng = 13.213;
a.city = "New York";
places.push(a);
var b = {};
b.lat = 3.123;
b.lng = 2.213;
b.city = "New York";
places.push(b);
var unique = {}
for (var i = 0; i < places.length; i++) {
var place = places[i];
unique[place.city] = place;
}
for (var name in unique) {
var place = unique[name];
console.log(place);
}

var places = [];
var a = {};
a.lat = 12.123;
a.lng = 13.213;
a.city = "New York";
places.push(a);
var b = {};
b.lat = 3.123;
b.lng = 2.213;
b.city = "New York";
places.push(b);
getUniqAR(places,'city'); //Return Uniq Array by property
function getUniqAR(Data,filter){
var uniar =[];
Data.forEach(function(item,ind,arr){
var dupi=false;
if(!uniar.length) uniar.push(item) //push first obj into uniq array
uniar.forEach(function(item2, ind2,arr){
if(item2[filter] == item[filter]){ //check each obj prop of uniq array
dupi=true; //if values are same put duplicate is true
}
})
if(!dupi){ uniar.push(item)} //if no duplicate insert to uniq
})
console.log(uniar)
return uniar;
}

In simple Javascript code to remove duplicate cities from places array list is
var places = [{ 'lat': 12.123, 'lng': 13.213, 'city': "New York"},
{ 'lat': 3.123, 'lng': 2.213, 'city': "New York"},
{ 'lat': 43.123, 'lng': 12.213, 'city': "London"}];
var unique = [];
var tempArr = [];
places.forEach((value, index) => {
if (unique.indexOf(value.city) === -1) {
unique.push(value.city);
} else {
tempArr.push(index);
}
});
tempArr.reverse();
tempArr.forEach(ele => {
places.splice(ele, 1);
});
console.log(places);

Generic Typescript answer based on https://stackoverflow.com/a/18773857/49564 above:
export function isDistinct<T>(mapper: (value: T) => string): (value: T) => boolean {
const keys: { [index: string]: boolean } = {};
return (entry: T) => {
const key = mapper(entry);
if (keys[key] !== undefined) {
return false;
}
return keys[key] = true;
};
}
// Usage example:
const items = [ { id: 1 }, { id: 2 }, { id: 3 }, { id: 1 } ];
const unique = items.filter(isDistinct(i => i.id));

I think you want this,
NOTE: No library is required.
let array = [{ id: 1}, {id: 2}, {id: 3}];
function addUniqeObj(data) {
let index = -1;
for(let i = 0, i < array.length; i++) {
if(array[i].id === data.id) {
index = i;
}
}
if(index > -1) {
array[index] = data;
} else {
array.push(data)
}
}

Another variation of the rafaelbiten approach:
const dedupExample = [
{id: 1, c: 'whatever'},
{id: 1, c: '1whatever'},
{id: 2, c: '2whatever'},
{id: 2, c: '2whatever'},
{id: 3, c: '2whatever'},
]
const getUniqueBy = (prop, list) => {
const objUniq = list.reduce((res, item) => ({ ...res, [item[prop]]: item }), {})
return Object.keys(objUniq).map(item => objUniq[item])
}
const uniq = getUniqueBy('id', dedupExample)
console.info('info', { uniq })
/* [
{id: 1, c: 'whatever'},
{id: 2, c: '2whatever'},
{id: 3, c: '2whatever'},
] */

const distinctArrayByCity= [
...new Map(array.map((item) => [item.city, item])).values(),
];

This thread may be old but thought I should share it. It is based on Pure JavaScript and removes Duplicate Objects based on the Properties Specified.
function removeDuplicates(originalArray, properties) {
var newArray = [];
var index = 0;
var lookupObject = {};
var totalProperties = properties.length;
for (var i = 0; i < originalArray.length; i++) {
var exists = false;
for (var a = 0; a < newArray.length; a++) {
var propsFound = 0;
for (var b = 0; b < totalProperties; b++) {
if (originalArray[i][properties[b]] == newArray[a][properties[b]]) {
propsFound++;
}
}
//If there is a match then break the for loop
if (propsFound == totalProperties) {
exists = true;
break;
}
} //End of New Array
if (!exists) {
newArray[index] = originalArray[i];
index++;
}
} //End of originalArray
return newArray;
}
You can view the fiddle here

Related

Can I add or edit object in array by field [duplicate]

I have this javascript object:
var arr1 = [{id:'124',name:'qqq'},
{id:'589',name:'www'},
{id:'45',name:'eee'},
{id:'567',name:'rrr'}]
var arr2 = [{id:'124',name:'ttt'},
{id:'45',name:'yyy'}]
I need to replace objects in arr1 with items from arr2 with same id.
So here is the result I want to get:
var arr1 = [{id:'124',name:'ttt'},
{id:'589',name:'www'},
{id:'45',name:'yyy'},
{id:'567',name:'rrr'}]
How can I implement it using javascript?
You can use Array#map with Array#find.
arr1.map(obj => arr2.find(o => o.id === obj.id) || obj);
var arr1 = [{
id: '124',
name: 'qqq'
}, {
id: '589',
name: 'www'
}, {
id: '45',
name: 'eee'
}, {
id: '567',
name: 'rrr'
}];
var arr2 = [{
id: '124',
name: 'ttt'
}, {
id: '45',
name: 'yyy'
}];
var res = arr1.map(obj => arr2.find(o => o.id === obj.id) || obj);
console.log(res);
Here, arr2.find(o => o.id === obj.id) will return the element i.e. object from arr2 if the id is found in the arr2. If not, then the same element in arr1 i.e. obj is returned.
What's wrong with Object.assign(target, source) ?
Arrays are still type object in Javascript, so using assign should still reassign any matching keys parsed by the operator as long as matching keys are found, right?
There is always going to be a good debate on time vs space, however these days I've found using space is better for the long run.. Mathematics aside let look at a one practical approach to the problem using hashmaps, dictionaries, or associative array's whatever you feel like labeling the simple data structure..
var marr2 = new Map(arr2.map(e => [e.id, e]));
arr1.map(obj => marr2.has(obj.id) ? marr2.get(obj.id) : obj);
I like this approach because though you could argue with an array with low numbers you are wasting space because an inline approach like #Tushar approach performs indistinguishably close to this method. However I ran some tests and the graph shows how performant in ms both methods perform from n 0 - 1000. You can decide which method works best for you, for your situation but in my experience users don't care to much about small space but they do care about small speed.
Here is my performance test I ran for source of data
var n = 1000;
var graph = new Array();
for( var x = 0; x < n; x++){
var arr1s = [...Array(x).keys()];
var arr2s = arr1s.filter( e => Math.random() > .5);
var arr1 = arr1s.map(e => {return {id: e, name: 'bill'}});
var arr2 = arr2s.map(e => {return {id: e, name: 'larry'}});
// Map 1
performance.mark('p1s');
var marr2 = new Map(arr2.map(e => [e.id, e]));
arr1.map(obj => marr2.has(obj.id) ? marr2.get(obj.id) : obj);
performance.mark('p1e');
// Map 2
performance.mark('p2s');
arr1.map(obj => arr2.find(o => o.id === obj.id) || obj);
performance.mark('p2e');
graph.push({ x: x, r1: performance.measure('HashMap Method', 'p1s', 'p1e').duration, r2: performance.measure('Inner Find', 'p2s','p2e').duration});
}
Since you're using Lodash you could use _.map and _.find to make sure major browsers are supported.
In the end I would go with something like:
function mergeById(arr) {
return {
with: function(arr2) {
return _.map(arr, item => {
return _.find(arr2, obj => obj.id === item.id) || item
})
}
}
}
var result = mergeById([{id:'124',name:'qqq'},
{id:'589',name:'www'},
{id:'45',name:'eee'},
{id:'567',name:'rrr'}])
.with([{id:'124',name:'ttt'}, {id:'45',name:'yyy'}])
console.log(result);
<script src="https://raw.githubusercontent.com/lodash/lodash/4.13.1/dist/lodash.js"></script>
Thanks to ES6 we can made it with easy way -> for example on util.js module ;))).
Merge 2 array of entity
export const mergeArrays = (arr1, arr2) =>
arr1 && arr1.map(obj => arr2 && arr2.find(p => p.id === obj.id) || obj);
gets 2 array and merges it.. Arr1 is main array which is priority is
high on merge process
Merge array with same type of entity
export const mergeArrayWithObject = (arr, obj) => arr && arr.map(t => t.id === obj.id ? obj : t);
it merges the same kind of array of type with some kind of type for
example: array of person ->
[{id:1, name:"Bir"},{id:2, name: "Iki"},{id:3, name:"Uc"}]
second param Person {id:3, name: "Name changed"}
result is
[{id:1, name:"Bir"},{id:2, name: "Iki"},{id:3, name:"Name changed"}]
I like to go through arr2 with foreach() and use findIndex() for checking for occurrence in arr1:
var arr1 = [{id:'124',name:'qqq'},
{id:'589',name:'www'},
{id:'45',name:'eee'},
{id:'567',name:'rrr'}]
var arr2 = [{id:'124',name:'ttt'},
{id:'45',name:'yyy'}]
arr2.forEach(element => {
const itemIndex = arr1.findIndex(o => o.id === element.id);
if(itemIndex > -1) {
arr1[itemIndex] = element;
} else {
arr1 = arr1.push(element);
}
});
console.log(arr1)
I'd like to suggest another solution:
const objectToReplace = this.array.find(arrayItem => arrayItem.id === requiredItem.id);
Object.assign(objectToReplace, newObject);
Considering that the accepted answer is probably inefficient for large arrays, O(nm), I usually prefer this approach, O(2n + 2m):
function mergeArrays(arr1 = [], arr2 = []){
//Creates an object map of id to object in arr1
const arr1Map = arr1.reduce((acc, o) => {
acc[o.id] = o;
return acc;
}, {});
//Updates the object with corresponding id in arr1Map from arr2,
//creates a new object if none exists (upsert)
arr2.forEach(o => {
arr1Map[o.id] = o;
});
//Return the merged values in arr1Map as an array
return Object.values(arr1Map);
}
Unit test:
it('Merges two arrays using id as the key', () => {
var arr1 = [{id:'124',name:'qqq'}, {id:'589',name:'www'}, {id:'45',name:'eee'}, {id:'567',name:'rrr'}];
var arr2 = [{id:'124',name:'ttt'}, {id:'45',name:'yyy'}];
const actual = mergeArrays(arr1, arr2);
const expected = [{id:'124',name:'ttt'}, {id:'589',name:'www'}, {id:'45',name:'yyy'}, {id:'567',name:'rrr'}];
expect(actual.sort((a, b) => (a.id < b.id)? -1: 1)).toEqual(expected.sort((a, b) => (a.id < b.id)? -1: 1));
})
// here find all the items that are not it the arr1
const temp = arr1.filter(obj1 => !arr2.some(obj2 => obj1.id === obj2.id))
// then just concat it
arr1 = [...temp, ...arr2]
Here a more transparent approach. I find the oneliners harder to read and harder to debug.
export class List {
static replace = (object, list) => {
let newList = [];
list.forEach(function (item) {
if (item.id === object.id) {
newList.push(object);
} else {
newList.push(item);
}
});
return newList;
}
}
If you don't care about the order of the array, then you may want to get the difference between arr1 and arr2 by id using differenceBy() and then simply use concat() to append all the updated objects.
var result = _(arr1).differenceBy(arr2, 'id').concat(arr2).value();
var arr1 = [{
id: '124',
name: 'qqq'
}, {
id: '589',
name: 'www'
}, {
id: '45',
name: 'eee'
}, {
id: '567',
name: 'rrr'
}]
var arr2 = [{
id: '124',
name: 'ttt'
}, {
id: '45',
name: 'yyy'
}];
var result = _(arr1).differenceBy(arr2, 'id').concat(arr2).value();
console.log(result);
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.13.1/lodash.js"></script>
I am only submitting this answer because people expressed concerns over browsers and maintaining the order of objects. I recognize that it is not the most efficient way to accomplish the goal.
Having said this, I broke the problem down into two functions for readability.
// The following function is used for each itertion in the function updateObjectsInArr
const newObjInInitialArr = function(initialArr, newObject) {
let id = newObject.id;
let newArr = [];
for (let i = 0; i < initialArr.length; i++) {
if (id === initialArr[i].id) {
newArr.push(newObject);
} else {
newArr.push(initialArr[i]);
}
}
return newArr;
};
const updateObjectsInArr = function(initialArr, newArr) {
let finalUpdatedArr = initialArr;
for (let i = 0; i < newArr.length; i++) {
finalUpdatedArr = newObjInInitialArr(finalUpdatedArr, newArr[i]);
}
return finalUpdatedArr
}
const revisedArr = updateObjectsInArr(arr1, arr2);
jsfiddle
function getMatch(elem) {
function action(ele, val) {
if(ele === val){
elem = arr2[i];
}
}
for (var i = 0; i < arr2.length; i++) {
action(elem.id, Object.values(arr2[i])[0]);
}
return elem;
}
var modified = arr1.map(getMatch);
I went with this, because it makes sense to me. Comments added for readers!
masterData = [{id: 1, name: "aaaaaaaaaaa"},
{id: 2, name: "Bill"},
{id: 3, name: "ccccccccc"}];
updatedData = [{id: 3, name: "Cat"},
{id: 1, name: "Apple"}];
updatedData.forEach(updatedObj=> {
// For every updatedData object (dataObj), find the array index in masterData where the IDs match.
let indexInMasterData = masterData.map(masterDataObj => masterDataObj.id).indexOf(updatedObj.id); // First make an array of IDs, to use indexOf().
// If there is a matching ID (and thus an index), replace the existing object in masterData with the updatedData's object.
if (indexInMasterData !== undefined) masterData.splice(indexInMasterData, 1, updatedObj);
});
/* masterData becomes [{id: 1, name: "Apple"},
{id: 2, name: "Bill"},
{id: 3, name: "Cat"}]; as you want.`*/
The accepted answer using array.map is correct but you have to remember to assign it to another variable since array.map doesnt change original array, it actually creates a new array.
//newArr contains the mapped array from arr2 to arr1.
//arr1 still contains original value
var newArr = arr1.map(obj => arr2.find(o => o.id === obj.id) || obj);
Array.prototype.update = function(...args) {
return this.map(x=>args.find((c)=>{return c.id===x.id}) || x)
}
const result =
[
{id:'1',name:'test1'},
{id:'2',name:'test2'},
{id:'3',name:'test3'},
{id:'4',name:'test4'}
]
.update({id:'1',name:'test1.1'}, {id:'3',name:'test3.3'})
console.log(result)
This is how I do it in TypeScript:
const index = this.array.indexOf(this.objectToReplace);
this.array[index] = newObject;

Reformatting array of arrays to nested json in Javascript [duplicate]

I have an array like
[
"parent1|child1|subChild1",
"parent1|child1|subChild2",
"parent|child2|subChild1",
"parent1|child2|subChild2",
"parent2|child1|subChild1",
"parent2|child1|subChild2",
"parent2|child2|subChild1",
.
.
.
]
Wherein my first string before | is the parent and the second string before | is the child and the third string after the second | is the subchild
How can I convert this array into an object like
[
{
"id": "parent1",
"children":[
{
"id": "child1",
"children":[
{
"id": "subChild1"
}
]
}
]
}
]
Parent -> child -> subchild object
Based on Sebastian's answer I tried below using typescript
private genTree(row) {
let self = this;
if (!row) {
return;
}
const [parent, ...children] = row.split('|');
if (!children || children.length === 0) {
return [{
id: parent,
children: []
}];
}
return [{
id: parent,
children: self.genTree(children.join('|'))
}];
}
private mergeDeep(children) {
let self = this;
const res = children.reduce((result, curr) => {
const entry = curr;
const existing = result.find((e) => e.id === entry.id);
if (existing) {
existing.children = [].concat(existing.children, entry.children);
} else {
result.push(entry);
}
return result;
}, []);
for (let i = 0; i < res.length; i++) {
const entry = res[i];
if (entry.children && entry.children.length > 0) {
entry.children = self.mergeDeep(entry.children);
}
};
return res;
}
private constructTree(statKeyNames){
let self = this;
const res = this.mergeDeep(statKeyNames.map(self.genTree).map(([e]) => e));
console.log(res);
}
but this gives me:
Cannot read property 'genTree' of undefined" error
Update:
As per Sebastian's comment changed self.genTree to this.genTree.bind(this) and it worked without any issues
You could use a mapper object which maps each object to it's unique path (You could map the object with each id, but id is not unique here). Then reduce each partial item in the array. Set the root object as the initialValue. The accumulator will be the parent object for the current item. Return the current object in each iteration.
const input = [
"parent1|child1|subChild1",
"parent1|child1|subChild2",
"parent1|child2|subChild1",
"parent1|child2|subChild2",
"parent2|child1|subChild1",
"parent2|child1|subChild2",
"parent2|child2|subChild1"
],
mapper = {},
root = { children: [] }
for (const str of input) {
let splits = str.split('|'),
path = '';
splits.reduce((parent, id, i) => {
path += `${id}|`;
if (!mapper[path]) {
const o = { id };
mapper[path] = o; // set the new object with unique path
parent.children = parent.children || [];
parent.children.push(o)
}
return mapper[path];
}, root)
}
console.log(root.children)
You have to use recursion for that. Take a look here:
const arr = [
"parent1|child1|subChild1",
"parent1|child1|subChild2",
"parent|child2|subChild1",
"parent1|child2|subChild2",
"parent2|child1|subChild1",
"parent2|child1|subChild2",
"parent2|child2|subChild1"
];
function genTree(row) {
const [parent, ...children] = row.split('|');
if (!children || children.length === 0) {
return [{
id: parent,
children: []
}];
}
return [{
id: parent,
children: genTree(children.join('|'))
}];
};
function mergeDeep(children) {
const res = children.reduce((result, curr) => {
const entry = curr;
const existing = result.find((e) => e.id === entry.id);
if (existing) {
existing.children = [].concat(existing.children, entry.children);
} else {
result.push(entry);
}
return result;
}, []);
for (let i = 0; i < res.length; i++) {
const entry = res[i];
if (entry.children && entry.children.length > 0) {
entry.children = mergeDeep(entry.children);
}
};
return res;
}
const res = mergeDeep(arr.map(genTree).map(([e]) => e));
console.log(JSON.stringify(res, false, 2));
I used two helpers here: genTree(row) which recursively generates a simple tree from each row, and mergeDeep(children) which reduces the first-level trees in the result of arr.map(genTree).map(([e]) => e), and then iterates over the array and recursively does the same thing to all children of each entry.

Transform an array to an object with nested properties according to the array

I have several arrays as the following:
[ 'businessOpenAccount', 'accountSettings1.page.js' ]
[ 'businessOpenAccount', 'accountSettings2.page.js' ]
[ 'mainTest', 'test', 'test1.page.js' ]
[ 'mainTest', 'test', 'test2.page.js' ]
My expected result is to have an object in this way:
{
businessOpenAccount: {
'accountSettings1.page.js': {},
'accountSettings2.page.js': {}
},
mainTest: {
test: {
'test1.page.js': {},
'test2.page.js': {}
}
}
}
So actually I want to parse the arrays and build a nested object to return from them, but being sure to check that if a property already exists (because defined from a previous array) I won't override it, but just add the new nested property in it, respecting the correct order of nesting.
I tried few approaches using reduce, reduceRight and simple forEach/for loops but I still cannot really achieve the solution I would like.
Any tips please?
This is the best way I have so far, but I override the properties cycling over multiple arrays (the example with a single array):
const relevantFilePath = ['businessOpenAccount', 'accountSettings.page.js'];
let obj = {};
relevantFilePath.forEach((el, ind) => {
if (ind === 0) {
obj[el] = {};
previousEl = obj[el];
} else {
previousEl[el] = {};
previousEl = previousEl[el];
}
});
console.log(obj);
You could iterate the given data and reduce the object and take the last item with splitted value.
var data = [['businessOpenAccount', 'accountSettings1.page.js'], ['businessOpenAccount', 'accountSettings2.page.js'], ['mainTest', 'test', 'test1.page.js'], ['mainTest', 'test', 'test2.page.js']],
object = {};
data.forEach(function (a) {
var last = a.pop().split('.')[0];
a.reduce(function (o, k) {
return o[k] = o[k] || {};
}, object)[last] = '';
});
console.log(object);
.as-console-wrapper { max-height: 100% !important; top: 0; }
ES6 with without mutating original data
var data = [['businessOpenAccount', 'accountSettings1.page.js'], ['businessOpenAccount', 'accountSettings2.page.js'], ['mainTest', 'test', 'test1.page.js'], ['mainTest', 'test', 'test2.page.js']],
object = {};
data.forEach(function (a) {
var temp = a.slice(),
last = temp.pop().split('.')[0];
temp.reduce((o, k) => o[k] = o[k] || {}, object)[last] = '';
});
console.log(object);
.as-console-wrapper { max-height: 100% !important; top: 0; }
A recursive solution which would work for anykind of sub array with unlimited depth.
const a = [
['businessOpenAccount', 'accountSettings1.page.js'],
['businessOpenAccount', 'accountSettings2.page.js'],
[ 'businessOpenAccount', 'test1',
[
['test2', 'test2.settings.page.js',
[
['test2', 'test2.settings.page.js'],
['test3', 'test3.settings.page.js'],
],
'test4',
],
['test3', 'test3.settings.page.js'],
]
],
['mainTest', 'test', 'test1.page.js'],
['mainTest', 'test', 'test2.page.js'],
];
const result = {};
const traverse = (result, arr) => {
let firstEl = arr.shift();
if (firstEl instanceof Array) {
if (arr.length >= 1) {
traverseTop(result, firstEl);
return traverse(result, arr);
}
return traverseTop(result, firstEl);
}
firstEl = firstEl.split('.')[0];
result[firstEl] = arr.length >= 1 ? (result[firstEl] || {}) : '';
if (arr.length >= 1) {
return traverse(result[firstEl], arr);
}
return;
};
const traverseTop = (result, arr) => {
arr.forEach((subArr) => {
const firstEl = subArr.shift().split('.')[0];
result[firstEl] = result[firstEl] || {};
traverse(result[firstEl], subArr)
});
return result;
};
console.log(traverseTop(result, a));

Replacing objects in array

I have this javascript object:
var arr1 = [{id:'124',name:'qqq'},
{id:'589',name:'www'},
{id:'45',name:'eee'},
{id:'567',name:'rrr'}]
var arr2 = [{id:'124',name:'ttt'},
{id:'45',name:'yyy'}]
I need to replace objects in arr1 with items from arr2 with same id.
So here is the result I want to get:
var arr1 = [{id:'124',name:'ttt'},
{id:'589',name:'www'},
{id:'45',name:'yyy'},
{id:'567',name:'rrr'}]
How can I implement it using javascript?
You can use Array#map with Array#find.
arr1.map(obj => arr2.find(o => o.id === obj.id) || obj);
var arr1 = [{
id: '124',
name: 'qqq'
}, {
id: '589',
name: 'www'
}, {
id: '45',
name: 'eee'
}, {
id: '567',
name: 'rrr'
}];
var arr2 = [{
id: '124',
name: 'ttt'
}, {
id: '45',
name: 'yyy'
}];
var res = arr1.map(obj => arr2.find(o => o.id === obj.id) || obj);
console.log(res);
Here, arr2.find(o => o.id === obj.id) will return the element i.e. object from arr2 if the id is found in the arr2. If not, then the same element in arr1 i.e. obj is returned.
What's wrong with Object.assign(target, source) ?
Arrays are still type object in Javascript, so using assign should still reassign any matching keys parsed by the operator as long as matching keys are found, right?
There is always going to be a good debate on time vs space, however these days I've found using space is better for the long run.. Mathematics aside let look at a one practical approach to the problem using hashmaps, dictionaries, or associative array's whatever you feel like labeling the simple data structure..
var marr2 = new Map(arr2.map(e => [e.id, e]));
arr1.map(obj => marr2.has(obj.id) ? marr2.get(obj.id) : obj);
I like this approach because though you could argue with an array with low numbers you are wasting space because an inline approach like #Tushar approach performs indistinguishably close to this method. However I ran some tests and the graph shows how performant in ms both methods perform from n 0 - 1000. You can decide which method works best for you, for your situation but in my experience users don't care to much about small space but they do care about small speed.
Here is my performance test I ran for source of data
var n = 1000;
var graph = new Array();
for( var x = 0; x < n; x++){
var arr1s = [...Array(x).keys()];
var arr2s = arr1s.filter( e => Math.random() > .5);
var arr1 = arr1s.map(e => {return {id: e, name: 'bill'}});
var arr2 = arr2s.map(e => {return {id: e, name: 'larry'}});
// Map 1
performance.mark('p1s');
var marr2 = new Map(arr2.map(e => [e.id, e]));
arr1.map(obj => marr2.has(obj.id) ? marr2.get(obj.id) : obj);
performance.mark('p1e');
// Map 2
performance.mark('p2s');
arr1.map(obj => arr2.find(o => o.id === obj.id) || obj);
performance.mark('p2e');
graph.push({ x: x, r1: performance.measure('HashMap Method', 'p1s', 'p1e').duration, r2: performance.measure('Inner Find', 'p2s','p2e').duration});
}
Since you're using Lodash you could use _.map and _.find to make sure major browsers are supported.
In the end I would go with something like:
function mergeById(arr) {
return {
with: function(arr2) {
return _.map(arr, item => {
return _.find(arr2, obj => obj.id === item.id) || item
})
}
}
}
var result = mergeById([{id:'124',name:'qqq'},
{id:'589',name:'www'},
{id:'45',name:'eee'},
{id:'567',name:'rrr'}])
.with([{id:'124',name:'ttt'}, {id:'45',name:'yyy'}])
console.log(result);
<script src="https://raw.githubusercontent.com/lodash/lodash/4.13.1/dist/lodash.js"></script>
I'd like to suggest another solution:
const objectToReplace = this.array.find(arrayItem => arrayItem.id === requiredItem.id);
Object.assign(objectToReplace, newObject);
Thanks to ES6 we can made it with easy way -> for example on util.js module ;))).
Merge 2 array of entity
export const mergeArrays = (arr1, arr2) =>
arr1 && arr1.map(obj => arr2 && arr2.find(p => p.id === obj.id) || obj);
gets 2 array and merges it.. Arr1 is main array which is priority is
high on merge process
Merge array with same type of entity
export const mergeArrayWithObject = (arr, obj) => arr && arr.map(t => t.id === obj.id ? obj : t);
it merges the same kind of array of type with some kind of type for
example: array of person ->
[{id:1, name:"Bir"},{id:2, name: "Iki"},{id:3, name:"Uc"}]
second param Person {id:3, name: "Name changed"}
result is
[{id:1, name:"Bir"},{id:2, name: "Iki"},{id:3, name:"Name changed"}]
I like to go through arr2 with foreach() and use findIndex() for checking for occurrence in arr1:
var arr1 = [{id:'124',name:'qqq'},
{id:'589',name:'www'},
{id:'45',name:'eee'},
{id:'567',name:'rrr'}]
var arr2 = [{id:'124',name:'ttt'},
{id:'45',name:'yyy'}]
arr2.forEach(element => {
const itemIndex = arr1.findIndex(o => o.id === element.id);
if(itemIndex > -1) {
arr1[itemIndex] = element;
} else {
arr1 = arr1.push(element);
}
});
console.log(arr1)
Considering that the accepted answer is probably inefficient for large arrays, O(nm), I usually prefer this approach, O(2n + 2m):
function mergeArrays(arr1 = [], arr2 = []){
//Creates an object map of id to object in arr1
const arr1Map = arr1.reduce((acc, o) => {
acc[o.id] = o;
return acc;
}, {});
//Updates the object with corresponding id in arr1Map from arr2,
//creates a new object if none exists (upsert)
arr2.forEach(o => {
arr1Map[o.id] = o;
});
//Return the merged values in arr1Map as an array
return Object.values(arr1Map);
}
Unit test:
it('Merges two arrays using id as the key', () => {
var arr1 = [{id:'124',name:'qqq'}, {id:'589',name:'www'}, {id:'45',name:'eee'}, {id:'567',name:'rrr'}];
var arr2 = [{id:'124',name:'ttt'}, {id:'45',name:'yyy'}];
const actual = mergeArrays(arr1, arr2);
const expected = [{id:'124',name:'ttt'}, {id:'589',name:'www'}, {id:'45',name:'yyy'}, {id:'567',name:'rrr'}];
expect(actual.sort((a, b) => (a.id < b.id)? -1: 1)).toEqual(expected.sort((a, b) => (a.id < b.id)? -1: 1));
})
// here find all the items that are not it the arr1
const temp = arr1.filter(obj1 => !arr2.some(obj2 => obj1.id === obj2.id))
// then just concat it
arr1 = [...temp, ...arr2]
Here a more transparent approach. I find the oneliners harder to read and harder to debug.
export class List {
static replace = (object, list) => {
let newList = [];
list.forEach(function (item) {
if (item.id === object.id) {
newList.push(object);
} else {
newList.push(item);
}
});
return newList;
}
}
If you don't care about the order of the array, then you may want to get the difference between arr1 and arr2 by id using differenceBy() and then simply use concat() to append all the updated objects.
var result = _(arr1).differenceBy(arr2, 'id').concat(arr2).value();
var arr1 = [{
id: '124',
name: 'qqq'
}, {
id: '589',
name: 'www'
}, {
id: '45',
name: 'eee'
}, {
id: '567',
name: 'rrr'
}]
var arr2 = [{
id: '124',
name: 'ttt'
}, {
id: '45',
name: 'yyy'
}];
var result = _(arr1).differenceBy(arr2, 'id').concat(arr2).value();
console.log(result);
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.13.1/lodash.js"></script>
I am only submitting this answer because people expressed concerns over browsers and maintaining the order of objects. I recognize that it is not the most efficient way to accomplish the goal.
Having said this, I broke the problem down into two functions for readability.
// The following function is used for each itertion in the function updateObjectsInArr
const newObjInInitialArr = function(initialArr, newObject) {
let id = newObject.id;
let newArr = [];
for (let i = 0; i < initialArr.length; i++) {
if (id === initialArr[i].id) {
newArr.push(newObject);
} else {
newArr.push(initialArr[i]);
}
}
return newArr;
};
const updateObjectsInArr = function(initialArr, newArr) {
let finalUpdatedArr = initialArr;
for (let i = 0; i < newArr.length; i++) {
finalUpdatedArr = newObjInInitialArr(finalUpdatedArr, newArr[i]);
}
return finalUpdatedArr
}
const revisedArr = updateObjectsInArr(arr1, arr2);
jsfiddle
function getMatch(elem) {
function action(ele, val) {
if(ele === val){
elem = arr2[i];
}
}
for (var i = 0; i < arr2.length; i++) {
action(elem.id, Object.values(arr2[i])[0]);
}
return elem;
}
var modified = arr1.map(getMatch);
I went with this, because it makes sense to me. Comments added for readers!
masterData = [{id: 1, name: "aaaaaaaaaaa"},
{id: 2, name: "Bill"},
{id: 3, name: "ccccccccc"}];
updatedData = [{id: 3, name: "Cat"},
{id: 1, name: "Apple"}];
updatedData.forEach(updatedObj=> {
// For every updatedData object (dataObj), find the array index in masterData where the IDs match.
let indexInMasterData = masterData.map(masterDataObj => masterDataObj.id).indexOf(updatedObj.id); // First make an array of IDs, to use indexOf().
// If there is a matching ID (and thus an index), replace the existing object in masterData with the updatedData's object.
if (indexInMasterData !== undefined) masterData.splice(indexInMasterData, 1, updatedObj);
});
/* masterData becomes [{id: 1, name: "Apple"},
{id: 2, name: "Bill"},
{id: 3, name: "Cat"}]; as you want.`*/
The accepted answer using array.map is correct but you have to remember to assign it to another variable since array.map doesnt change original array, it actually creates a new array.
//newArr contains the mapped array from arr2 to arr1.
//arr1 still contains original value
var newArr = arr1.map(obj => arr2.find(o => o.id === obj.id) || obj);
Array.prototype.update = function(...args) {
return this.map(x=>args.find((c)=>{return c.id===x.id}) || x)
}
const result =
[
{id:'1',name:'test1'},
{id:'2',name:'test2'},
{id:'3',name:'test3'},
{id:'4',name:'test4'}
]
.update({id:'1',name:'test1.1'}, {id:'3',name:'test3.3'})
console.log(result)
This is how I do it in TypeScript:
const index = this.array.indexOf(this.objectToReplace);
this.array[index] = newObject;

javascript find child object in nested arrays

I have a javascript structure like below (nested arrays of objects)
var categoryGroups = [
{
Id: 1, Categories: [
{ Id: 1 },
{ Id: 2 },
]
},
{
Id: 2, Categories: [
{ Id: 100 },
{ Id: 200 },
]
}
]
I want to find a child Category object matching an Id, assuming the Category Id's are all unique.
I've got this below, but was wondering if there is a more concise way of doing it:
var category, categoryGroup, found = false;
for (i = 0; i < categoryGroups.length ; i++) {
categoryGroup = categoryGroups[i];
for (j = 0; j < categoryGroup.Categories.length; j++) {
category = categoryGroup.Categories[j];
if (category.Id === id) {
found = true;
break;
}
}
if (found) break;
}
Using flatMap in ES2019
const category = categoryGroups.flatMap(cg => cg.Categories).find(c => c.Id === categoryId);
Caveat: This uses a couple of Array.prototype functions that were only added in ECMAScript 5 and thus will not work with older browsers unless you polyfill them.
You can loop over all first-level objects in your array, and then filter the categories based on your condition and collect all matches in an array. Your final result will be the first element in the array of matches (no match found if array is empty).
var matches = [];
var needle = 100; // what to look for
arr.forEach(function(e) {
matches = matches.concat(e.Categories.filter(function(c) {
return (c.Id === needle);
}));
});
console.log(matches[0] || "Not found");
JSFiddle: http://jsfiddle.net/b7ktf/1/
References:
Array.prototype.forEach
Array.prototype.concat
Array.prototype.filter
Using only Array.prototype.filter():
If you are sure that the id you are looking for exists, you can do:
var id = 200; // surely it exists
var category = arr.filter(g => g.Categories.filter(c => c.Id === id)[0])[0].Categories.filter(c => c.Id === id)[0];
If you are not sure that it exists:
var id = 201; // maybe it doesn't exist
var categoryGroup = arr.filter(e => e.Categories.filter(c => c.Id === id)[0])[0];
var category = categoryGroup ? categoryGroup.Categories.filter(c => c.Id === id)[0] : null;
jsfiddle
Using reduce and recursion :
function nestedSearch(value) {
return categoryGroups.reduce(function f(acc, val) {
return (val.Id === value) ? val :
(val.Categories && val.Categories.length) ? val.Categories.reduce(f, acc) : acc;
});
}
> try on JSFiddle
check the code in the fiddle
var categoryGroups = [
{
Id: 1, Categories: [
{ Id: 1 },
{ Id: 2 },
]
},
{
Id: 2, Categories: [
{ Id: 100 },
{ Id: 200 },
]
}
]
var id = 100;
var x = 'not found';
var category, categoryGroup, found = false;
for (i = 0; i < categoryGroups.length ; i++) {
categoryGroup = categoryGroups[i];
for (j = 0; j < categoryGroup.Categories.length; j++) {
category = categoryGroup.Categories[j];
if (category.Id == id) {
var x = category.Id;
found = true;
break;
}
}
if (found) break;
}
alert(x);
The above code checks if id = 100 is found in the array. If found will alert the value else alerts that its not found. value '100' has been hardcoded for the sake of demo
You could wrap it inside a function to get rid of the awkward break; syntax and you can load each element into a variable inside the for(;;) construct to shave off a few lines.
function subCategoryExists(groups, id)
{
for (var i = 0, group; group = groups[i]; ++i) {
for (var k = 0, category; category = group.Categories[k]; ++k) {
if (category.Id == id) {
return true;
}
}
}
return false;
}
var found = subCategoryExists(categoryGroups, 100);
Easy way using lodash library of NodeJS (assuming you are using NodeJS):
const _ = require('lodash');
let category ;
let categoryGroup = _.find(categoryGroups, (element)=>{
category = _.find(element.Categories, {Id : 100});
return category;
});
console.log(categoryGroup); // The category group which has the sub category you are looking for
console.log(category); // The exact category you are looking for
If you want to actually return the inner category (instead of just checking for it's presence) you can use reduce:
return categoryGroups.reduce((prev, curr) => {
//for each group: if we already found the category, we return that. otherwise we try to find it within this group
return prev || curr.Categories.find(category => category.Id === id);
}, undefined);
This short-circuits on the inner categories, and touches each categoryGroup once. It could be modified to short-cicuit on the categoryGroups as well.
Here's a JS Fiddle demonstration.
You could use underscore:
var cat = _(categoryGroups).
chain().
pluck('Categories').
flatten().
findWhere({Id: 2}).
value();
What I'm doing here is that I'm extracting all Categories values in a single array and then grepping for the correct categories.
EDIT: sorry, didn't get your question right the first time. As the comments suggest, you might not want to use underscore just for that, but that's how I would do it :)
We are using object-scan for our data processing now. It's very powerful once you wrap your head around it. For your questions this would look like this:
// const objectScan = require('object-scan');
const lookup = (id, data) => objectScan(['Categories.Id'], {
useArraySelector: false,
abort: true,
rtn: 'parent',
filterFn: ({ value }) => value === id
})(data);
const categoryGroups = [{ Id: 1, Categories: [{ Id: 1 }, { Id: 2 }] }, { Id: 2, Categories: [{ Id: 100 }, { Id: 200 }] }];
console.log(lookup(1, categoryGroups));
// => { Id: 1 }
console.log(lookup(100, categoryGroups));
// => { Id: 100 }
console.log(lookup(999, categoryGroups));
// => undefined
.as-console-wrapper {max-height: 100% !important; top: 0}
<script src="https://bundle.run/object-scan#13.8.0"></script>
Disclaimer: I'm the author of object-scan

Categories

Resources