Darkbound
Darkbound

Reputation: 3434

C# Linq Query - Extracting multiple groups from a collection (nested groups?)

I have a class called Person, with 2 properties Age and Name

I am trying to group the people into 3 different age groups.

I want my end result to be something like this:

YoungGroup (<20 y old):
    Person1Name, Age
    Person4Name, Age
    Person3Name, Age
AdultGroup (20> y <40 old):
    Person2Name, Age
    Person7Name, Age
    Person5Name, Age
SeniorGroup (40> y old):
    Person6Name, Age
    Person8Name, Age
    Person9Name, Age

I want to do it with group clause only (no join, if its even possible?)

This is what I have tried

var multiplegrps = from p in people
                   group p by p.Age into groups
                   let age = groups.Key
                   select new
                   {
                       YoungGroup = from p in groups
                                    where age <= 20
                                    group p by p.Age,
                       AdultGroup = from p in groups
                                    where age >= 21 && age <= 40
                                    group p by p.Age,
                      SeniorGroup = from p in groups
                                    where age >= 41
                                    group p by p.Age
                   };

Either it worked, or I dont actually know how to print it:

foreach (var item in multiplegrps)
{
    foreach (var i in item.YoungGroup)
    {
        Console.WriteLine($"{i.Key}");
    }
}

I do get the correct keys, but I have no access to the names, and I would have to write 3 internal foreachs for each of the groups

P.S. I want to do it with pure query only, no lambdas no new extension methods

P.S. I solved it and got the idea from @khoroshevj about the use of multiple ternary operators, it was as simple as:

        var peopleMultiGrouping = from p in people
                                  let ageSelection =
                                        p.Age < 20
                                            ? "Young" 
                                            : p.Age >= 20 && p.Age <= 40
                                                ? "Adult" 
                                                : "Senior"
                                  group p by ageSelection;

        foreach (var p in peopleMultiGrouping)
        {
            Console.WriteLine($"{p.Key}");
            foreach (var i in p)
            {
                Console.WriteLine($" {i.Age}");
            }
        }

Upvotes: 0

Views: 146

Answers (6)

Minus
Minus

Reputation: 729

Have you tried something like :

var multiplegrps = people.GroupBy(x=> x.age)
                         .ToDictionary(x=>x.Key,
                                       x=>x.Select(y=>new {  
                                                 YoungGroup=y.where(z=>z.age<= 20), 
                                                 AdultGroup=y.Where(z=>z.age >=21 && a.age <= 40), 
                                                SeniorGroup=y.Where(z=> z.age >=41) 
                                                          });

This is just an idea, did not tried it...

Upvotes: 0

Darkbound
Darkbound

Reputation: 3434

I solved it and got the idea from @khoroshevj about the use of multiple ternary operators, it was as simple as:

        var peopleMultiGrouping = from p in people
                                  let ageSelection =
                                        p.Age < 20
                                        ? "Young" 
                                        : p.Age >= 20 && p.Age <= 40
                                            ? "Adult" 
                                            : "Senior"
                                  group p by ageSelection;

        foreach (var p in peopleMultiGrouping)
        {
            Console.WriteLine($"{p.Key}");
            foreach (var i in p)
            {
                Console.WriteLine($" {i.Age}");
            }
        }

Upvotes: 0

Ahmed Mustafa
Ahmed Mustafa

Reputation: 111

----------
foreach (var item in multiplegrps)
            {
                foreach (var yItem in item.YoungGroup)
                {
                    if (item.YoungGroup.Count() > 0 && yItem.ElementAt(0) != null)
                    {
                        Console.WriteLine("Young : " + yItem.ElementAt(0).Name);
                    }
                }
                foreach (var aItem in item.AdultGroup)
                {
                    if (item.AdultGroup.Count() > 0 &&aItem.ElementAt(0) != null)
                    {
                        Console.WriteLine("Adult : " + aItem.ElementAt(0).Name);
                    }
                }
                foreach (var sItem in item.SeniorGroup)
                {
                    if (item.SeniorGroup.Count() > 0 &&sItem.ElementAt(0) != null)
                        {
                            Console.WriteLine("Senior : " + sItem.ElementAt(0).Name);
                        }
                }
            }

Upvotes: 0

StuartLC
StuartLC

Reputation: 107247

The final grouped output you seem to be looking for is 3 discrete collections of person, i.e. Young, Adult and Senior persons.

To me, it would make sense to classify each person once, since from a performance point of view, you also won't want to run through all people 3 times (once per group) - the groups are mutually exclusive, and each person can be classified once into the age group:

var classifiedPersons = people
   .Select(p => new{
       Person = p, 
       AgeClassification = 
          p.age <= 20
            ? "Young"
            : p.age <= 40
                ? "Adult"
                : "Senior" 
    })
   .GroupBy(cp => cp.AgeClassification)
   .ToDictionary(grp => grp.Key, grp => grp.Select(g => g.Person));

(You could use other classifications other than a string, such as enumeration here too)

You can now access the three groups via the "Young", "Adult" and "Senior" keys.

You can print out the groups generically like so:

foreach (var group in classifiedPersons)
{
    Console.WriteLine($"Group: {group.Key}:");
    foreach (var person in group.Value)
    {
        Console.WriteLine($"  - {person.Name}:");
    }
}

Seemingly you wanted a single anonymous object containing all 3 groups, such as:

var allGroupsInOneObject = new {
   YoungPeople = classifiedPersons["Young"],
   AdultPeople = classifiedPersons["Adult"],
   SeniorPeople = classifiedPersons["Senior"]
};

One Caveat - before accessing the groups via the Dictionary Key, you might just need to ensure that there are any persons present in the group.

Upvotes: 0

khoroshevj
khoroshevj

Reputation: 1137

You can do something like this

var multiplegrps =
    from p in people
    let groupName = GetGroupName(p.Age)
    group p by groupName
    into groups
    group groups by groups.Key;

foreach (var group in multiplegrps)
{
    foreach (var member in group.SelectMany(i => i))
    {
        Console.WriteLine($"{group.Key}, {member.Age}, {member.Name}");
    }
}

private static string GetGroupName(int age)
{
    return age <= 20
        ? "YoungGroup"
        : age <= 40
            ? "AdultGroup"
            : "SeniorGroup";
}

Upvotes: 1

Ousmane D.
Ousmane D.

Reputation: 56433

Your solution does indeed work as you expected. The issue is the way you're printing the result.

In the second level loop, i is of type IGrouping<int, Person> which basically represents a collection of objects that have a common key.

It clearly has the Key property as you're accessing it, but it doesn't have a Value property.

You can either perform another nested loop to iterate over the IGrouping and then you'll have access to the full Person properties, or if you want to only keep it at a two-level loop then you can flatten the item.YoungGroup with SelectMany into an IEnumerable<Person> and iterate over it like this:

foreach (var item in multiplegrps)
{
       foreach (var person in item.YoungGroup.SelectMany(x => x))
       {
            Console.WriteLine($"Name: {person.Name}, Age: {person.Age}");
       }
}

Upvotes: 0

Related Questions