-1

I'm trying to convert an array of arrays into an array of nested objects in JavaScript. Let's assume each subarray in the array represents a file path. I want to create an array of objects where each object has 2 properties, the name of the current file and any files/children that come after the current file/parent.

So for example, if I have this array of arrays where each subarray represents a file path:

[['A', 'B', 'C'], ['A', 'B', 'D'], ['L', 'M', 'N']]

I want to get this as the result:

[
  {
    name :'A',
    children: [
      {
        name: 'B',
        children: [
          {
            name: 'C',
            children: []
          },
          {
            name: 'D',
            children: []
          }
        ]
      }
    ]
  },
  {
    name: 'L',
    children: [
      {
        name: 'M',
        children: [
          {
            name: 'N',
            children: []
          }
        ]
      }
    ]
  }
]

I tried mapping through the array of arrays and creating an object for the current file/parent if it hasn't been created yet. I think I may be on the right track but I can't seem to think of the best way to do so.

IT goldman
  • 14,885
  • 2
  • 14
  • 28
simon777
  • 51
  • 1
  • 5
  • Can you show the code you wrote so far? – CherryDT Oct 30 '22 at 21:32
  • 1
    Does this answer your question? [Create a tree from a list of strings containing paths of files - javascript](https://stackoverflow.com/questions/57344694/create-a-tree-from-a-list-of-strings-containing-paths-of-files-javascript) – pilchard Oct 30 '22 at 21:44

3 Answers3

0

Something I could do in 5 minutes, it probably can be improved. This could also be written as a recursive function, I believe.

const result = [];

arr.forEach((subArr) => {
  var ref = result;
  subArr.forEach((name) => {
    const obj = ref.find((obj) => obj.name == name);
    if (obj) {
      ref = obj.children;
    } else {
      ref.push({ name, children: [] });
      ref = ref[ref.length - 1].children;
    }
  });
});
0

Here's mine:

// Function to convert path array ‘a’ from position ‘i’ into tree structure in ‘v’.
const tree = (a, i, v) => {
    if (i < a.length) { tree(a, i+1, v[a[i]] ||= { }) }
}

// Function to convert a simple node into the desired record format.
const record = (v) => {
    const a = [];
    for (const [k, w] of Object.entries(v)) {
        a.push({ name: k, children: record(w) });
    }
    return a;
}

const m = { }
for (const a of [['A', 'B', 'C'], ['A', 'B', 'D'], ['L', 'M', 'N']]) {
    tree(a, 0, m);
}
const result = record(m);
njlarsson
  • 2,128
  • 1
  • 18
  • 27
0

While it might be overkill for this requirement, I have handy a variant of one of my utility functions, setPath, that is used for adding such a path to an existing array. This is a bit different from the other answers in that it does this in an immutable way, returning a new object that shares as much structure as possible with the original one. I always prefer to work with immutable data.

Using that, we can write a hydrate function to do this job as the one-liner, const hydrate = (paths) => paths .reduce (setPath, []).

This is quite likely overkill here, as there is probably no reason to build your output one immutable level after another. But it is a demonstration of the value of keeping utility functions handy.

The code looks like this:

const call = (fn, ...args) => fn (...args)

const setPath = (xs, [name, ...names]) => call ( 
  (i = ((xs .findIndex (x => x .name == name) + 1) || xs .length + 1) - 1) => 
    name == undefined
      ? [...xs]
      : [
          ...xs .slice (0, i),
          {name, children: setPath ((i == xs .length) ? [] : xs [i] .children, names)},
          ...xs .slice (i + 1)
        ]
)

const hydrate = (paths) => paths .reduce (setPath, [])

console .log (
  hydrate ([['A', 'B', 'C'], ['A', 'B', 'D'], ['L', 'M', 'N']])
)
.as-console-wrapper {max-height: 100% !important; top: 0}

We have a trivial call helper function that I use here to avoid statements. As much as possible, I prefer to work with expressions, as these avoid temporal notions in code brought on by statements, and they compose much better into larger pieces. We could alternatively do this with default parameters, but they have other problems. A trivial call function handles this nicely.

In our main function, we destructure apart the first name in the input from the remaining ones. Then we pass to call a function that calculates the index of the element with our existing name in the input array. If it doesn't exist, the index will be the length of that array. This is perhaps over-tricky. findIndex returns -1 if no element matches. We add 1 to the result, and then, if it's 0, we choose one more than the length of the array. Finally we subtract 1 from the result, and now the index will be where we found our target or the length of the array if it wasn't found.

Now, if the path is empty, we return a copy of our array. (I prefer the copy just for consistency, but it would be legitimate to just return it directly.) If it's not empty, we use the index to tear apart our input array, keeping everything before it, building a new item for that index by recursively calling setPath with the remaining node names, and then keeping everything after that index.

And now, as noted, our hydrate function is a trivial fold of setPath starting with an empty array.

Scott Sauyet
  • 49,207
  • 4
  • 49
  • 103