-1

I have a collection containing planets and their moons as a Children collection.
It is a collection but it really represents a tree-like structure. I am showing only 2 tree levels for simplicity but each planet or moon could further have a collection of chemical elements, so I use 2 level tree only for simplicity.

Mercury

Venus

Mars
  - Deimos
  - Phobos

Jupiter
  - Europa
  - Ganymede
  - Io

I know how to convert this collection to a list, I just use

var myList = myCollection.Values.ToList();

I would like to search this list for each item containing "m" in its name. If parent does not have "m" in its name but any of its children moons has, I would like to include that child (moon) AND its parent (planet). In case of Jupiter, I would include both Jupiter and Ganymede in my list.

My search for "m" would therefore return following list

{Mercury, Mars, Deimos, Jupiter, Ganymede}

Id prefer using lambda for this but it is not required to do so

UPDATE: Structure

BodyNode
-ID      [Guid]
-Name    [string]
-IsChild [bool]
-Parent  [BodyNode]
-Children[BodyList ObservableCollection of BodyNode]

BodyTreeNode : BodyNode
-Expanded   [bool]
-Selected   [bool]
-Enabled    [bool]
pixel
  • 9,653
  • 16
  • 82
  • 149
  • Use a `DFS` to search the tree and if the name contains 'm' add it to a list. – dcg Mar 20 '17 at 18:53
  • If you want a case insensitive comparison you should use [`node.Name.IndexOf("m", StringComparison.OrdinalIgnoreCase) > -1`](https://msdn.microsoft.com/en-us/library/ms224425%28v=vs.110%29.aspx) instead of the mix of `ToUpper`/`ToLower` and [`Contains`](https://msdn.microsoft.com/en-us/library/dy85x1sa(v=vs.110).aspx) suggested in some of the answers. You could make an extension method if you want it to look nice: `public static bool Contains(this string source, string value, StringComparison comparison) => source.IndexOf(value, comparison) > -1;` and don't forget argument validation. – Johnbot Mar 21 '17 at 09:48

4 Answers4

2

If you are really sure that your data is a tree-like structure then you can do something like (without checking for cycles):

 bool GetNodes(Tree root, List<Tree> result, Func<Tree, bool> f) {
     bool add = f(root);
     foreach (var child in root.Children) {
         add ||= GetNodes(child, result, f);
     }
     if (add)
          result.Add(root);
     return add;
}

f is function that let you know whether or not to add a Tree. E.g. (t)=>t.Name.Contains("m").

Edit: Assuming all objects derive from Base and Base has a property public List<Base> GetChildren{get;} the above logic can be implemented like:

 bool GetNodes(Base b, List<Base> result, Func<Base, bool> f) {
     bool add = f(b);
     foreach (var child in b.GetChildren) {
          add ||= GetNodes(child);
     }
     if (add) result.Add(b);
     return add;
}

You would then use it as:

var r = new List<Base>();
myList.Foreach(o => GetNodes(o, r, (b) => b.Name.Contains("m")); 
dcg
  • 4,187
  • 1
  • 18
  • 32
  • Thanks, but I am struggling to follow it. Also, I dont have a Tree type, I have a collection that I convert to List. – pixel Mar 20 '17 at 19:11
  • All objects have a `List` that represents their children? Do they derive from a base class? – dcg Mar 20 '17 at 19:16
  • Take a look at my edit and let me know if it work for you. – dcg Mar 20 '17 at 19:36
2

Suppose you had a sequence of all elements of the collection. You could then use Where, or any other sequence operator, on that sequence. So your best bet is to build that sequence:

static class Extensions {
 public static IEnumerable<Nodes> Flatten(this IEnumerable<Node> nodes)
 {
  foreach(var node in nodes)
  {
    yield return node;
    foreach (var child in node.Children.Flatten())
      yield return child;
  }
 }
}

Easy peasy. And now you can simply say:

var results = from node in myCollection.Values.Flatten()
              where node.Name.ToLower().Contains("m")   
              select node;

That's a list of every element that has an m in the name:

Or using a lambda

var results = myCollection.Values.Flatten()
  .Where(node => ... and so on ... );

Now, what you want is a list of every element that has an m in the name or any child does. So write that; we have all the tools we need!

var results = from node in myCollection.Values.Flatten()
              where node.Name.Contains("m") || node.Children.Flatten().Any(node.Name.Contains("m"))
              select node;

Now, this is rather inefficient -- can you see why? -- but it does the trick. And now you have something working that you can analyze and try to make more efficient if you actually need to.

Eric Lippert
  • 647,829
  • 179
  • 1,238
  • 2,067
  • I didn't even think about `Flatten`. If you don't need to preserve the parent-child relationships in your results, this is the way to go. – CDove Mar 20 '17 at 20:18
1
var MRecords = myList.Where(x=>x.toUpper().Contains("M"));

var result = new HashSet<"yourClass">(); //i didn`t use hashset before but this as stated in [here](http://stackoverflow.com/questions/6391738/what-is-the-difference-between-hashsett-and-listt) eliminates duplicates 

foreach(var record in MRecords)
{
    result.Add(record);
    var ParentLooper = record; 
    while(ParentLooper.parent!=null) //i suppose roots have the parent as null
    {
         result.add(ParentLooper.parent);
         ParentLooper = ParentLooper.parent;
    }
}
return result;
Modar Na
  • 873
  • 7
  • 18
  • link is http://stackoverflow.com/questions/6391738/what-is-the-difference-between-hashsett-and-listt – Modar Na Mar 20 '17 at 19:05
  • Thanks but how will this include parent that has no "M" if its child has "M". It looks like this only includes all itesm (parents and childrens) with "M" in them because that is what MRecords will contain – pixel Mar 20 '17 at 19:10
  • sorry i was fast typing because of community xD ,, return the result not the MRecords – Modar Na Mar 20 '17 at 19:12
  • 1
    the MRecords is a helper list used to get all records having M .. then loop on Mrecords ,, add them with their parents to the final result ( which is a HashSet eliminating the duplicates – Modar Na Mar 20 '17 at 19:14
  • hmmm, although I can understand your solution best, I dont seem to be able to use it. The problem is that in your solution record would be my BodyTreeNode object but the ParentLooper.parent is my BodyNode object. BodyTreeNode descends from BodyNode but I am unable to do the 2 lines inside your while loop. – pixel Mar 20 '17 at 22:07
  • can you add the structure of BodyTreeNode and BodyNode – Modar Na Mar 20 '17 at 22:10
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/138573/discussion-between-modar-na-and-pixel). – Modar Na Mar 20 '17 at 22:27
0

Personally, I recommend using objects that contain IEnumerables of their own custom class when dealing with tree entities like this. Your Planet and your Moon both have the same properties, it seems, so they are essentially both of class Planet. Given that we're just dealing with names, I made a private mockup class as below:

private class Planet
        {
            public string Name { get; set; } = string.Empty;
            public IEnumerable<Planet> Children { get; set; } = new List<Planet>();
        }

This makes the use of Lambdas a bit simpler. I didn't get to test this one, but if it's not correct it's close to it and should get you there:

      IEnumerable<Planet> myCollection = new List<Planet>();
      myCollection = LetThereBeLight(true); //method to populate the list
      var myList = myCollection.ToList().Where(t => t.Name.ToUpper().Contains("M"));
      myList.ToList().AddRange
                            (
                             myCollection.SelectMany(t => t.Children.Where
                                    (
                                      v => v.Name.ToUpper().Contains("M")).Distinct()
                                    )
                            );

You could make it all one Lambda, but it's hard enough to read without trying to squeeze it all into one line. This will give you a single IEnumerable<Planet> If you want to signify a difference in the moons, like adding a "-" to the name string, you can loop through each Planet's Children collection. You may want to make sure you only use myList.Distinct(), as duplication is entirely possible here.

CDove
  • 1,940
  • 10
  • 19
  • but myList above includes only items with "M" in them. What if planet does not have "M" but only of its moon has "M"? They should be both included (i.e. Jupiter and its moon Ganymede). I dont see above including that. Or I am missing something? – pixel Mar 20 '17 at 19:47
  • Oops, you're right, my example gives you All "M" class planets and all "M" class moons (sorry, the joke was there, it had to be made). Amending... – CDove Mar 20 '17 at 20:15