0

I'm sure someone can explain this to me but even though the below function, see Main Code, finds the correct data it somehow returns the wrong data?

The return I get is

{
  name: 'section 3',
  sectionParentName: 'ROOT',
  sectionChildren: [
    {
      name: 'section 3.1',
      sectionParentName: 'section 3',
      sectionChildren: [Array]       
    },
    {
      name: 'section 3.2',
      sectionParentName: 'section 3',
      sectionChildren: []
    }
  ]
}

If I console.log(found) I get what I'm looking for

[
  {
    name: 'section 3.1.2.1',
    sectionParentName: 'section 3.1.2',
    sectionChildren: []
  },
  {
    name: 'section 3.1.2.2',
    sectionParentName: 'section 3.1.2',
    sectionChildren: []
  }
]

In fact, the only reason I have 'const found' instead of just 'return section.sectionChildren' is because changing 'section.sectionChildren' to 'section.name' did nothing to the return data as a test? At least when assigning 'section.sectionChildren' to 'const found' and console logging it out I could see it contained the correct data.

Main Code

const MenuRoot = [
  {
    name: "section 1",
    sectionParentName: "ROOT",
    sectionChildren: [
      {
        name: "section 1.1",
        sectionParentName: "section 1",
        sectionChildren: [],
      },
      {
        name: "section 1.2",
        sectionParentName: "section 1",
        sectionChildren: [],
      },
    ],
  },
  {
    name: "section 2",
    sectionParentName: "ROOT",
    sectionChildren: [
      {
        name: "section 2.1",
        sectionParentName: "section 2",
        sectionChildren: [],
      },
      {
        name: "section 2.2",
        sectionParentName: "section 2",
        sectionChildren: [],
      },
    ],
  },
  {
    name: "section 3",
    sectionParentName: "ROOT",
    sectionChildren: [
      {
        name: "section 3.1",
        sectionParentName: "section 3",
        sectionChildren: [
          {
            name: "section 3.1.1",
            sectionParentName: "section 3.1",
            sectionChildren: [],
          },
          {
            name: "section 3.1.2",
            sectionParentName: "section 3.1",
            sectionChildren: [
              {
                name: "section 3.1.2.1",
                sectionParentName: "section 3.1.2",
                sectionChildren: [],
              },
              {
                name: "section 3.1.2.2",
                sectionParentName: "section 3.1.2",
                sectionChildren: [],
              },
            ],
          },
        ],
      },
      {
        name: "section 3.2",
        sectionParentName: "section 3",
        sectionChildren: [],
      },
    ],
  },
];


const findParent = (MenuRoot, parentName) => {
  const findSection = MenuRoot.find((section) => {
    // if (section.name.match(parentName, "i")) {
    if (section.name === parentName) {
      const found = section.sectionChildren;
      return found;
    } else {
      return findParent(section.sectionChildren, parentName);
    }
  });
  return findSection;
};

console.log(findParent(MenuRoot, "section 3.1.2"));
  • `find` takes *a predicate* - a function that returns `true` or `false` (any other result will be coerced to a boolean). So, you're not supposed to do `return itemWanted` but `return true` which signifies that the current item is the wanted one. You cannot transform the value returned from within the callback. – VLAZ Apr 21 '21 at 11:46
  • Hi VLAZ, whilst I understand what you are saying the example in https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/find show there is an output of more that True or False. I can also reproduce their code. But if I try to apply this using recursion it doesn't work as expected (well, at least by me). What am I missing? How would you get the result I need? – Simon Edwards Apr 21 '21 at 15:48
  • The *callback* needs to return `true` or `false`. See the first example it's only a comparison: `element => element > 10`. However, the callback is applied to each element and the result of `.find()` is the first element where the callback returns `true` (or a truthy value). The item is not changed as a result. You can again see the first example: `array1.find(element => element > 10);` returns `12` - the item from the array, not the boolean value `true` which `12 > 10` will produce. – VLAZ Apr 21 '21 at 15:56

3 Answers3

1

The Array#find() method accepts a predicate - a function that returns true or false. Any other value will just be coerced to a boolean. The result tells .find() whether to return the current item or not. So, the return value does not change what .find() itself returns - it is always going to be an item from the array, not the return from the callback. See this example:

const arr = [{}, {foo: "hello"}, {bar: "world"}];

const result = arr.find(obj => obj.bar);

console.log(result);

The result is an object, not the value of the bar property.

So, what happens is that your findParent will correctly recursively traverse each item and its children, however since it only returns an object, that's taken as a truthy value. Only the first invocation of .find() will then get a truthy result and return the top level item - the recursive calls are irrelevant as their values are just converted to a boolean for the recursive calls of the .find() callback.


Instead, you can recursively flatten the array using Array#flatMap() and only traverse this single level array using .find() and return the value for sectionChildren using optional chaining if the result is found:

const flattenWithChildren = item => 
  item.flatMap(x => [x, ...flattenWithChildren(x.sectionChildren)]);

const findParent = (menu, parentName) =>
  flattenWithChildren(menu)
    .find(section => section.name === parentName)
    ?.sectionChildren

