Merge Array of Objects by Property using Lodash - javascript

I have two arrays of objects that represent email addresses that have a label and a value:
var original = [
{
label: 'private',
value: 'private#johndoe.com'
},
{
label: 'work',
value: 'work#johndoe.com'
}
];
var update = [
{
label: 'private',
value: 'me#johndoe.com'
},
{
label: 'school',
value: 'schhol#johndoe.com'
}
];
Now I want to compare and merge the two arrays by the label field, so that the result would look like this:
var result = [
{
label: 'private',
value: 'me#johndoe.com'
},
{
label: 'work',
value: 'work#johndoe.com'
},
{
label: 'school',
value: 'schol#johndoe.com'
}
]
How can I do this e.g. using lodash?

_.unionBy():
This method is like _.union except that it accepts iteratee which is invoked for each element of each arrays to generate the criterion by which uniqueness is computed. Result values are chosen from the first array in which the value occurs.
var original = [
{ label: 'private', value: 'private#johndoe.com' },
{ label: 'work', value: 'work#johndoe.com' }
];
var update = [
{ label: 'private', value: 'me#johndoe.com' },
{ label: 'school', value: 'schol#johndoe.com' }
];
var result = _.unionBy(update, original, "label");
console.log(result);
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.15.0/lodash.min.js"></script>

