Javascript: Remove duplicates in an array by compare function - javascript

I have an array with objects like this:
const array = [
{name:'obj1', address: 987, id: '123', location: 'zyx' },
{name:'obj2', address: 654, id: '456', location: 'wvu'},
{name:'obj3', address: 321, id: '123', location: 'zyx'}
];
and I want to remove the duplicates with a function to compare them:
const compareObjects = (a, b) => {
return a.id === b.id && a.location === b.location;
}
The function only compares the relevant properties of the objects.
How can I remove the duplicates from the array with this function?
Edit: To clarify, I want to use a function to compare some properties of the object and not the whole object.

You could reduce the array by checking the object of the temporary result set.
const
array = [{ name:'obj1', address: 987, id: '123', location: 'zyx' }, { name:'obj2', address: 654, id: '456', location: 'wvu' }, { name:'obj3', address: 321, id: '123', location: 'zyx' }],
compareObjects = (a, b) => a.id === b.id && a.location === b.location,
result = array.reduce((r, o) => {
if (!r.some(compareObjects.bind(null, o))) {
r.push(o);
}
return r;
}, []);
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }

The obvious solution is to compare each element against each other element (except for itself):
const result = array.filter((el, i) => !arr.some((el2, i2) => i < i2 && compareObjects(el, el2));
However that is O(n²) which will get very slow for large datasets, in that case hashtables help you:
const dupes = new Set;
const key = el => el.id + "|" + el.location;
const result = array.filter(it => !dupes.has(key(el)) && dupes.add(key(el)));
That is O(n) (aka super fast, but consumes more memory).

You can try a function like below
function unique(array , compareObjects){
array.sort(compareObjects);
for(var i = 1; i < array.length; ){
if( compareObjects(array[i-1], array[i]) === 0){
array.splice(i, 1);
} else {
i++;
}
}
return array;
}

Related

Merge Array of same level

I have an array which I need to combine with comma-separated of the same level and form a new array.
Input:
let arr = [
[{ LEVEL: 1, NAME: 'Mark' }, { LEVEL: 1, NAME: 'Adams' }, { LEVEL: 2, NAME: 'Robin' }],
[{ LEVEL: 3, NAME: 'Williams' }],
[{ LEVEL: 4, NAME: 'Matthew' }, { LEVEL: 4, NAME: 'Robert' }],
];
Output
[
[{ LEVEL: 1, NAME: 'Mark,Adams' }, { LEVEL: 2, NAME: 'Robin' }],
[{ LEVEL: 3, NAME: 'Williams' }],
[{ LEVEL: 4, NAME: 'Matthew,Robert' }],
];
I tried with the following code but not getting the correct result
let finalArr = [];
arr.forEach(o => {
let temp = finalArr.find(x => {
if (x && x.LEVEL === o.LEVEL) {
x.NAME += ', ' + o.NAME;
return true;
}
if (!temp) finalArr.push(o);
});
});
console.log(finalArr);
You could map the outer array and reduce the inner array by finding the same level and add NAME, if found. Otherwise create a new object.
var data = [[{ LEVEL: 1, NAME: "Mark" }, { LEVEL: 1, NAME: "Adams" }, { LEVEL: 2, NAME: "Robin"}], [{ LEVEL: 3, NAME: "Williams" }], [{ LEVEL: 4, NAME: "Matthew" }, { LEVEL: 4, NAME: "Robert" }]],
result = data.map(a => a.reduce((r, { LEVEL, NAME }) => {
var temp = r.find(q => q.LEVEL === LEVEL);
if (temp) temp.NAME += ',' + NAME;
else r.push({ LEVEL, NAME });
return r;
}, []));
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }
Assuming you only want to merge within the same array and not across arrays, and assuming there aren't all that many entries (e.g., fewer than several hundred thousand), the simple thing is to build a new array checking to see if it already has the same level in it:
let result = arr.map(entry => {
let newEntry = [];
for (const {LEVEL, NAME} of entry) {
const existing = newEntry.find(e => e.LEVEL === LEVEL);
if (existing) {
existing.NAME += "," + NAME;
} else {
newEntry.push({LEVEL, NAME});
}
}
return newEntry;
});
let arr= [
[{"LEVEL":1,"NAME":"Mark"},
{"LEVEL":1,"NAME":"Adams"},
{"LEVEL":2,"NAME":"Robin"} ],
[{"LEVEL":3,"NAME":"Williams"}],
[{"LEVEL":4,"NAME":"Matthew"},
{"LEVEL":4,"NAME":"Robert"}]
];
let result = arr.map(entry => {
let newEntry = [];
for (const {LEVEL, NAME} of entry) {
const existing = newEntry.find(e => e.LEVEL === LEVEL);
if (existing) {
existing.NAME += "," + NAME;
} else {
newEntry.push({LEVEL, NAME});
}
}
return newEntry;
});
console.log(result);
If the nested arrays can be truly massively long, you'd want to build a map rather than doing the linear search (.find) each time.
I'd try to do as much of this in constant time as possible.
var m = new Map();
array.forEach( refine.bind(m) );
function refine({ LABEL, NAME }) {
var o = this.get(NAME)
, has = !!o
, name = NAME
;
if (has) name = `${NAME}, ${o.NAME}`;
this.delete(NAME);
this.set(name, { NAME: name, LABEL });
}
var result = Array.from( m.values() );
I haven't tested this as I wrote it on my phone at the airport, but this should at least convey the approach I would advise.
EDIT
Well... looks like the question was edited... So... I'd recommend adding a check at the top of the function to see if it's an array and, if so, call refine with an early return. Something like:
var m = new Map();
array.forEach( refine.bind(m) );
function refine(item) {
var { LABEL, NAME } = item;
if (!NAME) return item.forEach( refine.bind(this) ); // assume array
var o = this.get(NAME)
, has = !!o
, name = NAME
;
if (has) name = `${NAME}, ${o.NAME}`;
this.delete(NAME);
this.set(name, { NAME: name, LABEL });
}
var result = Array.from( m.values() );
That way, it should work with both your original question and your edit.
EDIT
Looks like the question changed again... I give up.
Map the array values: every element to an intermediate object, then create the desired object from the resulting entries:
const basicArr = [
[{"LEVEL":1,"NAME":"Mark"},
{"LEVEL":1,"NAME":"Adams"},
{"LEVEL":2,"NAME":"Robin"} ],
[{"LEVEL":3,"NAME":"Williams"}],
[{"LEVEL":4,"NAME":"Matthew"},
{"LEVEL":4,"NAME":"Robert"}]
];
const leveled = basicArr.map( val => {
let obj = {};
val.forEach(v => {
obj[v.LEVEL] = obj[v.LEVEL] || {NAME: []};
obj[v.LEVEL].NAME = obj[v.LEVEL].NAME.concat(v.NAME);
});
return Object.entries(obj)
.map( ([key, val]) => ({LEVEL: +key, NAME: val.NAME.join(", ")}));
}
);
console.log(leveled);
.as-console-wrapper { top: 0; max-height: 100% !important; }
if you want to flatten all levels
const basicArr = [
[{"LEVEL":1,"NAME":"Mark"},
{"LEVEL":1,"NAME":"Adams"},
{"LEVEL":2,"NAME":"Robin"} ],
[{"LEVEL":3,"NAME":"Williams"}],
[{"LEVEL":4,"NAME":"Matthew"},
{"LEVEL":4,"NAME":"Robert"},
{"LEVEL":2,"NAME":"Cynthia"}],
[{"LEVEL":3,"NAME":"Jean"},
{"LEVEL":4,"NAME":"Martha"},
{"LEVEL":2,"NAME":"Jeff"}],
];
const leveled = basicArr.map( val => Object.entries (
val.reduce( (acc, val) => {
acc[val.LEVEL] = acc[val.LEVEL] || {NAME: []};
acc[val.LEVEL].NAME = acc[val.LEVEL].NAME.concat(val.NAME);
return acc;
}, {}))
.map( ([key, val]) => ({LEVEL: +key, NAME: val.NAME.join(", ")})) )
.flat() // (use .reduce((acc, val) => acc.concat(val), []) for IE/Edge)
.reduce( (acc, val) => {
const exists = acc.filter(x => x.LEVEL === val.LEVEL);
if (exists.length) {
exists[0].NAME = `${val.NAME}, ${exists.map(v => v.NAME).join(", ")}`;
return acc;
}
return [... acc, val];
}, [] );
console.log(leveled);
.as-console-wrapper { top: 0; max-height: 100% !important; }
ES6 way:
let say attributes is multidimensional array having multimple entries which need to combine like following:
let combinedArray = [];
attributes.map( attributes => {
combined = combinedArray.concat(...attributes);
});

