Merge and Dedupe Array of Complex Objects with Arrays - javascript

I have a pretty complex problem that I can't seem to figure out. I have two array of objects that I would like to merge scores for. It should merge/append certain properties based on the scores. For example between the two arrays there are 4 total gameId's with 3 of them being unique. When merging it should combine the _scores section if it's the same gameId so in this case it would be both EarthNormal merging. But the problem is sometimes the score in _scores can have duplicate scores so the BAR and BASH almost look the exact same but are different it can be appended but FOO score is the exact same on both so I don't want it merged into the scores (if that makes sense).
const arr1 = [{
"gameId": "AirNormal",
"_scores":
[{
"score": 144701,
"playerName": "FOO",
"fullCombo": true,
"timestamp": 1599968866
}]
}, {
"gameId": "EarthNormal",
"_scores":
[{
"score": 177352,
"playerName": "BAR",
"fullCombo": true,
"timestamp": 1599969253
}, {
"score": 164665,
"playerName": "FOO",
"fullCombo": false,
"timestamp": 1599970971
}]
}];
const arr2 = [{
"gameId": "EarthNormal",
"_scores":
[{
"score": 177352,
"playerName": "BASH",
"fullCombo": false,
"timestamp": 1512969017
}, {
"score": 164665,
"playerName": "FOO",
"fullCombo": false,
"timestamp": 1599970971
}]
}, {
"gameId": "FireNormal",
"_scores":
[{
"_score": 124701,
"_playerName": "FOO",
"_fullCombo": true,
"_timestamp": 1591954866
}]
}];
I would want the final merged array to look like:
mergedArray = [{
"gameId": "AirNormal",
"_scores":
[{
"score": 144701,
"playerName": "FOO",
"fullCombo": true,
"timestamp": 1599968866
}]
}, {
"gameId": "EarthNormal",
"_scores":
[{
"score": 177352,
"playerName": "BAR",
"fullCombo": true,
"timestamp": 1599969253
}, {
"score": 177352,
"playerName": "BASH",
"fullCombo": false,
"timestamp": 1512969017
}, {
"score": 164665,
"playerName": "FOO",
"fullCombo": false,
"timestamp": 1599970971
}]
}, {
"gameId": "FireNormal",
"_scores":
[{
"score": 124701,
"playerName": "FOO",
"fullCombo": true,
"timestamp": 1591954866
}]
}]
I have tried doing this and using lodash:
let merged = [...arr1, ...arr2];
merged = _.uniqBy[merged, 'gameId']
let scoresMerge = _.uniqBy[merged, '_scores']
console.log(scoresMerge);
but it didn't work as I expected. Am I approaching this incorrectly?

This is fairly straight forward using vanilla javascript.
merge the arrays using destructuring
reduce() the merged arrays into an object indexed by gameId
check all properties of each _score object against the accumulated _scores array using .some() and push if no match is found.
return the values of the reduced object using Object.values()
const arr1 = [{ "gameId": "AirNormal", "_scores": [{ "score": 144701, "playerName": "FOO", "fullCombo": true, "timestamp": 1599968866 }]}, { "gameId": "EarthNormal", "_scores": [{ "score": 177352, "playerName": "BAR", "fullCombo": true, "timestamp": 1599969253 }, { "score": 164665, "playerName": "FOO", "fullCombo": false, "timestamp": 1599970971 }]}];
const arr2 = [{"gameId": "EarthNormal","_scores":[{"score": 177352,"playerName": "BASH","fullCombo": false,"timestamp": 1512969017}, {"score": 164665,"playerName": "FOO","fullCombo": false,"timestamp": 1599970971}]}, {"gameId": "FireNormal","_scores":[{"_score": 124701,"_playerName": "FOO","_fullCombo": true,"_timestamp": 1591954866}]}];
const merged = Object.values([...arr1, ...arr2].reduce((a, {gameId, _scores}) => {
// retrieve gameId object otherwise initialize it.
a[gameId] = {...a[gameId] ?? {gameId, _scores: []}};
// iterate over all _score objects
_scores.forEach(s => {
// if accumulator _scores array doesn't have an object matching all properties, push _score
if (!a[gameId]['_scores'].some(o => {
return !Object.entries(s).some(([k, v]) => o[k] !== v)})
) {
a[gameId]['_scores'].push({...s});
}
});
return a;
}, {}));
console.log(merged);

