Convert recursive array object to flat array object - javascript

I'm looking for a way to convert this array of recursive objects into a flat array of objects to make it easier to work with.
[
{
"name": "bill",
"car": "jaguar",
"age": 30,
"profiles": [
{
"name": "stacey",
"car": "lambo",
"age": 23,
"profiles": [
{
"name": "martin",
"car": "lexus",
"age": 34,
"profiles": []
}
]
}
]
}
]
This is the expected output.
[
{
"name": "bill",
"car": "jaguar",
"age": 30,
},{
"name": "stacey",
"car": "lambo",
"age": 23,
},{
"name": "martin",
"car": "lexus",
"age": 34,
}
]
Each profiles array can have n amount of items, which may or may not have an empty array of sub profiles. Note the converted array objects don't contain profiles after the conversion.
I'm open to using underscore or lodash to achieve this.

Let's call your original data o, combining Array.prototype.reduce with recursion I came up with this:
o.reduce(function recur(accumulator, curr) {
var keys = Object.keys(curr);
keys.splice(keys.indexOf('profiles'), 1);
accumulator.push(keys.reduce(function (entry, key) {
entry[key] = curr[key];
return entry;
}, {}));
if (curr.profiles.length) {
return accumulator.concat(curr.profiles.reduce(recur, []));
}
return accumulator;
}, []);

I would use a recursive function and pass the resulting array in it to avoid working with globals, something in the lines of:
var target = [];
var extractElements(source, target) {
//TODO: check if source is array
for (var i=0; i<source.length; i++) {
// create a new element with our data
var newElement = {
name: source[i].name,
car: source[i].car,
age: source[i].age
};
// put it in our flattened array
target.push(newElement);
// check if we need to go deeper and pass our flattened array around
if (source[i].profiles instanceof Array &&
source[i].profiles.length>0)
extractElements(source[i].profiles, target);
}
}
console.log(target) // should list your elements nicely
I haven't tested it, so use it for inspiration but beware :)
(edit1: "var i" in for)

const _ = require('lodash')
const arrayFromObject = (currentObject, currentArray) => {
const {profiles, ...rest} = currentObject
if (!_.isEmpty(currentObject.profiles)) {
return arrayFromObject(currentObject.profiles!, [...currentArray, rest])
}
return [...currentArray, rest]
}
const flatArray = arrayFromObject(myRecursiveObject, [])

Hi this can also be tried...
var out = [];
var i=0;
var extract = function(s, out) {
if(s[0] == null){
i = out.length -1;
return false;
}else {
out.push(s[0]);
}
extract(s[0].profiles, out);
delete out[i--].profiles;
};
extract(a, out); /// here 'a' is the input array and 'out' output
console.log(out);
All the best...

var _ = require('lodash')
/**
* Flatten a array-object via recursive property
* #see {#link http://stackoverflow.com/questions/31829897/convert-recursive-array-object-to-flat-array-object}
* #param {Array} arr Array of objects with recursive props
* #param {String} recursiveProperty The string of the recursive property
* #return {Array} Flat array of all recursive properties without recursive property
*/
function arrExtract (arr, recursiveProperty) {
var extracted = []
function _arrExtract (children) {
_.each(children, function (item) {
if (item[recursiveProperty] && item[recursiveProperty].length) _arrExtract(item[recursiveProperty])
extracted.push(_.omit(item, recursiveProperty))
})
}
_arrExtract(arr)
return extracted
}
module.exports = arrExtract

