I'm looking for a way to convert this array of recursive objects into a flat array of objects to make it easier to work with.
[
{
"name": "bill",
"car": "jaguar",
"age": 30,
"profiles": [
{
"name": "stacey",
"car": "lambo",
"age": 23,
"profiles": [
{
"name": "martin",
"car": "lexus",
"age": 34,
"profiles": []
}
]
}
]
}
]
This is the expected output.
[
{
"name": "bill",
"car": "jaguar",
"age": 30,
},{
"name": "stacey",
"car": "lambo",
"age": 23,
},{
"name": "martin",
"car": "lexus",
"age": 34,
}
]
Each profiles
array can have n
amount of items, which may or may not have an empty array of sub profiles
. Note the converted array objects don't contain profiles
after the conversion.
I'm open to using underscore
or lodash
to achieve this.
I'm looking for a way to convert this array of recursive objects into a flat array of objects to make it easier to work with.
[
{
"name": "bill",
"car": "jaguar",
"age": 30,
"profiles": [
{
"name": "stacey",
"car": "lambo",
"age": 23,
"profiles": [
{
"name": "martin",
"car": "lexus",
"age": 34,
"profiles": []
}
]
}
]
}
]
This is the expected output.
[
{
"name": "bill",
"car": "jaguar",
"age": 30,
},{
"name": "stacey",
"car": "lambo",
"age": 23,
},{
"name": "martin",
"car": "lexus",
"age": 34,
}
]
Each profiles
array can have n
amount of items, which may or may not have an empty array of sub profiles
. Note the converted array objects don't contain profiles
after the conversion.
I'm open to using underscore
or lodash
to achieve this.
7 Answers
Reset to default 3Let's call your original data o
, bining Array.prototype.reduce with recursion I came up with this:
o.reduce(function recur(accumulator, curr) {
var keys = Object.keys(curr);
keys.splice(keys.indexOf('profiles'), 1);
accumulator.push(keys.reduce(function (entry, key) {
entry[key] = curr[key];
return entry;
}, {}));
if (curr.profiles.length) {
return accumulator.concat(curr.profiles.reduce(recur, []));
}
return accumulator;
}, []);
I would use a recursive function and pass the resulting array in it to avoid working with globals, something in the lines of:
var target = [];
var extractElements(source, target) {
//TODO: check if source is array
for (var i=0; i<source.length; i++) {
// create a new element with our data
var newElement = {
name: source[i].name,
car: source[i].car,
age: source[i].age
};
// put it in our flattened array
target.push(newElement);
// check if we need to go deeper and pass our flattened array around
if (source[i].profiles instanceof Array &&
source[i].profiles.length>0)
extractElements(source[i].profiles, target);
}
}
console.log(target) // should list your elements nicely
I haven't tested it, so use it for inspiration but beware :)
(edit1: "var i" in for)
const _ = require('lodash')
const arrayFromObject = (currentObject, currentArray) => {
const {profiles, ...rest} = currentObject
if (!_.isEmpty(currentObject.profiles)) {
return arrayFromObject(currentObject.profiles!, [...currentArray, rest])
}
return [...currentArray, rest]
}
const flatArray = arrayFromObject(myRecursiveObject, [])
Hi this can also be tried...
var out = [];
var i=0;
var extract = function(s, out) {
if(s[0] == null){
i = out.length -1;
return false;
}else {
out.push(s[0]);
}
extract(s[0].profiles, out);
delete out[i--].profiles;
};
extract(a, out); /// here 'a' is the input array and 'out' output
console.log(out);
All the best...
var _ = require('lodash')
/**
* Flatten a array-object via recursive property
* @see {@link http://stackoverflow./questions/31829897/convert-recursive-array-object-to-flat-array-object}
* @param {Array} arr Array of objects with recursive props
* @param {String} recursiveProperty The string of the recursive property
* @return {Array} Flat array of all recursive properties without recursive property
*/
function arrExtract (arr, recursiveProperty) {
var extracted = []
function _arrExtract (children) {
_.each(children, function (item) {
if (item[recursiveProperty] && item[recursiveProperty].length) _arrExtract(item[recursiveProperty])
extracted.push(_.omit(item, recursiveProperty))
})
}
_arrExtract(arr)
return extracted
}
module.exports = arrExtract
Almost three years later and still looking for a one-size fits solution for this. Here it is, heavily influenced by @axelduch's answer.
const {isPlainObject, isArray, get, omit, reduce} = require('lodash')
const recursiveFlatten = (tree, headProp, parentIdProp, parentRefProp, parent = {}) => {
tree = isArray(tree) ? tree : [tree]
return reduce(tree, (acq, current) => {
const currentWithoutHead = omit(current, [headProp])
if (parentIdProp && parentRefProp) currentWithoutHead[parentRefProp] = parent[parentIdProp] || null
acq = [...acq, currentWithoutHead]
const next = get(current, headProp)
if (isPlainObject(next) || isArray(next)) {
parent = currentWithoutHead
acq = [...acq, ...recursiveFlatten(next, headProp, parentIdProp, parentRefProp, parent)]
}
return acq
}, [])
}
Here's a simple example:
const example = recursiveFlatten({
name: 'bill',
love: true,
lovers: [{
name: 'jil',
love: false,
lovers: [{
name: 'diana',
love: false,
lovers: false
}, {
name: 'einstein',
love: false,
lovers: {
name: 'carl sagan',
love: false,
lovers: false
}
}]
}]
}, 'lovers')
[ { name: 'bill', love: true },
{ name: 'jil', love: false },
{ name: 'diana', love: false },
{ name: 'einstein', love: false },
{ name: 'carl sagan', love: false } ]
Here's an example adding parentId
prop via parentRef
.
const example = recursiveFlatten({
name: 'bill',
love: true,
lovers: [{
name: 'jil',
love: false,
lovers: [{
name: 'diana',
love: false,
lovers: false
}, {
name: 'einstein',
love: false,
lovers: {
name: 'carl sagan',
love: false,
lovers: false
}
}]
}]
}, 'lovers', 'name', 'parentName')
[ { name: 'bill', love: true, parentName: null },
{ name: 'jil', love: false, parentName: 'bill' },
{ name: 'diana', love: false, parentName: 'jil' },
{ name: 'einstein', love: false, parentName: 'jil' },
{ name: 'carl sagan', love: false, parentName: 'einstein' } ]
Here's a fairly simple technique that will solve the problem as originally defined.
const recursiveFlatten = (tree) =>
tree .length == 0
? []
: tree .flatMap (({profiles = [], ... rest}) => [{... rest}, ... recursiveFlatten (profiles)])
const tree = [{name: "bill", car: "jaguar", age: 30, profiles: [{name: "stacey", car: "lambo", age: 23, profiles: [{name: "martin", car: "lexus", age: 34, profiles: []}]}]}, {name: "denise", car: "pinto", age: 28}]
console .log (
recursiveFlatten (tree)
)
This hard-codes the name "profiles" and removes it, keeping the rest of the properties intact in the copy generated.
Your own answer suggest substantially more plex requirements. This version handles these through several optional parameters, the way your answer does, although the way it's called changes here and could easily be altered if necessary:
const recursiveFlatten = (headProp, parentIdProp, parentRefProp, parent = {}) => (tree) =>
tree .length == 0
? []
: tree .flatMap (({[headProp]: children = [], ... rest}) => [
{
... rest,
... (parentIdProp && parentRefProp ? {[parentRefProp]: parent[parentIdProp] || null} : {})
},
... recursiveFlatten (headProp, parentIdProp, parentRefProp, rest) (children)
])
const tree = [{name: "bill", car: "jaguar", age: 30, profiles: [{name: "stacey", car: "lambo", age: 23, profiles: [{name: "martin", car: "lexus", age: 34, profiles: []}]}]}, {name: "denise", car: "pinto", age: 28}]
console .log (recursiveFlatten ('profiles') (tree))
console .log (recursiveFlatten ('profiles', 'name', 'parentName') (tree))
I wouldn't be thrilled about this API in my own code-base, though. The differing behaviors depending on how many parameters are passed adds unnecessary plexity. I would probably bury them under an API such as
const recursiveFlatten = (parentIdProp, parentRefProp) => (headProp) => (tree) => ...
Then we could create functions we need, such as using
const flattenProfiles = recursiveFlatten (null, null) ('profiles')
and
const flattenAndExpand = recuriveFlatten ('name', 'parentName') ('profiles')
to replace the two call inside the console .log ()
statements above.