How to aggregate all columns of 2 json arrays with same keys - javascript

I'd like to go from
var var1 = [
{key:'key1',value1:'value11'},
{key:'key2',value1:'value12'}
];
var var2 = [
{key:'key1',value2:'value21'},
{key:'key2',value2:'value22'}
];
to here
var var3 = [
{key:'key1',value1:'value11',value2:'value21'},
{key:'key2',value1:'value12',value2:'value22'}
];
What's the easiest way ?

Slower solution
One solution would be to just iterate over array1 and, for each object, merge it if an object with its key already exists in array2.
This solution would have constant space but quadratic time O(arr1.length) * O(arr2.length) because for every object in array1, we're searching for a match in array2.
var var1=[{key:"key1",value1:"value11"},{key:"key2",value1:"value12"}],var2=[{key:"key1",value2:"value21"},{key:"key2",value2:"value22"}];
// O(N*M) time, O(1) space
function mergeQuadratic(arr1, arr2) {
const result = [];
arr1.forEach(obj1 => {
// try to find a match for the current arr1 object by searching through arr2
const obj2 = arr2.find(obj2 => obj2.key === obj1.key);
// if we found a match, we can merge these two objects
if (obj2) {
result.push(Object.assign({}, obj1, obj2));
}
});
return result;
}
console.log(mergeQuadratic(var1, var2));
ES5 version:
var var1=[{key:"key1",value1:"value11"},{key:"key2",value1:"value12"}],var2=[{key:"key1",value2:"value21"},{key:"key2",value2:"value22"}];
// O(N*M) time, O(1) space
function mergeQuadratic(arr1, arr2) {
var result = [];
arr1.forEach(function(obj1) {
// try to find a match for the current arr1 object by searching through arr2
var obj2 = arr2.find(function(obj2) {
return obj2.key === obj1.key
});
// if we found a match, we can merge these two objects
if (obj2) {
result.push(Object.assign({}, obj1, obj2));
}
});
return result;
}
console.log(mergeQuadratic(var1, var2));
Faster solution
An improvement to increase the speed is to tradeoff some space and create a map from array2 so we can reduce the lookup time for a matching key to be constant, giving us a linear run time O(array1.length) + O(array2.length) and linear space O(array2.length):
var var1=[{key:"key1",value1:"value11"},{key:"key2",value1:"value12"}],var2=[{key:"key1",value2:"value21"},{key:"key2",value2:"value22"}];
// O(N+M) time, O(N) space
function mergeLinear(arr1, arr2) {
// create a map of key->obj for every object in arr2
const map = arr2.reduce((map, curr) => {
map.set(curr.key, curr);
return map;
}, new Map());
const result = [];
arr1.forEach(obj1 => {
// check almost instantly if a matching object exists
const obj2 = map.get(obj1.key); // <-- Constant time lookup
// if we found a match, we can merge these two objects
if (obj2) {
result.push(Object.assign({}, obj1, obj2));
}
});
return result;
}
console.log(mergeLinear(var1, var2));
ES5 version:
var var1=[{key:"key1",value1:"value11"},{key:"key2",value1:"value12"}],var2=[{key:"key1",value2:"value21"},{key:"key2",value2:"value22"}];
// O(N+M) time, O(N) space
function mergeLinear(arr1, arr2) {
// create a map of key->obj for every object in arr2
var map = arr2.reduce(function(map, curr) {
map[curr.key] = curr;
return map;
}, Object.create(null));
var result = [];
arr1.forEach(function(obj1) {
// check almost instantly if a matching object exists
var obj2 = map[obj1.key]; // <-- Constant time lookup
// if we found a match, we can merge these two objects
if (obj2) {
result.push(Object.assign({}, obj1, obj2));
}
});
return result;
}
console.log(mergeLinear(var1, var2));

This seems to be a simple merge, if your keys are always going to be named like the names you provided. If the names could change, then this solution will have to modified.
This solution does not mutate either of the existing arrays. The function returns a new array.
var var1 = [
{key:'key1',value1:'value11'},
{key:'key2',value1:'value12'}
];
var var2 = [
{key:'key1',value2:'value21'},
{key:'key2',value2:'value22'}
];
function mergeArrays(arr1, arr2) {
var newArray = arr1;
newArray.forEach(function (obj1) {
arr2.forEach(function (obj2) {
if (obj1.key === obj2.key) {
obj1.value2 = obj2.value2;
}
});
});
return newArray;
}
var var3 = mergeArrays(var1, var2);
console.log(var3);