Almost three years later and still looking for a one-size fits solution for this. Here it is, heavily influenced by #axelduch's answer.
const {isPlainObject, isArray, get, omit, reduce} = require('lodash')
const recursiveFlatten = (tree, headProp, parentIdProp, parentRefProp, parent = {}) => {
tree = isArray(tree) ? tree : [tree]
return reduce(tree, (acq, current) => {
const currentWithoutHead = omit(current, [headProp])
if (parentIdProp && parentRefProp) currentWithoutHead[parentRefProp] = parent[parentIdProp] || null
acq = [...acq, currentWithoutHead]
const next = get(current, headProp)
if (isPlainObject(next) || isArray(next)) {
parent = currentWithoutHead
acq = [...acq, ...recursiveFlatten(next, headProp, parentIdProp, parentRefProp, parent)]
}
return acq
}, [])
}
Here's a simple example:
const example = recursiveFlatten({
name: 'bill',
love: true,
lovers: [{
name: 'jil',
love: false,
lovers: [{
name: 'diana',
love: false,
lovers: false
}, {
name: 'einstein',
love: false,
lovers: {
name: 'carl sagan',
love: false,
lovers: false
}
}]
}]
}, 'lovers')
[ { name: 'bill', love: true },
{ name: 'jil', love: false },
{ name: 'diana', love: false },
{ name: 'einstein', love: false },
{ name: 'carl sagan', love: false } ]
Here's an example adding parentId prop via parentRef.
const example = recursiveFlatten({
name: 'bill',
love: true,
lovers: [{
name: 'jil',
love: false,
lovers: [{
name: 'diana',
love: false,
lovers: false
}, {
name: 'einstein',
love: false,
lovers: {
name: 'carl sagan',
love: false,
lovers: false
}
}]
}]
}, 'lovers', 'name', 'parentName')
[ { name: 'bill', love: true, parentName: null },
{ name: 'jil', love: false, parentName: 'bill' },
{ name: 'diana', love: false, parentName: 'jil' },
{ name: 'einstein', love: false, parentName: 'jil' },
{ name: 'carl sagan', love: false, parentName: 'einstein' } ]

Here's a fairly simple technique that will solve the problem as originally defined.
const recursiveFlatten = (tree) =>
tree .length == 0
? []
: tree .flatMap (({profiles = [], ... rest}) => [{... rest}, ... recursiveFlatten (profiles)])
const tree = [{name: "bill", car: "jaguar", age: 30, profiles: [{name: "stacey", car: "lambo", age: 23, profiles: [{name: "martin", car: "lexus", age: 34, profiles: []}]}]}, {name: "denise", car: "pinto", age: 28}]
console .log (
recursiveFlatten (tree)
)
This hard-codes the name "profiles" and removes it, keeping the rest of the properties intact in the copy generated.
Your own answer suggest substantially more complex requirements. This version handles these through several optional parameters, the way your answer does, although the way it's called changes here and could easily be altered if necessary:
const recursiveFlatten = (headProp, parentIdProp, parentRefProp, parent = {}) => (tree) =>
tree .length == 0
? []
: tree .flatMap (({[headProp]: children = [], ... rest}) => [
{
... rest,
... (parentIdProp && parentRefProp ? {[parentRefProp]: parent[parentIdProp] || null} : {})
},
... recursiveFlatten (headProp, parentIdProp, parentRefProp, rest) (children)
])
const tree = [{name: "bill", car: "jaguar", age: 30, profiles: [{name: "stacey", car: "lambo", age: 23, profiles: [{name: "martin", car: "lexus", age: 34, profiles: []}]}]}, {name: "denise", car: "pinto", age: 28}]
console .log (recursiveFlatten ('profiles') (tree))
console .log (recursiveFlatten ('profiles', 'name', 'parentName') (tree))
I wouldn't be thrilled about this API in my own code-base, though. The differing behaviors depending on how many parameters are passed adds unnecessary complexity. I would probably bury them under an API such as
const recursiveFlatten = (parentIdProp, parentRefProp) => (headProp) => (tree) => ...
Then we could create functions we need, such as using
const flattenProfiles = recursiveFlatten (null, null) ('profiles')
and
const flattenAndExpand = recuriveFlatten ('name', 'parentName') ('profiles')
to replace the two call inside the console .log () statements above.

Related

how to compare elements of arrays inside two different objects and display which element belonged to which array that was inside the object