You need to identify objects with the same gameId, and then concat and dedupe their _.scores array.
It's easy to concat/dedup non primitive array items using Array.reduce() and a Map. For every item you check if the requested key is already in the Map. If it's not, you assign the current item to the Map's key. If it is you replace / merge the current item with the item in the Map.
After you finish iterating the Map, use Array.from() to convert the Map's .values() iterator to an array.
const arr1 = [{"gameId":"AirNormal","_scores":[{"score":144701,"playerName":"FOO","fullCombo":true,"timestamp":1599968866}]},{"gameId":"EarthNormal","_scores":[{"score":177352,"playerName":"BAR","fullCombo":true,"timestamp":1599969253},{"score":164665,"playerName":"FOO","fullCombo":false,"timestamp":1599970971}]}];
const arr2 = [{"gameId":"EarthNormal","_scores":[{"score":177352,"playerName":"BASH","fullCombo":false,"timestamp":1512969017},{"score":164665,"playerName":"FOO","fullCombo":false,"timestamp":1599970971}]},{"gameId":"FireNormal","_scores":[{"score":124701,"playerName":"FOO","fullCombo":true,"timestamp":1591954866}]}];
const dedupLastBy = (a1 = [], a2 = [], key) => Array.from(
[...a1, ...a2].reduce((acc, obj) => {
const keyName = obj[key];
if(acc.has(keyName)) acc.delete(keyName);
return acc.set(keyName, obj);
}, new Map()).values()
)
const handleDups = ({ _scores: a, ...o1 }, { _scores: b, ...o2 }) => ({
...o1,
...o2,
_scores: dedupLastBy(a, b, 'playerName')
});
const result = Array.from([...arr1, ...arr2]
.reduce((acc, o) => {
const { gameId } = o;
if(acc.has(gameId)) acc.set(gameId, handleDups(acc.get(gameId), o));
else acc.set(gameId, o);
return acc;
}, new Map()).values());
console.log(result);
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.20/lodash.min.js" integrity="sha512-90vH1Z83AJY9DmlWa8WkjkV79yfS2n2Oxhsi2dZbIv0nC4E6m5AbH8Nh156kkM7JePmqD6tcZsfad1ueoaovww==" crossorigin="anonymous"></script>

Related

Nested array issue in JavaScript

