Flatten nested object/array in javascript - 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)));

Related

insert multiple object with same key-value in array based on condition in javascript

I have object that has multiple values for example
let obj = {
a: "day1",
b: "",
c: "day3",
aa: 10,
bb: 11,
cc: 12,
}
let data = {};
let item = [];
for (let i in obj) {
if (i === 'a') {
data["title"] = obj.a;
data['value'] = obj.aa;
}
if (i === 'b') {
data["title"] = obj.b;
data['value'] = obj.bb;
}
if (i === 'c') {
data["title"] = obj.c;
data['value'] = obj.cc;
}
item.push(data);
}
console.log(item)
but I'm getting only last day3 value in multiple times .
item [
{title:"day3",value:12},
{title:"day3",value:12},
{title:"day3",value:11}
]
I want in the following format
item [
{title:"day1",value:10},
{title:"day3",value:11}
]
please help, thanks in advance.
From the above comments ...
"Of cause the OP gets the last state since the OP always reassigns the same properties (title and value) at one and the same data object. The OP wants to use an array and push at/each time a newly created data object into it."
Peter Seliger can you give me any example of solution – omkar p
const obj = {
a: "day1",
b: "",
c: "day3",
aa: 10,
bb: 11,
cc: 12,
}
const items = [];
// slightly fixed OP approach.
for (const key in obj) {
if (key === 'a') {
items.push({
title: obj.a,
value: obj.aa,
});
}
if (key === 'b') {
items.push({
title: obj.b,
value: obj.bb,
});
}
if (key === 'c') {
items.push({
title: obj.c,
value: obj.cc,
});
}
}
console.log({ items });
// or entirely generic/configurable and maybe even more expressive ...
console.log(
Object
.entries(obj)
.reduce(({ target, valueMap, result }, [key, value]) => {
if (key in valueMap) {
result.push({
title: value,
value: target[valueMap[key]],
});
}
return { target, valueMap, result };
}, {
target: obj,
valueMap: { a: 'aa', b: 'bb', c: 'cc' },
result: [],
}).result
);
.as-console-wrapper { min-height: 100%!important; top: 0; }
You should declare the data variable inside the loop because otherwise you are always changing its value at each iteration and that object is what you are adding to the item variable and its value it's always written over the previous one. If you create a new object at each iteration it will be on its own and no further iteration is going to overwrite its value.
*As it was pointed out in the comments, I added a check in your loop to skip iterations not belonging to the cases: 'a','b','c'; so that your final array won't contain empty objects.
let obj = {
a: "day1",
b: "",
c: "day3",
aa: 10,
bb: 11,
cc: 12,
}
let item = [];
for (let i in obj) {
//this check decides if you want to skip this iteration
//so that you won't have empty object appended to your result array
//if(i!='a' && i!='b' && i!='c')
// continue;
//this check was replaced by the final check later for better mantainance
//this was (before) the only thing I changed
let data = {};
if (i === 'a') {
data["title"] = obj.a;
data['value'] = obj.aa;
}
if (i === 'b') {
data["title"] = obj.b;
data['value'] = obj.bb;
}
if (i === 'c') {
data["title"] = obj.c;
data['value'] = obj.cc;
}
let isEmpty = Object.keys(data).length === 0;
//this will check if data is empty before pushing it to the array
if(!isEmpty)
item.push(data);
}
console.log(item)
Array loading object needs deep copy,
// 多个对象载入 数组中,需要深拷贝,不然键指相同
let objXXX = {
a:123,
b:456,
C:789
};
let arrYYY = [];
arrYYY[0] = JSON.parse(JSON.stringify(objXXX));
arrYYY[1] = JSON.parse(JSON.stringify(objXXX));
arrYYY[0].a = "123";
arrYYY[1].a = "9923";
// 0: Object { a: "123", b: 456, C: 789 }
// 1: Object { a: "9923", b: 456, C: 789 }
console.log(arrYYY);

Iterative depth-first traversal with remembering value's paths