let buckets = [
{ first: { fname: "David", locations: ["q1,""q2,"q3","q4"] } },
{ second: { fname: "Eric", locations: ["a1","a2","a3","a4"] } },
];
test : ["a1","q2","q4","w100"];
Here, the elements inside test can be and any element that is not present in locations needs to be ignored, since w100 is not present it needs to be ignored
For final output I needed something like below:
Since the first element of test belongs to location from second object, I need to have output as:
{
fname: "Eric",
testing: "a1",
name: "copying a1"
},
Since the second element of test belongs to location form first object, I need to have output as:
{
fname: "David",
testing: "q2",
name: "copying q2"
}
And the same rule for the third element too:
{
fname: "David",
testing: "q4",
name: "copying "q4"
}
const buckets = [{
first: {
fname: "David",
locations: ["q1", "q2", "q3", "q4"]
}
},
{
second: {
fname: "Eric",
locations: ["a1", "a2", "a3", "a4"]
}
},
]
const test = ["a1", "q2", "q4", "w100"];
//must take in objects you suspect test values derive from
function compare(obj1, obj2, test) {
//reduce each test ele into an initial empty array output
const output = test.reduce((arr, test) => {
//if test ele belongs to first obj
if (obj1.locations.indexOf(test) > -1) {
//add the following to output
arr.push({
fname: obj1.fname,
testing: test,
name: `copying ${test}`
})
//if test ele belongs to second obj
} else if (obj2.locations.indexOf(test) > -1) {
//add the following to output
arr.push({
fname: obj2.fname,
testing: test,
name: `copying ${test}`
})
}
//repeat cycle
return arr;
}, [])
return output;
}
//pass in the nested objects, not the root objects
console.log(compare(buckets[0].first, buckets[1].second, test));
Your question is not totally clear to me, but I'll give it a shot:
let buckets = [{
first: {
fname: "David",
locations: ["q1", "q2", "q3", "q4"]
}
}, {
second: {
fname: "Eric",
locations: ["a1", "a2", "a3", "a4"]
}
}, ];
const test = ["a1", "q2", "q4", "w100"];
// the "first" and "second" keys are not used for anything
const flatBuckets = buckets.map(e => {
return Object.values(e)[0]
})
// creating the result:
const result = test.map(e => {
const c = flatBuckets.find(el => {
return el.locations.includes(e)
})
return c ? {
fname: c.fname,
testing: e,
name: `copying ${ e }`
} : null
}).filter(e => e)
console.log(result)

Safe and efficient dynamic multi-criteria array filtering javascript

I have a situation, where I need to filter an array using dynamic filter (and sometimes also complex filter criteria).
I have extended the Array class to add a method for this. This works fine, as long as I can specify straightforward filter criteria like {gender: "m"} and I can of course and some of these criteria.
What I would like to do in addition, though, is to specify more complex filter criteria, basically a dynamic callback function for the Array.prototype.filter() method. I can get this to work with eval(), but the performance is horrible and "Don't use eval()" is written all over the internet.
Do you have any suggestions how to solve this? Any suggestion to safely and efficiently pre-compile the complex filter string like ("key1==key2 || (last=="Smith" && gender=="m")
<script>
var People = [
{ first: 'Peter', last: 'Henderson', key1:0, gender: "m", key2:0 },
{ first: 'Paul', last: 'Paulson', key1: 10, gender: "m", key2:10 },
{ first: 'Mary', last: 'Miller', key1: 2, gender: "f", key2:0 },
{ first: 'Mary', last: 'Smith', key1: 3 , gender: "f" , key2:3 },
{ first: 'Peter', last: 'Smith' , key1: 4, gender: "m", key2:0 }
];
console.log(People);
var newPeople = MyArray.from(People).filterBy("#key1 == #key2");
console.log(newPeople);
newPeople = MyArray.from(People).filterBy({gender: "m", last:"Smith"});
console.log(newPeople);
</script>
classs MyArray extends Array {
filterBy(argument) {
return this.filter(function(el){
if (typeof argument == "object") {
// object with key/parameter pairs for filtering:
// {key1: 0, gender: "m"}
for (let key in argument) {
if (argument[key] !== el[key]) return false;
};
return true;
} else if (typeof argument == "string") {
// string with logical expression with key names #-escaped, e.g.
// #gender == 'm' && #key1 == #key2
let expression = argument.split("#").join("el.")
return eval(expression);
}
});
}
}
One way to think about this is that you wish to implement a language of filtering operations. Like any language, this language can be reified into an abstract syntax tree, so that it can be passed around and obtained dynamically.
Based on the single example in your question ("key1==key2 || (last=="Smith" && gender=="m"), let's say you want for your language to support conjunction, disjunction, and equality assertions involving literal values and the fields of the input.
While it isn't strictly necessary, it's often convenient to model the AST of a language as an algebraic data type. There's many libraries for this out there, here is how things look with one:
const { adt, match } = require("#masaeedu/adt")
// First, we build a language of references and predicates
const ref = adt({
lit: ["some literal value"],
key: ["the name of some field"]
})
const { lit, key } = ref
const predicate = adt({
and: ["predicate", "predicate"],
or: ["predicate", "predicate"],
equals: ["ref", "ref"]
})
const { and, or, equals } = predicate
// Then we construct an expression in this language
// NB: you could also use a parsing library to parse this out of a string
const yourpredicate =
or (equals (key ("key1")) (key ("key2")))
(and (equals (key ("last")) (lit ("Smith")))
(equals (key ("gender")) (lit ("m"))))
console.log(yourpredicate)
/* =>
{
label: "or",
values: [
{
label: "equals",
values: [
{ label: "key", values: ["key1"] },
{ label: "key", values: ["key2"] }
]
},
{
label: "and",
values: [
{
label: "equals",
values: [
{ label: "key", values: ["last"] },
{ label: "lit", values: ["Smith"] }
]
},
{
label: "equals",
values: [
{ label: "key", values: ["gender"] },
{ label: "lit", values: ["m"] }
]
}
]
}
]
}
*/
// Then we interpret our language into an actual predicate on some value
const resolve = ref.match({
lit: v => _ => v,
key: k => a => a[k]
})
const interpret = predicate.match({
and: f => g => a => interpret(f)(a) && interpret(g)(a),
or: f => g => a => interpret(f)(a) || interpret(g)(a),
equals: r1 => r2 => a => resolve(r1)(a) === resolve(r2)(a)
})
const inputs = [
{ key1: "foo", key2: "bar", last: "Smith", gender: "m" },
{ key1: "foo", key2: "foo", last: "Hurpenglurper", gender: "m" },
{ key1: "foo", key2: "bar", last: "Hurpenglurper", gender: "m" }
]
const p = interpret(yourpredicate)
console.log(inputs.map(p))
// =>
// [true, true, false]
You can run the example and play around with it here: https://runkit.com/masaeedu/reified-predicates

merge objects in same array based on common value in Javascript [duplicate]

This question already has answers here:
JavaScript merging objects by id [duplicate]
(18 answers)
Closed 3 years ago.
I have an array:
[
{
assignmentId:17,
email:"john.smith#email.com"
expectation: "Make sure to proofread!",
firstName:"John"
id:23
ignoreForFeedback: true
lastName:"Smith"
level:2
levelFraction:null
score:35
},
{
assignmentId:17
countsPerCategory: Array(4)
email:"john.smith#email.com"
firstName:"John"
frequentErrors: Array(5)
id:23
ignoreForGrading: true
lastName:"Smith"
},
{
assignmentId:17,
email:"cl#email.com"
expectation: "cite sources",
firstName:"Cindy"
id:45
ignoreForFeedback: true
lastName:"Lee"
level:2
levelFraction:null
score:32
},
{
assignmentId:17
countsPerCategory: Array(4)
email:"cl#email.com"
firstName:"Cindy"
frequentErrors: Array(5)
id:45
ignoreForGrading: true
lastName:"Lee"
}
]
I want to combine the Objects with the same 'id' into the same object within the array. Their common keys should also be combined (eg: 'firstName', 'email'). Can someone suggest the best way to do this? Either with ES6 or Lodash
You can use lodash#groupBy to group all items in the array by id and then use lodash#map with an iteratee of lodash#assign that is wrapped with a lodash#spread to make the array callback as a list of arguments for lodash#assgin.
var result = _(array)
.groupBy('id')
.map(_.spread(_.assign))
.value();
var array = [
{
assignmentId:17,
email:"john.smith#email.com",
expectation: "Make sure to proofread!",
firstName:"John",
id:23,
ignoreForFeedback: true,
lastName:"Smith",
level:2,
levelFraction:null,
score:35
},
{
assignmentId:17,
countsPerCategory: Array(4),
email:"john.smith#email.com",
firstName:"John",
frequentErrors: Array(5),
id:23,
ignoreForGrading: true,
lastName:"Smith"
},
{
assignmentId:17,
email:"cl#email.com",
expectation: "cite sources",
firstName:"Cindy",
id:45,
ignoreForFeedback: true,
lastName:"Lee",
level:2,
levelFraction:null,
score:32
},
{
assignmentId:17,
countsPerCategory: Array(4),
email:"cl#email.com",
firstName:"Cindy",
frequentErrors: Array(5),
id:45,
ignoreForGrading: true,
lastName:"Lee"
}
];
var result = _(array)
.groupBy('id')
.map(_.spread(_.assign))
.value();
console.log(result);
body > div { min-height: 100%; top: 0; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.4/lodash.js"></script>
Here's an alternative solution that uses Array#filter which takes advantage of the 2nd argument of the Array#filter which gives context to the filter's callback function. We use the this context as a mechanism to store cached objects by their id and then use this to decide whether to retain these objects from the array or not.
var result = array.filter(function(v) {
return this[v.id]?
!Object.assign(this[v.id], v):
(this[v.id] = v);
}, {});
var array = [
{
assignmentId:17,
email:"john.smith#email.com",
expectation: "Make sure to proofread!",
firstName:"John",
id:23,
ignoreForFeedback: true,
lastName:"Smith",
level:2,
levelFraction:null,
score:35
},
{
assignmentId:17,
countsPerCategory: Array(4),
email:"john.smith#email.com",
firstName:"John",
frequentErrors: Array(5),
id:23,
ignoreForGrading: true,
lastName:"Smith"
},
{
assignmentId:17,
email:"cl#email.com",
expectation: "cite sources",
firstName:"Cindy",
id:45,
ignoreForFeedback: true,
lastName:"Lee",
level:2,
levelFraction:null,
score:32
},
{
assignmentId:17,
countsPerCategory: Array(4),
email:"cl#email.com",
firstName:"Cindy",
frequentErrors: Array(5),
id:45,
ignoreForGrading: true,
lastName:"Lee"
}
];
var result = array.filter(function(v) {
// does this `id` exist?
return this[v.id]?
// assign existing object with the same id
// from the `this` cache object. Make sure
// to negate the resulting object with a `!`
// to remove this value from the array
!Object.assign(this[v.id], v):
// Assign the value from the `this` cache.
// This also retains this value from the existing
// array
(this[v.id] = v);
}, {});
console.log(result);
body > div { min-height: 100%; top: 0; }
You can use JavaScript's built in Array.reduce() method. The idea is you can create a map with the IDs and use lodash.merge() method (or whatever method you choose for merging objects) to merge all of the objects with the same ID into a single object. Then you can use .map() on the idMap you created to get the objects back into a single array.
var data = [{
assignmentId: 17,
email: "john.smith#email.com",
expectation: "Make sure to proofread!",
firstName: "John",
id: 23,
ignoreForFeedback: true,
lastName: "Smith",
level: 2,
levelFraction: null,
score: 35
},
{
assignmentId: 17,
countsPerCategory: Array(4),
email: "john.smith#email.com",
firstName: "John",
frequentErrors: Array(5),
id: 23,
ignoreForGrading: true,
lastName: "Smith"
},
{
assignmentId: 17,
email: "cl#email.com",
expectation: "cite sources",
firstName: "Cindy",
id: 45,
ignoreForFeedback: true,
lastName: "Lee",
level: 2,
levelFraction: null,
score: 32
},
{
assignmentId: 17,
countsPerCategory: Array(4),
email: "cl#email.com",
firstName: "Cindy",
frequentErrors: Array(5),
id: 45,
ignoreForGrading: true,
lastName: "Lee"
}
];
var idMap = data.reduce(function(result, current) {
if (result[current.id] == null) {
result[current.id] = current;
} else {
_.merge(result[current.id], current);
}
return result;
}, {});
var results = Object.keys(idMap).map(function(key) {
return idMap[key];
});
console.log(results);
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.4/lodash.js"></script>
What I can suggest is to use a combination of forEach() and some() methods to iterate the array elements and test if the iterated object id is already processed or not.
This is the solution:
var merged = [];
arr.forEach(function(item) {
var idx;
var found = merged.some(function(el, i) {
idx = el.id === item.id ? i : null;
return el.id === item.id;
});
if (!found) {
merged.push(item);
} else if (idx !== null) {
for (k in Object.keys(item)) {
if (item.hasOwnProperty(k)) {
merged[idx][k] = item[k];
}
}
}
});
Working Demo:
var arr = [{
assignmentId: 17,
email: "john.smith#email.com",
expectation: "Make sure to proofread!",
firstName: "John",
id: 23,
ignoreForFeedback: true,
lastName: "Smith",
level: 2,
levelFraction: null,
score: 35
},
{
assignmentId: 17,
countsPerCategory: [],
email: "john.smith#email.com",
firstName: "John",
frequentErrors: [],
id: 23,
ignoreForGrading: true,
lastName: "Smith"
},
{
assignmentId: 17,
email: "cl#email.com",
expectation: "cite sources",
firstName: "Cindy",
id: 45,
ignoreForFeedback: true,
lastName: "Lee",
level: 2,
levelFraction: null,
score: 32
},
{
assignmentId: 17,
countsPerCategory: [],
email: "cl#email.com",
firstName: "Cindy",
frequentErrors: [],
id: 45,
ignoreForGrading: true,
lastName: "Lee"
}
];
var merged = [];
arr.forEach(function(item) {
var idx;
var found = merged.some(function(el, i) {
idx = el.id === item.id ? i : null;
return el.id === item.id;
});
if (!found) {
merged.push(item);
} else if (idx !== null) {
for (k in Object.keys(item)) {
if (item.hasOwnProperty(k)) {
merged[idx][k] = item[k];
}
}
}
});
console.log(merged);
Thank you for your help everyone, but I ended up going with my own implementation.
let ids = [];
let combinedUsers = [];
users.forEach(function (user) {
ids.push(user.id);
});
ids = _.uniq(ids);
ids.forEach(function(id){
let user = users.filter(function(userObj){
return id === userObj.id
});
if(user.length > 1){
user = Object.assign(user[0], user[1]);
combinedUsers.push(user);
} else {
combinedUsers.push(user[0]);
}
});
return combinedStudents;

Using map() and/or reduce() to simplify a forEach() statement in Node (or native JavaScript)

I've got some data:
var rows = [{
name: "name1",
description: "description1",
value: 101
}, {
name: "name2",
description: "description2",
value: 202
}]
Purely for fun, I'd like to turn this into a matrix of object keys containing arrays of the matching data points. Here's one-liner I got working using forEach():
var o = {}
rows.forEach(row => Object.keys(row).forEach(key => (o[key] === undefined) ? o[key] = [row[key]] : o[key].push(row[key])))
console.log(o)
/*
{
name: ['name1', 'name2'],
description: ['description1', 'description2'],
value: [101, 202]
}
*/
I have a feeling I can still shorten this expression using map() and/or reduce(), but I've so far been completely stymied!
Also, let's assume the keys are consistent, but not always known.
If the keys are consistent, you can do sth like:
var matrix = Object.keys(rows[0]).reduce((o, key) => ( o[key] = rows.map(row => row[key]), o), {});
You could use Array#reduce with a short cut for checking if a property exists.
var rows = [{ name: "name1", description: "description1", value: 101 }, { name: "name2", description: "description2", value: 202 }],
obj = rows.reduce(
(o, row) => (
Object.keys(row).forEach(key => (o[key] = o[key] || []).push(row[key])),
o
),
{}
);
console.log(obj);
.as-console-wrapper { max-height: 100% !important; top: 0; }
Using lodash you can do this with
var rows = [{
name: "name1",
description: "description1",
value: 101
}, {
name: "name2",
description: "description2",
value: 202
}];
 
var combined = _.mergeWith(...rows, (o, s) => _.castArray(o).concat(_.castArray(s)));
console.log(combined);
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.4/lodash.js"></script>

How to find duplicate values in a JavaScript array of objects, and output only unique values?

I'm learning JS. Supposing I have the below array of objects:
var family = [
{
name: "Mike",
age: 10
},
{
name: "Matt"
age: 13
},
{
name: "Nancy",
age: 15
},
{
name: "Adam",
age: 22
},
{
name: "Jenny",
age: 85
},
{
name: "Nancy",
age: 2
},
{
name: "Carl",
age: 40
}
];
Notice that Nancy is showing up twice (changing only the age). Supposing I want to output only unique names. How do I output the above array of objects, without duplicates? ES6 answers more than welcome.
Related (couldn't find a good way for usage on objects):
Remove Duplicates from JavaScript Array
Easiest way to find duplicate values in a JavaScript array
EDIT Here's what I tried. It works well with strings but I can't figure how to make it work with objects:
family.reduce((a, b) => {
if (a.indexOf(b) < 0 ) {
a.push(b);
}
return a;
},[]);
You could use a Set in combination with Array#map and a spread operator ... in a single line.
Map returns an array with all names, which are going into the set initializer and then all values of the set are returned in an array.
var family = [{ name: "Mike", age: 10 }, { name: "Matt", age: 13 }, { name: "Nancy", age: 15 }, { name: "Adam", age: 22 }, { name: "Jenny", age: 85 }, { name: "Nancy", age: 2 }, { name: "Carl", age: 40 }],
unique = [...new Set(family.map(a => a.name))];
console.log(unique);
For filtering and return only unique names, you can use Array#filter with Set.
var family = [{ name: "Mike", age: 10 }, { name: "Matt", age: 13 }, { name: "Nancy", age: 15 }, { name: "Adam", age: 22 }, { name: "Jenny", age: 85 }, { name: "Nancy", age: 2 }, { name: "Carl", age: 40 }],
unique = family.filter((set => f => !set.has(f.name) && set.add(f.name))(new Set));
console.log(unique);
The Solution
Store occurrences of name external to the loop in an object, and filter if there's been a previous occurrence.
https://jsfiddle.net/nputptbb/2/
var occurrences = {}
var filteredFamily = family.filter(function(x) {
if (occurrences[x.name]) {
return false;
}
occurrences[x.name] = true;
return true;
})
you can also generalize this solution to a function
function filterByProperty(array, propertyName) {
var occurrences = {}
return array.filter(function(x) {
var property = x[propertyName]
if (occurrences[property]) {
return false;
}
occurrences[property]] = true;
return true;
})
}
and use it like
var filteredFamily = filterByProperty(family, 'name')
Explanation
Don't compare objects using indexOf, which only uses the === operator between objects. The reason why your current answer doesn't work is because === in JS does not compare the objects deeply, but instead compares the references. What I mean by that you can see in the following code:
var a = { x: 1 }
var b = { x: 1 }
console.log(a === b) // false
console.log(a === a) // true
Equality will tell you if you found the same exact object, but not if you found an object with the same contents.
In this case, you can compare your object on name since it should be a unique key. So obj.name === obj.name instead of obj === obj. Moreover another problem with your code that affects its runtime and not its function is that you use an indexOf inside of your reduce. indexOf is O(n), which makes the complexity of your algorithm O(n^2). Thus, it's better to use an object, which has O(1) lookup.
This will work fine.
const result = [1, 2, 2, 3, 3, 3, 3].reduce((x, y) => x.includes(y) ? x : [...x, y], []);
console.log(result);
With the code you mentioned, you can try:
family.filter((item, index, array) => {
return array.map((mapItem) => mapItem['name']).indexOf(item['name']) === index
})
Or you can have a generic function to make it work for other array of objects as well:
function printUniqueResults (arrayOfObj, key) {
return arrayOfObj.filter((item, index, array) => {
return array.map((mapItem) => mapItem[key]).indexOf(item[key]) === index
})
}
and then just use printUniqueResults(family, 'name')
(FIDDLE)
I just thought of 2 simple ways for Lodash users
Given this array:
let family = [
{
name: "Mike",
age: 10
},
{
name: "Matt",
age: 13
},
{
name: "Nancy",
age: 15
},
{
name: "Adam",
age: 22
},
{
name: "Jenny",
age: 85
},
{
name: "Nancy",
age: 2
},
{
name: "Carl",
age: 40
}
]
1. Find duplicates:
let duplicatesArr = _.difference(family, _.uniqBy(family, 'name'), 'name')
// duplicatesArr:
// [{
// name: "Nancy",
// age: 2
// }]
2 Find if there are duplicates, for validation purpose:
let uniqArr = _.uniqBy(family, 'name')
if (uniqArr.length === family.length) {
// No duplicates
}
if (uniqArr.length !== family.length) {
// Has duplicates
}
Since most of the answers won't have a good performance, i thought i share my take on this:
const arrayWithDuplicateData = [{ id: 5, name: 'Facebook'}, { id: 3, name: 'Twitter' }, { id: 5, name: 'Facebook' }];
const uniqueObj = {};
arrayWithDuplicateData.forEach(i => {
uniqueObj[i.id] = i;
});
const arrayWithoutDuplicates = Object.values(uniqueObj);
We're leveraging the fact that keys are unique within objects. That means the last duplication item inside the first array, will win over its predecessors. If we'd want to change that, we could flip the array before iterating over it.
Also we're not bound to use only one property of our object for identifying duplications.
const arrayWithDuplicateData = [{ id: 5, name: 'Facebook'}, { id: 3, name: 'Twitter' }, { id: 5, name: 'Facebook' }];
const uniqueObj = {};
arrayWithDuplicateData.forEach(item => {
uniqueObj[`${item.id}_${item.name}`] = item;
});
const arrayWithoutDuplicates = Object.values(uniqueObj);
Or we could simply add a check, if the uniqueObj already holds a key and if yes, not overwrite it.
Overall this way is not very costly in terms of performance and served me well so far.
I would probably set up some kind of object. Since you've said ECMAScript 6, you have access to Set, but since you want to compare values on your objects, it will take a little more work than that.
An example might look something like this (removed namespace pattern for clarity):
var setOfValues = new Set();
var items = [];
function add(item, valueGetter) {
var value = valueGetter(item);
if (setOfValues.has(value))
return;
setOfValues.add(value);
items.push(item);
}
function addMany(items, valueGetter) {
items.forEach(item => add(item, valueGetter));
}
Use it like this:
var family = [
...
];
addMany(family, item => item.name);
// items will now contain the unique items
Explanation: you need to pull a value from each object as it's added and decide if it has already been added yet, based on the value you get. It requires a value getter, which is a function that given an item, returns a value (item => item.name). Then, you only add items whose values haven't already been seen.
A class implementation:
// Prevents duplicate objects from being added
class ObjectSet {
constructor(key) {
this.key = key;
this.items = [];
this.set = new Set();
}
add(item) {
if (this.set.has(item[this.key])) return;
this.set.add(item[this.key]);
this.items.push(item);
}
addMany(items) {
items.forEach(item => this.add(item));
}
}
var mySet = new ObjectSet('name');
mySet.addMany(family);
console.log(mySet.items);

Categories

Resources