Here's a totally recursion-free method using lodash. It occurred to me when I was thinking about how nice _.set and _.get were, and I realized I could replace object "paths" with sequences of children.
First, build an object/hash table with keys equal to the name properties of input array:
var names = _.object(_.pluck(input, 'name'));
// { foo: undefined, foo.bar: undefined, buzz.fizz: undefined, foo.hello.world: undefined }
(Don't try to JSON.stringify this object! Since its values are all undefined, it evaluates to {}…)
Next, apply two transforms on each of the elements: (1) cleanup title and subtitle into a sub-property data, and (2) and this is a bit tricky, find all intermediate paths like buzz and foo.hello that aren't represented in input but whose children are. Flatten this array-of-arrays and sort them by the number of . in the name field.
var partial = _.flatten(
input.map(o =>
{
var newobj = _.omit(o, 'title,subtitle'.split(','));
newobj.data = _.pick(o, 'title,subtitle'.split(','));
return newobj;
})
.map(o => {
var parents = o.name.split('.').slice(0, -1);
var missing =
parents.map((val, idx) => parents.slice(0, idx + 1).join('.'))
.filter(name => !(name in names))
.map(name => {
return {
name,
data : {},
}
});
return missing.concat(o);
}));
partial = _.sortBy(partial, o => o.name.split('.').length);
This code may seem intimidating but seeing what it outputs should convince you it's pretty straightforward: it's just a flat array containing the original input plus all intermediate paths that aren't in input, sorted by the number of dots in name, and a new data field for each.
[
{
"name": "foo",
"url": "/somewhere1",
"templateUrl": "foo.tpl.html",
"data": {
"title": "title A",
"subtitle": "description A"
}
},
{
"name": "buzz",
"data": {}
},
{
"name": "foo.bar",
"url": "/somewhere2",
"templateUrl": "anotherpage.tpl.html",
"data": {
"title": "title B",
"subtitle": "description B"
}
},
{
"name": "buzz.fizz",
"url": "/another/place",
"templateUrl": "hello.tpl.html",
"data": {
"title": "title C",
"subtitle": "description C"
}
},
{
"name": "foo.hello",
"data": {}
},
{
"name": "foo.hello.world",
"url": "/",
"templateUrl": "world.tpl.html",
"data": {
"title": "title D",
"subtitle": "description D"
}
}
]
We're almost home free. The last remaining bit of magic requires storing some global state. We're going to loop over this flat partial array, replacing the name field with a path that _.get and _.set can consume containing children and numerical indexes:
foo gets mapped to children.0
buzz to children.1,
foo.bar to children.0.children.0, etc.
As we iteratively (not recursively!) build this sequence of paths, we use _.set to inject each element of partial above into its appropriate place.
Code:
var name2path = {'empty' : ''};
var out = {};
partial.forEach(obj => {
var split = obj.name.split('.');
var par = name2path[split.slice(0, -1).join('.') || "empty"];
var path = par + 'children.' + (_.get(out, par + 'children') || []).length;
name2path[obj.name] = path + '.';
_.set(out, path, obj);
});
out = out.children;
This object/hash name2path converts names to _.settable paths: it's initialized with a single key, empty, and the iteration adds to it. It's helpful to see what this name2path is after this code is run:
{
"empty": "",
"foo": "children.0.",
"buzz": "children.1.",
"foo.bar": "children.0.children.0.",
"buzz.fizz": "children.1.children.0.",
"foo.hello": "children.0.children.1.",
"foo.hello.world": "children.0.children.1.children.0."
}
Note how the iteration increments indexes to store more than one entry in a children property array.
The final resulting out:
[
{
"name": "foo",
"url": "/somewhere1",
"templateUrl": "foo.tpl.html",
"data": {
"title": "title A",
"subtitle": "description A"
},
"children": [
{
"name": "foo.bar",
"url": "/somewhere2",
"templateUrl": "anotherpage.tpl.html",
"data": {
"title": "title B",
"subtitle": "description B"
}
},
{
"name": "foo.hello",
"data": {},
"children": [
{
"name": "foo.hello.world",
"url": "/",
"templateUrl": "world.tpl.html",
"data": {
"title": "title D",
"subtitle": "description D"
}
}
]
}
]
},
{
"name": "buzz",
"data": {},
"children": [
{
"name": "buzz.fizz",
"url": "/another/place",
"templateUrl": "hello.tpl.html",
"data": {
"title": "title C",
"subtitle": "description C"
}
}
]
}
]
The embedded snippet contains just code without intermediate JSON to distract you.
Is this better than my previous submission? I think so: there's a lot less bookkeeping here, less opaque "busy code", more high-level constructs. The lack of recursion I think helps. I think the final forEach might be replaced with a reduce, but I didn't try that because the rest of the algorithm is so vector-based and iterative, I didn't want to diverge from that.
And sorry to have left everything in ES6, I love it so much :)
var input = [{
name: 'foo',
url: '/somewhere1',
templateUrl: 'foo.tpl.html',
title: 'title A',
subtitle: 'description A'
}, {
name: 'foo.bar',
url: '/somewhere2',
templateUrl: 'anotherpage.tpl.html',
title: 'title B',
subtitle: 'description B'
}, {
name: 'buzz.fizz',
url: '/another/place',
templateUrl: 'hello.tpl.html',
title: 'title C',
subtitle: 'description C'
}, {
name: 'foo.hello.world',
url: '/',
templateUrl: 'world.tpl.html',
title: 'title D',
subtitle: 'description D'
}];
var names = _.object(_.pluck(input, 'name'));
var partial = _.flatten(
input.map(o => {
var newobj = _.omit(o, 'title,subtitle'.split(','));
newobj.data = _.pick(o, 'title,subtitle'.split(','));
return newobj;
})
.map(o => {
var parents = o.name.split('.').slice(0, -1);
var missing =
parents.map((val, idx) => parents.slice(0, idx + 1).join('.'))
.filter(name => !(name in names))
.map(name => {
return {
name,
data: {},
}
});
return missing.concat(o);
}));
partial = _.sortBy(partial, o => o.name.split('.').length);
var name2path = {
'empty': ''
};
var out = {};
partial.forEach(obj => {
var split = obj.name.split('.');
var par = name2path[split.slice(0, -1).join('.') || "empty"];
var path = par + 'children.' + (_.get(out, par + 'children') || []).length;
name2path[obj.name] = path + '.';
_.set(out, path, obj);
});
out = out.children;
input[].namesplit by.? Or can you formalize that relationship further?