Javascript Recursion Tree Building - javascript

I am having an issue building a tree from a flat array. I am building a category -> subcategory tree in which the parent has subcategories as an array.
Here is what the flat array would look like:
[
{
"id": 1
},
{
"id": 5,
},
{
"id": 2,
"parent_id": 1
},
{
"id": 3,
"parent_id": 1
},
{
"id": 42,
"parent_id": 5
},
{
"id": 67,
"parent_id": 5
}
]
And this is what I need the result to look:
[
{
"id":1,
"subcategories":[
{
"id":2,
"parent_id":1
},
{
"id":3,
"parent_id":1
}
]
},
{
"id":5,
"subcategories":[
{
"id":42,
"parent_id":5
},
{
"id":67,
"parent_id":5
}
]
}
]
I have tried to do this recursively by recursively searching for children and attaching it as an array and continuing to do so until I hit the bottom of the barrel but I am getting a cyclic structure. It appears that the parent_id in traverse is always the id of the parent... any ideas:
tree(passingInFlatObjectHere);
function topLevel (data) {
let blob = [];
data.forEach((each) => {
if (!each.parent_id) {
blob.push(each);
}
});
return blob;
}
function tree (data) {
let blob = topLevel(data).map(function (each) {
each.subcategories = traverse(data, each.id);
return each;
});
return blob;
}
function traverse (data, parent_id) {
let blob = [];
if (!parent_id) {
return blob;
}
data.forEach((each) => {
if (each.id === parent_id) {
each.subcategories = traverse(data, each.id);
blob.push(each);
}
});
return blob;
}

I don’t just want to help you fix your problem but would also like to help you take full advantage of ES6
First of all your topLevel function can be rewritten to this:
function topLevel(data) {
return data.filter(node => !node.parent_id);
}
Neat isn’t it? I also would recommend slightly changing tree for consistency but that’s of course just stylistic.
function tree(data) {
return topLevel(data).map(each => {
each.subcategories = traverse(data, each.id);
return each;
});
}
No logic issues so far. traverse, however, contains one, when you check each.id === parent_id. Like this, the functions searches for the node whose id is parent_id. Obviously a mistake. You wanted each.parent_id === parent_id.
Your issue is solved now. Stop reading if I bother you. But you could also take advantage of filter here and remove that slightly superfluous early exit and rewrite your function to:
function traverse(data, parentId) {
const children = data.filter(each => each.parent_id === parentId);
children.forEach(child => {
child.subcategories = traverse(data, child.id);
});
return children;
}

First, you need install lodash with below command with npm:
npm i lodash
Second, you must import _ from lodash
import _ from "lodash";
Finally, run this function:
export const recursive_lists = (data) => {
const grouped = _.groupBy(data, (item) => item. parent_id);
function childrenOf(parent_id) {
return (grouped[parent_id] || []).map((item) => ({
id: item.id,
child: childrenOf(item.id),
}));
}
return childrenOf(null);
};
or
First:
<script src="https://cdn.jsdelivr.net/npm/lodash#4.17.20/lodash.min.js"></script>
Second:
function recursive_lists(data) {
const grouped = _.groupBy(data, (item) => item.parent_id);
function childrenOf(parent_id) {
return (grouped[parent_id] || []).map((item) => ({
id: item.id,
child: childrenOf(item.id),
}));
}
return childrenOf(null);
};

Related

Trying to write a recursive asynchronous search in JavaScript