How to detect object differences between two arrays?

I'm trying to compare two arrays of objects and returns a list of updated objects. I don't want to use lodash just the javascript data structures and functions.
E.g:
I have a first array which named arr1 = [
{
name: 'attribute 1',
id: 12,
value: 40,
docs:[],
version: 1,
},
{
name: 'attribute 41',
id: 12,
value: 6,
version: 1,
}
]
And another array:
array2 = [
{
name: 'attribute 1',
attributeTypeId: 12,
value: 65,
docs: ['bla bla']
}
]
I'm trying to iterate through the two arrays and detect the differences and returns an array like that:
result = [
{
name: 'attribute 1',
id: 12,
value: 65,
docs:['bla bla'],
version: 1,
},
{
name: 'attribute 41',
id: 12,
value: 6,
version: 1,
}]
I wrote some uncomplete function (not optimized yet just a brute force solution):
const filterProperties = (e) => {
return e.toLowerCase() !== 'name' && e.toLowerCase() !== 'id'
}
// function sort
const sortProperties = (a, b) => a < b ? -1 : 1;
let result = []
attributesUpdate.forEach(attr => {
const attrProps = Object.getOwnPropertyNames(attr);
// iterate the attributes
for (let i = 0; i < attributes.length; i++) {
let attribute = attributes[i];
// check if the attribute to update has a different name or attributeTypeId
if (attribute.name !== attr.name) {
result = result.concat(attr);
}
// check if the attribute to update has the same name, id
// of the originalOne
if (attribute.name === attr.name && attribute.id=== attr.id) {
let obj = {
name: attribute.name,
id: attribute.id,
}
// get the properties of the attribute
const attributeProps = Object.getOwnPropertyNames(attribute);
// extract the name and id from the list
const filtredAttributeProps = attributeProps.filter(filterProperties);
const filteredattrProps = attrProps.filter(filterProperties);
// returns the length of each array of properties
const attrLength = filteredattrProps.length;
const attributeLength = filtredAttributeProps.length;
if (attrLength === attributeLength) {
for (let j = 0; j < attrLength; j++) {
const propName = filteredattrProps[j];
obj[propName] = attr[propName];
}
result = result.filter(e => e.name === attr.name
&& e.id=== attr.id)
.map(e => Object.assign(e, {obj}))
}
if (attrLength !== attributeLength) {
// sort the array of properties
const sortedAttrProps = filteredattrProps.sort(sortProperties);
const sortedAttributeProps = filtredAttributeProps.sort(sortProperties);
// check the shortest object
const min = attrLength < attributeLength ? attrLength : attributeLength;
// get the biggest object
const longestObjProps = attrLength === min ? sortedAttributeProps : sortedAttrProps;
const longestObj = attrLength === min ? attribute : attr
const shortestProps = attrLength === min ? sortedAttrProps: sortedAttributeProps;
const shortestObj = attrLength === min ? attr : attribute
// fill the object with attr properties
for(let j = 0; j < min; j++) {
const propName = shortestProps[j];
obj[propName] = shortestObj[propName];
}
// fill the remaining properties in the object
const remainingProperties = longestObjProps.filter(e => !shortestProps.includes(e));
for (let j = 0; j < remainingProperties.length; j++) {
const propName = remainingProperties[j];
obj[propName] = longestObj[propName]
}
if (!result.length || result.filter(e => e.name !== attr.name &&
e.id!== attr.id).length === 0) {
result.concat(obj);
}
}
}
}
})
console.log('result: ', result);
I got such a result :
[
{
name: 'attribute 1',
attributeTypeId: 12,
value: 65,
docs: ['bla bla']
}
]
How can I fix this code to get the desired results? I hope that my question will not be downvoted. Any suggestion will be welcome.
What this code does is loop through the objects in array2, and then when it finds that there is a matching name/id in arr1, it simply updates the properties of that object. If not found, it will add the object to arr1.
arr1 = [{
name: 'attribute 1',
id: 12,
value: 40,
docs: [],
version: 1,
},
{
name: 'attribute 41',
id: 12,
value: 6,
version: 1,
}
];
array2 = [{
name: 'attribute 1',
attributeTypeId: 12,
value: 65,
docs: ['bla bla']
}];
updateArray(arr1, array2);
console.log(arr1);
function updateArray(arrayToUpdate, dataToUpdateWith) {
dataToUpdateWith.forEach(function(obj) {
var objToUpdate = checkIfNameIdExists(arrayToUpdate, obj.name, obj.attributeTypeId);
if (objToUpdate === false) {
objToUpdate = obj;
arrayToUpdate.push(objToUpdate);
} else {
for (var prop in obj) {
if (objToUpdate.hasOwnProperty(prop)) {
var nameInFinalObject = prop;
if (prop === "attributeTypeId") {
nameInFinalObject = "id";
}
objToUpdate[nameInFinalObject] = obj[prop];
}
}
}
});
}
function checkIfNameIdExists(arrOfObj, name, id) {
if (name === null) {
return false;
}
var output = false;
arrOfObj.forEach(function(obj) {
if (obj.name === name) {
output = obj;
return true;
}
});
return output;
}
Assumptions:
The values in each of the objects are same type and values are not nested so there is a need to recursively traverse the tree to compare equality etc.
The first array is the source and the subsequent (with the same name) is the mutated form.
We are not handling removals of properties from the source object. From what is given by the OP we are only accounting for value changes.
const d1 = [{ name: 'attribute 1', id: 12, value: 40, docs: [], version: 1, }, { name: 'attribute 41', id: 12, value: 6, version: 1, } ]
const d2 = [{ name: 'attribute 1', attributeTypeId: 12, value: 65, docs: ['bla bla'] }]
const isChanged = (a, b) =>
Array.isArray(a) ? !a.every(x => b.includes(x)) : a !== b
const compare = (o1, o2) => Object.entries(o1).reduce((r, [k,v]) => {
if(k in o2 && isChanged(o2[k], v))
Object.assign(r, {[k]: o2[k]})
return r
}, o1)
const group = (a, b) => [...a, ...b].reduce((r,c) =>
(r[c.name] = [...r[c.name] || [], c], r), {})
const result = Object.values(group(d1,d2)).reduce((r,c) =>
(r.push(c.length == 2 ? compare(...c) : c[0]), r), [])
console.log(result)
The idea is to merge the objects in one array, group them by name and if there ware any changes the groups with length of 2 would be compared by the compare function. Otherwise just added to the end result.