I need help with specific implementation of iterative depth first traversal algorithm.
I have an object like this (it's just an example, object might have more properties and be deeper nested):
const root = {
a: 1,
b: {
c: {
d: {
e: 2,
f: 3,
}
},
g: [
{
h: 4,
i: 5,
},
{
j: 6,
k: 7,
}
]
}
}
What I need is a function that would traverse the whole object and return an array like this:
[
{"a": 1},
{"b.c.d.e": 2},
{"b.c.d.f": 3},
{"b.g.0.h": 4},
{"b.g.0.i": 5},
{"b.g.1.j": 6},
{"b.g.1.k": 7},
]
I managed to create an algorithm that sort of solves my problem, but needs one additional step in the end. Result of the algorithm is an array of strings like that:
[
'a^1',
'b.c.d.e^2',
'b.c.d.f^3',
'b.g.0.h^4',
'b.g.0.i^5',
'b.g.1.j^6',
'b.g.1.k^7'
]
so in order to achieve what I want I have to do one full iteration over the result of my algorithm, split strings by ^ symbol and then create objects based on that.
This is the part that I need help with - how can I improve/change my solution so I don't need to do that last step?
function dft(root) {
let stack = [];
let result = [];
const isObject = value => typeof value === "object";
stack.push(root);
while (stack.length > 0) {
let node = stack.pop();
if (isObject(node)) {
Object.entries(node).forEach(([childNodeKey, childNodeValue]) => {
if (isObject(childNodeValue)) {
const newObject = Object.fromEntries(
Object.entries(childNodeValue).map(([cnk, cnv]) => {
return [`${childNodeKey}.${cnk}`, cnv];
})
);
stack.push(newObject);
} else {
stack.push(`${childNodeKey}^${childNodeValue}`);
}
})
} else {
result.push(node);
}
}
return result.reverse();
}
I'd keep pairs <keys,value> in the stack and only create a string key when storing a newly created object:
function dft(obj) {
let stack = []
let res = []
stack.push([[], obj])
while (stack.length) {
let [keys, val] = stack.pop()
if (!val || typeof val !== 'object') {
res.push({
[keys.join('.')]: val
})
} else {
Object.entries(val).forEach(p => stack.push([
keys.concat(p[0]),
p[1],
]))
}
}
return res.reverse()
}
//
const root = {
a: 1,
b: {
c: {
d: {
e: 2,
f: 3,
}
},
g: [
{
h: 4,
i: 5,
},
{
j: 6,
k: 7,
}
]
}
}
console.log(dft(root))
You can push the childNodeKey childNodeValue pair directly as an object to your result array.
Change
stack.push(`${childNodeKey}^${childNodeValue}`);
to
const newEntry = {}
newEntry[childNodeKey] = childNodeValue
result.push(newEntry);
or with ES2015 syntax (you would need a transpiler for browser compatibility)
result.push({[childNodeKey]: childNodeValue});
Complete function:
const root = {
a: 1,
b: {
c: {
d: {
e: 2,
f: 3,
}
},
g: [
{
h: 4,
i: 5,
},
{
j: 6,
k: 7,
}
]
}
}
function dft(root) {
let stack = [];
let result = [];
const isObject = value => typeof value === "object";
stack.push(root);
while (stack.length > 0) {
let node = stack.pop();
if (isObject(node)) {
Object.entries(node).forEach(([childNodeKey, childNodeValue]) => {
if (isObject(childNodeValue)) {
const newObject = Object.fromEntries(
Object.entries(childNodeValue).map(([cnk, cnv]) => {
return [`${childNodeKey}.${cnk}`, cnv];
})
);
stack.unshift(newObject);
} else {
const newEntry = {}
newEntry[childNodeKey] = childNodeValue
result.push({[childNodeKey]: childNodeValue});
}
})
} else {
result.push(node);
}
}
return result;
}
console.log(dft(root))
As you mentioned, you almost got it complete. Just make the array entry an object just before pushing it into result. By splitting Array.prototype.split('^') you can get 'b.g.0.h^4' >>> ['b.g.0.h', '4']. So, rest is a cake:
if (isObject(node)) {
...
} else {
const keyAndValue = node.split('^')
// approach 1)
// const key = keyAndValue[0]
// const value = keyAndValue[1]
// dynamic key setting
// result.push({[key]: value});
// approach 2)
// or in short,
// dynamic key setting
result.push({[keyAndValue[0]]: keyAndValue[1]});
}
You could use a stack where each item has an iterator over the children, and the path up to that point:
function collect(root) {
const Node = (root, path) =>
({ iter: Object.entries(root)[Symbol.iterator](), path });
const result = [];
const stack = [Node(root, "")];
while (stack.length) {
const node = stack.pop();
const {value} = node.iter.next();
if (!value) continue;
stack.push(node);
const [key, child] = value;
const path = node.path ? node.path + "." + key : key;
if (Object(child) !== child) result.push({ [path]: child });
else stack.push(Node(child, path));
}
return result;
}
const root = {a:1,b:{c:{d:{e:2,f:3}},g:[{h:4,i:5},{j:6,k:7}]}};
console.log(collect(root));
I would suggest that the quickest fix to your code is simply to replace
return result.reverse();
with
return result.reverse()
.map ((s, _, __, [k, v] = s .split ('^')) => ({[k]: v}));
But I also think that we can write code to do this more simply. A function I use often will convert your input into something like this:
[
[["a"], 1],
[["b", "c", "d", "e"], 2],
[["b", "c", "d", "f"], 3],
[["b", "g", 0, "h"], 4],
[["b", "g", 0, "i"], 5],
[["b", "g", 1, "j"], 6],
[["b", "g", 1, "k"], 7]
]
and a fairly trivial wrapper can then convert this to your output. It could look like this:
const pathEntries = (obj) =>
Object (obj) === obj
? Object .entries (obj) .flatMap (
([k, x]) => pathEntries (x) .map (([p, v]) => [[Array.isArray(obj) ? Number(k) : k, ... p], v])
)
: [[[], obj]]
const transform = (o) =>
pathEntries (o)
.map (([k, v]) => ({[k .join ('.')] : v}))
const root = {a: 1, b: {c: {d: {e: 2, f: 3, }}, g: [{h: 4, i: 5, }, {j: 6, k: 7}]}}
console .log (transform (root))
.as-console-wrapper {max-height: 100% !important; top: 0}
I don't know your usecase, but I would find this output generally more helpful:
{
"a": 1,
"b.c.d.e": 2,
"b.c.d.f": 3,
"b.g.0.h": 4,
"b.g.0.i": 5,
"b.g.1.j": 6,
"b.g.1.k": 7
}
(that is, one object with a number of properties, rather than an array of single-property objects.)
And we could do this nearly as easily, with a small change to transform:
const transform = (o) =>
pathEntries (o)
.reduce ((a, [k, v]) => ((a[k .join ('.')] = v), a), {})

flattening the nested object in javascript

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)