I am trying to write some code that searches through a bunch of objects in a MongoDB database. I want to pull the objects from the database by ID, then those objects have ID references. The program should be searching for a specific ID through this process, first getting object from id, then ids from the object.
async function objectFinder(ID1, ID2, depth, previousList = []) {
let route = []
if (ID1 == ID2) {
return [ID2]
} else {
previousList.push(ID1)
let obj1 = await findObjectByID(ID1)
let connectedID = obj1.connections.concat(obj1.inclusions) //creates array of both references to object and references from object
let mapPromises = connectedID.map(async (id) => {
return findID(id) //async function
})
let fulfilled = await Promise.allSettled(mapPromises)
let list = fulfilled.map((object) => {
return object.value.main, object.value.included
})
list = list.filter(id => !previousList.includes(id))
for (id of list) {
await objectFinder(id, ID2, depth - 1, previousList).then(result => {
route = [ID1].concat(result)
if (route[route.length - 1] == ID2) {
return route
}})
}
}
if (route[route.length - 1] == ID2) {
return route
}
}
I am not sure how to make it so that my code works like a tree search, with each object and ID being a node.
I didn't look too much into your code as I strongly believe in letting your database do the work for you if possible.
In this case Mongo has the $graphLookup aggregation stage, which allows recursive lookups. here is a quick example on how to use it:
db.collection.aggregate([
{
$match: {
_id: 1,
}
},
{
"$graphLookup": {
"from": "collection",
"startWith": "$inclusions",
"connectFromField": "inclusions",
"connectToField": "_id",
"as": "matches",
}
},
{
//the rest of the pipeline is just to restore the original structure you don't need this
$addFields: {
matches: {
"$concatArrays": [
[
{
_id: "$_id",
inclusions: "$inclusions"
}
],
"$matches"
]
}
}
},
{
$unwind: "$matches"
},
{
"$replaceRoot": {
"newRoot": "$matches"
}
}
])
Mongo Playground
If for whatever reason you want to keep this in code then I would take a look at your for loop:
for (id of list) {
await objectFinder(id, ID2, depth - 1, previousList).then(result => {
route = [ID1].concat(result);
if (route[route.length - 1] == ID2) {
return route;
}
});
}
Just from a quick glance I can tell you're executing this:
route = [ID1].concat(result);
Many times at the same level. Additional I could not understand your bottom return statements, I feel like there might be an issue there.

How to iterate through out an array with a value path as reference

I don't really know how to express what I want, but I'll try.
So, I have an object with an array inside with the name of recipes, that I receive from my API, and a valuePath which is an object:
Object
{
recipes: [
{
test: {
items: [
{
type: 'test1',
}
]
}
}
]
}
ValuePath
{
"allRecipes": {
"array": "recipes",
"values": {
"allTypes": {
"array": "test",
"values": {
"type": "type"
}
}
}
}
}
Briefly what I have to do, is iterate over the array recipes through out the valuePath, dynamically, because the array and the values can change. I don't really know how to explain it better and how to iterate thought deeply nested objects/array's having a valuePath as a reference to find the values.
What I've tried so far...
export const test = (object, valuePath) => {
for (const prop in valuePath) {
object = object[valuePath[prop].array]; // find the array
if (Array.isArray(object)) {
object.forEach(objRef => {
console.log('valueRef', objRef);
});
}
console.log('props->', valuePath[prop].values); // find the values
}
};
I think i need a recursion, but have no clue how to do one.
If I understood your problem, this could be an implementation...
If you run it with your data and path, it will return test1.
// INPUTS
const data = {
recipes: [
{
test: {
items: [
{
type: 'test1',
}
]
}
}
]
}
const path = {
"allRecipes": {
"array": "recipes",
"values": {
"allTypes": {
"array": "test",
"values": {
"type": "type"
}
}
}
}
}
// this is just an helper method for arrays...
Array.prototype.first = function () { return this[0] }
// this is an helper function that tells us whether
// a path object is still traversable.
// from what I understood, if it contains an `array` property
// we should follow it...
const isTraversable = path => !!path.array
// this is the actual implementation of the algorithm
const traverse = (data, path) => {
const nextPath = Object.values(path).first()
if ( isTraversable(nextPath) ) {
const array = data[nextPath.array]
// I noticed that at a certain point in the data object,
// we need to traverse an array, and in another it is an
// object with an `items` property.
// this next lines helps determine how go down
const nextData = Array.isArray(array) ? array.first() : array.items
// we recurse on the traversed data and path
return traverse(nextData, nextPath.values)
}
return data.first()[path.type]
}
console.log(traverse(data, path))
Please try this, I hope it will help you..
let obj = {
recipes: [
{
test: {
items: [
{
type: 'test1',
},
],
},
},
],
};
obj.recipes.forEach(test => {
test.test.items.forEach(items => {
console.log(items.type);
});
});

