Ayoub k
Ayoub k

Reputation: 8918

Reformatting nested object to array of strings using recursion

I have the following object:

const modules = {
    celebrity: {
        actor: {
            male: 'male',
            female: 'female'
        },
        director: 'director'
    },
    movie: 'movie',
    user: 'user'
};

In result I want an array of string as the following:

[
    "celebrity/actor/male",
    "celebrity/actor/female",
    "celebrity/director",
    "movie",
    "user"
]

I create the following function:

function getPathsList(node, path = '') {
    let pathsList = [];
    const childs = Object.entries(node);
    for (const child of childs) {
        if (typeof child[1] === 'string') {
            path += `/${child[1]}`
            pathsList.push(path)
        } else {
            path += `/${child[0]}`
            pathsList = [...pathsList, ...getPathsList(child[1], path, pathsList)]
        }
    }
    return pathsList;
}

But I got:

[
    "/celebrity/actor/male", 
    "/celebrity/actor/male/female",
    "/celebrity/actor/director",
    "/celebrity/movie",
    "/celebrity/movie/user"
]

I know that the path variable should be initialized somewhere, but I can't figure it out.

Upvotes: 2

Views: 72

Answers (5)

Scott Sauyet
Scott Sauyet

Reputation: 50807

A very simple recursive solution using Object .entries returns an array containing only an empty string for non-objects, and otherwise, for every key-value, combines the key with the results of a recursive call to the value. The only slightly tricky part is to not insert the slash (/) before an empty string. It looks like this:

const getPathsList = (obj) => Object (obj) === obj
  ? Object .entries (obj) .flatMap (
      ([k, v]) => getPathsList (v) .map (p => p ? k + '/' + p : k)
    )
  : ['']

const modules = {celebrity: {actor: {male: 'male', female: 'female'}, director: 'director'}, movie: 'movie', user: 'user'}

console .log (getPathsList (modules))

But I would prefer to do this a slightly different way, building our function atop one that gathers the result into arrays of values (e.g. [['celebrity', 'actor', 'male'], ['celebrity', 'actor', 'female'], ... ['user']]), then simply joining those new arrays together with slashes. It's quite similar:

const getPaths = (obj) => Object (obj) === obj
  ? Object .entries (obj) .flatMap (
      ([k, v]) => getPaths (v) .map (p => [k, ...p])
    )
  : [[]]

const getPathsList = (obj) => 
  getPaths (obj) .map (xs => xs .join ('/'))

const modules = {celebrity: {actor: {male: 'male', female: 'female'}, director: 'director'}, movie: 'movie', user: 'user'}

console .log (getPathsList (modules))

I find that intermediate array format much more helpful.

This is a slightly less sophisticated version of getPaths than I generally write. Usually I distinguish between numeric array indices and string object keys, but that's not relevant here, since we're folding them back into strings, so this version is simplified.

Upvotes: 0

Mister Jojo
Mister Jojo

Reputation: 22431

Where is the difficulty ?

const
  modules = 
    { celebrity: 
      { actor:    { male: 'male', female: 'female' } 
      , director: 'director'
      } 
    , movie: 'movie'
    , user:  'user'
    } 
, pathsList = []
  ;
function getPathsList( obj, path='' )
  {
  for (let key in obj )
    {
    if (typeof(obj[key]) === 'object') getPathsList( obj[key], path+'/'+key )
    else pathsList.push( (path+'/'+key).substring(1) )
    }
  }

getPathsList( modules )


console.log( pathsList )

Upvotes: 0

grodzi
grodzi

Reputation: 5703

You may consider a "dfs" like algorithm where you explore every path from root to leaf.

You then join your path with '/'.

Subtlety: don't put the leaf itself into the path (e.g: otherwise you would get movie/movie)

Below an example using flatMap

const modules = {"celebrity":{"actor":{"male":"male","female":"female"},"director":"director"},"movie":"movie","user":"user"}
const flatten = x => {
  if (typeof(x) === 'string') { return [[]] }
  // each node has for children its entries
  // each node returns an array of path
  return Object.entries(x).flatMap(([k, v]) => {
    return flatten(v).map(path => [k , ...path])
  })
}
console.log(flatten(modules).map(path => path.join('/')))

Upvotes: 1

blex
blex

Reputation: 25648

Another way, using reduce:

const modules = {celebrity:{actor:{male:"male",female:"female"},director:"director"},movie:"movie",user:"user"};

function getPathsList(node, path = '') {
  return Object.entries(node)
    .reduce(
      (res, [k, v]) => res.concat(
        typeof v === "string" ? `${path}${v}` : getPathsList(v, `${path}${k}/`
      )
    ), []);
}

console.log(getPathsList(modules));

Upvotes: 1

Nina Scholz
Nina Scholz

Reputation: 386883

You could use an appraoch which works without a path, but assembles the path by iterating the nested part object.

function getPathsList(node) {
    const pathsList = [];
    for (const [key, value] of Object.entries(node)) {
        if (value && typeof value === 'object') {
            pathsList.push(...getPathsList(value).map(p => `${key}/${p}`))
        } else {
            pathsList.push(key);
        }
    }
    return pathsList;
}

const modules = {
    celebrity: {
        actor: {
            male: 'male',
            female: 'female'
        },
        director: 'director'
    },
    movie: 'movie',
    user: 'user'
};

console.log(getPathsList(modules));

Upvotes: 1

Related Questions