I'm trying to flatten an object where the keys will be the full path to the leaf node. I can recursively identify which are the leaf nodes but stuck trying to construct the whole path.
Sample Input:
{
one: 1,
two: {
three: 3
},
four: {
five: 5,
six: {
seven: 7
},
eight: 8
},
nine: 9
}
Output:
{
one: 1,
'two.three': 3,
'four.five': 5,
'four.six.seven': 7,
'four.eight': 8,
nine: 9
}
You could use a recursive approch and collect the keys of the object. This proposal looks for arrays as well.
function getFlatObject(object) {
function iter(o, p) {
if (o && typeof o === 'object') {
Object.keys(o).forEach(function (k) {
iter(o[k], p.concat(k));
});
return;
}
path[p.join('.')] = o;
}
var path = {};
iter(object, []);
return path;
}
var obj = { one: 1, two: { three: 3 }, four: { five: 5, six: { seven: 7 }, eight: 8 }, nine: 9 },
path = getFlatObject(obj);
console.log(path);
Partial solution :
Give the input as a full path to the function and it gives you the respective output
var obj = {
one: 1,
two: {
three: 3
},
four: {
five: 5,
six: {
seven: 7
},
eight: 8
},
nine: 9
};
function deepFind(obj, path) {
var paths = path.split('.')
, current = obj
, i;
for (i = 0; i < paths.length; ++i) {
if (current[paths[i]] == undefined) {
return undefined;
} else {
current = current[paths[i]];
}
}
return current;
}
console.log(deepFind(obj, 'four.six.seven'))
Using newest JS features like Object spread and Object.entries it should be pretty easy:
function flatObj(obj, path = []) {
let output = {};
Object.entries(obj).forEach(([ key, value ]) => {
const nextPath = [ ...path, key ];
if (typeof value !== 'object') {
output[nextPath.join('.')] = value;
return;
}
output = {
...output,
...flatObj(value, nextPath)
};
});
}
Please note that this code is probably not the most optimal one as it copies the object each time we want to merge it. Treat it more as a gist of what would it look like, rather than a complete and final solution.
var obj = {
one: 1,
two: {
three: 3
},
four: {
five: 5,
six: {
seven: 7
},
eight: 8
},
nine: 9
};
function flatten(obj) {
var flatObj = {}
function makeFlat(obj, path) {
var keys = Object.keys(obj);
if (keys.length) {
keys.forEach(function (key) {
makeFlat(obj[key], (path ? path + "." : path) + key);
})
} else {
flatObj[path] = obj;
}
}
makeFlat(obj, "");
return flatObj;
}
console.log(flatten(obj));
A non fancy approach, internally uses recursion.
var x = { one:1,two:{three:3},four:{five: 5,six:{seven:7},eight:8},nine:9};
var res = {};
var constructResultCurry = function(src){ return constructResult(res,src); }
function constructResult(target, src) {
if(!src) return;
target[src.key] = src.val;
}
function buildPath(key, obj, overAllKey) {
overAllKey += (overAllKey ? "." : "") + key;
if(typeof obj[key] != "object") return { key : overAllKey, val : obj[key] };
Object.keys(obj[key]).forEach(function(keyInner) {
constructResultCurry(buildPath(keyInner, obj[key], overAllKey));
});
}
Object.keys(x).forEach(function(k){
constructResultCurry(buildPath(k, x, ""));
});
console.log(res);
You might simply do as follows;
var obj = {one: 1, two: {three: 3}, four: {five: 5, six: {seven: 7}, eight: 8}, nine: 9},
flatObj = (o,p="") => { return Object.keys(o)
.map(k => o[k] === null ||
typeof o[k] !== "object" ? {[p + (p ? ".":"") + k]:o[k]}
: flatObj(o[k],p + (p ? ".":"") + k))
.reduce((p,c) => Object.assign(p,c));
};
console.log(flatObj(obj));
I find a tiny JavaScript utility to access properties using path. It is called object-path and is an opensource project on GitHub.
To get attribute from an object:
objectPath.get(obj, "a.b");
to set attribute:
objectPath.set(obj, "a.b", value);
to remove an attribute:
objectPath.del(obj, "a.b");
So easy!!
You can achieve it by using this function:
const obj = {
one: 1,
two: {
three: 3
},
four: {
five: 5,
six: {
seven: 7
},
eight: 8
},
nine: 9
}
const flatObject = (obj, keyPrefix = null) =>
Object.entries(obj).reduce((acc, [key, val]) => {
const nextKey = keyPrefix ? `${keyPrefix}.${key}` : key
if (typeof val !== "object") {
return {
...acc,
[nextKey]: val
};
} else {
return {
...acc,
...flatObject(val, nextKey)
};
}
}, {});
console.log(flatObject(obj))
Here is an interative solution using object-scan.
object-scan is a data processing tool, so the main advantage here is that it would be easy to do further processing or processing while extracting the desired information
// const objectScan = require('object-scan');
const myData = { one: 1, two: { three: 3 }, four: { five: 5, six: { seven: 7 }, eight: 8 }, nine: 9 };
const flatten = (data) => {
const entries = objectScan(['**'], {
reverse: false,
rtn: 'entry',
joined: true,
filterFn: ({ isLeaf }) => isLeaf
})(data);
return Object.fromEntries(entries);
};
console.log(flatten(myData));
// => { one: 1, 'two.three': 3, 'four.five': 5, 'four.six.seven': 7, 'four.eight': 8, nine: 9 }
.as-console-wrapper {max-height: 100% !important; top: 0}
<script src="https://bundle.run/object-scan#13.8.0"></script>
Disclaimer: I'm the author of object-scan
Try this
let x;
try{
x = JSON.parse(prompt("Input your JSON"))
}
catch(e) {
alert("not a valid json input")
}
var res = {};
var constructResultCurry = function(src){ return constructResult(res,src); }
function constructResult(target, src) {
if(!src) return;
target[src.key] = src.val;
}
function buildPath(key, obj, overAllKey) {
overAllKey += (overAllKey ? "." : "") + key;
if(typeof obj[key] != "object") return { key : overAllKey, val : obj[key] };
Object.keys(obj[key]).forEach(function(keyInner) {
constructResultCurry(buildPath(keyInner, obj[key], overAllKey));
});
}
Object.keys(x).forEach(function(k){
constructResultCurry(buildPath(k, x, ""));
});
console.log("**************ALL FIELDS****************")
console.log(res);
console.log("******************************************")
let conf = confirm("do you need a specific field from JSON");
if ( conf )
{
let field = prompt("Input field name")
let results = Object.fromEntries(
Object.entries(res).filter(([key]) => (key.toLowerCase()).includes((field.toLowerCase()))))
prompt("Copy to clipboard: Ctrl+C, Enter", JSON.stringify(results));
console.log(results)
}
else {
prompt("Copy to clipboard: Ctrl+C, Enter", JSON.stringify(res));
}
https://jsfiddle.net/amars404/2n9fprz8/57/
Related
So I need a deepSet() function in my React application and I know there are dozens of libraries available for that. However, my requirements are beyond that of the standard deepSet(). My function has to be able to support Arrays, and multiple Arrays in the path.
Example, simple deepSet()
var obj = { one: { two: {three: 'a'}} };
deepSet(obj, 'one.two.three', 'yay');
// {one: {two: { three: 'yay' } } }
What I need it to support (and have working)
var obj = { one: { two: [{three: 'a'}, {three: 'a'}] } };
deepSet(obj, 'one.two[].three', 'yay');
// { one: { two: [{three: 'yay'}, {three: 'yay'}] }};
What I also need it to support, and DO NOT have working yet
var obj = { one: { two: [{three: [{four:'a'}, {four:'b'}]}, {three: [{four:'a'}, {four:'b'}]}]}};
deepSet(obj, 'one.two[].three[].four', 'yay');
// { one: { two: [{three: [{four:'yay'}, {four:'yay'}]}, {three: [{four:'yay'}, {four:'yay'}]}]}};
My problem is that I can't figure out how to get that next level of arrays and iterate over them. I think a recursive approach is best here, but I can't figure that out to handle 2 or more Arrays. I've spent a few hours on this and am turning to SO for help.
Here's the algorithm I have so far, that supports 0 and 1 level of Arrays.
const deepSet = (object, path, value) => {
let fields = path.split('.');
for (let i=0; i<fields.length; i++) {
let f = fields[i];
if (f.indexOf("[]") > -1) {
let arrayfield = f.replace('[]', '');
f = fields[++i];
object[arrayfield].forEach((el, idx) => {
if (isEmpty(el[f])) {
el[f]= {};
}
if (i === fields.length - 1) {
el[f] = value;
}
});
object = object[arrayfield];
}
else {
if (isEmpty(object[f])) {
object[f] = {};
}
if (i === fields.length - 1) {
object[f] = value;
}
object = object[f];
}
};
}
You need to walk through branches as you find these, so that each branch entry becomes a new deepSet.
Example
const deepSet = (obj, path, value) => {
const re = /(\.|\[\]\.)/g;
let i = 0, match = null;
while (match = re.exec(path)) {
const sep = match[0];
const {length} = sep;
const {index} = match;
obj = obj[path.slice(i, index)];
i = index + length;
if (1 < length) {
path = path.slice(i);
obj.forEach(obj => {
deepSet(obj, path, value);
});
return;
}
}
obj[path.slice(i)] = value;
};
var obj = { one: { two: [{three: [{four:'a'}, {four:'b'}]}, {three: [{four:'a'}, {four:'b'}]}]}};
deepSet(obj, 'one.two[].three[].four', 'yay');
This will produce the expected result:
{
"one": {
"two": [
{
"three": [
{
"four": "yay"
},
{
"four": "yay"
}
]
},
{
"three": [
{
"four": "yay"
},
{
"four": "yay"
}
]
}
]
}
}
Hope it helps or, at least, it gave you a hint 👋
You could take a recursive approach
const
deepSet = (object, path, value) => {
let [key, ...keys] = path.split('.');
if (key.endsWith('[]')) {
object = object[key.slice(0, -2)];
Object.keys(object).forEach(k => {
if (keys.length) deepSet(object[k], keys.join('.'), value);
else object[k] = value;
});
} else {
if (keys.length) deepSet(object[key], keys.join('.'), value);
else object[key] = value;
}
},
object = { one: { two: [{ three: [{ four: 'a' }, { four: 'b' }] }, { three: [{ four: 'a' }, { four: 'b' }] }] } };
deepSet(object, 'one.two[].three[].four', 'yay');
console.log(object);
.as-console-wrapper { max-height: 100% !important; top: 0; }
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)));
I ran into this problem, I was able to write solution which can handle array of object (not posted here) or one level deep nested object but i couldn't solve when the given object has nested structure like below. I am curious to know how we can solve this.
const source = {
a: 1,
b: {
c: true,
d: {
e: 'foo'
}
},
f: false,
g: ['red', 'green', 'blue'],
h: [{
i: 2,
j: 3
}]
};
solution should be
const solution = {
'a': 1,
'b.c': true,
'b.d.e': 'foo',
'f': false,
'g.0': 'red',
'g.1': 'green',
'g.2': 'blue',
'h.0.i': 2,
'h.0.j': 3
};
attempt for one deep nested object
let returnObj = {}
let nestetdObject = (source, parentKey) => {
let keys = keyFunction(source);
keys.forEach((key) => {
let values = source[key];
if( typeof values === 'object' && values !== null ) {
parentKey = keys[0]+'.'+keyFunction(values)[0]
nestetdObject(values, parentKey);
}else{
let key = parentKey.length > 0 ? parentKey : keys[0];
returnObj[key] = values;
}
})
return returnObj
};
// Key Extractor
let keyFunction = (obj) =>{
return Object.keys(obj);
}
calling the function
nestetdObject(source, '')
But my attempt will fail if the object is like { foo: { boo : { doo : 1 } } }.
You should be able to do it fairly simply with recursion. The way it works, is you just recursively call a parser on object children that prepend the correct key along the way down. For example (not tested very hard though):
const source = {
a: 1,
b: {
c: true,
d: {
e: 'foo'
}
},
f: false,
g: ['red', 'green', 'blue'],
h: [{
i: 2,
j: 3
}]
}
const flatten = (obj, prefix = '', res = {}) =>
Object.entries(obj).reduce((r, [key, val]) => {
const k = `${prefix}${key}`
if(typeof val === 'object'){
flatten(val, `${k}.`, r)
} else {
res[k] = val
}
return r
}, res)
console.log(flatten(source))
I am very late to the party but it can be easily achieved with a module like Flatify-obj.
Usage:
const flattenObject = require('flatify-obj');
flattenObject({foo: {bar: {unicorn: '🦄'}}})
//=> { 'foo.bar.unicorn': '🦄' }
flattenObject({foo: {unicorn: '🦄'}, bar: 'unicorn'}, {onlyLeaves: true});
//=> {unicorn: '🦄', bar: 'unicorn'}
// Licensed under CC0
// To the extent possible under law, the author(s) have dedicated all copyright
// and related and neighboring rights to this software to the public domain
// worldwide. This software is distributed without any warranty.
const source = {
a: 1,
b: { c: true, d: { e: "foo" } },
f: false,
g: ["red", "green", "blue"],
h: [{ i: 2, j: 3 }],
};
function flatten(source, parentKey, result = {}) {
if (source?.constructor == Object || source?.constructor == Array) {
for (const [key, value] of Object.entries(source)) {
flatten(
value,
parentKey != undefined ? parentKey + "." + key : key,
result
);
}
} else {
result[parentKey] = source;
}
return result;
}
console.log(flatten(source));
one more simple example with Object.keys
const apple = { foo: { boo : { doo : 1 } } }
let newObj = {}
const format = (obj,str) => {
Object.keys(obj).forEach((item)=>{
if(typeof obj[item] ==='object'){
const s = !!str? str+'.'+item:item
format(obj[item],s)
} else {
const m = !!str?`${str}.`:''
newObj[m+item]= obj[item]
}
})
}
format(apple,'')
console.log(newObj)
I have two object, and I want to update the target object values from source, but only the values which exists in the target already.
const target = {
a: 1,
b: 2,
c: {
c_a: 3,
c_b: 4,
}
}
const source = {
a: 10,
c: {
c_a: 30,
c_d: 50,
},
d: 60
}
const result = assignOnlyIntersection(target, source);
JSON.stringify(result)
// {
// a: 10,
// b: 2,
// c: {
// c_a: 30,
// c_b: 4
// }
// }
Is there a good way in ES6 for this? If not, does lodash has a feature for this? If not how can it be solved in a nice way?
A simple recursive function will do:
function assignOnlyIntersection(target, source) {
if (Object(target) !== target || Object(source) !== source)
return source;
for (const p in source)
if (p in target)
target[p] = assignOnlyIntersection(target[p], source[p]);
return target;
}
You need to iterate through keys of target object, check if there is same key on source object and if so, replace the value. You also need recursion if you find an object on the way.
Something like this could do the job:
const target = { a: 1, b: 2, c: { c_a: 3, c_b: 4 }};
const source = { a: 10, c: { c_a: 30, c_d: 50 }, d: 60 };
function assignOnlyIntersection(t, s) {
Object
.keys(s)
.filter(k => k in t)
.forEach(k => {
if (typeof t[k] === 'object') {
assignOnlyIntersection(t[k], s[k]);
} else {
t[k] = s[k];
}
});
return t;
}
const result = assignOnlyIntersection(target, source);
console.log(JSON.stringify(result));
Also note that if you want to support replacing with falsy values like 0 or '', you will need to check for existence with typeof t[k] !== 'undefined'.
As #ghybs noted, if you want to also support replacing with undefined values, you will need to check via in operator k in t instead.
You could take a recursive approach by checking for objects.
function deepAssign(target, source) {
Object.keys(source).forEach(function (k) {
var objectCount = [source, target]
.map(o => o[k])
.map(o => o && typeof o === 'object')
.map(Boolean)
.reduce((a, b) => a + b)
if (!(k in target) || objectCount === 1) {
return;
}
if (objectCount === 2) {
return deepAssign(target[k], source[k]);
}
target[k] = source[k];
});
return target;
}
var source = { a: 10, c: { c_a: 30, c_d: 50, }, d: 60 },
target = { a: 1, b: 2, c: { c_a: 3, c_b: 4, } };
console.log(deepAssign(target, source));
.as-console-wrapper { max-height: 100% !important; top: 0; }
another ES6 approach
non-mutative:
const intersector = (target = {}, source = {}) => {
return Object.keys(source).reduce((acc, prop) => {
if (target.hasOwnProperty(prop)) {
if (typeof source[prop] === 'object' && source[prop] !== null) {
acc[prop] = intersector(target[prop], source[prop]);
}
else {
acc[prop] = source[prop];
}
}
return acc;
}, {});
}
mutative:
const intersector = (target = {}, source = {}) => {
return Object.keys(source).reduce((acc, prop) => {
if (target.hasOwnProperty(prop)) {
if (typeof source[prop] === 'object' && source[prop] !== null) {
acc[prop] = intersector(target[prop], source[prop]);
}
else {
acc[prop] = source[prop];
}
target[prop] = acc[prop];
}
return acc;
}, target);
};
You can combine Array.reduce() and Object.keys() ES6 features to achieve this:
let target = {
a: 1,
b: 2,
c: {
c_a: 3,
c_b: 4,
}
}
let source = {
a: 10,
c: {
c_a: 30,
c_d: 50,
},
d: 60
}
const assignOnlyIntersection = (sourceObj, targetObj) => {
return Object.keys(targetObj).reduce((intersection, key) => {
intersection[key] = (typeof targetObj[key] === 'object') ? assignOnlyIntersection(sourceObj[key], targetObj[key]) : sourceObj[key] || targetObj[key];
return intersection;
}, {})
}
target = assignOnlyIntersection(source, target);
console.log(target);
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));