Transform JS collection to an object with specific keys and grouped values

I would like to create this JS function with these arguments:
transform([{a:1, b:'1', c:true},{a:'1', b:2, c:3, d:false}, {a:1, c:'test'}], ['a','b','c']);
First argument is an array of objects
Second one is array of keys.
I would like to get this output object:
{a:[1, '1', 1], b:['1', 2],c:[true, 3, 'test']}
As you can see the second argument became the keys to the created object
and all values under these keys where grouped together.
And maybe an option to pass a unique argument to function and get this (duplicate values removed):
{a:[1, '1'], b:['1', 2], c:[true, 3, 'test']}
What is the fast and/or elegant way to do it?
Is there any lodash/underscore helper for it?
As an additional generalism. How can the input (the first argument) be a generic collection with nested levels (array or object of nested levels of arrays or objects) ?
Thanks.
You can use Array.prototype.reduce
let param1 = [{a:1,b:'1',c:true},{a:'1',b:2,c:3,d:false},{a:1,c:'test'}];
let param2 = ['a', 'b', 'c'];
function test(objArr, keys) {
let returnObject = {};
keys.forEach(key => returnObject[key] = []);
return objArr.reduce((ret, obj) => {
keys.forEach(key => {
if (obj[key] !== undefined)
ret[key].push(obj[key]);
});
return ret;
}, returnObject);
}
console.log(JSON.stringify(test(param1, param2)));
Outputs:
{"a":[1,"1",1],"b":["1",2],"c":[true,3,"test"]}
Try this:
function transform(data,keys){
let results = {};
//loop all you keys
keys.forEach(index => {
//loop your arrays
data.forEach(element => {
//if there is a match add the key to the results object
if(index in element) {
if(!(index in results)) results[index] = [];
//check if a value already exists for a given key.
if(!(element[index] in results[index])) results[index].push(element[index]);
}
});
});
return results;
}
console.log(transform([{a:1,b:'1',c:true},{a:'1',b:2,c:3,d:false},{a:1,c:'test'}], ['a','b','c']));
You can loop over the key array and pass this key to another function which will use forEach method. This getMatchedKeyValues using forEachwill return an array of elements whose key matches
var arr = [{
a: 1,
b: '1',
c: true
}, {
a: '1',
b: 2,
c: 3,
d: false
}, {
a: 1,
c: 'test'
}];
var keys = ['a', 'b', 'c']
function transform(keyArray) {
var newObj = {};
// looping over key array
keyArray.forEach(function(item) {
// adding key property and calling a function which will return
// an array of elements whose key is same
newObj[item] = getMatchedKeyValues(item)
})
return newObj;
}
function getMatchedKeyValues(keyName) {
var valArray = [];
arr.forEach(function(item) {
if (item[keyName]) {
valArray.push(item[keyName])
}
})
return valArray;
}
console.log(transform(keys))
I coded below , pls have a look this solution.
function test(arr, arr1) {
return arr.reduce((total, current) => {
arr1.forEach(curr => {
if (typeof total[curr] === "undefined") total[curr] = [];
if (current[curr]) total[curr].push(current[curr]);
});
return total;
}, {});
}
console.log(
test(
[
{ a: 1, b: "1", c: true },
{ a: "1", b: 2, c: 3, d: false },
{ a: 1, c: "test" }
],
["a", "b", "c"]
)
);

Unfold a plain-object with array of plain-objects into a flat plain-object

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));

Categories

Resources