-1

I have a class called TreeNode

class TreeNode {
  constructor(name) {
    // Name of the node visible to user. It is string
    self.name = name;

    //  Children of the node. it is array of TreeNode
    self.children = [];

    // If this field is true, children of the Node are shown to the user
    self.expanded = false;

    // If this field is true, it means that the node matches the current search and it is emphasized
    self.matched = false;
  }

The task is to perform the following: /* This function returns a new subtree of the original tree which satisfies the following requirements:

  • Function doesn't modify the original tree

  • The 'Node matches the searched' term means that Node's name contains the 'search' as a substring (case insensitive)

  • Node is included in the resulting subtree if Node, one of its ancestors, or one of its descendants matches the search

  • If Node matches the search, its matched property must be set to true, otherwise false

  • If at least one descendant of the Node matches the search, Node's expanded property must be set to true, otherwise false

    @returns TreeNode | null */

    makeTreeForSearchQuery(search) { // Do Something here return null; } }

I have a function within the class

makeTreeForSearchQuery(search)
{
if (self.children != null)
    {
        for(var i=0; i < self.children.length; i++)
        {
            self.children[i]= makeTreeForSearchQuery(search);
            if(children.matched[i] == true)
            {
                //self.parent = children.matched[i];
                //self.expanded = true;
            }
            
            //Something needs to be done here
        }
    }
        if(self.name === search)
        {
            self.matched = true;
            console.log(self.name);
        }
    
    return TreeNode;
}

I need to get this result: Search = 'two'
Result tree:

root - matched:false, expanded:true

left - matched:false, expanded:true

two - matched:true, expanded:false

Example
 * Original tree
 *       root
 *      |    \
 *    left  right
 *   |   |  \    \
 * one two three four
 *
 * Search = 'two'
 
 Result tree:
 *     root - matched:false, expanded:true
 *      |
 *    left - matched:false, expanded:true
 *      |
 *     two - matched:true, expanded:false
 Or if we describe it in JSON format
 Original tree
  {
  name: "root",
  expanded: false,
  matched: false,
  children: [
    {
      name: "left",
      expanded: false,
      matched: false,
      children: [
        {
          name: "one",
          expanded: false,
          matched: false,
          children: [],
        },
        {
          name: "two",
          expanded: false,
          matched: false,
          children: [],
        },
      ],
    },
    {
      name: "right",
      expanded: false,
      matched: false,
      children: [
        {
          name: "three",
          expanded: false,
          matched: false,
          children: [],
        },
        {
          name: "four",
          expanded: false,
          matched: false,
          children: [],
        },
      ],
    },
  ],
};
Result Tree:
{
  name: "root",
  expanded: true,
  matched: false,
  children: [
    {
      name: "left",
      expanded: true,
      matched: false,
      children: [
        {
          name: "two",
          expanded: false,
          matched: true,
          children: [],
        },
      ],
    },
  ],
}
Scott Sauyet
  • 49,207
  • 4
  • 49
  • 103
DJ Danny
  • 9
  • 1
  • 4
  • name: "root", expanded: false, matched: false, children: [ { name: "left", expanded: false, matched: false, children: [ { name: "one", expanded: false, matched: false, children: [], }, { name: "two", expanded: false, matched: false, children: [], }, ], }; //and so on. this is the sample data I have. The requirement is to use `self` – DJ Danny Jun 29 '22 at 14:25
  • `self` will not do what you hope here. In the browser, you will be setting `name`, `children`, `expanded`, and `matched` properties of the window. In Node, it will simply throw an error. – Scott Sauyet Jun 29 '22 at 14:35
  • Thanks, Scott. I have updated the question. Let me try with using `this` – DJ Danny Jun 29 '22 at 14:51
  • Is this a search tree? I.e. is it ordered somehow so that you can quickly know in which branch of the tree to search? Or will you need to search the entire tree every time? – Bergi Jun 29 '22 at 15:30

2 Answers2

1

I'm not sure what you (or your instructor) is looking for exactly, but we can do this in a more functional manner, without constructor functions or methods, with code something like this:

const treeNode = (name, children = []) => 
  ({name, children, expanded: false, matched: false})

const match = (query) => (
  {name, children: kids}, _, __, 
  children = kids .map (match (query)) .filter (n => n .matched || n.expanded)
) => ({
  name,
  children,
  expanded: children .length > 0,
  matched: name .toLowerCase () .includes (query .toLowerCase())
})

const tree = treeNode ("root", [treeNode ('left', [treeNode ('one'), treeNode ('two')]), treeNode ('right', [treeNode ('three'), treeNode ('four')])])


console .log ('Original tree', tree)
console .log ('Query "Two" result', match ('Two') (tree))
console .log ('Query "o" result', match ('o') (tree))
console .log ('Original tree is not modified', tree)
.as-console-wrapper {max-height: 100% !important; top: 0}

But the techniques may be beyond what you're supposed to be using for your course.

The factory function treeNode constructs a plain JSON object, in a similar manner to your constructor function (once you fix the self/this problem.)

match takes a query parameter and returns a function which takes your node, destructures it into name and children properties (we can safely ignore the others for now), aliasing kids for children, adds two blank parameters for technical reasons1 and then create a children parameter by recurring on the node's children, and filtering to include only those that are themselves matched or which have matched descendants (and are therefore expanded.) We return a new object with the existing name property, the new list of children, and expanded flag set true if we have any children, and a matched flag if the current name matches the query parameter.

The base case for our recursion is slightly difficult to see. It happens when children is empty; at that point, we don't make deeper calls to match. And our recursive case is simply kids .map (match (query)).

We can certainly convert this to to something more OO, as needed. But I think this is simpler.

Update

I went ahead and tried an OO solution. It may seem more familiar to you.

class TreeNode {
  constructor (name, children = []) {
    this .name = name
    this .children = children
    this .expanded = false
    this .matched = false
  }
  
  match (query) {
    const children = this .children .map (c => c .match (query))
                          .filter (n => n .matched || n.expanded)
    const newNode = new TreeNode (this .name, children)
    newNode .expanded = children .length > 0
    newNode .matched = this .name .toLowerCase () .includes (query .toLowerCase())
    
    return newNode
  }
}

const tree = new TreeNode ("root", [new TreeNode ('left', [new TreeNode ('one'), new TreeNode ('two')]), new TreeNode ('right', [new TreeNode ('three'), new TreeNode ('four')])])

console .log ('Original tree', tree)
console .log ('Query "Two" result', tree .match ('Two'))
console .log ('Query "o" result', tree .match ('o'))
console .log ('Original tree is not modified', tree)
.as-console-wrapper {max-height: 100% !important; top: 0}

Update 2

What Bergi mentioned in the comments is that I'm using a fairly obscure, and in some ways horrible, hack to squeeze an extra value into the parameters so that I can code with pure expressions, rather than statements. (The reasons for that involve a long discussion.)

But if you want, this version may seem more familiar:

const match = (query) => ({name, children: kids}) =>  {
  const children = kids .map (match (query)) .filter (n => n .matched || n.expanded)
  return {
    name,
    children,
    expanded: children .length > 0,
    matched: name .toLowerCase () .includes (query .toLowerCase())
  }
}

It has the exact same behavior as my first sample.

Update 3

And now that I've mentioned that version, it probably makes sense to describe the other two versions I offered in the comments. One, using a really simple call helper,

const call = (fn, ...args) => fn (...args)

which we might write in either of two ways:

const match = (query) => ({name, children: kids}) => call ((
  children = kids .map (match (query)) .filter (n => n .matched || n.expanded)
) => ({
  name,
  children,
  expanded: children .length > 0,
  matched: name .toLowerCase () .includes (query .toLowerCase())
}))

or

const match = (query) => ({name, children: kids}) => call (
  (children) => ({
    name,
    children,
    expanded: children .length > 0,
    matched: name .toLowerCase () .includes (query .toLowerCase())
  }), 
  kids .map (match (query)) .filter (n => n .matched || n.expanded)
)

The second replaces that call function with an IIFE. And again, we can write it in either of two ways:

const match = (query) => ({name, children: kids}) => (((
  children = kids .map (match (query)) .filter (n => n .matched || n.expanded)
) => ({
  name,
  children,
  expanded: children .length > 0,
  matched: name .toLowerCase () .includes (query .toLowerCase())
})))()

or

const match = (query) => ({name, children: kids}) => (((children) => ({
  name,
  children,
  expanded: children .length > 0,
  matched: name .toLowerCase () .includes (query .toLowerCase())
}))) (kids .map (match (query)) .filter (n => n .matched || n.expanded))

Any of these version would do the same thing, and they all help with my boycott of statements. I'm partial to the first call version in my own work, but don't tend to use it in answers on SO because there is often confusion about the simple call function, confusion which distracts from the main points of the answers.

I do use the IIFE version, but the syntax can be distracting.


1This is so that we can pass the match(query) function to map, which supplies index and array parameters we don't need, but still default the additional children parameter.

Scott Sauyet
  • 49,207
  • 4
  • 49
  • 103
  • "*adds two blank parameters for technical reasons*" - that's horrible. `children` should not be a parameter at all. Just declare a normal `const` variable in a normal function body. – Bergi Jun 29 '22 at 15:32
  • @Bergi: sure. And my OO solution might be more to your liking. When I can, I prefer to code with only expressions, not statements. This might be over the top. I really want proper `let` binding; this poor-man's version is at best unsatisfactory. (Another alternative with a `call` helper function or an IIFE is probably no better.) I will add an update to show that as an alternative, though. – Scott Sauyet Jun 29 '22 at 15:41
  • 1
    I prefer the functional solution, but "*code with only expressions*" is not useful in JS where variable declarations are statements. If you really want `let` expressions, the IIFE trick (`(children => (…))(kids.map(…))`) is still a lot better than declaring parameters in functions and hoping they won't accidentally be used. – Bergi Jun 29 '22 at 15:49
  • @Bergi: You're probably right. But it's a hard habit to break! I don't hold out much hope for real `let` bindings in the language, but that's what I really want. While I know that my function definitions are still statements, I love for their implementations to be expression-only. In any case, I added another version to the answer in the style you suggest. If nothing else, it's much more beginner-friendly. – Scott Sauyet Jun 29 '22 at 15:53
  • Isn't there a transpiler plugin or macro for [do expressions](https://github.com/tc39/proposal-do-expressions) yet? – Bergi Jun 29 '22 at 15:56
  • The above OO function is much more understandable. Thank you so much, Scott. You've been of great help in making me understand the whole thing :) – DJ Danny Jun 29 '22 at 15:56
  • In Update 1 the result for query two is showing as expanded = true and matched = false where it should be the other way around. – DJ Danny Jun 29 '22 at 16:11
  • @DJDanny: Are you sure? I may have misunderstood the requirements, but as far as I can tell, all my examples have the same behavior. And for that one, the nested `'Two'` node is not expanded but is matched. It's ancestors are expanded but not matched. Is that not correct? – Scott Sauyet Jun 29 '22 at 16:20
  • @Bergi: I don't know if there's any transpilation available, but a five-year old proposal that's still at Stage 1 doesn't feel to me like it's going anywhere. But I don't follow the TC-39 activity particularly closely. – Scott Sauyet Jun 29 '22 at 16:24
  • @ScottSauyet checkout the screenshot for the output here [link](https://www.screencast.com/t/xbTFF7VbQsTu) – DJ Danny Jun 29 '22 at 16:26
  • @DJDanny: Rereading the specs, I think I may have it wrong all around. I'm not capturing this correctly: "*Node is included in the resulting subtree if Node, one of its ancestors, or one of its descendants matches the search*". I'm out of time now, but will try to revisit this something soon. – Scott Sauyet Jun 29 '22 at 16:27
  • @ScottSauyet No problem! You still have been of much help. Thanks again. – DJ Danny Jun 29 '22 at 16:30
0

It's extremely rare for me to add a second answer to the same question, but the previous one, which has a number of interesting discussions and numerous edits, seems over-full. Here's another approach, which seems to capture a requirement that was missing in that earlier pass:

const treeNode = (name, children = []) => 
  ({name, expanded: false, matched: false, children})

const match = (query) => ({name, children: kids}, forced = false) => {
  const matched = name .toLowerCase () .includes (query .toLowerCase())
  const allChildren = kids .map (child => match (query) (child, forced || matched)) 
  const children = forced || matched ? allChildren : allChildren .filter (n => n .matched || n .expanded)
  const expanded = children .length > 0

  return {name, expanded, matched, children}
}

const tree = treeNode ("root", [treeNode ('left', [treeNode ('one'), treeNode ('two')]), treeNode ('right', [treeNode ('three'), treeNode ('four')])])

console .log ('Original tree', tree)
console .log ('Query "Two" result', match ('Two') (tree))  // two
console .log ('Query "o" result', match ('o') (tree))      // root, one, two, four
console .log ('Query "g" result', match ('g') (tree))      // right
console .log ('Original tree is not modified', tree)
.as-console-wrapper {max-height: 100% !important; top: 0}

We proceed as in Update 2 of that answer, with a simple functional approach, and with an imperative function body.

The substantive difference here is that we default an additional parameter to the function returned by match (query): the parameter force, which we default to false. This is used to decide whether we keep all children or only those that themselves either match or have a matching descendant. Before the recursive calls on the children, we check if our current node matches, and if so, set the new forced flag to true for those calls. This is to handle the phrase from the requirements I missed in the first answer:

Node is included in the resulting subtree if Node, one of its ancestors, or one of its descendants matches the search

If a node matches, then all of its descendants should be included. This handles that. This doesn't address the OP's comment on the previous answer that the code gets it wrong for the search of Two. As far as I can tell, this creates output identical to the sample, and that part of the logic should not have changed here.

AFAICT, modulo the change from a constructor to a factory function, this now matches all the requirements, except possibly this one:

returns TreeNode | null 

This will always return a treeNode object when supplied one. I'm not sure what null case here is meant to be considered. But we don't have one.


Since we delved into parameter handling on the earlier answer, maybe we should look at it here. This uses a technique which could lead to problems with calling code overriding our default parameter. If we're concerned by this, because it's possible that someone will call someArray .map (match ('Two')) or because we simply don't know how the function might be used, we can write a private implementation that uses that extra parameter, and supply it from a public wrapper function, like this:

const _match = (query) => ({name, children: kids}, forced) => {
  const matched = name .toLowerCase () .includes (query .toLowerCase())
  const allChildren = kids .map (child => _match (query) (child, forced || matched)) 
  const children = forced ? allChildren : allChildren .filter (n => n .matched || n.expanded)
  const expanded = children .length > 0
  return {name, expanded, matched, children}
}

const match  = (query) => (node) => 
  _match (query) (node, false)

Again, this should be straightforward to convert into an Object-Oriented style.

Update

Again, it seems relatively straightforward to convert to OO. This version should do it:

class TreeNode {
  constructor (name, children = []) {
    this .name = name
    this .expanded = false
    this .matched = false
    this .children = children
  }
  
  match (query, forced = false) {
    const matched = this .name .toLowerCase () .includes (query .toLowerCase())
    const allChildren = this .children .map (child => child .match (query, forced || matched)) 
    const children = forced || matched ? allChildren : allChildren .filter (n => n .matched || n .expanded)
    const newNode = new TreeNode (this .name, children)
    newNode .expanded = children .length > 0
    newNode .matched = matched
    
    return newNode
  }
}

const tree = new TreeNode ("root", [new TreeNode ('left', [new TreeNode ('one'), new TreeNode ('two')]), new TreeNode ('right', [new TreeNode ('three'), new TreeNode ('four')])])

console .log ('Original tree', tree)
console .log ('Query "Two" result', tree .match ('Two'))  // two
console .log ('Query "o" result', tree .match ('o'))      // root, one, two, four
console .log ('Query "g" result', tree .match ('g'))      // right
console .log ('Original tree is not modified', tree)
.as-console-wrapper {max-height: 100% !important; top: 0}

All we had to do is to take the body of match, make it a method, and convert all uses of the treeNode parameter (node, and children) into references to properties of this, and do the same for the recursive reference to match.

In doing this conversion, I noticed a bug when I tried an additional test case ('g'). It's fixed both in the original version and this one:

-   const children = forced ? allChildren : allChildren .filter (n => n .matched || n .expanded)
+   const children = forced || matched ? allChildren : allChildren .filter (n => n .matched || n .expanded)
Scott Sauyet
  • 49,207
  • 4
  • 49
  • 103
  • extremely helpful :) A little difficult for me to convert to object-oriented style as this is quite advance for my understanding – DJ Danny Jun 29 '22 at 19:14
  • Could you please elaborate this part in OO? const match = (query) => ({name, children: kids}, forced = false) => { const matched = name .toLowerCase () .includes (query .toLowerCase()) const allChildren = kids .map (child => match (query) (child, forced || matched)) const children = forced ? allChildren : allChildren .filter (n => n .matched || n .expanded) const expanded = children .length > 0 return {name, expanded, matched, children} } – DJ Danny Jun 29 '22 at 20:30
  • Damn it all. I pasted in the code from the wrong tab for the OO version. D'oh. Fixed now. – Scott Sauyet Jun 30 '22 at 02:34
  • 1
    Thank you so much. It's working perfectly and is very understandable. Much appreciated :) – DJ Danny Jun 30 '22 at 17:46