Sort a list by property and add an object before each first letter changes in JavaScript

So I am trying to make a UI like this:
And I have an array of users
[{name: 'Julia'}, {name: 'Ismeh'}, {name: 'Alison'}, {name: 'Andrea'}, {name: 'Betty'}]
What I am trying to do is to sort the array by first letter of the name property, and add a header object before each. For example in the picture, you can see the letter A, B, I, and J as the headers.
For now, I got it working like this:
let final = []
// sort by first letter
const sortedUsers = state.test_list.sort((a, b) => a.name.localeCompare(b.name))
for (let x = 0; x < sortedUsers.length; x++) {
const user = sortedUsers[x].name
if (user.charAt(0) === 'A') {
const checkIfExists = final.findIndex((f) => f.header === 'A')
// add the header A if it doesn't exist
if (checkIfExists < 0) final.push({header: 'A'})
}
else if (user.charAt(0) === 'B') {
const checkIfExists = final.findIndex((f) => f.header === 'B')
// add the header B if it doesn't exist
if (checkIfExists < 0) final.push({header: 'B'})
}
// else if up to the letter Z
final.push(user)
}
and if I log the final array, I get:
which is correct.
My concern is that the code is very long, and I have no idea if it can be optimized or make the code smaller.
Is there any other option to do something like this? Any help would be much appreciated.
Why don't you create a collection of names, which is grouped by the first letter? You can then loop on it, and create your list. Use Array#reduce to create the grouped collection.
And then use Object#keys to iterate over the grouped collection and render your results:
let data = [{
name: 'Julia'
}, {
name: 'Ismeh'
}, {
name: 'Alison'
}, {
name: 'Andrea'
}, {
name: 'Betty'
}];
let combined = data.reduce((result, item) => {
let letter = item.name[0].toUpperCase();
if (!result[letter]) {
result[letter] = [];
}
result[letter].push(item);
return result;
}, {});
console.log(combined);
// Iterate over the result
Object.keys(combined).forEach(key => {
// key will be the first letter of the user names and
// combined[key] will be an array of user objects
console.log(key, combined[key]);
});
One thing still to do is to sort the user arrays by user name, which you can do easily using Array#sort.
Simple enough, try sorting them and then using .reduce:
const unsortedPeople = [{name: 'Julia'}, {name: 'Ismeh'}, {name: 'Alison'}, {name: 'Andrea'}, {name: 'Betty'}];
const sortedUsers = unsortedPeople.sort((a, b) => a.name.localeCompare(b.name))
const final = sortedUsers.reduce((finalSoFar, user) => {
const thisUserFirstChar = user.name[0];
if (finalSoFar.length === 0) addHeader();
else {
const lastUserFirstChar = finalSoFar[finalSoFar.length - 1].name[0];
if (lastUserFirstChar !== thisUserFirstChar) addHeader();
}
finalSoFar.push(user);
return finalSoFar;
function addHeader() {
finalSoFar.push({ header: thisUserFirstChar });
}
}, []);
console.log(final);
Why don't you just keep track of the current abbreviation as you loop. Then you can add a head when it changes:
var users = [{name: 'Julia'}, {name: 'Ismeh'}, {name: 'Alison'}, {name: 'Andrea'}, {name: 'Betty'}]
const sortedUsers = users.sort((a, b) => a.name.localeCompare(b.name))
var currentHeader
let final = sortedUsers.reduce((a, user) => {
if (currentHeader !== user.name[0]) {
currentHeader = user.name[0]
a.push({header: currentHeader})
}
a.push(user)
return a
},[])
console.log(final)
Here's one way to do it:
const users = [{name: 'Julia'}, {name: 'Ismeh'}, {name: 'Alison'}, {name: 'Andrea'}, {name: 'Betty'}];
let lastIndex;
let result = [];
users.sort((a, b) => {
return a.name > b.name;
}).forEach((user) => {
const index = user.name.charAt(0);
if (index !== lastIndex) {
result.push({
header: index
});
}
lastIndex = index;
result.push(user.name);
}, []);
console.log(result);
You can use _.orderBy(collection, [iteratees=[_.identity]], [orders]) and _.groupBy(collection, [iteratee=_.identity]) method of lodash.
This orderBy is like _.sortBy except that it allows specifying the sort orders of the iteratees to sort by. If orders is unspecified, all values are sorted in ascending order. Otherwise, specify an order of "desc" for descending or "asc" for ascending sort order of corresponding values.
groupBy will creates an object composed of keys generated from the results of running each element of collection thru iteratee. The order of grouped values is determined by the order they occur in collection. The corresponding value of each key is an array of elements responsible for generating the key. The iteratee is invoked with one argument: (value).
example
// The `_.property` iteratee shorthand.
_.groupBy(['one', 'two', 'three'], 'length');
// => { '3': ['one', 'two'], '5': ['three'] }
// Sort by `user` in ascending order and by `age` in descending order.
_.orderBy(users, ['user', 'age'], ['asc', 'desc']);
With lodash
let myArr = [{
name: 'Julia'
}, {
name: 'Ismeh'
}, {
name: 'Andrea'
}, {
name: 'Alison'
}, {
name: 'Betty'
}];
myArr = _.orderBy(myArr, ['name'], ['asc']);
let r = _.groupBy(myArr, o => {
return o.name.charAt(0).toUpperCase();
})
console.log(r);
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.4/lodash.js"></script>
IN ES5
var arr = [{
name: 'Julia'
}, {
name: 'Ismeh'
}, {
name: 'Andrea'
}, {
name: 'Alison'
}, {
name: 'Betty'
}],
fChar = '';
arr = arr.sort(function(a, b) {
a = a.name.toUpperCase(); // ignore upper and lowercase
b = b.name.toUpperCase(); // ignore upper and lowercase
return a < b ? -1 : (a > b ? 1 : 0);
}).reduce(function(r, o) {
fChar = o.name.charAt(0).toUpperCase();
if (!r[fChar]) {
r[fChar] = [];
}
r[fChar].push({
name: o.name
});
return r;
}, {});
console.log(arr);
IN ES6
const arr = [{
name: 'Julia'
}, {
name: 'Ismeh'
}, {
name: 'Andrea'
}, {
name: 'Alison'
}, {
name: 'Betty'
}];
let result = arr.sort((a, b) => {
a = a.name.toUpperCase(); // ignore upper and lowercase
b = b.name.toUpperCase(); // ignore upper and lowercase
return a < b ? -1 : (a > b ? 1 : 0);
}).reduce((r, o) => {
let fChar = o.name.charAt(0).toUpperCase();
if (!r[fChar]) {
r[fChar] = [];
}
r[fChar].push({
name: o.name
});
return r;
}, {});
console.log(result);

