将数组、对象+字符串的深层嵌套数据结构扁平化为数据项列表,同时也映射以前的父子关系

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

将对象数组重组为新数组

问题

有一个包含纯字符串的对象数组,也可能包含嵌套数组。我们要创建一个新数组,该数组将为数组中的每个项目包含一个节点,并为连接到其父项的每个数组项目包含单独的节点。每个父节点应具有以下结构:

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

每个具有以上架构的数组节点,还应该有一个连接边项添加到具有以下属性的数组中:

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

例子

例如,我们有以下对象数组:

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

预期输出为:

[
  {
    "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"
  }
]

必须选择一种自递归方法,该方法可以通用方式处理 array-items 和 object-entries。此外,在递归过程发生时,不仅需要创建和收集 consecutively/serially 编号(递增的 id 值)数据节点,但另外需要跟踪每个数据节点的parent引用,以便最终连接 edge 项列表(如 OP 所称)到数据节点列表

function flattenStructureRecursively(source = [], result = [], tracker = {}) {
  let {
    parent = null, edgeItems = [],
    getId = (id => (() => ++id))(0),
  } = tracker;

  const createEdgeItem = (id, pid) => ({
    id: `e${ id }-${ pid }`,
    source: id,
    target: pid,
  });
  const putNodeData = node => {
    result.push(node);

    if (parent !== null) {
      edgeItems.push(createEdgeItem(node.id, parent.id));
    }
    // every data node is a parent entity too.
    parent = node;
  };

  if (Array.isArray(source)) {
    result.push(
      ...source.flatMap(item =>
        flattenStructureRecursively(item, [], {
          getId, parent, edgeItems,
        })
      )
    );
  } else {
    let {
      dataNode,
      childEntries,
    } = Object
      .entries(source)
      .reduce(({ dataNode, childEntries }, [key, value]) => {
        if (value && (Array.isArray(value) || (typeof value === 'object'))) {
          // collect any object's iterable properties.

          childEntries.push([key, value]);
        } else {
          // aggregate any object's non iterable
          // properties at data node level.
          (dataNode ??= {

            id: getId(),
            data: { label: {} }

          }).data.label[key] = value;
        }
        return { dataNode, childEntries };

      }, { dataNode: null, childEntries: [] });

    if (dataNode !== null) {
      putNodeData(dataNode);
    }
    childEntries
      .forEach(([key, value]) => {
        // every object's iterable property is supposed
        // to be created as an own parent entity.

        dataNode = {
          id: getId(),
          data: { label: key },
        };
        putNodeData(dataNode);

        result.push(
          ...flattenStructureRecursively(value, [], {
            getId, parent, edgeItems,
          })
        );
      });
  }
  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",
    pseudonym: "J.D.",
    books: [{
      title: "Book 1",
    }, {
      title: "Book 2",
      chapters: [{
        title: "No Way Home",
        page: 256,
      }],
    }],
    age: 26,
  }])
);
.as-console-wrapper { min-height: 100%!important; top: 0; }

首先,如果没有好的答案,我不会回答。请在 Whosebug 上始终展示您自己的尝试并解释您遇到困难的地方。不过既然已经有了答案,我觉得这个版本可能会简单一些。

其次,我假设此输出格式是某种有向图,前半部分是顶点列表,后半部分是边列表。如果是这样我不知道你的输出格式是否在这里受到限制。但是,如果您可以选择,我认为更好的结构是具有 verticesedges 属性的 object,每个属性都包含一个数组。然后您可能不需要边的 ID。而且代码还可以简化。

这个版本首先转换成这样的中间结构:

[
    {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: []}
                ]}
            ]}
        ]}
    ]}
]

然后我们将该结构展平到输出的第一部分,并用它来计算第二部分中嵌套节点之间的关系(边?)。

代码如下所示:

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", page: 256}]}]}]

console .log (transform (input))
.as-console-wrapper {max-height: 100% !important; top: 0}

我们使用三个独立的递归函数。一种是递归提取到该中间格式。一路上,它使用 nextId 有状态函数添加 id 节点(我通常避免这样做,但似乎在这里简化了事情。)然后 flatten 简单地递归地提升 children坐在他们的 parent 旁边。 relationships(再次递归地)使用 parent- 和 child-nodes 的 id 添加边缘节点。

使用这三个独立的递归调用可能比其他一些解决方案效率低,但我认为它会产生更清晰的代码。