3

Using a semi-complex structure, I am trying to 'combine' several objects into one using the Linq Aggregate method (though if there is a better way, I am open to ideas).

Here is my basic class design.

class Aspect {
  string Name { get; set; }
}
class Arrangement {
  Aspect Aspect { get; set; }
  IList<Aperture> Apertures { get; set; }
  IList<Step> Steps { get; set; }
}

class Step {
  int Rank { get; set; }
  int Required { get; set; }
}
class Aperture {
  string Name { get; set; }
  int Size { get; set; }
}

Basically, I am trying to aggregate the entire hierarchy of an IEnumerable<Arrangement> and keep everything on the base level, but where things can appropriately overwrite, I want to overwrite them.

Update

I want to get all Arrangements that share the same Aspect.Name, and get a complete list of Steps, overwriting lower level Steps where higher level Arrangements have the same Rank with a different Required value.

So take for instance...

var list = new List<Arrangement>{
    new Arrangement{
      Aspect = Aspects.Named("One"),
      Steps = new List<Step>{
            new Step {
               Rank = 1,
               Required = 2
            },
            new Step {
               Rank = 2,
               Required = 4
            }
        }
    },
    new Arrangement{
      Aspect = Aspects.Named("One"),
      Steps = new List<Step>{
            new Step {
               Rank = 1,
               Required = 3
            }
        }
    }
  }

When aggregated properly, it should come out to look like ...

Arrangement
 - Aspect
    - Name : One
 - Steps 
    - Rank : 1
    - Required : 3
    - Rank : 2
    - Required : 4

I have attempted to use Distinct and Aggregate and it just isn't getting me anywhere. I keep ending up not getting one list or the other. Can anyone help with this?

Update

Here is an example of my current aggregation.

public static Layouts.Template Aggregate(this IList<Layouts.Template> source) {
            return source.Aggregate(
                source.First(),
                (current, next) => new Layouts.Template {
                    Apertures = (current.Apertures.Concat(next.Apertures).Distinct().ToList()),
                    Arrangements = (current.Arrangements.Concat(next.Arrangements).Distinct().ToList()),
                    Pages = (current.Pages.Concat(next.Pages).Distinct().ToList())

    });
        }

My problem is that I'm having a lot of trouble wrapping my head around how to do this at all, much less in one expression. I'm not unwilling to use multiple methods, but if I could encapsulate it all, it would be really useful. I am fascinated by LINQ in general and I really want to get my head around this.

Update 2

The other collection, Apertures, will work in a similar manner, but it is unrelated to the Steps. Simply two different arrays I must do the same thing to, but they have nothing in common with one another. Learning how to do this with one will give me the knowledge to do it with the other.

Ciel
  • 17,312
  • 21
  • 104
  • 199
  • I don't understand how you want to aggregate the different steps, can you clarify? – BrokenGlass Apr 25 '11 at 19:32
  • `Steps` is just one of many list objects, it's the one I am using as an example. But basically there will be an `IList` and I need to concatenate them all together, then go through the concatenated result's properties and 'merge' things so that the higher-level layers supersede the lower level ones. So, the second list has a `Step` with a `Rank` of 1 that has a different `Required` value than the first one, so the first element of the second array should supersede the first element of the first array, but the second element of the first array should remain intact. – Ciel Apr 25 '11 at 19:35

3 Answers3

2

If there's no correlation between steps and apertures you can do this:

var result = new Arrangement{
    Steps = list.SelectMany(arrangement => arrangment.Steps)
                .GroupBy(step => step.Rank)
                .Select(l => l.Last())
                .OrderBy(step => step.Rank)
                .ToList()),
}

If there is you'll need to combine the two somehow. If steps index into apertures then you can use something similar.

Talljoe
  • 14,593
  • 4
  • 43
  • 39
  • This may be what I am trying to do, yes. When I tried `GroupBy` I continued to get awkward errors stating that it was no longer an `IEnumerable`. I already have an aggregation going over the `IEnumerable`, what I hope to do is keep all of the logic contained in that one place instead of breaking it up all over the place. – Ciel Apr 25 '11 at 19:41
  • What is the current aggregation you are doing? – Talljoe Apr 25 '11 at 19:44
  • I have updated my post with an example of what I already have. – Ciel Apr 25 '11 at 20:41
  • Your solution works if I have only one `Arrangement`, but as you can see I have multiple. Each `Arrangement` has a Property called `Aspect` which is the similar object to group by. I'll update the sample code to reflect this, it didn't occur to me. I want to get all `Arrangements` that share the same `Aspect.Name`, and get a complete list of `Steps`, overwriting lower level Steps where higher level Arrangements have the same Rank with a different Required value. – Ciel Apr 25 '11 at 20:49
  • I hope that clarifies it all a bit more. I'm sorry I was not clear enough to begin with. I am a little scatterbrained sometimes. Your example makes sense, but it isn't quite what I'm trying to do. The example you give only gets the list of steps, but I need to get the list of arrangements, each with its own list of steps. – Ciel Apr 25 '11 at 21:10
  • 1
    My example will take a list of arrangements and give you one arrangement. If you want to group your arrangements by Name before performing this step you can use that as input to this step. – Talljoe Apr 25 '11 at 21:33
