Home > Mobile >  How does one convert a tree like nested data structure of arrays and objects into a list of items wi
How does one convert a tree like nested data structure of arrays and objects into a list of items wi

Time:11-05

An API returns a nested data structure of arrays and objects. The data comes as a list of tree like objects, each with a possible parent-child relationship. The structure itself is shown by the following example code.

[{
  label: "search me",
  value: "searchme",
  children: [{

    label: "search me too",
    value: "searchmetoo",
    children: [{

      label: "No one can get me",
      value: "anonymous",
    }],
  }],
}, {
  label: "search me2",
  value: "searchme2",
  children: [{

    label: "search me too2",
    value: "searchmetoo2",
    children: [{

      label: "No one can get me2",
      value: "anonymous2",
    }],
  }],
}]

The above data has to be converted into a (flat) array of objects where each object will represent a former node element but with a unique primary key (id). Also a node's parent-id equals its parent's id (the primary key) except for root nodes that do not have a parent, and therefore the parent-id then should be null.

A possible target data structure could look similar to ...

[{
  DIAGID: 1,
  DIAGNOSIS: "Certain infectious or parasitic diseases ",
  DIAGTYPE: "Chapter",
  PARENTID: null,
}, {
  DIAGID: 2,
  DIAGNOSIS: "Gastroenteritis or colitis of infectious origin  ",
  DIAGTYPE: "Section",
  PARENTID: 1,
}, {
  DIAGID: 3,
  DIAGNOSIS: "Bacterial intestinal infections",
  DIAGTYPE: "Category",
  PARENTID: 2,
}]

CodePudding user response:

Here is a recursive function dfs that performs a pre-order traversal through the input tree, and passes along a counter that feeds the id property that will be used in the output. Also the current node's id is passed as parentId to the recursive call:

const dfs = ({children=[], ...node}, counter={id: 1}, parentId=null) =>
    [{ ...node, id: counter.id  , parentId}].concat(
        children.flatMap(child => dfs(child, counter, node.id))
    );

const response = {"label":"search me","value":"searchme","children":[{"label":"search me too","value":"searchmetoo","children":[{"label":"No one can get me","value":"anonymous"}]}]};
const result = dfs(response);
console.log(result);
<iframe name="sif1" sandbox="allow-forms allow-modals allow-scripts" frameborder="0"></iframe>

CodePudding user response:

You could take a recursive method for Array#flatMap and store parent for the next call.

const
    flatTree = (parent = null) => ({ children = [], ...object }) => [
        { ...object, parent }, ...children.flatMap(flatTree(object.id))
    ],
    tree = [{ id: 0, label: 'search me', value: 'searchme', children: [{ id: 1, label: 'search me too', value: 'searchmetoo', children: [{ id: 2, label: 'No one can get me', value: 'anonymous' }] }] }],
    flat = tree.flatMap(flatTree());

console.log(flat);
.as-console-wrapper { max-height: 100% !important; top: 0; }
<iframe name="sif2" sandbox="allow-forms allow-modals allow-scripts" frameborder="0"></iframe>

CodePudding user response:

I was going to say a recursive tree walk is all you need, but you can accomplish the same thing easily with a generator:

function *visitNodes( root, parent = null, id = 0 ) {
  
  const node = {
    ...root,
    id :   id,
    parentId = parent ? parent.id : null
  };
  delete node.children;
  
  yield node;
  
  for ( const child of root.children ?? [] ) {
    yield *visitNodes(child, node, id);
  }
  
}

Having defined the generator, you can either iterate over the nodes:

for (const node of visitNodes( tree ) ) {
  // do something useful with node here
}

You can convert it into a list easily, either with the spread operator:

const nodes  = [...visitNodes(tree)];

or by using Array.from():

const nodes = Array.from( visitNodes(tree) );

CodePudding user response:

A single recursively implemented collecting reduceer functionality does the job.

It utilizes a collector object as the reduce method's 2nd argument (and the reducer's initial value). The collector's result array collects any item. And count gets incremented constantly and assigned as a collected item's DIAGID whereas parentId gets updated as needed in order to always reflect the current recursive call stack thus a(n) (child) item's corresponding (and correct) PARENTID ...

function countMapAndCollectNestedItemRecursively(collector, item) {

  let { count = 0, parentId = null, result } = collector;
  const { label, value, children } = item;

  result.push({
    DIAGID:   count,
    DIAGNOSIS: label,
    DIAGTYPE: value,
    PARENTID: parentId,
  });
  if (Array.isArray(children)) {
    count = children.reduce(

      countMapAndCollectNestedItemRecursively,
      { count, parentId: count, result }

    ).count;
  }
  return { count, parentId, result };
}

const sampleData = [{
  label: "FOO",
  value: "foo",
  children: [{

    label: "FOO BAR",
    value: "fooBar",
    children: [{

      label: "FOO BAR BAZ",
      value: "fooBarBaz",
    }],
  }, {
    label: "FOO BIZ",
    value: "fooBiz",
    children: [{

      label: "FOO BIZ BUZ",
      value: "fooBizBuz",
    }],
  }],
}, {
  label: "BAR",
  value: "bar",
  children: [{

    label: "BAR BAZ",
    value: "barBaz",
    children: [{

      label: "BAR BAZ BIZ",
      value: "barBazBiz",
      children: [{

        label: "BAR BAZ BIZ BUZ",
        value: "barBazBizBuz",
      }],
    }, {
      label: "BAR BAZ BUZ",
      value: "barBazBuz",
    }],
  }, {
    label: "BAR BIZ",
    value: "barBiz",
    children: [{

      label: "BAR BIZ BUZ",
      value: "barBizBuz",
    }],
  }],
}];

console.log(
  sampleData.reduce(

    countMapAndCollectNestedItemRecursively,
    { result: [] },

  ).result
);
.as-console-wrapper { min-height: 100%!important; top: 0; }
<iframe name="sif3" sandbox="allow-forms allow-modals allow-scripts" frameborder="0"></iframe>

CodePudding user response:

This version is something of a mash-up of ideas from trincot and Nicholas Carey. From trincot's answer (which actually answers a different, but closely related question) I steal his {ctr: id: 1} handling. And from Nicholas, I use the generator to iterate through the values more easily, although I simplify a bit from there.

I think they combine to give a nice solution:

const flatten = function * (xs, ctr = {id: 1}, parent = null) {
  for (let {children = [], ...x} of xs) {
    const node = {id: ctr .id   , parentId: parent ? parent .id : null, ...x}
    yield node
    yield * flatten (children, ctr, node)
  }
}

const transform = (xs) => [...flatten (xs)]

const response = [{label: "search me", value: "searchme", children: [{label: "search me too", value: "searchmetoo", children: [{label: "No one can get me", value: "anonymous"}]}]}, {label: "search me2", value: "searchme2", children: [{label: "search me too2", value: "searchmetoo2", children: [{label: "No one can get me2", value: "anonymous2"}]}]}]

console .log (transform (response))
.as-console-wrapper {max-height: 100% !important; top: 0}
<iframe name="sif4" sandbox="allow-forms allow-modals allow-scripts" frameborder="0"></iframe>

  • Related