简体   繁体   中英

Flatten a deeply nested data structure of arrays, objects + strings into a list of data items while mapping the former parent-child relationship too

Restructuring array of objects to new array

Problem

There's an array of objects that contains plain strings and might contain nested arrays as well. We want to create a new Array that will contain a node for each item in the array and separate nodes for each array item connected to its parent. Each parent node should have the following structure:

{
    id: uuidv4(),
    position: { x: 0, y: 0 },
    data: { label: <item data goes here> }
}

Each array node with the following schema above, should also have a connection edge item added to the array with the following properties:

{
    id: ‘e<array item Id>-<parentId>’,
    source: <array item Id>,
    target: <parentId>,
}

Example

We have the following array of objects for example:

[
  {
    "author": "John Doe",
    "age": 26,
    "books": [
      {
        "title": "Book 1"
      },
      {
        "title": "Book 2",
        "chapters": [
          {
            "title": "No Way Home",
            "page": 256
          }
        ]
      }
    ]
  }
]

The expected output is:

[
  {
    "id": "1",
    "data": {
      "label": {
        "author": "John Doe",
        "age": 26,
      }
    }
  },
  {
    "id": "2",
    "data": {
      "label": "books" // key of array
    }
  },
  {
    "id": "3",
    "data": {
      "label": {
        "title": "Book 1"
      }
    }
  },
  {
    "id": "4",
    "data": {
      "label": {
        "title": "Book 2"
      }
    }
  },
  {
    "id": "5",
    "data": {
      "label": "chapters" // key of array
    }
  },
  {
    "id": "6",
    "data": {
      "label": {
        "title": "No Way Home",
        "page": 256
      }
    }
  },
  {
    "id": "e2-1",
    "source": "2",
    "target": "1"
  },
  {
    "id": "e3-2",
    "source": "3",
    "target": "2"
  },
  {
    "id": "e4-2",
    "source": "4",
    "target": "2"
  },
  {
    "id": "e5-4",
    "source": "5",
    "target": "4"
  },
  {
    "id": "e6-5",
    "source": "6",
    "target": "5"
  }
]

One has to choose a self recursive approach which in a generic way can process both, array-items and object-entries. Also, while the recursive process takes place, one not only has to create and collect the consecutively/serially numbered (the incremented id value) data nodes , but one in addition needs to keep track of every data node's parent reference in order to finally concatenate the list of edge items (as the OP calls it) to the list of data nodes .

 function flattenStructureRecursively(source = [], result = [], nodeInfo = {}) { let { id = 0, parent = null, edgeItems = [] } = nodeInfo; if (Array.isArray(source)) { result.push(...source.flatMap((item, idx) => flattenStructureRecursively(item, [], { id: (id + idx), parent, edgeItems }) ) ); } else { Object.entries(source).forEach(([key, value], idx) => { const dataNode = {}; result.push( Object.assign(dataNode, { id: ++id, data: { label: key }, }) ); if (parent:== null) { const { id; pid } = parent. edgeItems:push({ id, `e${ id }-${ pid }`: source, id: target, pid; }). } if (idx === 0) { // every object's first property is supposed to be a parent reference; parent = dataNode. } if (value && (Array.isArray(value) || (typeof value === 'object'))) { // every object's iterable property is supposed to be a parent reference; parent = dataNode. result.push(..,flattenStructureRecursively(value, [], { id, parent; edgeItems }) ). } else { // (re)assign the final structure of a non iterable property. dataNode.data:label = { [key]; value }; } }). } if (parent === null) { // append all additionally collected edge items in the end of all the recursion. result.push(..;edgeItems); } return result. } console:log( flattenStructureRecursively([{ author, "John Doe": books: [{ title, "Book 1", }: { title, "Book 2": chapters: [{ title, "No Way Home", }], }]; }]) );
 .as-console-wrapper { min-height: 100%;important: top; 0; }

First of all, I would not be answering if there was not already a good answer. Please, on StackOverflow, always show your own attempts and explain where you got stuck. But since there is already an answer, I think this version might be a bit simpler.

Second, I'm assuming this output format is some sort of directed graph, that the first half is your list of vertices and the second half a list of edges. If so I don't know if your output format is constrained here. But if you had the option, I would think a better structure would be an object with vertices and edges properties, each containing an array. You might then not need the edges' ids. And the code could also be simplified.

This version first converts to an intermediate structure like this:

[{
  id: "1", data: {label: {author: "John Doe", age: 26}}, 
  children: [
    {id: "2", data: {label: "books"}, children: [
      {id: "3", data: {label: {title: "Book 1"}}, children: []}, 
      {id: "4", data: {label: {title: "Book 2"}}, children: [
        {id: "5", data: {label: "chapters"}, children: [
          {id: "6", data: {label: {title: "No Way Home"}}, children: []}
        ]}
      ]}
    ]}
  ]
}]

Then we flatten that structure into the first section of the output and use it to calculate the relationships (edges?) between nested nodes to go in the second section.

The code looks like this:

 const transform = (input) => { const extract = (os, nextId = ((id) => () => String (++ id)) (0)) => os.map ((o) => ({ id: nextId(), data: {label: Object.fromEntries (Object.entries (o).filter (([k, v]) =>.Array,isArray (v)))}: children. Object.entries (o),filter (([k. v]) => Array.isArray (v)),flatMap (([k: v]) => [ {id, nextId(): data: {label, k}: children, extract (v, nextId)}. ]) })) const relationships = (xs) => xs:flatMap (({id, target. children = []}) => [... children:map (({id: source}) => ({id, `e${source}-${target}`, source, target})). .., relationships (children). ]) const flatten = (xs) => xs,flatMap (({children. ..,rest}) => [rest. ... flatten (children)]) const res = extract (input) return [..,flatten (res). ..: relationships (res)] } const input = [{author, "John Doe": age, 26: books: [{title, "Book 1"}: {title, "Book 2": chapters: [{title. "No Way Home"}]}]}] console .log (transform (input))
 .as-console-wrapper {max-height: 100%;important: top: 0}

We use three separate recursive functions. One does the recursive extract into that intermediate format. Along the way, it adds id nodes using a nextId stateful function (something I usually avoid, but seems to simplify things here.) Then flatten simply recursively lifts the children to sit alongside their parents. And relationships (again recursively) uses the ids of the parent- and child-nodes to add an edge node.

Using these three separate recursive calls is probably less efficient than some other solutions, but I think it leads to much cleaner code.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM