I have an array of objects as following :
[
{"id":1,"lib":"A","categoryID":10,"categoryTitle":"Cat10","moduleID":"2","moduleTitle":"Module 2"},
{"id":2,"lib":"B","categoryID":10,"categoryTitle":"Cat10","moduleID":"2","moduleTitle":"Module 2"},
...
{"id":110,"lib":"XXX","categoryID":90,"categoryTitle":"Cat90","moduleID":"4","moduleTitle":"Module 4"}
]
I want to group this array by (moduleID,moduleTitle) and then by (categoryID,categoryTitle).
This is what I tried :
function groupBy(data, id, text) {
return data.reduce(function (rv, x) {
var el = rv.find(function(r){
return r && r.id === x[id];
});
if (el) {
el.children.push(x);
} else {
rv.push({ id: x[id], text: x[text], children: [x] });
}
return rv;
}, []);
}
var result = groupBy(response, "moduleID", "moduleTitle");
result.forEach(function(el){
el.children = groupBy(el.children, "categoryID", "categoryTitle");
});
The above code is working as expected, but as you can see, after the first grouping I had to iterate again over the array which was grouped by the moduleId in order to group by the categoryId.
How can I modify this code so I can only call groupBy function once on the array ?
Edit:
Sorry this might be late, but I want this done by using ES5, no Shim and no Polyfill too.
Here's one possible (although may be a bit advanced) approach:
class DefaultMap extends Map {
constructor(factory, iter) {
super(iter || []);
this.factory = factory;
}
get(key) {
if (!this.has(key))
this.set(key, this.factory());
return super.get(key);
}
}
Basically, it's the a Map that invokes a factory function when a value is missing. Now, the funny part:
let grouper = new DefaultMap(() => new DefaultMap(Array));
for (let item of yourArray) {
let firstKey = item.whatever;
let secondKey = item.somethingElse;
grouper.get(firstKey).get(secondKey).push(item);
}
For each firstKey this creates a Map inside grouper, and the values of those maps are arrays grouped by the second key.
A more interesting part of your question is that you're using compound keys, which is quite tricky in JS, since it provides (almost) no immutable data structures. Consider:
items = [
{a: 'one', b: 1},
{a: 'one', b: 1},
{a: 'one', b: 2},
{a: 'two', b: 2},
]
let grouper = new DefaultMap(Array);
for (let x of items) {
let key = [x.a, x.b]; // wrong!
grouper.get(key).push(x);
}
So, we're naively grouping objects by a compound key and expecting to see two objects under ['one', 1] in our grouper (which is one level for the sake of the example). Of course, that won't work, because each key is a freshly created array and all of them are different for Map or any other keyed storage.
One possible solution is to create an immutable structure for each key. An obvious choice would be to use Symbol, e.g.
let tuple = (...args) => Symbol.for(JSON.stringify(args))
and then
for (let x of items) {
let key = tuple(x.a, x.b); // works
grouper.get(key).push(x);
}
You could extend your function by using an array for the grouping id/names.
function groupBy(data, groups) {
return data.reduce(function (rv, x) {
groups.reduce(function (level, key) {
var el;
level.some(function (r) {
if (r && r.id === x[key[0]]) {
el = r;
return true;
}
});
if (!el) {
el = { id: x[key[0]], text: x[key[1]], children: [] };
level.push(el);
}
return el.children;
}, rv).push({ id: x.id, text: x.lib });
return rv;
}, []);
}
var response = [{ id: 1, lib: "A", categoryID: 10, categoryTitle: "Cat10", moduleID: "2", moduleTitle: "Workflow" }, { id: 2, lib: "B", categoryID: 10, categoryTitle: "Cat10", moduleID: "2", moduleTitle: "Module 2" }, { id: 110, lib: "XXX", categoryID: 90, categoryTitle: "Cat90", moduleID: "4", moduleTitle: "Module 4" }],
result = groupBy(response, [["moduleID", "moduleTitle"], ["categoryID", "categoryTitle"]]);
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }
Version with path as id.
function groupBy(data, groups) {
return data.reduce(function (rv, x) {
var path = [];
var last = groups.reduce(function (level, key, i) {
path.length = i;
path[i] = key[0].slice(0, -2).toUpperCase() + ':' + x[key[0]];
var id = path.join(';'),
el = level.find(function (r) {
return r && r.id === id;
});
if (!el) {
el = { id: path.join(';'), text: x[key[1]], children: [] };
level.push(el);
}
return el.children;
}, rv);
last.push({ id: path.concat('NODE:' + x.id).join(';') });
return rv;
}, []);
}
var response = [{ id: 1, lib: "A", categoryID: 10, categoryTitle: "Cat10", moduleID: "2", moduleTitle: "Workflow" }, { id: 2, lib: "B", categoryID: 10, categoryTitle: "Cat10", moduleID: "2", moduleTitle: "Module 2" }, { id: 110, lib: "XXX", categoryID: 90, categoryTitle: "Cat90", moduleID: "4", moduleTitle: "Module 4" }];
var result = groupBy(response, [["moduleID", "moduleTitle"], ["categoryID", "categoryTitle"]]);
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }
You could do it like this:
const exit = Symbol("exit");
function groupBy(arr, ...props){
const root = {};
for(const el of arr){
const obj = props.map(key => el[key])
.reduce((obj, key) => obj[key] || (obj[key] = {}), root);
(obj[exit] || (obj[exit] = [])).push(el);
}
}
So you can access it like:
const grouped = groupBy(response, "moduleID", "moduleTitle");
console.log( grouped[2]["workflow"][exit] );
You might leave away that exit symbol, but it feels a bit wrong to mix a nested tree with arrays.
Related
I'm trying to find a specific Object in a nested Object by id and wrote this function, which works like a charm:
const findNestedObjById = (tree, myFunction, id) => {
if(tree.attributes.node_id === id){
myFunction(tree)
} else{
if(tree.children){
tree.children.forEach(child => {
findNestedObjById(child, myFunction, id)
});
}
}
};
const doThat = (tree) => {
console.log("Got it: " + tree.name)
}
findNestedObjById(myObj, doThat, "0.1.2.1");
But i want to be able to get the "path" of the object (e.g. myObj.children[0].children[2]) (The children property of my object is an array)
So I wanted to rewrite the function using a fori loop instead of a foreach, so that I could later add the index of the array (saved in i of the fori loop at the time) to a path string.
So I wanted to start with this function:
const findWithFori = (tree, myFunction, id) => {
if(tree.attributes.node_id === id){
myFunction(tree)
} else{
if(tree.children){
for (let i = 0; i < tree.length; i++) {
const child = tree.children[i];
findNestedObjById(child, myFunction, id)
}
}
}
};
But it doenst work, it's able to locate the object by id, if the inital myObj already has the right id, but it doesn't find nested objects, like the first function does and I don't understand why.
If it helps answerign the question, myObj looks like this btw.:
const myObj = {
name: "Mein zweiter Baum",
attributes: {
node_id: "0"
},
children: [
{
name: "Lorem",
attributes: {
node_id: "0.1",
done: true
},
children: [
{
name: "Ipsum",
attributes: {
node_id: "0.1.1",
done: true
},
children: [
{
name: "Dolor",
attributes: {
node_id: "0.1.1.1",
done: false
}
}
]
},
{
name: "Sit",
attributes: {
node_id: "0.1.2",
done: false
},
children: [
{
name: "Anet",
attributes: {
node_id: "0.1.2.1"
}
}
]
}
]
}
]
};
You could return the indices.
If an item is found return an empty array, or undefined. Inside of some get the result of children and if not undefined add the actual index in front of the array.
const
findNestedObjById = (tree, id, callback) => {
if (tree.attributes.node_id === id) {
callback(tree);
return [];
}
if (tree.children) {
let path;
tree.children.some((child, index) => {
path = findNestedObjById(child, id, callback);
if (path) {
path.unshift(index);
return true;
}
});
return path;
}
},
doThat = tree => {
console.log("Got it: " + tree.name);
},
data = { name: "Mein zweiter Baum", attributes: { node_id: "0" }, children: [{ name: "Lorem", attributes: { node_id: "0.1", done: true }, children: [{ name: "Ipsum", attributes: { node_id: "0.1.1", done: true }, children: [{ name: "Dolor", attributes: { node_id: "0.1.1.1", done: false } }] }, { name: "Sit", attributes: { node_id: "0.1.2", done: false }, children: [{ name: "Anet", attributes: { node_id: "0.1.2.1" } }] }] }] }
console.log(findNestedObjById(data, "0.1.2.1", doThat)); // [0, 1, 0]
.as-console-wrapper { max-height: 100% !important; top: 0; }
I would do this by building atop some reusable functions. We can write a function that visits a node and then recursively visits all its children's nodes. To use this for a find, however, we want to be able to stop once its found, so a generator function would make sense here. We can extend a basic version of this 1 to allow each stop to include not only the values, but also their paths.
Then we can layer on a generic find-path-by-predicate function, testing each node it generates until one matches the predicate.
Finally we can easily write a function using this to search by node_id. It might look like this:
function * visit (value, path = []) {
yield {value, path}
for (let i = 0; i < (value .children || []) .length; i ++) {
yield * visit (value .children [i], path .concat (i))
}
}
const findDeepPath = (fn) => (obj) => {
for (let o of visit (obj)) {
if (fn (o .value)) {return o .path}
}
}
const findPathByNodeId = (id) =>
findDeepPath (({attributes: {node_id}}) => node_id === id)
const myObj = {name: "Mein zweiter Baum", attributes: {node_id: "0"}, children: [{name: "Lorem", attributes: {node_id: "0.1", done: true}, children: [{name: "Ipsum", attributes: {node_id: "0.1.1", done: true}, children: [{name: "Dolor", attributes: {node_id: "0.1.1.1", done: false}}]}, {name: "Sit", attributes: {node_id: "0.1.2", done: false}, children: [{name: "Anet", attributes: {node_id: "0.1.2.1"}}]}]}]}
console .log (findPathByNodeId ('0.1.2.1') (myObj)) //=> [0, 1, 0]
If we want to return the node and the path, it's simply a matter of replacing
if (fn (o .value)) {return o .path}
with
if (fn (o .value)) {return o}
and we would get back something like:
{
value: {attributes: {node_id: "0.1.2.1"}, name: "Anet"},
path: [0, 1, 0],
}
1 A basic version for nodes without their paths might look like this:
function * visit (obj) {
yield obj
for (let child of (obj .children || [])) {
yield * visit (child)
}
}
and we might write a generic search for values matching a predicate with
const findDeep = (fn) => (obj) => {
for (let o of visit (obj)) {
if (fn (o)) {return o}
}
}
Layering in the path handling adds some complexity, but not a great deal.
I have an array which I need to combine with comma-separated of the same level and form a new array.
Input:
let arr = [
[{ LEVEL: 1, NAME: 'Mark' }, { LEVEL: 1, NAME: 'Adams' }, { LEVEL: 2, NAME: 'Robin' }],
[{ LEVEL: 3, NAME: 'Williams' }],
[{ LEVEL: 4, NAME: 'Matthew' }, { LEVEL: 4, NAME: 'Robert' }],
];
Output
[
[{ LEVEL: 1, NAME: 'Mark,Adams' }, { LEVEL: 2, NAME: 'Robin' }],
[{ LEVEL: 3, NAME: 'Williams' }],
[{ LEVEL: 4, NAME: 'Matthew,Robert' }],
];
I tried with the following code but not getting the correct result
let finalArr = [];
arr.forEach(o => {
let temp = finalArr.find(x => {
if (x && x.LEVEL === o.LEVEL) {
x.NAME += ', ' + o.NAME;
return true;
}
if (!temp) finalArr.push(o);
});
});
console.log(finalArr);
You could map the outer array and reduce the inner array by finding the same level and add NAME, if found. Otherwise create a new object.
var data = [[{ LEVEL: 1, NAME: "Mark" }, { LEVEL: 1, NAME: "Adams" }, { LEVEL: 2, NAME: "Robin"}], [{ LEVEL: 3, NAME: "Williams" }], [{ LEVEL: 4, NAME: "Matthew" }, { LEVEL: 4, NAME: "Robert" }]],
result = data.map(a => a.reduce((r, { LEVEL, NAME }) => {
var temp = r.find(q => q.LEVEL === LEVEL);
if (temp) temp.NAME += ',' + NAME;
else r.push({ LEVEL, NAME });
return r;
}, []));
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }
Assuming you only want to merge within the same array and not across arrays, and assuming there aren't all that many entries (e.g., fewer than several hundred thousand), the simple thing is to build a new array checking to see if it already has the same level in it:
let result = arr.map(entry => {
let newEntry = [];
for (const {LEVEL, NAME} of entry) {
const existing = newEntry.find(e => e.LEVEL === LEVEL);
if (existing) {
existing.NAME += "," + NAME;
} else {
newEntry.push({LEVEL, NAME});
}
}
return newEntry;
});
let arr= [
[{"LEVEL":1,"NAME":"Mark"},
{"LEVEL":1,"NAME":"Adams"},
{"LEVEL":2,"NAME":"Robin"} ],
[{"LEVEL":3,"NAME":"Williams"}],
[{"LEVEL":4,"NAME":"Matthew"},
{"LEVEL":4,"NAME":"Robert"}]
];
let result = arr.map(entry => {
let newEntry = [];
for (const {LEVEL, NAME} of entry) {
const existing = newEntry.find(e => e.LEVEL === LEVEL);
if (existing) {
existing.NAME += "," + NAME;
} else {
newEntry.push({LEVEL, NAME});
}
}
return newEntry;
});
console.log(result);
If the nested arrays can be truly massively long, you'd want to build a map rather than doing the linear search (.find) each time.
I'd try to do as much of this in constant time as possible.
var m = new Map();
array.forEach( refine.bind(m) );
function refine({ LABEL, NAME }) {
var o = this.get(NAME)
, has = !!o
, name = NAME
;
if (has) name = `${NAME}, ${o.NAME}`;
this.delete(NAME);
this.set(name, { NAME: name, LABEL });
}
var result = Array.from( m.values() );
I haven't tested this as I wrote it on my phone at the airport, but this should at least convey the approach I would advise.
EDIT
Well... looks like the question was edited... So... I'd recommend adding a check at the top of the function to see if it's an array and, if so, call refine with an early return. Something like:
var m = new Map();
array.forEach( refine.bind(m) );
function refine(item) {
var { LABEL, NAME } = item;
if (!NAME) return item.forEach( refine.bind(this) ); // assume array
var o = this.get(NAME)
, has = !!o
, name = NAME
;
if (has) name = `${NAME}, ${o.NAME}`;
this.delete(NAME);
this.set(name, { NAME: name, LABEL });
}
var result = Array.from( m.values() );
That way, it should work with both your original question and your edit.
EDIT
Looks like the question changed again... I give up.
Map the array values: every element to an intermediate object, then create the desired object from the resulting entries:
const basicArr = [
[{"LEVEL":1,"NAME":"Mark"},
{"LEVEL":1,"NAME":"Adams"},
{"LEVEL":2,"NAME":"Robin"} ],
[{"LEVEL":3,"NAME":"Williams"}],
[{"LEVEL":4,"NAME":"Matthew"},
{"LEVEL":4,"NAME":"Robert"}]
];
const leveled = basicArr.map( val => {
let obj = {};
val.forEach(v => {
obj[v.LEVEL] = obj[v.LEVEL] || {NAME: []};
obj[v.LEVEL].NAME = obj[v.LEVEL].NAME.concat(v.NAME);
});
return Object.entries(obj)
.map( ([key, val]) => ({LEVEL: +key, NAME: val.NAME.join(", ")}));
}
);
console.log(leveled);
.as-console-wrapper { top: 0; max-height: 100% !important; }
if you want to flatten all levels
const basicArr = [
[{"LEVEL":1,"NAME":"Mark"},
{"LEVEL":1,"NAME":"Adams"},
{"LEVEL":2,"NAME":"Robin"} ],
[{"LEVEL":3,"NAME":"Williams"}],
[{"LEVEL":4,"NAME":"Matthew"},
{"LEVEL":4,"NAME":"Robert"},
{"LEVEL":2,"NAME":"Cynthia"}],
[{"LEVEL":3,"NAME":"Jean"},
{"LEVEL":4,"NAME":"Martha"},
{"LEVEL":2,"NAME":"Jeff"}],
];
const leveled = basicArr.map( val => Object.entries (
val.reduce( (acc, val) => {
acc[val.LEVEL] = acc[val.LEVEL] || {NAME: []};
acc[val.LEVEL].NAME = acc[val.LEVEL].NAME.concat(val.NAME);
return acc;
}, {}))
.map( ([key, val]) => ({LEVEL: +key, NAME: val.NAME.join(", ")})) )
.flat() // (use .reduce((acc, val) => acc.concat(val), []) for IE/Edge)
.reduce( (acc, val) => {
const exists = acc.filter(x => x.LEVEL === val.LEVEL);
if (exists.length) {
exists[0].NAME = `${val.NAME}, ${exists.map(v => v.NAME).join(", ")}`;
return acc;
}
return [... acc, val];
}, [] );
console.log(leveled);
.as-console-wrapper { top: 0; max-height: 100% !important; }
ES6 way:
let say attributes is multidimensional array having multimple entries which need to combine like following:
let combinedArray = [];
attributes.map( attributes => {
combined = combinedArray.concat(...attributes);
});
I try to write a function in JavaScript which filter an array by a selected property (an value).
But it works for 2 level only I do not understand what do I missing.
The data I want to filter:
var data = [
{
name: "john_pc",
children: [
{
name: "sabrina_pc",
children: [
{
name: "sabrina_pc"
},
{
name: "john_pc"
}
]
},
{
name: "john_pc"
}
]
},
{
name: "sabrina_pc"
}
]
The childrenFilter funciton :
const childrenFilter = (childrenData, filters) => {
let filteredData = childrenData.filter(item => {
for (var property in filters) {
var optionalValues = filters[property];
var value = item[property];
if (item.children) {
item.children = childrenFilter(item.children, filters);
}
let hasValue = value == optionalValues;
if (hasValue) {
return true;
}
return false;
}
return false;
}, this);
return filteredData;
}
Calling the function:
As you can see the 'childrenFilter' get an object which the key is property in the data and the key is value I want to keep.
let result = childrenFilter(data, {
"name": "a1"
});
console.log(JSON.stringify(result, null, 2))
The wanted result :
[
{
"name": "john_pc",
"children": [
{
"name": "sabrina_pc",
"children": [
{
"name": "john_pc"
}
]
},
{
"name": "john_pc"
}
]
}
]
Your filter function does not take into account whether or not children elements match the pattern, therefore even though some child elements of the object match the pattern, the object itself is being filtered out.
Here is the explanation:
{
name: "a2", // does not match filter {name:'a1} so is removed alongside child objects
children: [ // gets removed with parent object
{
name: "a2"
},
{
name: "a1"
}
]
}
This should produce the desired output:
const childrenFilter = (childrenData, filters) => {
let filteredData = childrenData.filter(item => {
for (var property in filters) {
var optionalValues = filters[property];
var value = item[property];
if (item.children) {
item.children = childrenFilter(item.children, filters);
}
let hasValue = value == optionalValues;
if (hasValue || item.children.length) { // include item when children mathes the pattern
return true;
}
return false;
}
return false;
}, this);
return filteredData;
}
You could build new array for each step of filtering, beginning from the leaves and check if this contains the wanted value.
This approach generates new objects and does not mutate the original data.
function filter(array, filters) {
return array.reduce((r, o) => {
var children = filter(o.children || [], filters);
return children || Object.entries(filters).every(([k, v]) => o[k] === v)
? (r || []).concat(Object.assign({}, o, children && { children }))
: r;
}, undefined);
}
var data = [{ name: "a1", children: [{ name: "a2", children: [{ name: "a2" }, { name: "a1" }] }, { name: "a1" }] }, { name: "b1" }];
console.log(filter(data, { name: "a1" }));
.as-console-wrapper { max-height: 100% !important; top: 0; }
I have two arrays, with nested objects, downloaded as part of calls to API endpoints, one (preview) has just numbers.
Example:
[{
obj1:[1, 2],
obj2:[3, 4]
}]
I had to make a second call to another endpoint, to get a list of IDs with strings
Example:
[{
obj1:[{
id:1,
name:'string_name1'
}, {
id:2,
name:'string_name2'
}]
}, {
obj2:[{
id:3,
name:'string_name3'
}, {
id:4,
name:'string_name4'
}]
}];
I need to match the IDs to the first array of objects numbers, so I have strings/text values to display on my web page
I have 2 functions
The first one, pulls the numbers from the preview array and pushes them to my own editable array that I will use to display on the page
This is the array before function runs
objName = [['obj1'], ['obj2']];
This is the first function, matches the names in preview to the names in my array and pushes values
setNumbers(){
for(let i = 0; i < this.objName.length; i++){
for(var name in this.preview[0]) {
if (name == this.objName[i][0]){
for(var val in this.preview[0][name]) {
this.objName[i].push(this.preview[0][name][val])
}
}
}
}
this.setStrings()
}
The second matches the IDs in fields to the numbers in objName and replaces with the string value
public setStrings(){
let feildId, feildName;
for(let i = 0; i < this.fields.length; i++){
var obj = this.fields[i]
for(var name in obj) {
if(this.objName[i][0] == name){
for(let j = 0; j < obj[name].length; j++){
feildId = obj[name][j].id
feildName = obj[name][j].name;
for(let x = 0; x < this.objName[i].length; x++){
if (this.objName[i][x] == feildId){
var index = this.objName[i].indexOf(feildId)
if (index !== -1) {
this.objName[i][index] = feildName;
}
}
}
}
}
}
}
console.log(this.objName)
}
The objName array, for output, ends up looking like:
[['obj1', 'string_name1', 'string_name2'], ['obj2', 'string_name3', 'string_name4']]
It works, but makes my eyes hurt, there must be an easier cleaner way of doing this?
Plunker link:
https://plnkr.co/edit/KBDu3ZehHl04er6eut6r?p=preview
Your data structures are not ideal for this kind of transformation. For instance, it would have been better if the display strings could be addressed directly given an "obj"-property and array index, without having to iterate through arrays.
Anyway, using the existing structure, you can still improve by using array functions, such as find and map:
class App {
constructor(preview, objName, fields) {
this.preview = preview;
this.objName = objName;
this.fields = fields;
this.setNumbers();
}
setNumbers() {
this.objName = this.objName.map( arr => arr.concat(this.preview[0][arr[0]]) );
this.setStrings();
}
setStrings() {
this.objName = this.objName.map( arr =>
[arr[0]].concat(arr.slice(1).map( val =>
this.fields.find( field => arr[0] in field )[arr[0]]
.find( item => item.id === val ).name
))
);
console.log(this.objName);
}
}
var objName = [['obj1'], ['obj2']],
preview = [{
obj1: [1, 2],
obj2: [3, 4]
}],
fields = [{
obj1:[{
id:1,
name:'string_name1'
}, {
id:2,
name:'string_name2'
}]
}, {
obj2:[{
id:3,
name:'string_name3'
}, {
id:4,
name:'string_name4'
}]
}];
new App(preview, objName, fields);
.as-console-wrapper { max-height: 100% !important; top: 0; }
Note that this code assumes all searches lead to matches. If this is not your case, you'll have to add some code to define which values should be returned in case of non-matching references.
Here is such a variant of the code:
class App {
constructor(preview, objName, fields) {
this.preview = preview;
this.objName = objName;
this.fields = fields;
this.setNumbers();
}
setNumbers() {
this.objName = this.objName.map( arr =>
arr[0] in this.preview[0]
? arr.concat(this.preview[0][arr[0]])
: arr
);
this.setStrings();
}
setStrings() {
this.objName = this.objName.map( arr =>
[arr[0]].concat(arr.slice(1).map( val => {
let find = this.fields.find( field => arr[0] in field );
if (find) find = find[arr[0]].find( item => item.id === val );
return find ? find.name : val;
}))
);
console.log(this.objName);
}
}
var objName = [['obj1'], ['obj2'], ['obj3']],
preview = [{
obj1: [1, 2],
obj2: [3, 4, 5],
}],
fields = [{
obj1:[{
id:1,
name:'string_name1'
}, {
id:2,
name:'string_name2'
}]
}, {
obj2:[{
id:3,
name:'string_name3'
}, {
id:4,
name:'string_name4'
}]
}];
new App(preview, objName, fields);
.as-console-wrapper { max-height: 100% !important; top: 0; }
It's easier and cleaner to do this if you break it down into smaller pieces:
let objsToMap = [{
obj1: [1, 2, 7],
obj2: [3, 4],
obj3: [1, 2]
}]
let objValues = [{
obj1: [{
id: 1,
name: 'string_name1'
}, {
id: 2,
name: 'string_name2'
}]
}, {
obj2: [{
id: 3,
name: 'string_name3'
}, {
id: 4,
name: 'string_name4'
}]
}];
function findValueForId(objDef, id) {
let idKeyMap = objDef.find(item => item.id === id);
return idKeyMap ? idKeyMap.name : null;
}
function findObjectValues(valueMapping, key) {
let objectWithObjectValues = valueMapping.find(item => key in item);
return objectWithObjectValues ? objectWithObjectValues[key] : null;
}
// returns an array containing key followed by the values corresponding to the specified ids
function lookupObject(key, ids, valueMapping) {
let objDef = findObjectValues(valueMapping, key) || [];
let valuesForIds = ids.map(id => findValueForId(objDef, id));
let valuesWithoutBlanks = valuesForIds.filter(value => value);
return [key].concat(valuesWithoutBlanks);
}
let result = Object.entries(objsToMap[0]).map(([k, v]) => lookupObject(k, v, objValues));
console.log(result);
You'll notice that this approach uses .find() in two places because your second data structure nests everything into arrays instead of having direct property references. This isn't very good because it's not good for performance and makes the code more convoluted than it has to be.
Another option is to rearrange the second array before consuming it, so that it's like this:
let objValues = {
obj1: {
'1': 'string_name1',
'2': 'string_name2'
},
obj2: {
'3': 'string_name3',
'4': 'string_name4'
}
};
Here's how you could do that:
let objsToMap = [{
obj1: [1, 2, 7],
obj2: [3, 4],
obj3: [1, 2]
}]
let objValuesRaw = [{
obj1: [{
id: 1,
name: 'string_name1'
}, {
id: 2,
name: 'string_name2'
}]
}, {
obj2: [{
id: 3,
name: 'string_name3'
}, {
id: 4,
name: 'string_name4'
}]
}];
function cleanupObjDef(objDef) {
return objDef.reduce(function(acc, el) {
acc[el.id] = el.name;
return acc;
}, {});
}
function cleanupObjValues(objValues) {
let allCombined = Object.assign({}, ...objValues);
return Object.entries(allCombined).reduce(function (acc, [k, v]) {
acc[k] = cleanupObjDef(v);
return acc;
}, {});
}
// returns an array containing key followed by the values corresponding to the specified ids
function lookupObject(key, ids, valueMapping) {
let objDef = valueMapping[key] || {};
let valuesForIds = ids.map(id => objDef[id]);
let valuesWithoutBlanks = valuesForIds.filter(value => value);
return [key].concat(valuesWithoutBlanks);
}
let objValues = cleanupObjValues(objValuesRaw);
let result = Object.keys(objsToMap[0]).map(key => lookupObject(key, objsToMap[0][key], objValues));
console.log(result);
There is an array:
let docs = [
{ "_id":"1", parent:"_", "title":"one"},
{ "_id":"2", parent:"1", "title":"two"},
{ "_id":"4", parent:"_", "title":"title"},
{ "_id":"5", parent:"4", "title":"www"},
{"_id":"_", "name":"root" },
];
I need to get out of it that's a tree:
{'_id':'_','name':'root','child':
[
{'_id':'1','parent':'_','title':'one','child':
[
{'_id':'2','parent':'1','title':'two','child':[]}
]
},
{'_id':'4','parent':'_','title':'title','child':
[
{'_id':'6','parent':'4','title':'vvv','child':[]}
]
}
]
}
But my code only works if the parent element is always higher on the list than the children, and I want to make that work universally.
This is code:
let node = {};
for (let doc of docs) {
doc.child = [];
node[doc._id] = doc;
if (typeof doc.parent === "undefined")
tree = doc;
else
node[doc.parent].child.push(doc);
}
console.log('tree->', JSON.stringify(tree));
code on codepen:
http://codepen.io/alex183/pen/OWvrPG?editors=0112
You can create recursive function using reduce method and basically check in each iteration of the parent property of current object is equal to passed parent param in function call.
let docs = [
{ "_id":"1", parent:"_", "title":"one"},
{ "_id":"2", parent:"1", "title":"two"},
{ "_id":"4", parent:"_", "title":"title"},
{ "_id":"5", parent:"4", "title":"www"},
{"_id":"_", "name":"root" }
];
function makeTree(data, parent = undefined) {
return data.reduce((r, e) => {
// check if current e.parent is equal to parent
if (e.parent === parent) {
// make a copy of current e so we keep original as is
const o = { ...e }
// set value as output of recursive call to child prop
o.child = makeTree(data, e._id)
// push to accumulator
r.push(o)
}
return r
}, [])
}
console.log(makeTree(docs))
This is a proposal with Array#reduce and Map. It sorts the array in advance.
var docs = [{ _id: "1", parent: "_", title: "one" }, { _id: "2", parent: "1", title: "two" }, { _id: "4", parent: "_", title: "title" }, { _id: "5", parent: "4", title: "www" }, { _id: "_", name: "root" }],
order = { undefined: -2, _: -1 },
tree = docs
.sort((a, b) => (order[a.parent] || a.parent) - (order[b.parent] || b.parent) || a._id - b._id)
.reduce(
(m, a) => (
m
.get(a.parent)
.push(Object.assign({}, a, { child: m.set(a._id, []).get(a._id) })),
m
),
new Map([[undefined, []]])
)
.get(undefined);
console.log(tree);
.as-console-wrapper { max-height: 100% !important; top: 0; }
The quick and dirty way is to use a sort function.
docs = docs.sort((a, b) => (a._id - b._id));