Yerlan Yeszhanov
Yerlan Yeszhanov

Reputation: 2449

Parse array with nested objects by given indexes , get their name

Link to sandbox: https://codesandbox.io/s/cool-northcutt-7c9sr

I have a catalog with furniture and I need to make dynamic breadcrumbs. There is an array with nested objects which 5 levels deep. When I'm rendering list of furniture, I 'm saving all indexes from which array this list.

Expected output: Using my indexes, I need to parse object with nested array , get name of each object where belongs that index and save it in array

Indexes that I saved when user clicked on inventories . Key is a object name and property is actual index.

menuIndexes : {
  buildings: 0,
  building_styles: 3,
  rooms: 2,
  room_fillings: 0,
  filling_types: 1,
}

That piece of data , where i'm rendering from list of furniture . Name property is a name of a link in menu

{
  buildings: [
    {
      name: 'office',
      building_styles: [
        {
          name: 'interior',
          rooms: [
            {
              name: 'open space',
              filling_types: [
                {
                  name: 'furniture',
                  room_fillings: [
                    {
                      name: 'items',
                       // rendering inventories
                      inventories: [

                        {
                          id: 1,
                          name: 'tv'
                        },
                        {
                          id: 2,
                          name: 'chair'
                        },
                        {
                          id: 3,
                          name: 'table'
                        },
                      ]
                    }
                  ]

                }
              ]
            }
          ]
        },
      ]
    },
  ]
}

This image to understand where I'm getting this saved indexes

enter image description here

I have tried to make recursive function but it only gets first array and doesn't go any further to nested arrays

  const displayBreadCrumbs = () => {
    let menuKeys = Object.keys(menuIndexes)
    console.log({ menuIndexes });
    console.log(file);
    let arr = []
    let i = 0
    let pathToParse = file


    if (i < menuKeys.length) {
      if (menuKeys[i] in pathToParse) {
        pathToParse = pathToParse[menuKeys[i]][menuIndexes[menuKeys[i]]]
        console.log('pathToParse', pathToParse);
        i = i + 1
        // displayBreadCrumbs()
      }
    }
  }

Upvotes: 0

Views: 445

Answers (3)

Scott Sauyet
Scott Sauyet

Reputation: 50797

As usual, I would build such a function atop some reusable parts. This is my approach:

// Utility functions
const path = (obj, names) =>
  names .reduce ((o, n) => (o || {}) [n], obj)

const scan = (fn, init, xs) => 
  xs .reduce (
    (a, x, _, __, n = fn (a .slice (-1) [0], x)) => [...a, n], 
    [init]
  ) .slice (1)
  
const pluck = (name) => (xs) =>
  xs .map (x => x [name])


// Main function
const getBreadCrumbs = (data, indices) => 
  pluck ('name') (scan (path, data, Object .entries (indices)))


// Sample data
const data = {buildings: [{name: "office", building_styles: [{building_style: 0}, {building_style: 1}, {building_style: 2}, {name: "interior", rooms: [{room: 0}, {room: 1}, {name: "open space", filling_types: [{filling_type: 0}, {name: "furniture", room_fillings: [{name: "items", inventories: [{id: 1, name: "tv"}, {id: 2, name: "chair"}, {id: 3, name: "table"}]}, {room_filling: 1}]}, {filling_type: 2}]}, {room: 3}]}, {building_style: 4}]}]}
const menuIndexes = {buildings: 0, building_styles: 3, rooms: 2, filling_types: 1, room_fillings: 0}


// Demo
console .log (getBreadCrumbs (data, menuIndexes))

We have three reusable functions here:

  • path takes an object and a list of node names (strings or integers) and returns the value at the given path or undefined if any node is missing.1 For example:

    path ({a: {b: [{c: 10}, {c: 20}, {c: 30}, {c: 40}]}}, ['a', 'b', 2, 'c']) //=> 30.
    
  • scan is much like Array.prototype.reduce, except that instead of returning just the final value, it returns a list of the values calculated at each step. For example if add is (x, y) => x + y, then:

    scan (add, 0, [1, 2, 3, 4, 5]) //=> [1, 3, 6, 10, 15]
    
  • pluck pulls the named property off each of a list of objects:

    pluck ('b') ([{a: 1, b: 2, c: 3}, {a: 10, b: 20, c: 30}, {a: 100, b: 200, c: 300}]) 
    //=> [2, 20, 200]
    

In practice, I would actually factor these helpers even further, defining path in terms of const prop = (obj, name) => (obj || {}) [name], and using const last = xs => xs.slice (-1) [0] and const tail = (xs) => xs .slice (-1) in defining scan. But that's not important to this problem.

