Search code examples
javascripttreereduce

Reducing a tree of paths to all combinations of paths using reduce function


I'm trying to implement a function using JavaScript's reduce method to process a tree-like string indented by tabs. Consider that the input is always correctly tab-indented. I know it can be done using backtrack, but I was wondering if that was possible with reduce. My code is shown below, but I can't reach the desired output. Can anyone help me understand what I'm doing wrong and how I can fix this problem?

Given the input:

/wp-content/
    /uploads
    /plugins/
        /akismet/
    /themes/
        /twentyeleven/
            /colors/

I expect the output to be a list of combinations of paths, one per line, including intermediate paths (e.g. /wp-content/ and /wp-content/plugins):

/wp-content/
/wp-content/uploads
/wp-content/plugins/
/wp-content/plugins/akismet/
/wp-content/themes/
/wp-content/themes/twentyeleven/
/wp-content/themes/twentyeleven/colors

However, my code is not working as expected as I'm getting repeated output instead:

/wp-admin/wp-content/plugins/akismet/themes/twentyeleven/colors 
/wp-admin/wp-content/plugins/akismet/themes/twentyeleven/images 
/wp-admin/wp-content/uploads 
/wp-admin/wp-content/plugins/akismet/themes/twentyeleven/colors 
/wp-admin/wp-content/plugins/akismet/themes/twentyeleven/images 
/wp-admin/wp-content/uploads 

Here's what I've tried

rm = (s, ch='\t') => s?.replaceAll(ch,'')
count = (s, ch='\t') => s?.match(new RegExp(ch,'g'))?.length
wrapper = (arr, fun, start) => arr.reduce(fun, start)

function fn(acc, curr, i, self){
  x = count(curr) 
    y = count(self[i+1])
  curr = rm(curr)
  acc = rm(acc)
  selfi = rm(self[i+1]) 
  if(!selfi) return acc
  
  if(curr.at(-1) === '/' && acc.at(0) === '/') {
     curr = curr.slice(0,-1)
  } // ends and start with /
    if(x===y){
            console.log(acc+curr)
      return acc
    } else { 
      idx = self.indexOf(self.slice(0,i+1).filter(k => count(k) === x)[0]) 
      if(!idx) idx = -1
      wrapper(self.slice(i,idx), fn, acc) 
      return acc+curr
  }
}

wrapper(ar, fn, '/wp-admin')

Solution

  • As indicated by others, this is typically solved with a stack.

    But for one input line, the stack may need to get rid of more than one item, while it should always get a new entry.

    I have added a few more lines to your example input to illustrate this:

    function paths(input, prefix = "") {
        const stack = [{length: -1, name: prefix}];
        return Array.from(input.matchAll(/^(.*?)(\S.+?)(\/?)\s*$/gm), 
            ([, {length}, name, tail]) => {
                while (stack.at(-1).length >= length) stack.pop();
                stack.push({ length, name });
                return stack.map(({ name }) => name).join("") + tail;
            }
        );
    }
    
    console.log(paths(`\
    /wp-content/
        /uploads
        /plugins/
            /akismet/
        /themes/
            /twentyeleven/
                /colors/
        /images/
            /thumbnails/
    /other-content/`, "/wp-admin"));
    .as-console-wrapper { max-height: 100% !important; top: 0; }