Merge objects concatenating values, using lodash

I'm trying to manipulate this sample array of objects.
[ { name: 'John Wilson',
id: 123,
classes: ['java', 'c++']},
{ name: 'John Wilson',
id: 123,
classes: 'uml'},
{ name: 'Jane Smith',
id: 321,
classes: 'c++'} ]
What I need to do is to merge objects with the same 'id', concatenating 'classes' and keeping one 'name'.
The result should be:
[ { name: 'John Wilson',
id: 123,
classes: ['java', 'c++', 'uml']},
{ name: 'Jane Smith',
id: 321,
classes: 'c++'} ]
I tried using .merge but it doesn't concatenate the values from 'classes', it just keeps the values from the last equal object.
What is the simplest way to do that, using lodash?
The function you're looking for is _.uniqWith, with a special twist which I will explain in a minute.
_.uniqWith is a lot like _.uniq in that it generates a unique array, but it allows you to pass your own custom comparator function that will be called to determine what counts as "equality."
Sane programmers would understand that this comparator should be side-effect free. The way this code works is by breaking that rule, and using a comparison function that does extra magic behind the scenes. However, this results in very concise code that will work no matter how many of these objects are in your array, so I feel like the transgression is well-justified.
I named the comparator function compareAndMerge so as not to hide its impure nature. It will merge both classes arrays and update the relevant property on both objects, but only if their id values are identical.
function merge(people) {
return _.uniqWith(people, compareAndMerge)
}
function compareAndMerge(first, second) {
if (first.id === second.id) {
first.classes = second.classes = [].concat(first.classes, second.classes)
return true
}
return false
}
var people = [{
name: 'John Wilson',
id: 123,
classes: ['java', 'c++']
}, {
name: 'John Wilson',
id: 123,
classes: 'uml'
}, {
name: 'Jane Smith',
id: 321,
classes: 'c++'
}]
console.log(merge(people))
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.2/lodash.min.js"></script>
An aside: You were missing square brackets around your original classes lists. I made sure that the code above doesn't care whether or not the classes property holds a single string or an array of strings, though, just in case.
Using ES6 you can do so with a Map to hold the unique values, Array#reduce to populate it, and the spread operator with Map#values to convert it back to array:
const arr = [{"name":"John Wilson","id":123,"classes":["java","c++"]},{"name":"John Wilson","id":123,"classes":"uml"},{"name":"Jane Smith","id":321,"classes":"c++"}];
const result = [...arr.reduce((hash, { id, name, classes }) => {
const current = hash.get(id) || { id, name, classes: [] };
classes && (current.classes = current.classes.concat(classes));
return hash.set(id, current);
}, new Map).values()];
console.log(result);
Not sure using lodash... here's a way to do it with normal JS:
var combined = arr.reduce(function(a, item, idx) {
var found = false;
for (var i = 0; i < a.length; i++) {
if (a[i].id == item.id) {
a[i].classes = a[i].classes.concat(item.classes);
found = true;
break;
}
}
if (!found) {
a.push(item);
}
return a;
}, []);
Fiddle: https://jsfiddle.net/6zwr47mt/
use _.mergeWith to set merging customizer
_.reduce(data, function(result, item) {
item = _.mergeWith(
item,
_.find(result, {id: item.id}),
function(val, addVal) {
return _.isArray(val) ? _.concat(val, addVal) : val;
});
result = _.reject(result, {id: item.id})
return _.concat(result, item);
}, []);
The following algorithm is not the best one but at least I know what it does :-)
console.log(clean(data));
function clean (data) {
var i, x, y;
var clean = [];
var m = clean.length;
var n = data.length;
data.sort((x, y) => x.id - y.id);
for (i = 0; i < n; i++) {
y = data[i];
if (i == 0 || x.id != y.id) {
clean.push(x = clone(y)), m++;
} else {
clean[m - 1] = merge(x, y);
}
}
return clean;
}
function clone (x) {
var z = {};
z.id = x.id;
z.name = x.name;
z.classes = x.classes.slice();
return z;
}
function merge (x, y) {
var z = {};
z.id = x.id;
z.name = x.name;
z.classes = unique(
x.classes.concat(y.classes)
);
return z;
}
function unique (xs) {
var i, j, n;
n = xs.length;
for (i = 1; i < n; i++) {
j = 0; while (j < i && xs[i] !== xs[j]) j++;
if (j < i) swap(xs, i, n - 1), i--, n--;
}
return xs.slice(0, n);
}
function swap (xs, i, j) {
var x = xs[i];
xs[i] = xs[j];
xs[j] = x;
}
<script>
var data = [{
id: 123,
name: 'John Wilson',
classes: ['java', 'c++']
}, {
id: 123,
name: 'John Wilson',
classes: ['uml', 'java']
}, {
id: 321,
name: 'Jane Smith',
classes: ['c++']
}];
</script>

