What are transforming approaches that collect array items from a nested data-structure into a single flat array?

Viewed 101

Given is a nested data-structure which I want to get transformed into a single flat array of non nested array-items.

The data and the code I came up with so far are as follows ...

const data = [{
  heading: [{
    name: "Heading 01",
    Items: [{
      name: "Item 01",
      layers: [{
        name: "layer01",
        id: 4,
        parent: 3,
        droppable: false,
      }, {
        name: "layer02",
        id: 5,
        parent: 3,
        droppable: false,
      }],
      id: 3,
      parent: 2,
      droppable: true,
    }],
    id: 2,
    parent: 1,
    droppable: true,
  }],
  id: 1,
  parent: 0,
  droppable: true,
}];

const flatArray = [];
const flatObject = {};
  
for (let index = 0; index < data.length; index++) {
  for (const prop in data[index]) {

    const value = data[index][prop];

    if (Array.isArray(value)) {
      for (let i = 0; i < value.length; i++) {
        for (const inProp in value[i]) {
          flatObject[inProp] = value[i][inProp];
        }
      }
    } else {
      flatObject[prop] = value;
    }
  }
  flatArray.push(flatObject);
}

console.log(flatArray)
.as-console-wrapper { min-height: 100%!important; top: 0; }

... and the result is expected to be equal to the next provided data structure ...

[{
  id: 1,
  parent: 0,
  droppable: true,
}, {
  name: "Heading 01",
  id: 2,
  parent: 1,
  droppable: true,
}, {
  name: "Item 01",
  id: 3,
  parent: 2,
  droppable: true,
}, {
  name: "layer01",
  id: 4,
  parent: 3,
  droppable: false,
}, {
  name: "layer02",
  id: 5,
  parent: 3,
  droppable: false,
}]
4 Answers

One could come up with a recursive approach which targets specific array items by any wanted array's key/identifier which are all going to be used by a destructuring assignment with default values and rest property.

The advantage of the destructuring is the separation and the direct access of all relevant data, the possible to be targeted array(s) and the rest of the currently processed data item.

function collectSpecificArrayItemsRecursively(data) {
  const result = [];

  if (Array.isArray(data)) {

    result
      .push(
        ...data
          .flatMap(collectSpecificArrayItemsRecursively)
      );
  } else {

    const {
      // destructuring into specific
      // arrays and the data's rest.
      heading = [], Items = [], layers = [], ...rest
    } = data;

    result
      .push(
        rest,
        ...heading
          .concat(Items)
          .concat(layers)
          .flatMap(collectSpecificArrayItemsRecursively),
      );    
  }
  return result;
}
const data = [{
  heading: [{
    name: "Heading 01",
    Items: [{
      name: "Item 01",
      layers: [{
        name: "layer01",
        id: 4,
        parent: 3,
        droppable: false,
      }, {
        name: "layer02",
        id: 5,
        parent: 3,
        droppable: false,
      }],
      id: 3,
      parent: 2,
      droppable: true,
    }],
    id: 2,
    parent: 1,
    droppable: true,
  }],
  id: 1,
  parent: 0,
  droppable: true,
}];

console.log(
  collectSpecificArrayItemsRecursively(data)
);
.as-console-wrapper { min-height: 100%!important; top: 0; }

Your data is not really in a recursive structure. So, while we can write a function that will do the transform, it's more complex and more fragile than one would hope.

Here is one possibility:

const flatten = (data) =>
  data .flatMap (({heading = [], Items = [], layers = [], ...rest}) => [
    rest, 
    ...flatten (heading), 
    ...flatten (Items), 
    ...flatten (layers)
  ])

const data = [{heading: [{name: "Heading 01", Items: [{name: "Item 01", layers: [{name: "layer01", id: 4, parent: 3, droppable: !1}, {name: "layer02", id: 5, parent: 3, droppable: !1}], id: 3, parent: 2, droppable: !0}], id: 2, parent: 1, droppable: !0}], id: 1, parent: 0, droppable: !0}]

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