Convert the lists to objects keyed by label, merge them by _.assign, and convert it back to an array. It will even retain order of the items on most browsers.
var original = [
{
label: 'private',
value: 'private#johndoe.com'
},
{
label: 'work',
value: 'work#johndoe.com'
}
];
var update = [
{
label: 'private',
value: 'me#johndoe.com'
},
{
label: 'school',
value: 'schol#johndoe.com'
}
];
console.log(
_.map(
_.assign(
_.mapKeys(original, v => v.label),
_.mapKeys(update, v => v.label)
)
)
);
// or remove more duplicated code using spread
console.log(
_.map(
_.assign(
...[original, update].map(
coll => _.mapKeys(coll, v => v.label)
)
)
)
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.15.0/lodash.js"></script>

Perhaps a bit late, but all the solutions I have seen don't join both arrays correctly, they use one of the arrays to loop on and any excess elements in the second array don't get added (assuming this is what is required).
The right way is to sort both arrays and move forward within both arrays, merging the matches elements and adding the missing elements from both arrays.
Please find full solution below. This also takes O(n+m) which is the best you can get (without the computational costs for sort itself). In my code I already got the data sorted from the database.
function mergeObjectsBasedOnKey(array1, array2, compareFn, mergeFn, alreadySorted) {
var array1Index = 0;
var array2Index = 0;
const merged = [];
if (!alreadySorted) {
array1.sort(compareFn);
array2.sort(compareFn);
}
while (array1Index < array1.length && array2Index < array2.length) {
var comparedValue = compareFn(array1[array1Index], array2[array2Index]);
if (comparedValue === 0) {
merged.push(mergeFn(array1[array1Index], array2[array2Index]));
array1Index++;
array2Index++;
} else if (comparedValue < 0) {
merged.push(mergeFn(array1[array1Index]));
array1Index++;
} else {
merged.push(mergeFn(array2[array2Index]));
array2Index++;
}
}
while (array1Index < array1.length) {
merged.push(mergeFn(array1[array1Index]));
array1Index++;
}
while (array2Index < array2.length) {
merged.push(mergeFn(array2[array2Index]));
array2Index++;
}
return merged;
}
const array1 = [{
"id": 10,
isArray1: true
},
{
"id": 11,
isArray1: true
},
{
"id": 12,
isArray1: true
},
];
const array2 = [{
"id": 8,
isArray2: true
},
{
"id": 11,
isArray2: true
},
{
"id": 15,
isArray2: true
},
];
const result = mergeObjectsBasedOnKey(array1, array2, function(a, b) {
return a.id - b.id;
}, function(a, b) {
if (b) {
return _.merge(a, b);
}
return _.merge(a, {
isArray1: true,
isArray2: true
});
});
console.log(result);
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.4/lodash.min.js"></script>
And the results would be:
[ { id: 8, isArray2: true, isArray1: true },
{ id: 10, isArray1: true, isArray2: true },
{ id: 11, isArray1: true, isArray2: true },
{ id: 12, isArray1: true, isArray2: true },
{ id: 15, isArray2: true, isArray1: true } ]

In case you are using lodash 3.x where _.unionBy() was not there, you can combine _.union() and _.uniq() to get the same result.
var original = [
{ label: 'private', value: 'private#johndoe.com' },
{ label: 'work', value: 'work#johndoe.com' }
];
var update = [
{ label: 'private', value: 'me#johndoe.com' },
{ label: 'school', value: 'schol#johndoe.com' }
];
var result = _.uniq(_.union(update, original), "label");
console.log(result);

I know it is not what asked for but just in case someone stumbled up on this page here is how you do this in ramda:
var original = [
{ label: 'private', value: 'private#johndoe.com' },
{ label: 'work', value: 'work#johndoe.com' }
];
var updated = [
{ label: 'private', value: 'me#johndoe.com' },
{ label: 'school', value: 'schol#johndoe.com' }
];
unionWith(eqBy(prop('label')), updated, original);

Here is another way to merge two objects using Lodash:
let a = [{
content: 'aaa',
name: 'bbb2'
},
{
content: 'aad',
name: 'ccd'
}
];
let b = [{
content: 'aaa',
name: 'bbb'
},
{
content: 'aad1',
name: 'ccd1'
}
];
let c = [...a, ...b];
let d = _.uniq(c, function(data) {
return data.content;
})
console.log(d);
<script src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.9.1/underscore-min.js"></script>

Perhaps a bit late, but all the solutions I have seen don't join both arrays correctly, they use one of the arrays to loop on and any excess elements in the second array don't get added (assuming this is what is required).
I had the same observation so put something together myself. This is working for my use case, which is to merge each object if the value of the 'label' field matches:
const dataSetHashes = dataSets.map(dataSet => _.keyBy(dataSet, 'label'))
const resultHash = _.merge(
{},
...dataSetLookups
)
const result = Object.values(resultLookup)

Related

Create new array from existing array with unique joined values from existing array

Does anyone know how I can create a new array from existing array with unique joined values from existing array?
const originalArray = [
[
{ value: 'red', id: 99 },
{ value: 'blue', id: 100 },
],
[
{ value: 'small', id: 101 },
{ value: 'medium', id: 102 },
],
[
{ value: 'modern', id: 103 },
{ value: 'classic', id: 104 },
],
];
//
//
const newArrayBasedOnOriginalArray = [
{ value: 'red/small/modern' },
{ value: 'red/small/classic' },
{ value: 'red/medium/modern' },
{ value: 'red/medium/classic' },
{ value: 'blue/small/modern' },
{ value: 'blue/small/classic' },
{ value: 'blue/medium/modern' },
{ value: 'blue/medium/classic' },
];
I calculated that the length of the new array should always be as following:
// length of new array
const lengthOfNewArray = originalArray
.map((value) => {
return value.length;
})
.reduce((current, old) => {
return current * old;
});
//
//
console.log('length of new array:', lengthOfNewArray); // 8
You can do it recursively
const originalArray = [
[
{ value: 'red', id: 99 },
{ value: 'blue', id: 100 },
],
[
{ value: 'small', id: 101 },
{ value: 'medium', id: 102 },
],
[
{ value: 'modern', id: 103 },
{ value: 'classic', id: 104 },
],
];
const getPossibleCombination = (currentValue, arraysRemaining) => {
if(arraysRemaining.length === 0) return currentValue
const values = []
const firstArray = arraysRemaining[0]
firstArray.forEach(({value}) => {
values.push(getPossibleCombination(`${currentValue}/${value}`, arraysRemaining.slice(1, arraysRemaining.length)))
})
return values.flat()
}
const values = getPossibleCombination('', originalArray)
console.log(values)
In this case, you do not necessarily need recursion. Array.reduce() greatly does the job:
const originalArray = [
[
{ value: 'red', id: 99 },
{ value: 'blue', id: 100 },
],
[
{ value: 'small', id: 101 },
{ value: 'medium', id: 102 },
],
[
{ value: 'modern', id: 103 },
{ value: 'classic', id: 104 },
],
];
const newArray = originalArray
.map(elem => elem.map(({value}) => value))
.reduce((acc, cur) => acc.flatMap(seq => cur.map(part => `${seq}/${part}`)))
.map(elem => ({value: elem}))
console.log(newArray)
Aside from the initial and final map(), used to simplify the input objects, what I am doing is continuously combining the accumulator with the next sub-array.
For each object in the sub-array I duplicate every object in the accumulator, using the nested map(). flatMap() is used to keep the accumulator flat, with a simple map() the accumulator depth would increase every time we visit a new sub-array.
First of all if values in each of your arrays is unique then the concatenated values will be unique as well. After you make sure values are unique you can use this code to create combinations of strings:
const newArrayBasedOnOriginalArray = originalArray.reduce(
(acc, el) =>
el.flatMap(({ value }) =>
acc.length ? acc.map((str) => str + "/" + value) : value
),
[]
).map(value=>({value});

Compare two array of objects and filter element not present in second array

Suppose I have two array of object as:
const array1 = [
{ name: 'detail1', title: 'detail1' },
{ name: 'detail2 ', title: 'detail2 ' },
{ name: 'detail3', title: 'detail3' },
{ name: 'detail4', title: 'detail4' },
{ name: 'detail5', title: 'detail5' },
{ name: 'detail6', title: 'detail6' },
{ name: 'detail7', title: 'detail7' }
]
const array2 = [
{ name: 'detail1', title: 'detail1' },
{ name: 'detail2 ', title: 'detail2 ' },
{ name: 'detail3', title: 'detail3' },
{ name: 'detail4', title: 'detail4' },
]
I want to compare two arrays i.e. array1 and array2 and get the missing element of aaray2.
For this I tried as:
var absent = array2.filter(e=>!array1.includes(e));
But I am unable to get missing element of array2.
My expected O/P :
[ { name: 'detail5', title: 'detail5' },
{ name: 'detail6', title: 'detail6' },
{ name: 'detail7', title: 'detail7' }]
These are all the elements which are not present in array2.
What exactly am I doing wrong here?
Please let me know if anyone needs any further information.
You could build a normalised object with key and values and filter the objects.
const
array1 = [{ name: 'detail1', title: 'detail1' }, { name: 'detail2 ', title: 'detail2 ' }, { name: 'detail3', title: 'detail3' }, { name: 'detail4', title: 'detail4' }, { name: 'detail5', title: 'detail6' }, { name: 'detail7', title: 'detail7' }, { name: 'detail8', title: 'detail8' }],
array2 = [{ name: 'detail1', title: 'detail1' }, { name: 'detail2 ', title: 'detail2 ' }, { name: 'detail3', title: 'detail3' }, { name: 'detail4', title: 'detail4' }],
sortEntriesByKey = ([a], [b]) => a.localeCompare(b),
filter = array2.reduce((r, o) => {
Object
.entries(o)
.sort(sortEntriesByKey)
.reduce((o, [k, v]) => (o[k] ??= {})[v] ??= {}, r);
return r;
}, {});
absent = array1.filter((o) => {
let f = filter;
return !Object
.entries(o)
.sort(sortEntriesByKey)
.every(([k, v]) => f = f[k]?.[v]);
});
console.log(absent);
.as-console-wrapper { max-height: 100% !important; top: 0; }
Edit: you want objects in A not in B.
It is ideal to loop through A, find if the element exists in B. If yes, then do not include it.
In javacript object references are compared when you do "==" or "===" or other array search methods.
{} == {} will return false. You can check in your dev console.
In your case, you will have to check specific properties.
var absent = array1.filter(e=>{
let findInd = array2.findIndex((a)=>{
return (a.name == e.name && a.title == e.title);});
return (findInd == -1); });
In the inner findIndex, I am finding index based on a condition. In filter method, I am returning true only if that index is -1(not found).
This worked for me:
let cart = [{
"id": "6304a51af5726921c0dadd64",
"qty": 1
},
{
"id": "8704a51af5726921c0dadd64",
"qty": 1
},
{
"id": "4704a51af5726921c0dadd64",
"qty": 1
}
]
let cartList = [{
"id": "6304a51af5726921c0dadd64",
"qty": 1
},
{
"id": "9704a51af5726921c0dadd64",
"qty": 1
}
]
let test = cart.some((element) =>
cartList.some((e) => element.id === e.id)
);
console.log(" if any single object matched:", test);
let test1 = cart.filter((element) =>
cartList.some((e) => element.id === e.id)
);
console.log("display matching objects :", test1);

Javascript Alphabetised Object Key Sorting

I'm wondering, I have the following data structure:
data = [
{
name: 'Alpha',
},
{
name: 'Alfa',
},
{
name: 'Bravo',
},
{
name: 'Brafo',
},
{
name: 'Charlie',
},
{
name: 'Charly',
},
...
{
name: 'Zulu',
},
{
name: 'Zulo',
},
]
I'm expecting there to be at least one, usually more, key for each letter of the alphabet. However, if there isn't a single data.name I would still like in the below data structure to have an empty domains array [].
I was wondering, how could this be manipulated into the following data structure:
data = {
a: {
domains: [
{
name: 'Alpha',
},
{
name: 'Alfa',
},
],
},
b: {
domains: [
...
]
},
...
z: {
domains: [
...
]
},
};
I have used a few methods, which involved a pre-constructed "alphbetised" key = object array, then filtered each on the first letter of the data.name value...but I was wondering if there was a standard and performant method to acheive this?
Using reduce()
const data = [{name:"Alpha"},{name:"Alfa"},{name:"Bravo"},{name:"Brafo"},{name:"Charlie"},{name:"Charly"},{name:"Zulu"},{name:"Zulo"}]
const res = data.reduce((a, v) => {
// prepare key
let key = v.name.substring(0,1).toLowerCase()
// check key in accumulator
if (!a[key]) {
// assign domain object
a[key] = {domains: []}
}
// push domain array
a[key].domains.push(v)
return a
}, {})
console.log(res)
Here is what you want:
data = [
{
name: 'Alpha',
},
{
name: 'Alfa',
},
{
name: 'Bravo',
},
{
name: 'Brafo',
},
{
name: 'Charlie',
},
{
name: 'Charly',
},
{
name: 'Zulu',
},
{
name: 'Zulo',
},
];
console.log(data.reduce((a, c) => {
const firstLetter = c.name[0].toLowerCase();
if (a[firstLetter]) {
a[firstLetter].domains.push(c);
} else {
a[firstLetter] = { domains: [c] };
}
return a;
}, {}));

Javascript lodash Filter Objects in Array based on value

I have the following array of objects:
var array = [
{
name: isSale,
value: true
},
{
name: isSale,
value: false
},
{
name: isNew,
value: true
}
]
I need to filter the array so that I have only 2 objects at the end:
var array = [
{
name: isSale,
value: true
},
{
name: isNew,
value: true
}
]
Meaning if I have both true and false values for the same name (isSale) I need to leave the object with the true value.
But if my array looks like this:
var array = [
{
name: isSale,
value: false
},
{
name: isNew,
value: true
}
]
meaning there is no duplicate isSale object it should stay like this and the object with the false value should not be removed from the array.
I prefer a solution with ES5 (you can write it in ES6/7 and transpile it with babel to ES5) and you can use lodash as well.
Thank you for the suggestions and cheers!
You could seach for same name in the result set and replace if the former value is false.
const
filter = array => array.reduce((r, o) => {
var index = r.findIndex(({ name }) => name === o.name)
if (index === -1) r.push(o);
else if (!r[index].value) r[index] = o;
return r;
}, []),
array1 = [{ name: 'isSale', value: true }, { name: 'isSale', value: false }, { name: 'isNew', value: true }],
array2 = [{ name: 'isSale', value: false }, { name: 'isNew', value: true }];
console.log(filter(array1));
console.log(filter(array2));
.as-console-wrapper { max-height: 100% !important; top: 0; }

Filter array of objects from nested array and nested array of objects

I have the following array of object
const skus = [
{
id: 1,
features: ["Slim"],
fields: [
{ label: "Material", value: "Material1" },
{ label: "Type", value: "Type1" }
]
},
{
id: 2,
features: ["Cotton"],
fields: [
{ label: "Material", value: "Material2" },
{ label: "Type", value: "Type2" }
]
},
{
id: 3,
features: ["Slim"],
fields: [
{ label: "Material", value: "Material3" },
{ label: "Type", value: "Type1" }
]
}
]
And i want the expected output to be
const output = [
{ label: "features", value: ["Slim", "Cotton"] },
{ label: "Material", value: ["Material1", "Material2", "Material3"] },
{ label: "Type", value: ["Type1", "Type2"] }
]
I tried the following way
const output = [];
let featureArr = [];
let fieldsArr = []
skus.forEach(e => {
e.features.forEach(f => {
featureArr.push(f);
});
e.fields.forEach(f => {
fieldsArr.push({ label: f.label, value: f.value });
});
});
featureArr = _.uniq(featureArr);
fieldsArr = _.uniqBy(fieldsArr, 'value')
fieldsArr = _.groupBy(fieldsArr, 'label');
output.push({ label: 'Features', value: featureArr })
for (const k in fieldsArr) {
let valArr = []
valArr = fieldsArr[k].map(v => v.value)
output.push({ label: k, value: valArr });
}
I'm getting the expected output, but here multiple loops are present. Is there a way on how can i write the solution in more optimized way.
You could take a grouping function for nested properties, where a map, an array for iterating, group and value keys are handed over. The result is a map with all collected values for each group.
Later get all unique values from the map and build a new array of objects.
const
skus = [{ id: 1, features: ["Slim"], fields: [{ label: "Material", value: "Material1" }, { label: "Type", value: "Type1" }] }, { id: 2, features: ["Cotton"], fields: [{ label: "Material", value: "Material2" }, { label: "Type", value: "Type2" }] }, { id: 3, features: ["Slim"], fields: [{ label: "Material", value: "Material3" }, { label: "Type", value: "Type1" }] }],
getGrouped = (map, array, key, value) => array.reduce((m, o) =>
m.set(o[key], [...(m.get(o[key]) || []), o[value]]), map),
result = Array.from(
skus.reduce((m, o) =>
getGrouped(
m.set('features', [...(m.get('features') || []), ...o.features]),
o.fields,
'label',
'value'
),
new Map
),
([label, value]) => ({ label, value: [...new Set(value)] })
);
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }
First Build an object with values as Sets. Then convert the object of sets into array of array.
const skus = [
{
id: 1,
features: ["Slim"],
fields: [
{ label: "Material", value: "Material1" },
{ label: "Type", value: "Type1" }
]
},
{
id: 2,
features: ["Cotton"],
fields: [
{ label: "Material", value: "Material2" },
{ label: "Type", value: "Type2" }
]
},
{
id: 3,
features: ["Slim"],
fields: [
{ label: "Material", value: "Material3" },
{ label: "Type", value: "Type1" }
]
}
];
const update = data => {
const res = {};
data.forEach(item => {
const features = res["features"] || new Set();
item.features.forEach(fea => features.add(fea));
res["features"] = features;
item.fields.forEach(field => {
const labels = res[field.label] || new Set();
labels.add(field.value);
res[field.label] = labels;
});
});
return Object.keys(res).map(key => ({ label: key, value: [...res[key]] }));
};
console.log(update(skus));
If you can use them, Sets will be your friend here:
//data
const skus = [{id: 1,features: ["Slim"],fields: [{ label: "Material", value: "Material1" },{ label: "Type", value: "Type1" }]},{id: 2,features: ["Cotton"],fields: [{ label: "Material", value: "Material2" },{ label: "Type", value: "Type2" }]},{id: 3,features: ["Slim"],fields: [{ label: "Material", value: "Material3" },{ label: "Type", value: "Type1" }]}];
//solution
const output = Object.entries(skus.reduce((map,sku) => {
sku.features.forEach(feat => map.features.add(feat));
sku.fields.forEach(field => (map[field.label] = (map[field.label] || new Set()).add(field.value)));
return map;
}, {features: new Set()})).map(([label, set]) => ({label, value: Array.from(set)}));
//display
console.log(output);
Each feature array and field array only get iterated exactly once using this approach.
If you can't use Sets, you can emulate their behavior using js objects. The goal is to use some structure that doesn't need to be iterated again to find unique values.
The following function will do the job
const fn = (array) => {
return array.reduce((result, element) => {
const features = result[0].value
const feature = element.features[0]
if (!features.includes(feature)) {
features.push(feature)
}
const materials = result[1].value
const material = element.fields[0].value
if (!materials.includes(material)) {
materials.push(material)
}
const types = result[2].value
const type = element.fields[1].value
if (!types.includes(type)) {
types.push(type)
}
return result
}, [
{ label: 'features', value: [] },
{ label: 'Material', value: [] },
{ label: 'Type', value: [] }
])
}
BUT, your object structure is quite messy, you should likely build accessor functions that extract information from your initial elements, and use some helper functions to populate your result object.
Anyway, read more about the 'reduce' function used here ;)
https://developer.mozilla.org/fr/docs/Web/JavaScript/Reference/Objets_globaux/Array/reduce

Categories