const MenuRoot = [ { name: "section 1", sectionParentName: "ROOT", sectionChildren: [ { name: "section 1.1", sectionParentName: "section 1", sectionChildren: [], }, { name: "section 1.2", sectionParentName: "section 1", sectionChildren: [], }, ], }, { name: "section 2", sectionParentName: "ROOT", sectionChildren: [ { name: "section 2.1", sectionParentName: "section 2", sectionChildren: [], }, { name: "section 2.2", sectionParentName: "section 2", sectionChildren: [], }, ], }, { name: "section 3", sectionParentName: "ROOT", sectionChildren: [ { name: "section 3.1", sectionParentName: "section 3", sectionChildren: [ { name: "section 3.1.1", sectionParentName: "section 3.1", sectionChildren: [], }, { name: "section 3.1.2", sectionParentName: "section 3.1", sectionChildren: [ { name: "section 3.1.2.1", sectionParentName: "section 3.1.2", sectionChildren: [], }, { name: "section 3.1.2.2", sectionParentName: "section 3.1.2", sectionChildren: [], }, ], }, ], }, { name: "section 3.2", sectionParentName: "section 3", sectionChildren: [], }, ], }, ];

console.log(findParent(MenuRoot, "section 3.1.2"));

An alternative approach would be to write function that recursively checks each item and then their descendents:

const findParent = (menu, parentName) => {
  if (Array.isArray(menu)){
    for (const section of menu) {
      const found = findParent(section, parentName);
      
      if (found) 
        return found;
    }
    
    return;
  }
  
  if (menu.name === parentName)
    return menu.sectionChildren;
    
  return findParent(menu.sectionChildren, parentName);
}

const MenuRoot = [ { name: "section 1", sectionParentName: "ROOT", sectionChildren: [ { name: "section 1.1", sectionParentName: "section 1", sectionChildren: [], }, { name: "section 1.2", sectionParentName: "section 1", sectionChildren: [], }, ], }, { name: "section 2", sectionParentName: "ROOT", sectionChildren: [ { name: "section 2.1", sectionParentName: "section 2", sectionChildren: [], }, { name: "section 2.2", sectionParentName: "section 2", sectionChildren: [], }, ], }, { name: "section 3", sectionParentName: "ROOT", sectionChildren: [ { name: "section 3.1", sectionParentName: "section 3", sectionChildren: [ { name: "section 3.1.1", sectionParentName: "section 3.1", sectionChildren: [], }, { name: "section 3.1.2", sectionParentName: "section 3.1", sectionChildren: [ { name: "section 3.1.2.1", sectionParentName: "section 3.1.2", sectionChildren: [], }, { name: "section 3.1.2.2", sectionParentName: "section 3.1.2", sectionChildren: [], }, ], }, ], }, { name: "section 3.2", sectionParentName: "section 3", sectionChildren: [], }, ], }, ];

console.log(findParent(MenuRoot, "section 3.1.2"));

On a mostly unrelated note, you can take out some of the boilerplate when making recursive structures by using a simple data constructor:

const Section = (name, sectionParentName, ...sectionChildren) =>
  ({name, sectionParentName, sectionChildren});

const Section = (name, sectionParentName, ...sectionChildren) =>
  ({name, sectionParentName, sectionChildren});

const MenuRoot = [
  Section("section 1", "ROOT",
    Section("section 1.1", "section 1"),
    Section("section 1.2", "section 1"),
  ),
  Section("section 2", "ROOT", 
    Section("section 2.1", "section 2"),
    Section("section 2.2", "section 2"),
  ),
  Section("section 3", "ROOT",
    Section("section 3.1", "section 3",
      Section("section 3.1.1", "section 3.1"),
      Section("section 3.1.2", "section 3.1", 
        Section("section 3.1.2.1", "section 3.1.2"),
        Section("section 3.1.2.2", "section 3.1.2"),
      ),
    ),
    Section("section 3.2", "section 3"),
  ),
];

console.log(MenuRoot);
.as-console-wrapper { max-height: 100% !important; top: 0; }

Also possible to simplify further into two data constructors: the children can automatically get their parent's name.

const Section = (sectionParentName, name, ...children) =>
  ({ name, sectionParentName, sectionChildren: children.map(c => c(name)) });

const ChildSection = (...rest) => parentName =>
  Section(parentName, ...rest);

const Section = (sectionParentName, name, ...children) =>
  ({ name, sectionParentName, sectionChildren: children.map(c => c(name)) });

const ChildSection = (...rest) => parentName =>
  Section(parentName, ...rest);


const MenuRoot = [
  Section("ROOT", "section 1", 
    ChildSection("section 1.1"),
    ChildSection("section 1.2"),
  ),
  Section("ROOT", "section 2", 
    ChildSection("section 2.1"),
    ChildSection("section 2.2"),
  ),
  Section("ROOT", "section 3", 
    ChildSection("section 3.1",
      ChildSection("section 3.1.1"),
      ChildSection("section 3.1.2", 
        ChildSection("section 3.1.2.1"),
        ChildSection("section 3.1.2.2"),
      ),
    ),
    ChildSection("section 3.2"),
  ),
];

