1

I have an flat array of Folders like this one :

const foldersArray = [{id: "1", parentId: null, name: "folder1"}, {id: "2", parentId: null, name: "folder2"}, {id: "1.1", parentId: 1, name: "folder1.1"}, {id: "1.1.1", parentId: "1.1", name: "folder1.1.1"},{id: "2.1", parentId: 2, name: "folder2.1"}]

I want to output an array of all parents of a given folder to generate a Breadcrumb-like component of Folder path.

I have presently a code that does what I need but I'd like to write it better in a more "functional" way, using reduce recursively.

If I do :

    getFolderParents(folder){ 
      return this.foldersArray.reduce((all, item) => { 
        if (item.id === folder.parentId) { 
            all.push (item.name) 
            this.getFolderParents(item)
         }
         return all
       }, [])
     }

and I log the output, I can see it successfully finds the first Parent, then reexecute the code, and outputs the parent's parent... as my initial array is logically reset to [] at each step... Can't find a way around though...

Matt
  • 43,482
  • 6
  • 101
  • 102
Pierre_T
  • 1,094
  • 12
  • 29

3 Answers3

0

You're thinking about it in a backwards way. You have a single folder as input and you wish to expand it to a breadcrumb list of many folders. This is actually the opposite of reduce which takes as input many values, and returns a single value.

Reduce is also known as fold, and the reverse of a fold is unfold. unfold accepts a looping function f and an init state. Our function is given loop controllers next which add value to the output and specifies the next state, and done which signals the end of the loop.

const unfold = (f, init) =>
  f ( (value, nextState) => [ value, ...unfold (f, nextState) ]
    , () => []
    , init
    )

const range = (m, n) =>
  unfold
    ( (next, done, state) =>
        state > n
          ? done ()
          : next ( state        // value to add to output
                 , state + 1    // next state
                 )
    , m // initial state
    )

console.log (range (3, 10))
// [ 3, 4, 5, 6, 7, 8, 9, 10 ]

Above, we start with an initial state of a number, m in this case. Just like the accumulator variable in reduce, you can specify any initial state to unfold. Below, we express your program using unfold. We add parent to make it easy to select a folder's parent

const parent = ({ parentId }) =>
  data .find (f => f.id === String (parentId))

const breadcrumb = folder =>
  unfold
    ( (next, done, f) =>
        f == null
          ? done ()
          : next ( f          // add folder to output
                 , parent (f) // loop with parent folder
                 )
    , folder // init state
    )

breadcrumb (data[3])
// [ { id: '1.1.1', parentId: '1.1', name: 'folder1.1.1' }
// , { id: '1.1', parentId: 1, name: 'folder1.1' }
// , { id: '1', parentId: null, name: 'folder1' } ]

breadcrumb (data[4])
// [ { id: '2.1', parentId: 2, name: 'folder2.1' }
// , { id: '2', parentId: null, name: 'folder2' } ]

breadcrumb (data[0])
//  [ { id: '1', parentId: null, name: 'folder1' } ]

You can verify the results of the program below

const data =
  [ {id: "1", parentId: null, name: "folder1"}
  , {id: "2", parentId: null, name: "folder2"}
  , {id: "1.1", parentId: 1, name: "folder1.1"}
  , {id: "1.1.1", parentId: "1.1", name: "folder1.1.1"}
  , {id: "2.1", parentId: 2, name: "folder2.1"}
  ]
  
const unfold = (f, init) =>
  f ( (value, state) => [ value, ...unfold (f, state) ]
    , () => []
    , init
    )

const parent = ({ parentId }) =>
  data .find (f => f.id === String (parentId))

const breadcrumb = folder =>
  unfold
    ( (next, done, f) =>
        f == null
          ? done ()
          : next ( f          // add folder to output
                 , parent (f) // loop with parent folder
                 )
    , folder // init state
    )

console.log (breadcrumb (data[3]))
// [ { id: '1.1.1', parentId: '1.1', name: 'folder1.1.1' }
// , { id: '1.1', parentId: 1, name: 'folder1.1' }
// , { id: '1', parentId: null, name: 'folder1' } ]
   
console.log (breadcrumb (data[4]))
// [ { id: '2.1', parentId: 2, name: 'folder2.1' }
// , { id: '2', parentId: null, name: 'folder2' } ]

console.log (breadcrumb (data[0]))
//  [ { id: '1', parentId: null, name: 'folder1' } ]

If you trace the computation above, you see that find is called once per folder f added to the outupt in the unfolding process. This is an expensive operation, and if your data set is significantly large, could be a problem for you.

A better solution would be to create an additional representation of your data that has a structure better suited for this type of query. If all you do is create a Map of f.id -> f, you can decrease lookup time from linear to logarithmic.

unfold is really powerful and suited for a wide variety of problems. I have many other answers relying on it in various ways. There's even some dealing with asynchrony in there, too.

If you get stuck, don't hesitate to ask follow-up questions :D

Mulan
  • 129,518
  • 31
  • 228
  • 259
0

You could do this with a Map so you avoid iterating over the array each time you need to retrieve the next parent. This way you get an O(n) instead of an O(n²) time complexity:

const foldersArray = [{id: "1", parentId: null, name: "folder1"}, {id: "2", parentId: null, name: "folder2"}, {id: "1.1", parentId: "1", name: "folder1.1"}, {id: "1.1.1", parentId: "1.1", name: "folder1.1.1"},{id: "2.1", parentId: "2", name: "folder2.1"}];
const folderMap = new Map(foldersArray.map( o => [o.id, o] ));
const getFolderParents = folder => 
    (folder.parentId ? getFolderParents(folderMap.get(folder.parentId)) : [])
    .concat(folder.name);

// Example call:
console.log(getFolderParents(foldersArray[4]));

Just a minor remark: your parentId data type is not consistent: it better be always a string, just like the data type of the id property. If not, you need to cast it in your code, but it is really better to have the data type right from the start. You'll notice that I have defined parentId as a string consistently: this is needed for the above code to work. Alternatively, cast it to string in the code with String(folder.parentId).

Secondly, the above code will pre-pend the parent folder name (like is done in file folder notations). If you need to append the parent name after the child, then swap the concat subject and argument:

[folder.name].concat(folder.parentId ? getFolderParents(folderMap.get(folder.parentId)) : []);
trincot
  • 317,000
  • 35
  • 244
  • 286
0

You can do what you're looking for with a rather ugly looking while loop. Gets the job done though. Each loop iteration filters, looking for an instance of a parent. If that doesn't exist, it stops and exits. If it does exist, it pushes that parent into the tree array, sets folder to its parent to move up a level, then moves on to the next iteration.

const foldersArray = [{
  id: "1",
  parentId: null,
  name: "folder1"
}, {
  id: "2",
  parentId: null,
  name: "folder2"
}, {
  id: "1.1",
  parentId: 1,
  name: "folder1.1"
}, {
  id: "1.1.1",
  parentId: "1.1",
  name: "folder1.1.1"
}, {
  id: "2.1",
  parentId: 2,
  name: "folder2.1"
}]

function getParents(folder){
 const tree = [], storeFolder = folder
  let parentFolder
  while((parentFolder = foldersArray.filter(t => t.id == folder.parentId)[0]) !== undefined){
    tree.push(parentFolder)
    folder = parentFolder
  }
  
  console.log({ originalFolder: storeFolder, parentTree: tree})
}

getParents(foldersArray[3])
jmcgriz
  • 2,819
  • 1
  • 9
  • 11