Our main function then can simply use these, along with Object.entries2, to first grab the entries from your index, passing that, our path function, and the data to scan to get a list of the relevant object nodes, then passing the result to pluck along with the string 'name' that we want to extract.

I use path and pluck nearly daily. scan is less common, but it's important enough that it's included in my usual utility libraries. With functions like that easily at hand, it's pretty simple to write something like getBreadCrumbs.


1 Side note, I usually define this as (names) => (obj) => ..., which I find most commonly useful. This form happens to fit better with the code used, but it would be easy enough to adapt the code to my preferred form: instead of scan (path, data, ...), we could just write scan ((a, ns) => path (ns) (a), data, ...)

2 As noted in the answer from Nick Parsons and that comment on it from Thankyou, there is a good argument to be made for storing this information in an array, which is explicitly ordered, rather than depending on the strange and arbitrary ordering that general objects get. If you did that, this code would only change by removing the Object .entries call in the main function.

Upvotes: 1

Nick Parsons
Nick Parsons

Reputation: 50759

You can loop through each key-value pair object in menuIndexes as see which key belongs to the current pathToParse object. Once you have the key which applies to the current object, you can access it's associated array as well as the index you need to look at. You can remove the key-value pair from the entries once you've found the key-value pair and again, recursively look for the next key within your new object. You can continue this until you can't find a key from menuIndexes which falls into your current data object (ie: findIndex returns -1);

const pathToParse = { buildings: [ { name: 'office', building_styles: [ { name: 'interior', rooms: [ { name: 'open space', filling_types: [ { name: 'furniture', inventories: [{ id: 1, name: 'tv' }, { id: 2, name: 'chair' }, { id: 3, name: 'table' }, ] } ] } ] }, ] }, ] }

const menuIndexes = {
  buildings: 0,
  building_styles: 0,
  rooms: 0,
  room_fillings: 0,
  filling_types: 0,
}

function getPath(data, entries) {
  const keyIdx = entries.findIndex(([key]) => key in data);
  if(keyIdx <= -1)
    return [];
    
  const [objKey, arrIdx] = entries[keyIdx];
  const obj = data[objKey][arrIdx];
  entries.splice(keyIdx, 1);
  return [obj.name].concat(getPath(obj, entries)); 
}

console.log(getPath(pathToParse, Object.entries(menuIndexes)));

The use of Object.entries() is to search the data object for the key to look at (as we don't want to rely on the key ordering of the menuIndexes object). If you have more control over menuIndexes, you could store it in an array where you can safely rely on the ordering of elements and thus keys:

const pathToParse = { buildings: [ { name: 'office', building_styles: [ { name: 'interior', rooms: [ { name: 'open space', filling_types: [ { name: 'furniture', inventories: [{ id: 1, name: 'tv' }, { id: 2, name: 'chair' }, { id: 3, name: 'table' }, ] } ] } ] }, ] }, ] };

const menuIndexes = [{key: 'buildings', index: 0}, {key: 'building_styles', index: 0}, {key: 'rooms', index: 0}, {key: 'filling_types', index: 0}, {key: 'inventories', index: 0}, ];

function getPath(data, [{key, index}={}, ...rest]) {
  if(!key)
    return [];
  const obj = data[key][index];
  return [obj.name, ...getPath(obj, rest)]; 
}

console.log(getPath(pathToParse, menuIndexes));

Upvotes: 2

Abito Prakash
Abito Prakash

Reputation: 4770

You can define a recursive function like this, it iterates through menuIndexes keys and find corresponding object in data. Once it finds the data, it pushes the name into output array and calls the function again with this object and menuIndexes

const displayBreadCrumbs = (data, menuIndexes) => {
    const output = [];
    Object.keys(menuIndexes).forEach(key => {
        if (data[key] && Array.isArray(data[key]) && data[key][menuIndexes[key]]) {
            output.push(data[key][menuIndexes[key]].name);
            output.push(...displayBreadCrumbs(data[key][menuIndexes[key]], menuIndexes));
        }
    });
    return output;
};
const data = { buildings: [ { name: 'office', building_styles: [ { name: 'interior', rooms: [ { name: 'open space', filling_types: [ { name: 'furniture', inventories: [{ id: 1, name: 'tv' }, { id: 2, name: 'chair' }, { id: 3, name: 'table' }, ] } ] } ] }, ] }, ] };

const menuIndexes = {
  buildings: 0,
  building_styles: 0,
  rooms: 0,
  room_fillings: 0,
  filling_types: 0,
};

displayBreadCrumbs(data, menuIndexes); // ["office", "interior", "open space", "furniture"]

Upvotes: 0

Related Questions