I have the following array
Array["MyArray",
{
"isLoaded":true,
"items":
[{
"id":"4",
"name":"ProductA",
"manufacturer":"BrandA",
"quantity":1,
"price":"25"
},{
"id":"1",
"name":"ProductB",
"manufacturer":"BrandB",
"quantity":5,
"price":"20"
}],
"coupons":null
}
]
I need to load product names and their quantity from the array.
const result = [key, value].map((item) => `${item.name} x ${item.quantity}`);
Here's one possible way to achieve the desired result:
const getProductsAndQuantity = ([k , v] = arr) => (
v.items.map(it => `${it.name} x ${it.quantity}`)
);
How to use it within the context of the question?
localforage.iterate(function(value, key, iterationNumber) {
console.log([key, value]);
const val2 = JSON.parse(value);
if (val2 && val2.items && val2.items.length > 0) {
console.log(val2.items.map(it => `${it.name} x ${it.quantity}`).join(', '))
};
});
How it works?
Among the parameters listed in the question ie, value, key, iterationNumber, only value is required.
The above method accepts the key-value pair as an array (of 2 elements) closely matching the console.log([key, value]); in the question
It uses only v (which is an object). On v, it accesses the prop named items and this items is an Array.
Next, .map is used to iterate through the Array and return each product's name and quantity in the desired/expected format.
Test it out on code-snippet:
const arr = [
"MyArray",
{
"isLoaded": true,
"items": [{
"id": "4",
"name": "ProductA",
"manufacturer": "BrandA",
"quantity": 1,
"price": "25"
}, {
"id": "1",
"name": "ProductB",
"manufacturer": "BrandB",
"quantity": 5,
"price": "20"
}],
"coupons": null
}
];
const getProductsAndQuantity = ([k, v] = arr) => (
v.items.map(
it => `${it.name} x ${it.quantity}`
)
);
console.log(getProductsAndQuantity());
I understood. You should learn about array methods such as map, filter, reduce. Here you go...
const items = [{
"id":"4",
"name":"ProductA",
"manufacturer":"BrandA",
"quantity":1,
"price":"25"
},{
"id":"1",
"name":"ProductB",
"manufacturer":"BrandB",
"quantity":5,
"price":"20"
}];
const result = items.map((item) => `${item.name} x ${item.quantity}`);
console.log(result);
I think I understand the question to say that the input is an array of objects, each containing an array of items. The key is that a nested array requires a nested loop. So, we iterate the objects and their internal items (see the lines commented //outer loop and // inner loop below)
Also, half-guessing from the context, it looks like the that the OP aims to assemble a sort of invoice for each object. First a demo of that, (and see below for the version simplified to exactly what the OP asks)...
const addInvoice = obj => {
let total = 0;
// inner loop
obj.invoice = obj.items.map(i => {
let subtotal = i.quantity * i.price;
total += subtotal
return `name: ${i.name}, qty: ${i.quantity}, unit price: ${i.price}, subtotal: ${subtotal}`
});
obj.invoice.push(`invoice total: ${total}`);
}
const objects = [{
"isLoaded": true,
"items": [{
"id": "4",
"name": "ProductA",
"manufacturer": "BrandA",
"quantity": 1,
"price": "25"
}, {
"id": "1",
"name": "ProductB",
"manufacturer": "BrandB",
"quantity": 5,
"price": "20"
}],
"coupons": null
}]
// outer loop
objects.forEach(addInvoice);
console.log(objects);
If my guess about the goal went to far, just remove the unit price, subtotal and total lines from the invoice function...
const objects = [{
"isLoaded": true,
"items": [{
"id": "4",
"name": "ProductA",
"manufacturer": "BrandA",
"quantity": 1,
"price": "25"
}, {
"id": "1",
"name": "ProductB",
"manufacturer": "BrandB",
"quantity": 5,
"price": "20"
}],
"coupons": null
}]
const summaryString = obj => {
return obj.items.map(i => `${i.name}, ${i.quantity}`);
}
const strings = objects.map(summaryString);
console.log(strings);

How to get only specific elements from array by comparing them in javascript?

I have two arrays.Second arrayB has some elements identical to arrayA. Now after comparing the arrays arrayA should have only those values those are not in arrayB. Please tell me best way to do it.
let arrayA = [{ "displayName": "John" }, { "displayName": "Sandra" },{ "displayName": "Peter" }]
let arrayB = [{ "name": "Bobby" }, { "name": "John" }, { "name": "Sandra" }]
arrayA.forEach(function(cust, index) {
arrayB.forEach(function(comp) {
if (comp.name == cust.displayName) {
delete arrayA[index]
}
})
})
console.log("Final"+JSON.stringify(arrayA))
Output -> Final[null,null,{"displayName":"Peter"}]
Filter and only keep the ones that do not exist in the other array
let arrayA = [{ "displayName": "John" }, { "displayName": "Sandra" },{ "displayName": "Peter" }]
let arrayB = [{ "name": "Bobby" }, { "name": "John" }, { "name": "Sandra" }]
arrayA = arrayA.filter(a =>
!arrayB.find(b => a.displayName === b.name)
);
console.log("Final"+JSON.stringify(arrayA))
To remove the elements, that aren't contained by arrayB you can use map() and filter():
arrayB = arrayB.map((key) => key.name)
arrayA = arrayA.filter((key) => arrayB.includes(key.displayName))
In the first line you'll get an array with only names
In the second line you'll remove from arrayA elements, that aren't in arrayB
You can also add map():
arrayB = arrayB.map((key) => key.name)
arrayA = arrayA.filter((key) => arrayB.includes(key.displayName))
.map((key) => key.displayName)
So you'll have in second array only names, but not objects.
let arrayA = [{ "displayName": "John" }, { "displayName": "Sandra" },{ "displayName": "Peter" }]
let arrayB = [{ "name": "Bobby" }, { "name": "John" }, { "name": "Sandra" }]
const shouldFilterOutByName = {};
for (let item of arrayB) {
shouldFilterOutByName[item.name] = true;
}
const filteredAry = arrayA.filter(item => !shouldFilterOutByName[item.displayName]);
console.log(filteredAry);
Taplar's solution is probably the one you want. The reason I haven't deleted my answer is in case time complexity is important. Taplar's solution has time complexity of O(n^2) and my solution is O(n).

Javascript -sort array based on another javascript object properties

I have one javascript array and one object . Need help to sort javascript object keys based on the order number in another array
In subgroup array , I have name , order number. Need to sort Offerings keys based on that order number
const subgroup = [
{
"code": "6748",
"name": "test123",
"orderNumber": "0"
},
{
"code": "1234",
"name": "customdata",
"orderNumber": "1"
}
]
const offerings = {
"customdata" : [
{
"code": "Audi",
"color": "black"
}
],
"test123" : [
{
"brand": "Audi",
"color": "black"
}
]
}
I believe this should work for you. I've added some comments in the code that should hopefully do an okay job of explaining what is happening.
var subgroup = [{
"code": "6748",
"name": "test123",
"orderNumber": "0"
}, {
"code": "1234",
"name": "customdata",
"orderNumber": "1"
}];
var offerings = {
"customdata": [{
"code": "Audi",
"color": "black"
}],
"test123": [{
"brand": "Audi",
"color": "black"
}]
}
function sortObjectFromArray(refArray, sortObject, orderKey = 'order', linkKey = 'key') {
// Get copy of refArray
let reference = refArray.slice();
// Sort sortObject [ into an array at this point ]
let sorted = [];
for (let key in sortObject) {
// Searches the refArray for the linkKey, and returns the intended index
let index = reference.find((item) => item[linkKey] === key)[orderKey];
// Places the sortObject's value in the correct index of the 'sorted' Array
sorted[parseInt(index)] = [key, sortObject[key]];
};
// Return an object, created from previous 'sorted' Array
return sorted.reduce((obj, [key, value]) => {
obj[key] = value;
return obj;
}, {});
};
offerings = sortObjectFromArray(subgroup, offerings, 'orderNumber', 'name');
console.log(offerings);

Unable to convert a nested object into an array of objects and viceversa

I am having an object of the following kind:
var sourceObj = {
"feature1": [
{"id":"1","name":"abc","enabled":false,"type":"type1"},
{"id":"2","name":"xyz","enabled":false,"type":"type1"}
]
,
"feature2": [
{"id":"3","name":"lmn","enabled":true,"type":"type2"},
{"id":"4","name":"pqr","enabled":false,"type":"type2"}
]
}
Need to get converted to an array of objects of the following type:
var destArr = [
{ "feature_name":"feature1",
"feature_details":[
{"id":"1","name":"abc","enabled":false,"type":"type1"},
{"id":"2","name":"xyz","enabled":true,"type":"type1"}
]
},
{ "feature_name":"feature2",
"feature_details":[
{"id":"3","name":"lmn","enabled":true,"type":"type2"}
{"id":"4","name":"pqr","enabled":false,"type":"type2"}
]
}
]
I have tried the following approaches for conversion of source object to resultant array of objects and resultant array of objects back to the source object
//Converting source object to array of objects
let arr = Object.keys(sourceObj).map(key => {
return sourceObj[key];
})
converting array of objects back to source objetc
let obj = Object.assign({}, ...destArr.map(item => ({ [item.name]: item.value })));
You could use Object.entries to map everything in a single shot.
To go back to the original structure, you could use reduce against the generated array (see backToOriginalArray below)
var sourceObj = {
"feature1": [{
"id": "1",
"name": "abc",
"enabled": false,
"type": "type1"
},
{
"id": "2",
"name": "xyz",
"enabled": false,
"type": "type1"
}
],
"feature2": [{
"id": "3",
"name": "lmn",
"enabled": true,
"type": "type2"
},
{
"id": "4",
"name": "pqr",
"enabled": false,
"type": "type2"
}
]
};
// Step 1: use object.entries against the original object to build an array of objects.
var destArray = Object.entries(sourceObj).map(([key, value]) => ({
"feature_name": key,
"feature_details": value
}));
console.log(destArray);
// Step 2: use reduce against the generated array to get an object with the same structure of the original one.
var backToOriginalArray = destArray.reduce((acc, {feature_name, feature_details}) => (acc[feature_name] = feature_details, acc), {});
console.log(backToOriginalArray);
Map the Object.entries of your initial object:
var sourceObj = {
"feature1": [
{"id":"1","name":"abc","enabled":false,"type":"type1"},
{"id":"2","name":"xyz","enabled":false,"type":"type1"}
],
"feature2": [
{"id":"3","name":"lmn","enabled":true,"type":"type2"},
{"id":"4","name":"pqr","enabled":false,"type":"type2"}
]
};
const destArr = Object.entries(sourceObj).map(
([feature_name, feature_details]) => ({ feature_name, feature_details })
);
console.log(destArr);
To go the other way around, use Object.fromEntries:
const arr=[{feature_name:"feature1",feature_details:[{id:"1",name:"abc",enabled:!1,type:"type1"},{id:"2",name:"xyz",enabled:!1,type:"type1"}]},{feature_name:"feature2",feature_details:[{id:"3",name:"lmn",enabled:!0,type:"type2"},{id:"4",name:"pqr",enabled:!1,type:"type2"}]}];
const obj = Object.fromEntries(arr.map(
({ feature_name, feature_details }) => [feature_name, feature_details]
));
console.log(obj);

Lodash to find if object property exists in array

I have an array of objects like this:
[ {"name": "apple", "id": "apple_0"},
{"name": "dog", "id": "dog_1"},
{"name": "cat", "id": "cat_2"}
]
I want to insert another element, also named apple, however, because I don't want duplicates in there, how can I use lodash to see if there already is an object in the array with that same name?
You can use Lodash _.find() like this.
var data = [ {"name": "apple", "id": "apple_0"},
{"name": "dog", "id": "dog_1"},
{"name": "cat", "id": "cat_2"}
]
if(!_.find(data, {name: 'apple'})) {
data.push({name: 'apple2'});
}
console.log(data)
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.15.0/lodash.min.js"></script>
Reference documentation: https://lodash.com/docs/4.17.14#find
This is Form
_.has(object, path)
Example:
const countries = { country: { name: 'Venezuela' } }
const isExist = _.has(countries, 'country.name')
// isExist = true
For more information Document Lodash
You can use Array.prototype.find() or lodash's _.find():
const addItem = (arr, item) => {
if(!arr.find((x) => x.name === item.name)) { // you can also change `name` to `id`
arr.push(item);
}
};
const arr = [
{"name": "apple", "id": "apple_0"},
{"name": "dog", "id": "dog_1"},
{"name": "cat", "id": "cat_2"}
];
addItem(arr, { "name": "apple", "id": "apple_0" });
addItem(arr, { "name": "pear", "id": "pear_3" });
console.log(arr);
And a bit shorter but less readable version:
const addItem = (arr, item) => arr.find((x) => x.name === item.name) || arr.push(item); // you can also change `name` to `id`
const arr = [
{"name": "apple", "id": "apple_0"},
{"name": "dog", "id": "dog_1"},
{"name": "cat", "id": "cat_2"}
];
addItem(arr, { "name": "apple", "id": "apple_0" });
addItem(arr, { "name": "pear", "id": "pear_3" });
console.log(arr);
Here is an other example with lodash
var a = [ {"name": "apple", "id": "apple_0"},
{"name": "dog", "id": "dog_1"},
{"name": "cat", "id": "cat_2"}
]
var b = _.find(a, ['name', "apple2"]);
if(_.isObject(b)){
console.log('exists')
}else{
console.log('insert new')
}
https://jsfiddle.net/jorge182/s4og07jg/
This is what worked for me (after testing out the different solutions):
addItem(items, item) {
let foundObject = _.find(items, function(e) {
return e.value === item.value;
});
if(!foundObject) {
items.push(item);
}
return items;
}
If you're interested in inserting in the array only one value, then using _.find could be an option. However, if you were interested in inserting one or more than one, I'd suggest using _.unionBy instead:
var currentArr = [{
"name": "apple",
"id": "apple_0"
}, {
"name": "dog",
"id": "dog_1"
}, {
"name": "cat",
"id": "cat_2"
}],
arrayOneValue = [{
"name": "apple",
"id": "apple_0"
}],
arrayTwoValues = arrayOneValue.concat({
"name": "lemon",
"id": "lemon_0"
})
console.log(_.unionBy(currentArr, arrayOneValue, 'name'));
console.log(_.unionBy(currentArr, arrayTwoValues, 'name'));
// It also allow you to perform the union using more than one property
console.log(_.unionBy(currentArr, arrayTwoValues, 'name', 'id'));
<script src="https://cdn.jsdelivr.net/lodash/4.16.4/lodash.min.js"></script>
Here are three ways of achieving this using lodash 4.17.5:
Say you want to add object entry to an array of objects numbers, only if entry does not exist already.
let numbers = [
{ to: 1, from: 2 },
{ to: 3, from: 4 },
{ to: 5, from: 6 },
{ to: 7, from: 8 },
{ to: 1, from: 2 } // intentionally added duplicate
];
let entry = { to: 1, from: 2 };
/*
* 1. This will return the *index of the first* element that matches:
*/
_.findIndex(numbers, (o) => { return _.isMatch(o, entry) });
// output: 0
/*
* 2. This will return the entry that matches. Even if the entry exists
* multiple time, it is only returned once.
*/
_.find(numbers, (o) => { return _.isMatch(o, entry) });
// output: {to: 1, from: 2}
/*
* 3. This will return an array of objects containing all the matches.
* If an entry exists multiple times, if is returned multiple times.
*/
_.filter(numbers, _.matches(entry));
// output: [{to: 1, from: 2}, {to: 1, from: 2}]
/*
* 4. This will return `true` if the entry exists, false otherwise.
*/
_.some(numbers, entry);
// output: true
If you want to return a Boolean (i.e., assuming that you are not using _.some()), in the first case, you can simply check the index value that is being returned:
_.findIndex(numbers, (o) => { return _.isMatch(o, entry) }) > -1;
// output: true
Lodash documentation is great source of examples and experimentation.

Categories

Resources