For good browsers (not IE, but polyfills for Object.assign and for Array#find are available)
var var3 = var1.map(function(o1) {
return Object.assign({}, o1, var2.find(function(o2) {
return o2.key === o1.key;
}));
});
or in modern speak
var var3 = var1.map(o1 => Object.assign({}, o1, var2.find(o2 => o2.key === o1.key)));

Using underscore.js (you can test it out on the console there)
I'll leave it as an exercise to make this a one-liner using _.filter and _.map
function extendArrayByKey(var1, var2) {
for (obj of var1) {
var key = obj.key;
for (obj2 of var2) {
if (obj2.key == key) {
_.extend(obj, obj2);
}
}
}
}
Note that this modified the objects in var1 with shallow copies of the values in var2

Related

Combine 2 JSON objects of unequal size with ID

Problem
I would like to have the below two JSON combined together using the ID and have the expected result as mentioned below. I have tried a few solutions that were available but none worked for my use case. Any suggestions will be great !!
Tried to do:
How to merge two json object values by id with plain Javascript (ES6)
Code
var json1 = [
{
"id":"A123",
"cost":"5020.67",
"fruitName":"grapes"
},
{
"id":"A456",
"cost":"341.30",
"fruitName":"apple"
},
{
"id":"A789",
"cost":"3423.04",
"fruitName":"banana"
}
];
var json2 = [
{
"id":"A123",
"quantity":"7"
},
{
"id":"A789",
"quantity":"10"
},
{
"id":"ABCD",
"quantity":"22"
}
];
Below is the code I tried:
var finalResult = [...[json1, json2].reduce((m, a) => (a.forEach(o => m.has(o.id) && Object.assign(m.get(o.id), o) || m.set(o.id, o)), m), new Map).values()];
Expected result:
[
{
"id":"A123",
"cost":"5020.67",
"fruitName":"grapes",
"quantity":"7"
},
{
"id":"A456",
"cost":"341.30",
"fruitName":"apple"
},
{
"id":"A789",
"cost":"3423.04",
"fruitName":"banana",
"quantity":"10"
},
{
"id":"ABCD",
"quantity":"22"
}
]
You can accomplish this fairly easily without getting too fancy. Here's the algorithm:
Put the items from json1 into an object by id, so that you can look them up quickly.
For each item in json2: If it already exists, merge it with the existing item. Else, add it to objectsById.
Convert objectsById back to an array. I've used Object.values, but you can also do this easily with a loop.
var json1 = [
{
"id":"A123",
"cost":"5020.67",
"fruitName":"grapes"
}, {
"id":"A456",
"cost":"341.30",
"fruitName":"apple"
}, {
"id":"A789",
"cost":"3423.04",
"fruitName":"banana"
}
];
var json2 = [
{
"id":"A123",
"quantity":"7"
}, {
"id":"A789",
"quantity":"10"
}
];
const objectsById = {};
// Store json1 objects by id.
for (const obj1 of json1) {
objectsById[obj1.id] = obj1;
}
for (const obj2 of json2) {
const id = obj2.id;
if (objectsById[id]) {
// Object already exists, need to merge.
// Using lodash's merge because it works for deep properties, unlike object.assign.
objectsById[id] = _.merge(objectsById[id], obj2)
} else {
// Object doesn't exist in merged, add it.
objectsById[id] = obj2;
}
}
// All objects have been merged or added. Convert our map to an array.
const mergedArray = Object.values(objectsById);
I think a few steps are being skipped in your reduce function. And it was a little difficult to read because so many steps are being combined in one.
One critical piece that your function does not account for is that when you add 2 numerical strings together, it concats the strings.
const stringTotal = "5020.67" + "3423.04" // result will be "5020.673423.04"
The following functions should give you the result you are looking for.
// calculating the total cost
// default values handles cases where there is no obj in array 2 with the same id as the obj compared in array1
const calcualteStringTotal = (value1 = 0, value2 = 0) => {
const total = parseFloat(value1) + parseFloat(value2)
return `${total}`
}
const calculateTotalById = (array1, array2) => {
const result = []
// looping through initial array
array1.forEach(outterJSON => {
// placeholder json obj - helpful in case we have multiple json in array2 with the same id
let combinedJSON = outterJSON;
// looping through second array
array2.forEach(innerJSON => {
// checking ids
if(innerJSON.id === combinedJSON.id) {
// calls our helper function to calculate cost
const updatedCost = calcualteStringTotal(innerJSON.cost, outterJSON.cost)
// updating other properties
combinedJSON = {
...outterJSON,
...innerJSON,
cost: updatedCost
}
}
})
result.push(combinedJSON)
})
return result
}
const combinedResult = calculateTotalById(json1, json2)
I figured that by using reduce I could make it work.
var finalResult = [...[json1, json2].reduce((m, a) => (a.forEach(o => m.has(o.id) && Object.assign(m.get(o.id), o) || m.set(o.id, o)), m), new Map).values()];

clone js array w/ objects and primitives

Given the array:
['1', {type:'2'}, ['3', {number: '4'}], '5']
I need to make a clone without using the slice, json.parse and other methods.
At the moment, the code is working, but it will not clone objects:
var myArr =['1',{type:'2'},['3',{number:'4'}],'5'];
var arrClone=[];
for(var i=0;i<myArr.length;i++){
if(typeof(myArr[i])==='object')
{
var arr=[];
for(var j=0;j<myArr[i].length;j++){
arr[j]=myArr[i][j];
}
arrClone[i]=arr;
}else { arrClone[i]=myArr[i]}
}
You could check if an object is supplied and if so, another check is made for an array. Then retrn either the cloned array or the object, or the value itself.
function getClone(value) {
if (value && typeof value === 'object') {
return Array.isArray(value)
? value.map(getClone)
: Object.assign(
...Object.entries(value).map(([k, v]) => ({ [k]: getClone(v) }))
);
}
return value;
}
var data = ['1', { type: '2' }, ['3', { number: '4' }], '5'],
clone = getClone(data);
console.log(getClone(data));
Here is a simple implementation without Array methods:
function deepClone(obj) {
if (typeof obj !== "object" || obj === null) return obj; // primitives
// It could be an array or plain object
const result = obj.constructor.name == "Array" ? [] : {};
for (const key in obj) {
result[key] = deepClone(obj[key]); // recursive call
}
return result;
}
// Demo
var myArr =['1',{type:'2'},['3',{number:'4'}],'5'];
var arrClone = deepClone(myArr);
console.log(arrClone);
Note that this only works for simple data types. As soon as you start to work with Dates, regex objects, Sets, Maps, ...etc, you need much more logic. Also think of self references and functions, and how they should be dealt with.
For more advanced cloning see How to Deep Clone in JavaScript, but expect the use of several methods.

Javascript - map value to keys (reverse object mapping)

I want to reverse the mapping of an object (which might have duplicate values). Example:
const city2country = {
'Amsterdam': 'Netherlands',
'Rotterdam': 'Netherlands',
'Paris': 'France'
};
reverseMapping(city2country) Should output:
{
'Netherlands': ['Amsterdam', 'Rotterdam'],
'France': ['Paris']
}
I've come up with the following, naive solution:
const reverseMapping = (obj) => {
const reversed = {};
Object.keys(obj).forEach((key) => {
reversed[obj[key]] = reversed[obj[key]] || [];
reversed[obj[key]].push(key);
});
return reversed;
};
But I'm pretty sure there is a neater, shorter way, preferably prototyped so I could simply do:
const country2cities = city2country.reverse();
You could use Object.assign, while respecting the given array of the inserted values.
const city2country = { Amsterdam: 'Netherlands', Rotterdam: 'Netherlands', Paris: 'France' };
const reverseMapping = o => Object.keys(o).reduce((r, k) =>
Object.assign(r, { [o[k]]: (r[o[k]] || []).concat(k) }), {})
console.log(reverseMapping(city2country));
There is no such built-in function in JavaScript. Your code looks fine, but given that there are so many edge cases here that could wrong, I'd suggesting using invertBy from lodash, which does exactly what you describe.
Example
var object = { 'a': 1, 'b': 2, 'c': 1 };
_.invertBy(object);
// => { '1': ['a', 'c'], '2': ['b'] }
You can use something like this to get raid of duplicates first :
function removeDuplicates(arr, key) {
if (!(arr instanceof Array) || key && typeof key !== 'string') {
return false;
}
if (key && typeof key === 'string') {
return arr.filter((obj, index, arr) => {
return arr.map(mapObj => mapObj[key]).indexOf(obj[key]) === index;
});
} else {
return arr.filter(function(item, index, arr) {
return arr.indexOf(item) == index;
});
}
}
and then use this to make it reverse :
function reverseMapping(obj){
var ret = {};
for(var key in obj){
ret[obj[key]] = key;
}
return ret;
}
You could try getting an array of values and an array of keys from the current object, and setup a new object to hold the result. Then, as you loop through the array of values -
if the object already has this value as the key, like Netherlands, you create a new array, fetch the already existing value (ex: Rotterdam), and add this and the new value (Amsterdam) to the array, and set up this array as the new value for the Netherlands key.
if the current value doesn't exist in the object, set it up as a new string, ex: France is the key and Paris is the value.
Code -
const city2country = {
'Amsterdam': 'Netherlands',
'Rotterdam': 'Netherlands',
'Paris': 'France',
};
function reverseMapping(obj) {
let values = Object.values(obj);
let keys = Object.keys(obj);
let result = {}
values.forEach((value, index) => {
if(!result.hasOwnProperty(value)) {
// create new entry
result[value] = keys[index];
}
else {
// duplicate property, create array
let temp = [];
// get first value
temp.push(result[value]);
// add second value
temp.push(keys[index]);
// set value
result[value] = temp;
}
});
console.log(result);
return result;
}
reverseMapping(city2country)
The benefit here is - it adjusts to the structure of your current object - Netherlands being the repeated values, gets an array as it's value in the new object, while France gets a string value Paris as it's property. Of course, it should be very easy to change this.
Note - Object.values() might not be supported across older browsers.
You could use reduce to save the declaration line reduce.
Abusing && to check if the map[object[key]] is defined first before using Array.concat.
It's shorter, but is it simpler? Probably not, but a bit of fun ;)
const reverseMapping = (object) =>
Object.keys(object).reduce((map, key) => {
map[object[key]] = map[object[key]] && map[object[key]].concat(key) || [key]
return map;
}, {});
#Nina Scholz answer works well for this exact question. :thumbsup:
But if you don't need to keep both values for the Netherlands key ("Netherlands": ["Amsterdam", "Rotterdam"]), then this is a little bit shorter and simpler to read:
const city2country = { Amsterdam: 'Netherlands', Rotterdam: 'Netherlands', Paris: 'France' };
console.log(
Object.entries(city2country).reduce((obj, item) => (obj[item[1]] = item[0]) && obj, {})
);
// outputs `{Netherlands: "Rotterdam", France: "Paris"}`

How to check if two objects properties match?

Lets say there are two objects but one object has property different from the other. Is there a way to figure out what properties match?
for example:
var objectOne = {
boy: "jack",
girl: "jill"
}
var objectTwo = {
boy: "john",
girl: "mary",
dog: "mo"
}
edit: It should tell me boy and girl property name are found in both the objects.
var in_both = [];
for (var key in objectOne) { // simply iterate over the keys in the first object
if (Object.hasOwnProperty.call(objectTwo, key)) { // and check if the key is in the other object, too
in_both.push(key);
}
}
C.f. https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Object/hasOwnProperty
Now, if you want to test if the values are the same, too, than simply add more code to the condition/body of the inner if.
Using Object.keys
Object.keys(objectOne).filter(k => Object.hasOwnProperty.call(objectTwo, k))
You can use Object.keys and use Array.prototype.reduce to loop through once and list out the common keys - see demo below:
var objectOne={boy:"jack",girl:"jill"};
var objectTwo={boy:"john",girl:"mary",dog:"mo"};
var result = Object.keys(objectOne).reduce(function(p,c){
if(c in objectTwo)
p.push(c);
return p;
},[]);
console.log(result);
If you want to find out which keys match given two objects, you could loop through all of the keys of the objects using a for... in loop. In my function, it will loop through the keys and return an array of all of the matching keys in the two objects.
let objectOne = {
boy: "jack",
girl: "jill"
}
let objectTwo = {
boy: "john",
girl: "mary",
dog: "mo"
}
function matchingKeys (obj1, obj2) {
let matches = [];
let key1, key2;
for (key1 in obj1) {
for (key2 in obj2) {
if ( key1 === key2) {
matches.push(key1);
}
}
}
return matches
}
const result = matchingKeys(objectOne, objectTwo);
console.log(result)
Try this on for size:
function compare(obj1, obj2) {
// get the list of keys for the first object
var keys = Object.keys(obj1);
var result = [];
// check all from the keys in the first object
// if it exists in the second object, add it to the result
for (var i = 0; i < keys.length; i++) {
if (keys[i] in obj2) {
result.push([keys[i]])
}
}
return result;
}
This isn't better than some solutions here, but I thought I'd share:
function objectHas(obj, predicate) {
return JSON.stringify(obj) === JSON.stringify({ ...obj, ...predicate })
}

Sorting a JavaScript object by property name

I've been looking for a while and want a way to sort a Javascript object like this:
{
method: 'artist.getInfo',
artist: 'Green Day',
format: 'json',
api_key: 'fa3af76b9396d0091c9c41ebe3c63716'
}
and sort is alphabetically by name to get:
{
api_key: 'fa3af76b9396d0091c9c41ebe3c63716',
artist: 'Green Day',
format: 'json',
method: 'artist.getInfo'
}
I can't find any code that will do this. Can anyone give me some help?
UPDATE from the comments:
This answer is outdated. In ES6 objects keys are now ordered. See this question for an up-to-date answer
By definition, the order of keys in an object is undefined, so you probably won't be able to do that in a way that is future-proof. Instead, you should think about sorting these keys when the object is actually being displayed to the user. Whatever sort order it uses internally doesn't really matter anyway.
By convention, most browsers will retain the order of keys in an object in the order that they were added. So, you could do this, but don't expect it to always work:
function sortObject(o) {
var sorted = {},
key, a = [];
for (key in o) {
if (o.hasOwnProperty(key)) {
a.push(key);
}
}
a.sort();
for (key = 0; key < a.length; key++) {
sorted[a[key]] = o[a[key]];
}
return sorted;
}
this function takes an object and returns a sorted array of arrays of the form [key,value]
function (o) {
var a = [],i;
for(i in o){
if(o.hasOwnProperty(i)){
a.push([i,o[i]]);
}
}
a.sort(function(a,b){ return a[0]>b[0]?1:-1; })
return a;
}
The object data structure does not have a well defined order. In mathematical terms, the collection of keys in an object are an Unordered Set, and should be treated as such.
If you want to define order, you SHOULD use an array, because an array having an order is an assumption you can rely on. An object having some kind of order is something that is left to the whims of the implementation.
Just use sorted stringify() when you need to compare or hash the results.
// if ya need old browser support
Object.keys = Object.keys || function(o) {
var result = [];
for(var name in o) {
if (o.hasOwnProperty(name))
result.push(name);
}
return result;
};
var o = {c: 3, a: 1, b: 2};
var n = sortem(o);
function sortem(old){
var newo = {}; Object.keys(old).sort().forEach(function(k) {new[k]=old[k]});
return newo;
}
// deep
function sortem(old){
var newo = {}; Object.keys(old).sort().forEach(function(k){ newo[k]=sortem(old[k]) });
return newo;
}
sortem({b:{b:1,a:2},a:{b:1,a:2}})
Here is a one-liner for you.
Array.prototype.reduce()
let data = {
method: 'artist.getInfo',
artist: 'Green Day',
format: 'json',
api_key: 'fa3af76b9396d0091c9c41ebe3c63716'
};
let sorted = Object.keys(data).sort().reduce( (acc, currValue) => {
acc[currValue] = data[currValue];
return acc;
}, {});
console.log(sorted);
Good luck!!
ES5 Compatible:
function sortByKey(obj) {
var keys = Object.keys(obj);
keys.sort();
var sorted = {};
for (var i = 0; i < keys.length; i++) {
var key = keys[i];
sorted[key] = obj[key];
}
return sorted;
}
This should be used with caution as your code shouldn't rely on Object properties order. If it's just a matter of presentation (or just for the fun !), you can sort properties deeply like this :
function sortObject(src) {
var out;
if (typeof src === 'object' && Object.keys(src).length > 0) {
out = {};
Object.keys(src).sort().forEach(function (key) {
out[key] = sortObject(src[key]);
});
return out;
}
return src;
}

Categories