1

After your updates:

var query = arrangements
    .GroupBy(a => a.Aspect.Name)
    .Select(g => 
    new Arrangement
    { 
        Steps = ga.SelectMany(a => a.Steps)
                  .GroupBy(s => s.Rank)
                  .Select(gs => gs.Last()),
        Aspect = ga.First().Aspect
    });

This will create output as in your example.

Now, how to merge it with your current aggregation method? As to my understanging, you want to create one big layout from all current layout contents (including arrangements, pages, etc)?

You don't need aggregate at all, just split it into 3 LINQ queries:

// get all arrangements object from all layouts [flattening with SelectMany]
var arrangements = source.SelectMany(s => s.Arrangements);
// and filter them
var filteredArrangements = // enter larger query from above here         

// repeat the same for Apertures and Pages

... 

// and return single object
return new Layouts.Template 
       {
           Apertures = filteredApertures,
           Arrangements = filteredArrangements,
           Pages = filteredPages
       };
k.m
  • 30,794
  • 10
  • 62
  • 86
  • Fixed my example, sorry for the typo. – Ciel Apr 25 '11 at 19:39
  • Alright, I'm sorry for the bad example. I have updated my code to be much more clear. – Ciel Apr 25 '11 at 20:53
  • @Ciel: no worries mate, I've updated my answer - fill it up and check if you can go without `Aggregate` at all. – k.m Apr 25 '11 at 21:29
  • Hrnm. I'm not entirely sure. I cannot get your example to work, it is rejecting it claiming that the anonymous type cannot be assigned to the new Template.Arrangements property. – Ciel Apr 25 '11 at 21:36
  • Ok, solve that. I wasn't converting to a List. Why are you using `.Last()`? This confuses me a little bit. Doesn't this mean that anything in the 'inherited' arrangements will be lost? – Ciel Apr 25 '11 at 21:39
  • `.Last()` is only to get rid of duplicated `Steps`. You see, I group steps within arrangement sharing same aspect name using their `Step.Rank`. If there are more Steps with same Rank, only *most fresh* (from most recent arrangement) is picked - nothing is lost. That's the overwriting you wanted to do. – k.m Apr 25 '11 at 21:43
  • Oh! I see! I think this is exactly what I am wanting to do. I'm going to run some more tests and be sure I haven't overlooked anything. – Ciel Apr 25 '11 at 21:52
  • Yes! Thank you very much. I get it now. This will do what I am trying to get done! Thank you so much! – Ciel Apr 25 '11 at 22:18
1

I assume this isn't the complete solution that you want:

private static Arrangement Accumulate(IEnumerable<Arrangement> arrangements)
{
    var stepsByRank = new Dictionary<int, Step>();

    foreach (var arrn in arrangements)
        foreach (var step in arrn.Steps)
            stepsByRank[step.Rank] = step;

    return new Arrangement { Steps = stepsByRank.Values.ToArray() };
}

What's missing from this? How else do you want to "aggregate" the arrangements and steps? (edit: after reading your comment, maybe this actually is what you want.)

mqp
  • 70,359
  • 14
  • 95
  • 123
  • I cannot use Dictionaries, unfortunately. – Ciel Apr 25 '11 at 19:37
  • 1
    Er...okay. Well, conceptually this is what you're going to be doing -- the other one-liner answers posted here use `GroupBy` to put the steps in a hash table, just like this does, but phrased differently. If your religion prohibits you from using hash tables on Mondays and Wednesdays, I guess you can pick an alternative kosher data structure like a tree to play the same role. – mqp Apr 25 '11 at 19:39
  • I misread part of your answer, sorry. I meant that I cannot return the data as a dictionary because it has to go back to a database, and working with dictionaries and ORMs has proven more frustrating than it is worth. I will try this when I get back to my work machine, but I don't think it will run inline with the rest of the aggregate method... – Ciel Apr 25 '11 at 19:43
  • Ok. Sorry about that. When I made my post, I got called away, and I kept reading via my phone. When I read your first answer I misinterpreted it simply due to how much I could see on the screen. Yes, all of the solutions will inevitably create a dictionary, so this isn't an issue. I thought you were trying to make it return a dictionary. – Ciel Apr 25 '11 at 20:46
  • I have updated my code to be more clear about my intentions. I am very sorry it wasn't straightforward enough to begin with. – Ciel Apr 25 '11 at 21:10