Related
I want to combine the content of multiple objects that have the same key with that key as the property in JavaScript.
Objects:
const cat1 = { med1: { a: 10, b: 12 }, med2: { c: 14, d: 16 } };
const cat2 = { med1: { e: 18, f: 20 }, med2: { g: 22, h: 24 } };
Expected output:
{
med1: { a: 10, b: 12, e: 18, f: 20 },
med2: { c: 14, d: 16, g: 22, h: 24 }
}
I have tried to use both object spreading and Object.assign with no sucess.
With object spreading, since objects have no iterator this is returning an error. Object.assign would work however since the two objects to combine have the same key, the second object is overwriting the first.
You can iterate over the object keys and create a new object out of their aggregation.
You can use ES6 spread operator (...) which allows us to quickly copy all or part of an existing array or object into another array or object.
const cat1 = { med1: { a: 10, b: 12 }, med2: { c: 14, d: 16 } };
const cat2 = { med1: { e: 18, f: 20 }, med2: { g: 22, h: 24 } };
let resultObject = {};
Object.keys(cat1).map(key => { // iterate over the keys
resultObject = {
...resultObject,
[key]: {...cat1[key], ...cat2[key]} // merge two objects
}
return;
});
console.log(resultObject);
For combining two objects with a single level of indirection 👇
const cat1 = { med1: { a: 10, b: 12 }, med2: { c: 14, d: 16 } }
const cat2 = { med1: { e: 18, f: 20 }, med2: { g: 22, h: 24 } }
const merge = (o1, o2) =>
Object.keys(o1)
.reduce((p, key) =>
(p[key] = { ...o1[key], ...o2[key] }, p), {})
console.log(merge(cat1, cat2))
This works, but I was wondering if there was a better way than creating a string with a and b and later splitting it:
const data = [
{ a: 10, b: 20, c: 30, d: 40 },
{ a: 10, b: 20, c: 31, d: 41 },
{ a: 12, b: 22, c: 32, d: 42 }
];
d3.rollups(
data,
x => ({
c: x.map(d => d.c),
d: x.map(d => d.d)
}),
d => `${d.a} ${d.b}`
)
.map(([key, values]) => {
const [a, b] = key.split(' ');
return {a, b, ...values};
});
// OUTPUT
// [
// {a: "10", b: "20", c: [30, 31], d: [40, 41]},
// {a: "12", b: "22", c: [32], d: [42]}
// ]
With d3 v7 released, there is now a better way to do this using the new d3.flatRollup.
const data = [
{ a: 10, b: 20, c: 30, d: 40 },
{ a: 10, b: 20, c: 31, d: 41 },
{ a: 12, b: 22, c: 32, d: 42 }
];
const result = d3.flatRollup(
data,
x => ({
c: x.map(d => d.c),
d: x.map(d => d.d)
}),
d => d.a,
d => d.b
);
console.log(result);
const flattened = result.map(([a, b, values]) => ({a, b, ...values}));
console.log(flattened);
<script src="https://cdn.jsdelivr.net/npm/d3-array#3.0.2/dist/d3-array.min.js"></script>
As you already know d3.rollups() will create nested arrays if you have more than one key:
If more than one key is specified, a nested Map [or array] is returned.
Therefore, as d3.rollups doesn't fit your needs, I believe it's easier to create a plain JavaScript function (I'm aware of "using D3" in your title, but even in a D3 code nothing forbids us of writing plain JS solutions where D3 has none).
In the following example I'm purposefully writing a verbose function (with comments) so each part of it is clear, avoiding more complex features which could make it substantially short (but more cryptic). In this function I'm using reduce, so the data array is looped only once. myKeys is the array of keys you'll use to rollup.
Here is the function and the comments:
function groupedRollup(myArray, myKeys) {
return myArray.reduce((a, c) => {
//Find the object in the acc with all 'myKeys' equivalent to the current
const foundObject = a.find(e => myKeys.every(f => e[f] === c[f]));
//if found, push the value for each key which is not in 'myKeys'
if (foundObject) {
for (let key in foundObject) {
if (!keys.includes(key)) foundObject[key].push(c[key]);
};
//if not found, push the current object with all non 'myKeys' keys as arrays
} else {
const copiedObject = Object.assign({}, c);//avoids mutation
for (let key in copiedObject) {
if (!keys.includes(key)) copiedObject[key] = [copiedObject[key]];
};
a.push(copiedObject);
};
return a;
}, [])
};
Here is the demo:
const data = [{
a: 10,
b: 20,
c: 30,
d: 40
},
{
a: 10,
b: 20,
c: 31,
d: 41
},
{
a: 12,
b: 22,
c: 32,
d: 42
}
];
const keys = ["a", "b"];
console.log(groupedRollup(data, keys))
function groupedRollup(myArray, myKeys) {
return myArray.reduce((a, c) => {
const foundObject = a.find(e => myKeys.every(f => e[f] === c[f]));
if (foundObject) {
for (let key in foundObject) {
if (!keys.includes(key)) foundObject[key].push(c[key]);
};
} else {
const copiedObject = Object.assign({}, c);
for (let key in copiedObject) {
if (!keys.includes(key)) copiedObject[key] = [copiedObject[key]];
};
a.push(copiedObject);
};
return a;
}, [])
};
And here is a demo with a more complex data:
const data = [{
a: 10,
b: 20,
c: 30,
d: 40,
e: 5,
f: 19
},
{
a: 10,
b: 55,
c: 37,
d: 40,
e: 5,
f: 19
},
{
a: 10,
b: 20,
c: 31,
d: 48,
e: 5,
f: 18
},
{
a: 80,
b: 20,
c: 31,
d: 48,
e: 5,
f: 18
},
{
a: 1,
b: 2,
c: 3,
d: 8,
e: 5,
f: 9
},
{
a: 10,
b: 88,
c: 44,
d: 33,
e: 5,
f: 19
}
];
const keys = ["a", "e", "f"];
console.log(groupedRollup(data, keys))
function groupedRollup(myArray, myKeys) {
return myArray.reduce((a, c) => {
const foundObject = a.find(e => myKeys.every(f => e[f] === c[f]));
if (foundObject) {
for (let key in foundObject) {
if (!keys.includes(key)) foundObject[key].push(c[key]);
};
} else {
const copiedObject = Object.assign({}, c);
for (let key in copiedObject) {
if (!keys.includes(key)) copiedObject[key] = [copiedObject[key]];
};
a.push(copiedObject);
};
return a;
}, [])
};
Finally, pay attention that this function will push duplicated values (in the above example d: [40, 40, 33]). If that's not what you want then just check for duplicates.
The approach below allows you to remove the split, but does not prevent the need to create a string for the compound key. In this case, using JSON.stringify({a: d.a, b: d.b}) instead of ${d.a} ${d.b}, allows for the map to return an object where the c and d properties can be assigned to the parse of the key.
This preserves some of the 'd3-ishness' of your question and the utility of rollups to deal with the creation of the arrays for c and d.
const data = [
{ a: 10, b: 20, c: 30, d: 40 },
{ a: 10, b: 20, c: 31, d: 41 },
{ a: 12, b: 22, c: 32, d: 42 }
];
const groups = d3.rollups(
data,
x => ({
c: x.map(d => d.c),
d: x.map(d => d.d)
}),
d => JSON.stringify({a: d.a, b: d.b}) // compare with `${d.a} ${d.b}`
).map(arr => Object.assign(JSON.parse(arr[0]), arr[1]));
console.log(groups);
.as-console-wrapper { max-height: 100% !important; top: 0; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.5.0/d3.min.js"></script>
The approach can accommodate the extensibility of #Gerado Furtado's answer, but I fear it's getting a little hectic:
const data = [
{a: 10, b: 20, c: 30, d: 40, e: 5, f: 19},
{a: 10, b: 55, c: 37, d: 40, e: 5, f: 19},
{a: 10, b: 20, c: 31, d: 48, e: 5, f: 18},
{a: 80, b: 20, c: 31, d: 48, e: 5, f: 18},
{a: 1, b: 2, c: 3, d: 8, e: 5, f: 9},
{a: 10, b: 88, c: 44, d: 33, e: 5, f: 19}
];
const keys = ["a", "e", "f"];
const groupedRollup = (data, keys) => {
const others = Object.keys(data[0])
.filter(k => !keys.includes(k)); // finds b, c, d as not part of compound key
return d3.rollups(
data,
x => Object.assign(
{},
...others.map(k => {
return {[k]: x.map(d => d[k])} // dynamically create reducer
})
),
d => JSON.stringify(
Object.assign(
{},
...keys.map(k => {
return {[k]: d[k]} // dynamically add keys
})
)
) // and stringify for compound key
).map(arr => Object.fromEntries( // sorting the output object
Object.entries( // keys in alpha order
Object.assign(JSON.parse(arr[0]), arr[1])).sort() // same approach
)
);
}
console.log(groupedRollup(data, keys));
.as-console-wrapper { max-height: 100% !important; top: 0; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.5.0/d3.min.js"></script>
There's some interesting talk about the introduction of use of InternMap in rollups and the associated functions - but I don't see either that it's ready, or that it's useful for what you are trying to do.
JavaScript findIndex returns the very first finded index in case of duplicate values
const arr = [{a: 10, b: 20, c: 30},{a: 15, b: 25, c: 32},{a: 10, b: 23, c: 350}]
const index = arr.findIndex(m => m.a === 10)
console.log(index);
The above code will only return 0 index.
What should I do to get index 2 as well.
You could filter the keys of the array like this:
const arr = [{a: 10, b: 20, c: 30},{a: 15, b: 25, c: 32},{a: 10, b: 23, c: 350}]
const indices = [...arr.keys()].filter(i => arr[i].a === 10)
console.log(indices)
Or, just use a for loop
const arr = [{a: 10, b: 20, c: 30},{a: 15, b: 25, c: 32},{a: 10, b: 23, c: 350}]
const output = [];
for (let i = 0; i < arr.length; i++) {
if (arr[i].a === 10)
output.push(i)
}
console.log(output)
arr.reduce((result, current, index) => {
return result.concat(current.a == 10 ? [index]: [])
})
You can use of Array.reduce to loop on your arr and returns every indexes
Single line :
const arr = [{
a: 10,
b: 20,
c: 30,
}, {
a: 15,
b: 25,
c: 32,
}, {
a: 10,
b: 23,
c: 350,
}]
// Value to check for
const v = 10;
// we loop on every entry of the arr
// we start with an empty array and look if the entries
// contains the value 10, if they do, we push the index of the
// entry in the array that was empty
//
// we do that for every entry of arr, when we treat all, we return
// the list of matching entries
const idx = arr.reduce((tmp, x, xi) => (x.a === v ? [...tmp, xi] : tmp), []);
console.log(idx);
Explained :
const arr = [{
a: 10,
b: 20,
c: 30,
}, {
a: 15,
b: 25,
c: 32,
}, {
a: 10,
b: 23,
c: 350,
}]
const valToCheck = 10;
const indexes = arr.reduce((tmp, x, xi) => {
if (x.a === valToCheck) {
tmp.push(xi);
}
return tmp;
}, []);
console.log(indexes);
Use map and filter:
const arr = [{a: 10, b: 20, c: 30},{a: 15, b: 25, c: 32},{a: 10, b: 23, c: 350}];
const res = arr.map(({ a }, i) => a == 10 ? i : "").filter(String);
console.log(res);
You could map the indices of the found items and filter only valid indices.
const
array = [{ a: 10, b: 20, c: 30 }, { a: 15, b: 25, c: 32 }, { a: 10, b: 23, c: 350 }],
indices = array
.map((m, i) => m.a === 10 ? i : -1)
.filter(i => i != -1);
console.log(indices);
You can just iterate through array and push matching index in new array.
const arr = [{a: 10, b: 20, c: 30},{a: 15, b: 25, c: 32},{a: 10, b: 23, c: 350}]
let p = []
arr.forEach((ele,index)=> ele.a===10 ? p.push(index) : '')
console.log(p)
You can try like this:
const arr = [{a: 10, b: 20, c: 30},{a: 15, b: 25, c: 32},{a: 10, b: 23, c: 350}];
const indices = arr.map((x, index) => {
if (x.a === 10) {
return index;
}
})
.filter(x => x !== undefined);
console.log(indices);
If you want a function which works directly with Array, add this in Array.prototype
Array.prototype.findAllIndexes = function(iteratee){
var resultArray = [];
this.forEach(function(element) {
var validated = iteratee.call(this, element);
if(validated) {
resultArray.push(element);
}
});
return resultArray;
}
Then you can use it anywher you want with array object
(please handle your corner cases based od data type you use)
const arr = [{a: 10, b: 20, c: 30},{a: 15, b: 25, c: 32},{a: 10, b: 23, c: 350}]
const index = arr.findAllIndexes(m => m.a === 10)
console.log(index);
You can use some helper libraries to this kind of stuff like lodash.js, you can include them in your project.
I have an array of objects. I don't know how many objects there will be until the code is being run (it is returned from an API), but let's assume this is the array:
var arr = [ { A: 40, B: 88, C: 11 },
{ A: 10, B: 98, C: 65 }, // sum of A = 188
{ A: 11, B: 15, C: 18 }, // sum of B = 310
{ A: 16, B: 55, C: 16 }, // sum of C = 136
{ A: 22, B: 23, C: 13 },
{ A: 89, B: 31, C: 13 } ]
I want to look over every object in the array. I'd like the end-result to be a list of keys and values, sorted in descending order. So, if we were to use the array above, the code would return something like this:
[["B", 310], ["A", 188], ["C", 136]]
I hope it's not too much to ask if you can add comments in your code, as sometimes the answers here can be very short and efficient (and work brilliantly) but difficult to understand for a beginner with algorithms :)
Many thanks in advance.
EDIT: Each object does not necessarily always have three keys, it is around 30-40 keys.
BIT MORE INFO: I'm writing this for a stacked bar chart where I want to then only extract the top 10 keys and bucket the rest of the values into an "Others" key, but this is irrelevant to the question and only here for information.
If you are about to get sorted result -like you mentioned in your terms- for your complete array then this may be the answer.
You can first calculate sum of corresponding properties of every object in the array with a simple Array.prototype.reduce then convert the result to the structure you want (I preferred to loop over object keys) and then sort your structured array.
var arr = [ { A: 40, B: 88, C: 11 },
{ A: 10, B: 98, C: 65 },
{ A: 11, B: 15, C: 18 },
{ A: 16, B: 55, C: 16 },
{ A: 22, B: 23, C: 13 },
{ A: 89, B: 31, C: 13 }
];
var sum = arr.reduce((p, c) => {
var result = Object.create(null);
Object.keys(p).forEach(k => result[k] = p[k] + c[k]);
return result;
});
var sorted = Object.keys(sum).map(k => [k, sum[k]]).sort((a, b) => a[1] - b[1]);
console.log(sorted);
var x = [{ a: 1, b: 2}, { a: 11, b: 12}, { a: 31, b: 23}, { a: 51, b: 24}]
how do you find a = 11 ?
for simple arrays one can do x.indexOf('1'); so perhaps the solution should be something like
var a1 = x.indexOf({a: 1});
ofcourse, I want to obtain the entire JSON for which the value matches.
you can do it with a simple function, no third party modules needed:
var x = [{ a: 1, b: 2}, { a: 11, b: 12}, { a: 31, b: 23}, { a: 51, b: 24}];
function getIndexOf(value){
for(var i=0; i<x.lengh; i++){
if(x[i].a == value)
return i;
}
}
alert(getIndexOf(value)); // output is: 1
You can use Array.Filter with shim support on older browsers.
var x = [{
a: 1,
b: 2
}, {
a: 11,
b: 12
}, {
a: 31,
b: 23
}, {
a: 51,
b: 24
}],
tocomp = 11;
var res = x.filter(function (ob) {
return ob.a === tocomp;
});
Result will be array of object that matches the condition.
Fiddle
And if you just care for single match and get back the matched object just use a for loop.
var x = [{
a: 1,
b: 2
}, {
a: 11,
b: 12
}, {
a: 31,
b: 23
}, {
a: 51,
b: 24
}],
tocomp = 11, i, match;
for (i=0, l=x.length; i<l; i++){
if(x[i].a === tocomp){
match = x[i];
break; //break after finding the match
}
}
Simply iterate over the array to get the value.
for(var i = 0;i < x.length; i++){
alert(x[i].a);
}
JsFiddle
You can use native js or you can use underscoreJS lib.
UnderscoreJS