The fragility I mention has to do with the fact that we specifically have to hard-code the levels here, heading, Items, layers. That they are indiscriminately capitalized and pluralized is a minor problem. The major problem is that they are all different. We are searching every level for each possible name for its children. And if we reused a name for a different purpose at one level (say the top level also had layers: 7, this would fall apart.

However, if you're in charge of that format, you can simplify it by combining these under the name, children. That will lead to simpler, more robust code, and more consistent data structures.

It would mean making your input look like this:

const data = [{
  children: [{          // the node formerly known as `heading`
    name: "Heading 01",
    children: [{        // the node formerly known as `Items`
      name: "Item 01",
      children: [{      // the node formerly known as `layers`
        name: "layer01",
        id: 4,
        parent: 3,
        droppable: false,
      }, {
        name: "layer02",
        id: 5,
        parent: 3,
        droppable: false,
      }],
      id: 3,
      parent: 2,
      droppable: true,
    }],
    id: 2,
    parent: 1,
    droppable: true,
  }],
  id: 1,
  parent: 0,
  droppable: true,
}]

The name children could be replaced with anything you like, as long as it's consistent.

With that, we could simply do this:

const flatten = (data) =>
  data .flatMap (({children = [], ...rest}) => [rest, ...flatten (children)])

const data = [{children: [{name: "Heading 01", children: [{name: "Item 01", children: [{name: "layer01", id: 4, parent: 3, droppable: !1}, {name: "layer02", id: 5, parent: 3, droppable: !1}], id: 3, parent: 2, droppable: !0}], id: 2, parent: 1, droppable: !0}], id: 1, parent: 0, droppable: !0}]

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

Here we simply separate out children from the rest of the node, return the rest as a new object followed by the result of recurring on children. This is an elegant little function, easy to write and easy to understand. But it requires a consistent data format to get it to work.

Here is my solution.

const c = (item: any) => "name" in item ? [{
  name: item.name,
  id: item.id,
  parent: item.parent,
  droppable: item.droppable
}] : []

function flat(item: any) {
  // "heading" is array of items
  // heading.map(function) will map over every heading and applies `function` to item
  if ("heading" in item) {
    // flatMap is .map(function).flat()
    return c(item).concat(item.heading.flatMap(flat))
  }

  // checking all cases where recursion goes deeper
  if ("Items" in item) {
    return c(item).concat(item.Items.flatMap(flat))
  }

  if ("layers" in item) {
    return c(item).concat(item.layers.flatMap(flat))
  }

  return c(item) // edge case, when there is no children items
}

console.log(data.flatMap(flat))

Explanation: I saw what you have recursive structure in your data variable. I started from creating a function flat, that will traverse all paths, and printing out the smallest piece, called leaves. Then I saw, that you need to have name, id, parent, droppable properties and creating helper function, that extracts all of these if any presents "name" in item is enough for that. Then just call helper on every step and concat with item.

Recursive approach:

const data = [{"id": 1,"parent": 0,"droppable": true,"heading": [{"name": "Heading 01","Items": [{"name": "Item 01","layers": [{"name": "layer01","id": 4,"parent": 3,"droppable": false},{"name": "layer02","id": 5,"parent": 3,"droppable": false}],"id": 3,"parent": 2,"droppable": true}],"id": 2,"parent": 1,"droppable": true}]}];
  
const iter = (data) => data.reduce((acc, obj) => {
    const entries = Object.entries(obj);
    const arrays = entries.filter(([, value]) => Array.isArray(value)); 
    const objWithoutArrays = entries
      .reduce((acc, [key, value]) => Array.isArray(value) 
        ? acc 
        : { ...acc, [key]: value }
      , {});
      
    acc.push(objWithoutArrays);
    acc.push(...arrays.flatMap(([, value]) => iter(value)));
    
    return acc;
}, []);

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

Related