console.log(MenuRoot);
.as-console-wrapper { max-height: 100% !important; top: 0; }
VLAZ
  • 26,331
  • 9
  • 49
  • 67
1

VLAZ well explained the issue with your version, and gave several nice suggestions as well. My approach would be a bit different. I would build this on top of a deepFind function which recursively searches a tree until it finds a node matching your predicate. And that function I would build on top of a generator function, traverse which returns the nodes of your tree one-by-one in a preorder traversal.

The generator function is useful here to allow us to stop the traversal when we choose.

It could look like this:

function * traverse (xs = []) {
  for (let x of xs) {
    yield x;
    yield * traverse (x.sectionChildren)
  }
}

const deepFind = (pred) => (obj) => {
  for (let node of traverse (obj)) {
    if (pred (node)) {return node} 
  }
}

const findParent = (tree, target) =>
  deepFind (({name}) => name == target) (tree) ?.sectionChildren

const MenuRoot = [{name: "section 1", sectionParentName: "ROOT", sectionChildren: [{name: "section 1.1", sectionParentName: "section 1", sectionChildren: []}, {name: "section 1.2", sectionParentName: "section 1", sectionChildren: []}]}, {name: "section 2", sectionParentName: "ROOT", sectionChildren: [{name: "section 2.1", sectionParentName: "section 2", sectionChildren: []}, {name: "section 2.2", sectionParentName: "section 2", sectionChildren: []}]}, {name: "section 3", sectionParentName: "ROOT", sectionChildren: [{name: "section 3.1", sectionParentName: "section 3", sectionChildren: [{name: "section 3.1.1", sectionParentName: "section 3.1", sectionChildren: []}, {name: "section 3.1.2", sectionParentName: "section 3.1", sectionChildren: [{name: "section 3.1.2.1", sectionParentName: "section 3.1.2", sectionChildren: []}, {name: "section 3.1.2.2", sectionParentName: "section 3.1.2", sectionChildren: []}]}]}, {name: "section 3.2", sectionParentName: "section 3", sectionChildren: []}]}]

console .log (findParent(MenuRoot, 'section 3.1.2'))

Note that findParent is quite simple. The complexity we might otherwise include is abstracted away in traverse and deepFind. But those two functions are themselves very simple.

I find this breakdown into a collection of simple functions an elegant way to code.

Scott Sauyet
  • 49,207
  • 4
  • 49
  • 103
  • 1
    Hi Scott, thank you for the feedback. As someone trying to learn code it's never a dull day :-). A bit of generator here, a splash of currying there. I can see that it works, I just need some time to understand it. – Simon Edwards Apr 23 '21 at 13:58
0

Edit: After writing this answer I realized that you are probably looking to learn and not for a simple library solution. So please take it as that: There are libraries out there that can help you :)


Here is an iterative solution using object-scan

// const objectScan = require('object-scan');

const myMenuRoot = [{ name: 'section 1', sectionParentName: 'ROOT', sectionChildren: [{ name: 'section 1.1', sectionParentName: 'section 1', sectionChildren: [] }, { name: 'section 1.2', sectionParentName: 'section 1', sectionChildren: [] }] }, { name: 'section 2', sectionParentName: 'ROOT', sectionChildren: [{ name: 'section 2.1', sectionParentName: 'section 2', sectionChildren: [] }, { name: 'section 2.2', sectionParentName: 'section 2', sectionChildren: [] }] }, { name: 'section 3', sectionParentName: 'ROOT', sectionChildren: [{ name: 'section 3.1', sectionParentName: 'section 3', sectionChildren: [{ name: 'section 3.1.1', sectionParentName: 'section 3.1', sectionChildren: [] }, { name: 'section 3.1.2', sectionParentName: 'section 3.1', sectionChildren: [{ name: 'section 3.1.2.1', sectionParentName: 'section 3.1.2', sectionChildren: [] }, { name: 'section 3.1.2.2', sectionParentName: 'section 3.1.2', sectionChildren: [] }] }] }, { name: 'section 3.2', sectionParentName: 'section 3', sectionChildren: [] }] }];

const search = objectScan(['++(^sectionChildren$)'], {
  reverse: false,
  useArraySelector: false,
  rtn: 'parent',
  abort: true,
  filterFn: ({ gparent, context }) => gparent.name === context
});

console.log(search(myMenuRoot, 'section 3.1.2'));
/* => [
  {
    name: 'section 3.1.2.1',
    sectionParentName: 'section 3.1.2',
    sectionChildren: []
  },
  {
    name: 'section 3.1.2.2',
    sectionParentName: 'section 3.1.2',
    sectionChildren: []
  }
] */
.as-console-wrapper {max-height: 100% !important; top: 0}
<script src="https://bundle.run/object-scan@16.0.0"></script>

Disclaimer: I'm the author of object-scan

vincent
  • 1,953
  • 3
  • 18
  • 24