Related
const obj={a:{b:{n:{d:1},m:{f:1}}}}
How to find and return {a:{b:{n:{d:1}}}}} by key 'n'.
JSON parse stringify
Here is a fairly concise recursive solution. It avoids recursing on properties whose values aren't objects (Arrays are objects, thus the additional .isArray() check, and null is a special case)* and terminates when it finds an object whose Object.keys() include the passed key, otherwise it returns an empty object.
const getNestedKeyPath = (obj, key) => {
if (Object.keys(obj).includes(key)) {
return { [key]: obj[key] };
}
for (const _key of Object.keys(obj)) {
if (obj[_key] !== null && typeof obj[_key] === 'object' && !Array.isArray(obj[_key])) {
const nextChild = getNestedKeyPath(obj[_key], key);
if (Object.keys(nextChild).length !== 0) {
return { [_key]: nextChild };
}
}
};
return {};
}
console.log(getNestedKeyPath(obj, 'n'))
<script>
const obj = {
c: {
x: new Date(),
y: undefined,
d: {
l: { w: [2, 3] },
k: { l: null }
}
},
a: {
b: {
n: { d: 1 },
m: { f: 1 }
}
},
};
</script>
*This is not a definitive check but meets our needs. see: JavaScript data types and data structures and Check if a value is an object in JavaScript
I'm new to Javascript and I have nested objects and arrays that I would like to flatten.
I have...
[{ a: 2, b: [{ c: 3, d: [{e: 4, f: 5}, {e: 5,f: 6}]},
{ c: 4, d: [{e: 7, f: 8}]}
]
}]
and would like...
[{a:2,c:3,e:4,f:5}, {a:2,c:3,e:5,f:6}, {a:2,c:4,e:7,f:8}]
I've tried to adapt the following function written for an object for my array but i only get the final object within the array [{a:2,c:4,e:7,f:8}] https://stackoverflow.com/a/33158929/14313188. I think my issue is knowing how to iterate through arrays and objects?
original script:
function flatten(obj) {
var flattenedObj = {};
Object.keys(obj).forEach(function(key){
if (typeof obj[key] === 'object') {
$.extend(flattenedObj, flatten(obj[key]));
} else {
flattenedObj[key] = obj[key];
}
});
return flattenedObj;
}
my scripts (same result for both):
flat_array=[];
function superflat(array){
for (var i = 0; i < array.length; i++) {
var obj = array[i]
var flattenedObj = {};
Object.keys(obj).forEach(function(key){
if (typeof obj[key] === 'object') {
$.extend(flattenedObj, flatten(obj[key]));
} else {
flattenedObj[key] = obj[key];
}
});
flat_array.push(flattenedObj);
}
};
mega_flat_array=[];
function megaflatten(obj) {
Object.keys(obj).forEach(function(key){
var flattenedObj = {};
if (typeof obj[key] === 'object') {
$.extend(flattenedObj, flatten(obj[key]));
} else {
flattenedObj[key] = obj[key];
}
mega_flat_array.push(flattenedObj);
});
}
Thanks for your help
I would suggest starting with simpler data objects to test your function with, then progressively add more complex objects until your function performs as expected. forEach
Here you can see how I started with a simple test1 object, then test2 and so on so that the logic gets broken down into smaller increments.
To remove the duplicates we previously had, I had to throw and catch an error to break out of the recursive forEach loops, which added the unnecessary duplicate "rows" - perhaps it is better to use a normal for loop out of which you can simply break; and then use error handling for real errors.
The basic idea of the recursive function is to check the type of object (either array or object) and then loop through them to add values, but another check is needed on those to see if they are not Arrays and not Objects, if they are, call the function again. When a duplicate key is found i.e { c: 3}, remove the current key and add the new one before continuing the loop.
You can add some more tests if you have some more sample data, but there are better libraries to help you with TDD (test-driven development).
const isArray = (arr) => {
return Array.isArray(arr);
};
const isObject = (obj) => {
return typeof obj === "object" && obj !== null;
};
const flatten = (tree, row, result) => {
try {
if (isArray(tree)) {
tree.forEach((branch, index) => {
flatten(branch, row, result);
});
} else if (isObject(tree)) {
Object.keys(tree).forEach((key) => {
//we don't want to add objects or arrays to the row -
if (!isArray(tree[key]) && !isObject(tree[key])) {
if (key in row) {
// new row detected, get existing keys to work with
let keysArray = Object.keys(row);
// we are going to loop backwards and delete duplicate keys
let end = Object.keys(row).length;
let stopAt = Object.keys(row).indexOf(key);
//delete object keys from back of object to the newly found one
for (let z = end; z > stopAt; z--) {
delete row[keysArray[z - 1]];
}
row[key] = tree[key];
} else {
row[key] = tree[key];
}
} else {
flatten(tree[key], row, result);
throw "skip";
}
});
//all other rows in results will be overridden if we don't stringify
result.push(JSON.stringify(row));
}
} catch (e) {
//console.log(e)
} finally {
return result.map((row) => JSON.parse(row));
}
};
///tests
const test1 = [
{
a: 2,
b: 3,
},
];
const expected1 = [{ a: 2, b: 3 }];
const test2 = [
{
a: 2,
b: [
{
c: 3,
},
],
},
];
const expected2 = [{ a: 2, c: 3 }];
const test3 = [
{
a: 2,
b: [
{
c: 3,
},
{ c: 4 },
{ c: 5 },
],
},
];
const expected3 = [
{ a: 2, c: 3 },
{ a: 2, c: 4 },
{ a: 2, c: 5 },
];
let test4 = [
{
a: 2,
b: [
{
c: 3,
d: [
{ e: 4, f: 5 },
{ e: 5, f: 6 },
],
},
{ c: 4, d: [{ e: 7, f: 8 }] },
],
},
];
const expected4 = [
{ a: 2, c: 3, e: 4, f: 5 },
{ a: 2, c: 3, e: 5, f: 6 },
{ a: 2, c: 4, e: 7, f: 8 },
];
const test = (name, res, expected) => {
console.log(
`${name} passed ${JSON.stringify(res) === JSON.stringify(expected)}`
);
//console.log(res, expected);
};
//test("test1", flatten(test1, {}, []), expected1);
//test("test2", flatten(test2, {}, []), expected2);
//test("test3", flatten(test3, {}, []), expected3);
test("test4", flatten(test4, {}, []), expected4);
It's a bit of a behemoth, and it doesn't preserve the keys' order, but it does work with no duplicates.
It is recursive, so watch out for the call stack.
First, loop through the items in the array,
If an item is an array, make a recursive call.
On returning from that call, if the number of objects returned is more than there currently are in the final result, then update the returned objects with the properties from the objects in the final result, being careful to avoid overwriting pre-existing properties.
Otherwise update the final results with the properties in the returned result, again being careful not to overwrite existing properties.
If the item is not an array
If this is the first item put it into the final result
Otherwise add the item's properties to all the items in the final result, without overwriting any.
function makeFlat(arr) //assume you're always passing in an array
{
let objects = [];
arr.forEach(item =>
{
let currentObject = {};
const keys = Object.keys(item);
keys.forEach(key =>
{
const obj = item[key];
if(Array.isArray(obj))
{
let parts = makeFlat(obj);
if(objects.length > 0)
{
if(parts.length > objects.length)
{
parts.forEach(part =>
{
objects.forEach(ob =>
{
Object.keys(ob).forEach(k =>
{
if(Object.keys(part).indexOf(k) == -1)
{
part[k] = ob[k];
}
});
});
});
objects = parts;
}
else
{
objects.forEach(ob =>
{
parts.forEach(part =>
{
Object.keys(part).forEach(k =>
{
if(Object.keys(ob).indexOf(k) == -1)
{
ob[k] = part[k];
}
});
});
});
}
}
else
{
objects = parts;
}
}
else
{
if(Object.keys(currentObject).length == 0)
{
objects.push(currentObject);
}
currentObject[key] = item[key];
objects.forEach(ob =>
{
if(Object.keys(ob).indexOf(key) == -1)
{
ob[key] = currentObject[key]
}
});
}
});
});
return objects;
}
const inp = [{ a: 2, b: [{ c: 3, d: [{e: 4, f: 5}, {e: 5,f: 6}]},
{ c: 4, d: [{e: 7, f: 8}]}
], g:9
}];
let flattened = makeFlat(inp);
flattened.forEach(item => console.log(JSON.stringify(item)));
update: I described the problem in a wrong way and have rewritten the description completely, along with the code that works but is ugly as hell as well as limited.
Let's pretend there's an object
const input = {
a: 1,
b: '2',
c: {
d: true,
e: '4'
},
f: [{
g: 5,
h: {
i: '6'
}
}, {
g: 7,
h: {
i: '8'
}
}]
}
what I'm looking for is a collection of all possible arrangements of nested arrays, with object's keys flattened and joined with ".", like
[{
a: 1,
b: '2',
'c.d': true,
'c.e': '4',
'f.g': 5,
'f.h.i': '6'
}, {
a: 1,
b: '2',
'c.d': true,
'c.e': '4',
'f.g': 7,
'f.h.i': '8'
}]
Note that there are no keys that would have non-primitive values, for example, 'f.h' that would point at an object.
So, what I do first, is collect all the keys, and artificially add # sign to every key that points at an array item, so # kind of means "every index in that array":
function columns(data, prefix = '') {
if (_.isArray(data)) {
return columns(_.first(data), `${prefix}.#`);
} else if (_.isObject(data)) {
return _.filter(_.flatMap(_.keys(data), key => {
return _.concat(
!_.isObject(_.result(data, key)) ? `${prefix}.${key}` : null,
columns(data[key], `${prefix}.${key}`)
);
}));
} else {
return null;
}
}
console.log(columns(input)); // -> [".a", ".b", ".c.d", ".c.e", ".f.#.g", ".f.#.h.i"]
Now, I wield lodash. The leading "." in keys isn't a problem for lodash, so I just leave it as is. With lodash, I squash the object into a one-level object with weird keys:
function flattenKeys(original, keys) {
return _.mapValues(_.groupBy(_.map(keys, key => ({
key,
value: _.result(original, key)
})), 'key'), e => _.result(e, '0.value'));
}
console.log(flattenKeys(input, columns(input))) // -> {".a":1,".b":"2",".c.d":true,".c.e":"4"}
And now I run (in a very wrong way) through every array-like property of original object and produce an array of objects, setting keys like .f.#.h.i with the values of .f.0.h.i for first element, etc.:
function unfold(original, keys, iterables) {
if (!_.isArray(iterables)) {
return unfold(original, keys, _.uniq(_.map(_.filter(keys, key => /#/i.test(key)), key => _.replace(key, /\.\#.*/, ''))));
} else if (_.isEmpty(iterables)) {
return [];
} else {
const first = _.first(iterables);
const rest = _.tail(iterables);
const values = _.result(original, first);
const flatKeys = _.mapKeys(_.filter(keys, key => _.includes(key, first)));
const updated = _.map(values, (v, i) => ({
...flattenKeys(original, keys),
..._.mapValues(flatKeys, k => _.result(original, _.replace(k, /\#/, i)))
}));
return _.concat(updated, unfold(original, keys, rest));
}
}
console.log(unfold(input, columns(input))) // -> [{".a":1,".b":"2",".c.d":true,".c.e":"4",".f.#.g":5,".f.#.h.i":"6"},{".a":1,".b":"2",".c.d":true,".c.e":"4",".f.#.g":7,".f.#.h.i":"8"}]
So in the end, I only need to clean keys, which, in fact, isn't necessary in my case.
The question is, aside of ugliness of the code, how can I make it work with possible multiple array-like properties in original objects?
Now, I understand, that this question is more suitable for CodeReview StackExchange, so if somebody transfers it there, I'm okay with that.
Based on your updated structure, the following recursive function does the trick:
function unfold(input) {
function flatten(obj) {
var result = {},
f,
key,
keyf;
for(key in obj) {
if(obj[key] instanceof Array) {
obj[key].forEach(function(k) {
f = flatten(k);
for(keyf in f) {
result[key+'.'+keyf] = f[keyf];
}
output.push(JSON.parse(JSON.stringify(result))); //poor man's clone object
});
} else if(obj[key] instanceof Object) {
f = flatten(obj[key]);
for(keyf in f) {
result[key+'.'+keyf] = f[keyf];
}
} else {
result[key] = obj[key];
}
}
return result;
} //flatten
var output = [];
flatten(input);
return output;
} //unfold
Snippet:
function unfold(input) {
function flatten(obj) {
var result = {},
f,
key,
keyf;
for(key in obj) {
if(obj[key] instanceof Array) {
obj[key].forEach(function(k) {
f = flatten(k);
for(keyf in f) {
result[key+'.'+keyf] = f[keyf];
}
output.push(JSON.parse(JSON.stringify(result))); //poor man's clone object
});
} else if(obj[key] instanceof Object) {
f = flatten(obj[key]);
for(keyf in f) {
result[key+'.'+keyf] = f[keyf];
}
} else {
result[key] = obj[key];
}
}
return result;
} //flatten
var output = [];
flatten(input);
return output;
} //unfold
const input = {
a: 1,
b: '2',
c: {
d: true,
e: '4'
},
f: [{
g: 5,
h: {
i: '6'
}
}, {
g: 7,
h: {
i: '8'
}
}]
};
document.body.innerHTML+= '<pre>' + JSON.stringify(unfold(input), null, 2) + '</pre>';
I'll leave my original answer, which worked with your original structure:
var o = {a: [{b: 1, c: 2}], d: [{e: 4, f: 5}]},
keys = Object.keys(o),
result = [];
keys.forEach(function(i, idx1) {
keys.forEach(function(j, idx2) {
if(idx2 > idx1) { //avoid duplicates
for(var k in o[i][0]) {
for(var l in o[j][0]) {
result.push({
[i + '.' + k]: o[i][0][k],
[j + '.' + l]: o[j][0][l]
});
}
}
}
});
});
console.log(JSON.stringify(result));
I get an array of objects from a MongoDB through API.
I then need to filter the result furthermore (client side).
I'll work with long lists (could be some thousand of results), each object has about 10 properties with some arrays in it.
Example of an object:
{
_id: xxxxxxx,
foo: [
{ a: "b", c: "d" },
{ a: "b", c: "d" }
],
data: {
a: "b",
c: "d"
}
}
I loop the array async to improve speed:
async.filter(documents, function(value) {
// Search inside the object to check if it contains the given "value"
}, function(results) {
// Will do something with the result array
});
How can I search inside the current object to check if it contains the given value without know in which property I'll find the value?
Though I've not included the async part but I believe overall searching approach could be like this:
// Input Array
var inpArr = [{
id: 1,
foo: [{
a: "dog",
b: "cat"
}]
}, {
id: 2,
foo: [{
a: "kutta",
b: "billi"
}]
}];
var myFilter = function(val, item, index, array) {
var searchResult = scanProperties(item, val);
return searchResult;
};
// Note: pass additional argument to default filter.
// using Function.Prototype.Bind
var filterResult = inpArr.filter(myFilter.bind(null, "dog"));
alert(filterResult);
console.log(filterResult);
// Recursively scan all properties
function scanProperties(obj, val) {
var result = false;
for (var property in obj) {
if (obj.hasOwnProperty(property) && obj[property] != null) {
if (obj[property].constructor == Object) {
result = result || scanProperties(obj[property], val);
} else if (obj[property].constructor == Array) {
for (var i = 0; i < obj[property].length; i++) {
result = result || scanProperties(obj[property][i], val);
}
} else {
result = result || (obj[property] == val);
}
}
}
return result;
};
JS Fiddle Searching an Array of Objects
You can simply iterate through each and every item recursively, like this
var data = {
_id: 1243,
foo: [{
a: "b",
c: "d"
}, {
a: "b",
c: "d"
}],
data: {
a: "b",
c: "d"
}
};
function findValue(value) {
function findItems(document) {
var type = Object.prototype.toString.call(document);
if (type.indexOf("Array") + 1) {
return document.some(findItems);
} else if (type.indexOf("Object") + 1) {
return Object.keys(document).some(function(key) {
return findItems(document[key]);
});
} else {
return document === value;
}
}
return findItems;
}
console.log(findValue('dd')(data));
# false
console.log(findValue('d')(data));
# true
Closed. This question does not meet Stack Overflow guidelines. It is not currently accepting answers.
We don’t allow questions seeking recommendations for books, tools, software libraries, and more. You can edit the question so it can be answered with facts and citations.
Closed 8 years ago.
Improve this question
I wonder if there is some helper JavaScript library that has a function similar to underscore.js's _.extend.
What I am looking for is a function that given an associative array like the following:
{ foo: 1, bar: 2 }
and another "extending" associative array like the following:
{ foo : 3 }
can easily build the following "augmented" structure:
{ foo: [1, 3], bar: 2 }
otherwise I have to do it manually, but this task seems general enough to be a function in some helper library.
clarification with an example on what should happen with different objects:
base object: { foo: 1, bar: 2 }
extending object: { quuz: 3, bar: 4 }
result: { foo: 1, bar: [2, 4], quuz: 3 }
Actually, now it is clear to me that the operation is commutative (base and extending can be switched with the result being always the same)
additional example:
base object: { foo: 1, bar: [2,5] }
extending object: { foo: {a: 'A', b: 'B'} , bar: 4 }
result: { foo: [1, {a: 'A', b: 'B'}], bar: [2, 4, 5], quuz: 3 }
Your requirement is not very clear, what do you want to happen with different objects? But, this is a shallow extend that does what you seem to be asking, or use it as a base for your experiments and tweek it to your design.
Javascript
function curstomExtend(target) {
var typeTarget = typeof target;
if (target === null || typeTarget !== 'object' && typeTarget !== 'function') {
throw new TypeError('target');
}
Array.prototype.slice.call(arguments, 1).forEach(function (source) {
var typeSource = typeof source;
if (source === null || typeSource !== 'object' && typeSource !== 'function') {
throw new TypeError('source');
}
Object.keys(source).forEach(function (key) {
var temp;
if (target.hasOwnProperty(key)) {
if (Array.isArray(target[key])) {
target[key].push(source[key]);
} else {
target[key] = [target[key]];
target[key].push(source[key]);
}
} else {
target[key] = source[key];
}
});
});
return target;
};
var a = {
foo: 1,
bar: 2,
fy: {
a: 1,
b: 2
}
},
b = {
foo: 3,
fy: {
s: 3,
t: 4
}
},
c = {
foo: [4, 5]
},
d = {
foo: {
x: 4,
y: 5
},
fy: {
s: 5,
t: 6
}
};
curstomExtend(a, b, c, d);
console.log(JSON.stringify(a));
Output
{"foo":[1,3,[4,5],{"x":4,"y":5}],"bar":2,"fy":[{"a":1,"b":2},{"s":3,"t":4},{"s":5,"t":6}]}
On jsFiddle
my solution
I wrote the following functions blend and blend_many that, combined,
are able to recursively "blend" the two objects.
/**
* applies blend to all the objects contained in the input list.
* The output is an object that contains a blend of all the input objects.
*/
function blend_many(list) {
var ret = _.reduce(list, blend, {});
for (var key in ret) {
var value = ret[key];
if (_.isArray(value)) {
var all_are_objects = _.reduce(value, function (a, b) {
return a && _.isObject(b);
}, true);
if (all_are_objects) {
// recurse!
value = blend_many(value);
}
}
ret[key] = value;
}
return ret;
}
function blend(base, extending) {
for (key in extending) {
var value = extending[key];
if (_.isArray(value)) {
// recurse
value = blend_many(value);
}
if (base[key] === undefined) {
base[key] = value;
} else if (_.isArray(base[key])) {
base[key].push(value)
} else {
var prev = base[key];
base[key] = [prev, value];
}
}
return base;
}
for example, calling:
list_of_objs = [
{
a: {
foo: 1
}
},
{
a: {
foo: 2
}
}
];
blend_many(list_of_objs);
will return the following object:
{
a: {
foo: [ 1, 2 ]
}
}
(very useful for building mongodb queries!)