Fill in missing properties in an array of objects

What is the best way to fill in missing properties in an array of objects, such as this example:
[
{
name: 'Tom',
number: '01234 567 890',
website: 'http://www.tom.com'
},
{
name: 'Richard',
number '07777 666 555'
},
{
name: 'Harry',
website: 'http://www.harry.com'
}
]
I need to add the missing properties with a null value, so that when I pass this array on to be rendered in something such as a HTML table or CSV file, everything lines up correctly. I was thinking of passing over the array twice, once to get all the possible properties, and a second time to add those missing properties with a null value to each object where it doesn't exist. Is there a better way to do this?
EDIT: I won't know what the keys are until I have the data, it's coming from an API and the keys are not always requested explicitly.
My final solution
Thanks all, it seems the two pass approach is indeed the best approach. After I started to write this using the examples provided, I realised that the order of the properties wasn't being maintained. This is how I achieved filling in the missing props, and maintaining the correct order. Any suggestions for potential improvements are welcome.
var fillMissingProps = function(arr) {
// build a list of keys in the correct order
var keys = [];
arr.forEach(function(obj) {
var lastIndex = -1;
Object.keys(obj).forEach(function(key, i) {
if (keys.includes(key)) {
// record the position of the existing key
lastIndex = keys.lastIndexOf(key);
if (lastIndex < i) {
// this key is in the wrong position so move it
keys.splice(i, 0, keys.splice(lastIndex, 1)[0]);
lastIndex = i;
}
} else {
// add the new key in the correct position
// after the previous existing key
lastIndex++;
keys.splice(lastIndex, 0, key);
}
});
});
// build a template object with all props set to null
// and in the correct position
var defaults = {};
keys.forEach(function(key) {
defaults[key] = null;
});
// and update the array by overwriting each element with a
// new object that's built from the template and the original object
arr.forEach(function(obj, i, arr) {
arr[i] = Object.assign({}, defaults, obj);
});
return arr;
};
/** TEST **/
var currentArray = [
{
website: 'http://www.unknown.com'
},
{
name: 'Tom',
number: '01234 567 890',
website: 'http://www.tom.com'
},
{
title: 'Mr',
name: 'Richard',
gender: 'Male',
number: '04321 666 555'
},
{
id: '003ABCDEFGHIJKL',
name: 'Harry',
website: 'http://www.harry.com',
mobile: '07890 123 456',
city: 'Brentwood',
county: 'Essex'
}
];
var newArray = fillMissingProps(currentArray);
for (var i = 0; i < newArray.length; i++) {
for (var prop in newArray[i]) {
console.log(prop + ": " + newArray[i][prop]);
}
console.log('---------');
}
Given that you don't know apriori which keys are supposed to exist, you have no choice but to iterate over the array twice:
// build a map of unique keys (with null values)
var keys = {}
array.forEach(el => Object.keys(el).forEach(k => keys[k] = null));
// and update the array by overwriting each element with a
// new object that's built from the null map and the original object
array.forEach((el, ix, a) => a[ix] = Object.assign({}, keys, el));
Use Array.prototype.map():
const arr = [
{
name: 'Tom',
number: '01234 567 890',
website: 'http://www.tom.com',
},
{
name: 'Richard',
number: '07777 666 555',
},
{
name: 'Harry',
website: 'http://www.harry.com',
},
];
const newArr = arr.map(x => (
arr.map(x => Object.keys(x))
.reduce((a, b) =>
(b.forEach(z => a.includes(z) || a.push(z)), a)
)
.forEach(
y => (x[y] = x.hasOwnProperty(y) ? x[y] : null)
), x)
);
console.log(newArr);
Here is a more interesting answer, its a tad fun one but it will build up your objects on the fly as new properties appear:
var currentArray = [
{
name: 'Tom',
number: '01234 567 890',
website: 'http://www.tom.com'
},
{
name: 'Richard',
number: '07777 666 555'
},
{
name: 'Harry',
website: 'http://www.harry.com'
}
]
var newArray = []
function NewObject() {
}
for(var i = 0; i < currentArray.length; i++){
var nObj = new NewObject();
for(var prop in currentArray[i]){
if(!NewObject.hasOwnProperty(prop))
NewObject.prototype[prop] = null;
nObj[prop]=currentArray[i][prop];
}
newArray.push(nObj);
}
for(var i = 0; i < newArray.length; i++){
for(var prop in newArray[i]){
console.log(prop+ ": "+newArray[i][prop]);
}
console.log('---------');
}
It builds new objects from the ones you provide and adds new properties to the objects if they don't exist already.
This idea was more for curiosities sake tho so any comments would be interesting :)
You can get all keys and set all keys using for..of loop, .map() to iterate all Object.keys(), redefine original array
var arr = [{
name: 'Harry',
website: 'http://www.harry.com'
},{
name: 'Tom',
number: '01234 567 890',
website: 'http://www.tom.com'
}, {
name: 'Richard',
number: '07777 666 555'
}];
for (var obj of arr) {
for (var key of Object.keys(obj)) {
arr = arr.map(o => (o[key] = o[key] || null, o))
}
};
console.log(arr);
Something like this could work:
for (var i = 0; i < arrayLength; i++) {
yourArray[i].name = yourArray[i].name || null;
yourArray[i].number = yourArray[i].number || null;
yourArray[i].website= yourArray[i].website|| null;
}

Categories