How to iterate through complex Json object and do something on each property which equals a certain value

I got the following problem,
I need to iterate through a big Json object ( child nodes consist of array's, strings and objects with at least 4-5 layers of depth in terms of nested properties ).
In some parts across the big Json file there is a specific object structure, it has a property named "erpCode". I need to scan the Json and find all the objects with that property, take the value use that code to ask a different API for details and once I get the details insert them into the object with the current 'erpCode'.
Just to clarify, in my case the parent node property name in the Json always equals the value in 'typeSysname' field which located on the same 'level' as the erpCode property.
A simple example :
{
"cars": [
{
"name": "X222",
"carType": {
"erpCode": "skoda",
"value": null,
"typeSysName": "carType"
}
}
],
"model": {
"year": 1999,
"details": {
"erpCode": "112"
"value": null,
"typeSysName": "details"
}
}
}
In this example I need to find 2 properties get the values skoda and 112 out of them and get the value and description data from a different API and set it into this Json in the right location.
P.S. Any chance there is a good npm package which can help me with that?
Edit:
I got a solution in C# from a few months ago which runs in a generic way on the Json and handles the complexity of the structure in a generic way.
But I now need to convert this into Javascript and I am a bit lost.
public static string TranslateDocErpCodes(string jsonString, string topRetailerSysName)
{
try
{
var doc = JObject.Parse(jsonString);
var erpCodeList = doc.SelectTokens("$..erpCode").ToList();
foreach (var erpCodeJToken in erpCodeList)
{
var value = erpCodeJToken?.Value<string>();
var erpCodeParent = erpCodeJToken?.Parent.Parent;
var erpCodeProperty = erpCodeParent?.Path.Split(".").Last();
var result =
_dataService.GetLovFromErpCode(topRetailerSysName, erpCodeProperty, value);
if (result == null)//reset lov obj
{
if (erpCodeParent?.Parent is JProperty prop)
prop.Value = JObject.FromObject(new LovObject { ErpCode = value });
}
else//set lov obj
{
result.ErpCode = value;
if (erpCodeParent?.Parent is JProperty prop)
prop.Value = JObject.FromObject(result);
}
}
return JsonConvert.SerializeObject(doc);
}
catch (Exception e)
{
throw new Exception("ErpConvert.TranslateDocErpCodes() : " + e);
}
}
mb something like;
function processObject(jsonData) {
for (prop in jsonData) {
if (jsonData.hasOwnProperty(prop)) {
// We get our prop
if (prop === 'code') {
let codeValue = jsonData[prop]
doSomeAsync(codeValue)
.then(response => {
jsonData[prop] = response;
})
}
let curValue = jsonData[prop];
if (Array.isArray(curValue)) {
// Loop through the array, if array element is an object, call processObject recursively.
processArray(curValue);
} else if (typeof curValue === 'object') {
processObject(curValue);
}
}
}
}
I took the answer from Aravindh as a starting point and managed to reach what seems to be a complete solution.
I will share it here,
async function convertErpCodes(jsonData, orgName, parentPropertyName){
for (let prop in jsonData) {
if (jsonData.hasOwnProperty(prop)) {
if (prop === 'erpCode') {
const erpCodeValue = jsonData[prop]
const req = {"query": {"erpCode": erpCodeValue, "orgName": orgName, "typeSysName": parentPropertyName}};
const result = await viewLookupErpService.findOne(req);
if(result)
return result;
}
const curValue = jsonData[prop];
if (Array.isArray(curValue)) {
for(let i in curValue){
const res = await convertErpCodes(curValue[i], orgName, prop);
}
} else if (curValue && typeof curValue === 'object') {
const response = await convertErpCodes(curValue, orgName, prop);
if(response){
jsonData[prop] = response;
}
}
}
}
}
P.S.
I set up the values only if I get a response from the third party API ( this is the reason for the result and response logic in the recursion.
I'd use object-scan and lodash.set in combination
// const objectScan = require('object-scan');
// const lodash = require('lodash');
const stats = { cars: [{ name: 'X222', carType: { erpCode: 'skoda', value: null, typeSysName: 'carType' } }], model: { year: 1999, details: { erpCode: '112', value: null, typeSysName: 'details' } } };
const entries = objectScan(['**.erpCode'], { rtn: 'entry' })(stats);
console.log(entries);
// => [ [ [ 'model', 'details', 'erpCode' ], '112' ], [ [ 'cars', 0, 'carType', 'erpCode' ], 'skoda' ] ]
// where you would query the external api and place results in entries
entries[0][1] = 'foo';
entries[1][1] = 'bar';
entries.forEach(([k, v]) => lodash.set(stats, k, v));
console.log(stats);
// => { cars: [ { name: 'X222', carType: { erpCode: 'bar', value: null, typeSysName: 'carType' } } ], model: { year: 1999, details: { erpCode: 'foo', value: null, typeSysName: 'details' } } }
.as-console-wrapper {max-height: 100% !important; top: 0}
<script src="https://bundle.run/object-scan#13.8.0"></script>
<script src="https://bundle.run/lodash#4.17.20"></script>
Disclaimer: I'm the author of object-scan

Creating a new key from child elements in Json with Javascript

I want to create new key from child values inside the json with ES6/ES5. I tried with arrow functions but i couldn't get the result. Firstly you can see my part of json in below,
[
{
matchId:307,
matchStatusId:5,
matchHomeScore:0,
matchAwayScore:0,
matchTime:0,
homeClubId:608,
homeClub:{
clubId:608,
clubName:"Annaba"
},
awayClubId:609,
awayClub:{
clubId:609,
clubName:"Bazer Sakhra"
},
leagues:[
{
leagueId:65,
parentLeagueId:null,
leagueName:"ALGERIA"
},
{
leagueId:66,
parentLeagueId:65,
leagueName:"Algeria Cup"
}
]
},
]
I want to create new parent key. It will get the values from values of child items and it will combine. Child item numbers changeable. Not everytime 2 items.
leaguesGeneral:"ALGERIA - Algeria Cup"
leaguesGeneral:"ALGERIA - Algeria Cup"
leagues: [
{
leagueId:65,
parentLeagueId:null,
leagueName:"ALGERIA"
},
{
leagueId:66,
parentLeagueId:65,
leagueName:"Algeria Cup"
}
]
I found this method. But it combines everything from the parent.
data = data.map(function (x) {
var keys = Object.keys(x);
x.newKeyValue = keys.map(key => x[key]).join('-');
return x;
});
The leaguesGeneral key can be added to each object in the array using
o.leagues.map(({ leagueName }) => leagueName).join(' - ')
const data = [
{
matchId:307,
matchStatusId:5,
matchHomeScore:0,
matchAwayScore:0,
matchTime:0,
homeClubId:608,
homeClub:{
clubId:608,
clubName:"Annaba"
},
awayClubId:609,
awayClub:{
clubId:609,
clubName:"Bazer Sakhra"
},
leagues:[
{
leagueId:65,
parentLeagueId:null,
leagueName:"ALGERIA"
},
{
leagueId:66,
parentLeagueId:65,
leagueName:"Algeria Cup"
}
]
},
];
const result = data.map(o => {
if (o.leagues) {
o.leaguesGeneral = o.leagues.map(({ leagueName }) => leagueName).join(' - ');
}
return o;
})
console.log(result)

Rxjs Map Array in Json Document to new Array type

I am retrieving a document from PouchDB in an Angular Service. The document is retrieved in the following format:
{
"_id":"segments",
"_rev":"1-4f0ed65cde23fe724db13bea1ae3bb13",
"segments":[
{ "name":"Aerospace" },
{ "name":"Auto repair" },
{ "name":"Commercial" },
{ "name":"Education" },
{ "name":"Energy" },
{ "name":"Farm/ranch" },
{ "name":"Furniture" },
{ "name":"Heavy Equipment" },
{ "name":"Hobbyist" },
{ "name":"Infrastructure" },
{ "name":"Luxury/Leisure" },
{ "name":"Military" },
{ "name":"MUP" },
{ "name":"Processing" },
{ "name":"Rail" },
{ "name":"Transportation" }
]}
And I want to map that to a new Array that would look like:
[
{ value: "Aerospace", viewValue: "Aerospace" },
{ value: "Auto Repair", viewValue: "Auto Repair" },
{ value: "Commercial", viewValue: "Commercial" }
...
]
To accomplish this, I have tried this code in my Service:
getSegments(): Observable<any[]> {
return from(this.database.get('segments'))
.pipe(
map((results) => results.segments)
);
}
And I transform the array in my Component like this:
segments: SegmentsLookup[] = [];
...
this.lookupDbService.getSegments()
.subscribe(data => {
data.forEach(element => {
this.segments.push({value: element.name, viewValue: element.name});
});
});
This works but I know there is a way to map this properly back in the Service code. Also, when done this way, the compiler complains about the "results.segments" stating "Property "segments" does not exist on type '{}'.
How do I map the data retrieved to the Array that I need in the Service's "getSegments" method?
You can do the transformation is 2 steps:
pipe/map to extract the segments
array/map to convert to the final data type
Please see an example here: https://stackblitz.com/edit/angular-ikb2eg?file=src%2Fapp%2Fapp.component.ts
let transformedData = observableData.pipe(
map(data => {
console.log(data, data.segments.length);
return data.segments.map(element => {
return { value: element["name"], viewValue: element["name"] };
});
})
);
transformedData.subscribe(data => {
this.mylist = data;
});
You can use the RxJS operator flatMap, which is a alias of mergeMap.
The official documentation describes flatMap as follows:
Projects each element of an observable sequence to an observable
sequence and merges the resulting observable sequences or Promises or
array/iterable into one observable sequence.
We can use flatMap and then the RxJS operator map like this:
flatMap to extract the segments
map to convert to the final data type
const transformedData = observableData.pipe(
flatMap(data => data.segments),
map(segment => ({
value: segment['name'],
viewValue: segment['name'],
})),
)
transformedData.subscribe(data => {
this.mylist = data;
});
Detailed explanation of how this works:
This article nicely explains how flatMap works and implements a version of flatMap, which works with arrays rather than RxJS observables, as follows:
function flatMap (arr, fn) {
return arr.reduce((flatArr, subArray) => flatArr.concat(fn(subArray)), [])
}
If we use this with the result of your database query we'll see we extract the segments.
const data = {
"_id": "segments",
"_rev": "1-4f0ed65cde23fe724db13bea1ae3bb13",
"segments": [
{ "name": "Aerospace" },
{ "name": "Auto repair" },
// ...
]};
const segments = flatMap([data], x => x.segments);
console.log(segments);
// > [
// > { "name": "Aerospace" },
// > { "name": "Auto repair" },
// > ...
// > ]
The RxJS flatMap operator returns an observable stream of segments, rather than an array of segments.
You can extend the map function and remove it from the component as follows :
map(result => {
result = result.segments;
let data = [];
result.forEach(element => {
data.push({value: element.name, viewValue: element.name});
});
return data;
});

Categories