GroupBy with multiple groups as a hierarchy - c#

I am using GroupBy create a hierarchical set of groups to use in multiple child grids.
Assume I have a query with with 6 columns, a, b, c, d, e, f.
Now, I need to group by a, then by b, then by c. and return the entire row in the group of c's.
var q = rows.GroupBy(x => x.a)
Ok, that's nice. That gives me my group of a's. Next, we look to group them by a and b.
var q1 = q.Select(g =>new {
Key = g.Key,
SubGroup = g.GroupBy(x => x.b)
}
Ok, that also works nice. I get my group of a's with subgroups of b's.
Now I'm stumped at the third level. I've tried various syntaxes, but most won't even compile. The ones that do do not give the correct results.
var q2 = q1.Select(g1 => new {
Key = g1.Key,
SubGroup = g1.GroupBy(x => x.c)
}
This doesn't compile. Tells me that there is no GroupBy on g1.
var q2 = q.Select(g1 => new {
Key = g1.Key,
SubGroup = g1.GroupBy(x => x.c)
}
This doesn't give me the b subgroup, only the a and c.
Any idea of what i'm doing wrong here?
EDIT:
The Following also does not work, saying there is no definition for the g1.Key
var q2 = q.Select(g => new {
Key = g.Key,
SubGroup = g.Select(g1 => new {
Key = g1.Key
SubGroup = g1.GroupBy(a => a.c)
})
I have such a poor grasp on what this is doing internally.

Now, I'm not saying this is actually a good approach; it's probably going to be slow and the right way to do this, if performance matters, may be to sort the whole collection by these different criteria and then look at the different parts of the sorted collection.
But if you want to use GroupBy and IGroupings to manage it, you're working at it from the wrong end. You want to start at the deepest level first and work up.
var groups = rows
.GroupBy(x => new { x.A, x.B, x.C, x.D, x.E, x.F })
.GroupBy(x => new { x.Key.A, x.Key.B, x.Key.C, x.Key.D, x.Key.E })
.GroupBy(x => new { x.Key.A, x.Key.B, x.Key.C, x.Key.D, })
.GroupBy(x => new { x.Key.A, x.Key.B, x.Key.C })
.GroupBy(x => new { x.Key.A, x.Key.B })
.GroupBy(x => x.Key.A);
groups.First().Key; // will be an A value
groups.First().First().First(); // will be an A, B, C group

GroupBy actually supports giving a list of elements to group by. Each group will contain the same first 3 items (A, B & C). You can get the key with the .Key method, and play around with the different rows with foreach. See Example:
var groups = Elements.GroupBy(x => new {x.A, x.B, x.C});
foreach (var group in groups)
{
Trace.WriteLine(group.Key + ": " + group.Count());
foreach (var row in group)
{
Trace.WriteLine(row.D);
}
}
Edit: Ahh, ok - what you need is this then:
var groups = Elements
.GroupBy(a => new {a.A})
.Select(g1 => new {
A = g1.Key,
Groups = g1
.GroupBy(b=> new {b.B})
.Select(g2 => new {
B = g2.Key,
Groups = g2
.GroupBy(c => new {c.C})
.Select(g3 => new {
C = g3.Key,
Rows = g3
})
})
});
foreach (var groupA in groups)
{
Trace.WriteLine(groupA.A);
foreach (var groupB in groupA.Groups)
{
Trace.WriteLine("\t" + groupB.B);
foreach (var groupC in groupB.Groups)
{
Trace.WriteLine("\t\t" + groupC.C);
foreach (var row in groupC.Rows)
{
Trace.WriteLine("Row: " + row.ToString());
}
}
}
}

Related

foreach to linq expression help needed

having some trouble writing the following code to some nicer/less lines :)
any one have the good solution?
//custom implementation for popular filters
var popularFilter = new Dictionary<string, int>();
foreach (var car in allFilteredCars)
{
foreach (var offering in car.Offerings)
{
if (popularFilter.ContainsKey(offering))
popularFilter[offering] = popularFilter[offering] + 1;
else
popularFilter.Add(offering, 1);
}
}
categories.Add(new Category
{
Name = "popular",
Code = "popular",
Values = popularFilter.Select(p => new Value
{
Code = p.Key,
Name = p.Key,
Count = p.Value
}).ToList()
});
If it is possible i want i directly to add it in the categories list.
car.offerings = list<string>
so basicly something like:
Categories.Add(allFilteredCars.SelectMany(
c => c.Offerings.Select(
o => new {
something magical here}
.Select(a =>
new Category{
code.. etc etc..}
));
It looks like you just want to do a SelectMany to get the offerings, then group them and select the Count.
categories.Add(new Category
{
Name = "popular",
Code = "popular",
Values = allFilteredCars.SelectMany(c => c.Offerings)
.GroupBy(o => o)
.Select(grp => new Value
{
Code = grp.Key,
Name = grp.Key,
Count = grp.Count()
}).ToList()
});
Your non linq code already looks quite fine.
You can create your dictionary with linq by using a GroupBy & ToDictionary:
var dictionary = offerings
.GroupBy(x => x)
.ToDictionary(x => x.Key, x => x.Count());

Converting LINQ query to Dynamic Linq

I have a function that looks like this:
public void GetAvailableResourcesByLocationChart(List<DateTime> dates, List<ChartResourceModel> data)
{
var totals = (from r in data
group new { Dates = r.Dates } by r.Location into g
select new
{
Location = g.Key,
Dates = dates.Select(d => new
{
Date = d,
Available = g.SelectMany(x => x.Dates.Where(y => y.Date == d)).Count(x => x.Available)
})
.OrderBy(x => x.Date)
.ToList()
})
.OrderBy(x => x.Location)
.ToList();
}
This example groups the data based on Location. But I want to be able to pass in a string that specifies what it should group on. I was thinking DynamicLinq would be the right way to go about this but I'm having trouble reproducing it.
I started off doing this, but am getting stuck reproducing the SelectMany inside the select:
public void GetAvailableResourcesByLocationChart(List<DateTime> dates, List<ChartResourceModel> data, string grouping)
{
var totals = data.GroupBy(grouping, "it").Select("new (it.Key as Group, it as Dates)").Cast<dynamic>();
}
Any ideas on what I need to do next?
this
from r in data
group new { Dates = r.Dates } by r.Location into g
...
is the same as this
data.GroupBy(r => r.Location, b => b.Dates)
so if we have variable named grouper
data.GroupBy(r => {
if (grouper == "L")
return r.Location
else
return r.Dates }, b => b.Dates);
This should get you on the right track?

c# loop on group by key

I have made a group by statement on a datatable like this:
var finalResult = (from r in result.AsEnumerable()
group r by new
{
r.Agent,
r.Reason
} into grp
select new
{
Agent = grp.Key.Agent,
Reason = grp.Key.Reason,
Count = grp.Count()
}).ToList();
The finalResult will be like this:
agent1 reason1 4
agent1 reason2 7
agent2 reason1 8
agent2 reason2 3
..
...
...
agentn reason1 3
agentn reason2 11
I want to loop over agent name in order to get the reasons and the counts for each reason for each agent. In other words: i need to build this :
can you tell me please how to loop over agent name from the finalResult variable?
You need one more GroupBy and you are done:
var solution =
finalResult
.GroupBy(x => x.Agent);
foreach (var group in solution)
{
// group.Key is the agent
// All items in group are a sequence of reasons and counts for this agent
foreach (var item in group)
{
// Item has <Agent, Reason, Count> and belongs to the agent from group.Key
}
}
Outer loop goes over all the agents (so Agent1, Agent2, etc.) while inner loop will go through all reasons for the current agent.
You might want to try GroupBy in LINQ :
You can read more about it here
Perhaps:
var agentGroups = finalResult
.GroupBy(x => x.Agent)
.Select(ag => new
{
Agent = ag.Key,
ReasonCounts = ag.GroupBy(x => x.Reason)
.Select(g => new
{
Agent = ag.Key,
Reason = g.Key,
Count = g.Sum(x => x.Count)
}).ToList(),
Total_Count = ag.Sum(x => x.Count)
});
foreach (var agentGroup in agentGroups)
{
string agent = agentGroup.Agent;
int totalCount = agentGroup.Total_Count;
foreach (var reasonCount in agentGroup.ReasonCounts)
{
string reason = reasonCount.Reason;
int count = reasonCount.Count;
}
}

Concatinating properties in a list of items

I am having trouble with a small section of code.
I have a List of a MapItem class with a couple properties, Address and Html, and I need to concatenate the Html properties for each item with an identical Address property
For example:
firstMapItem = new MapItem { Address = "1122 Elm Street",
Html="<p>some html</p>" };
secondMapItem = new MapItem { Address = "1122 Elm Street",
Html="<p>different html</p>" };
would become:
firstMapItem.Address == "1122 Elm Street";
firstMapItem.Html == "<p>some html</p><p>different html</p>";
secondMapItem.Address == "1122 Elm Street";
secondMapItem.Html == "<p>some html</p><p>different html</p>";
This is what I have tried so far:
foreach (MapItem item in mapItems)
{
var sameAddress = from m in mapItems
where m.Address == item.Address
select m;
if (sameAddress.Count() > 1)
{
//tried inserting -> item.Html = ""; right here as well
foreach (MapItem single in sameAddress)
{
item.Html += single.Html;
}
}
}
I am probably making this more complicated than it needs to be.
Thanks in advance.
You could group by Address and then concatenate the Html values:
var results = from m in mapItems
group m by m.Address into ms
select new MapItem
{
Address = ms.Key,
Html = string.Concat(ms.Select(m => m.Html))
};
Use a grouping on the address, then just string.Join the Html of all the items in the group to produce a new MapItem:
var resultList = mapItems.GroupBy(m => m.Address)
.Select(g => new MapItem() { Address = g.Key, Html = string.Join("", g.Select(x => x.Html)) })
.ToList();
Edit:
Like the other solutions presented so far above approach will remove duplicates - that doesn't seem to be what you want - below a solution that creates a list that is not deduplicated (so will produce 2 items for the sample input)
var resultList = mapItems.GroupBy(m => m.Address)
.Select(g => g.Select( item => new MapItem() { Address = g.Key, Html = string.Join("", g.Select(x => x.Html)) } ))
.SelectMany( x=> x)
.ToList();
If you group by Address, you'll end up with only one item when you have items with the same Address. If that's OK with, go with Group By. However, if you need all the original items, with the Html concatenated, you should do like that:
var newMapItems = mapItems
.Select(mi => new MapItem() { Address = mi.Address,
Html = mapItems.Where(mi2 => mi2.Address == mi.Address)
.Select(mi3 => mi3.Html)
.Aggregate((acc, html) => acc += html)
}
);
You can do this with a GroupBy and Select:
var result = items
.GroupBy(m => m.Address, m => m.Html)
.Select(g => new MapItem() {Address = g.Key, Html = string.Concat(g.Select(h => h))});
This code should update your existing objects with appended values.
foreach (MapItem item in mapItems)
{
var sameAddress = from m in mapItems
group m by m.Address into ms
select string.Join("", ms.Select(e => e.Html).ToArray());
foreach (string concatHtml in sameAddress)
{
item.Html = concatHtml;
}
}

How do I find a subset of items in two sets of data that partially differ?

I am trying to get the subset of items in dataA that are in dataB, and have different values of property c. The properties a and b can be used as an index, so I have tried to filter out only the useful pairs then check to see if they have a different c value.
This is the linq expression I came up with, and it does work, but It seems like there has to be a better/faster way of finding this subset.
var itemsInBoth = from item in dataA
from item2 in dataB
where item.a == item2.a && item.b == item2.b
select new
{
first= item,
second = item2
};
var haveDifferentC = from item in itemsInBoth
where item.first.c != item.second.c
select item.first;
Based on the answer provided by David B, I eventually settled on a slightly modified version of his method. Although the differences are minor, I thought I would share this, primarily to show a version that for those (like me) that prefer the expressive syntax.
Also, instead of grouping, I decided to use an anonymous key/value pair to simplify the structure.
var dictA = (from item in dataA
select new
{
key = CreateIndexValue(item.a, item.b),
value = item
}).ToDictionary(kv => kv.key, kv => kv.value);
var dictB = (from item in dataB
select new
{
key = CreateIndexValue(item.a, item.b),
value = item
}).ToDictionary(kv => kv.key, kv => kv.value);
var filesInBoth = from item in dictA
where dictB.ContainsKey(item.Key)
select new
{
itemA = dictA[item.Key],
itemB = dictB[item.Key]
};
var differentSize = from item in filesInBoth
where item.itemA.c!= item.itemB.c
select item.itemA;
Faster? What you have there is O(n^2). Each item in the first list will fully iterate the items in the second list. You need to remove the redundant iteration in that join. One way to do that is to use another structure to do O(1) lookups for matchs.
Here's some untested (unchecked) code:
var dictionaryA = dataA
.GroupBy(item => new {a = item.a, b = item.b})
.ToDictionary(g => g.Key, g => g.ToList());
var dictionaryB = dataB
.GroupBy(item => new {a = item.a, b = item.b})
.ToDictionary(g => g.Key, g => g.ToList());
var results = dictionaryA
.Where(g1 => dictionaryB.ContainsKey(g1.Key))
.Select(g1 => new {g1 = g1, g2 = dictionaryB[g1.Key]})
.SelectMany(pair =>
pair.g1.SelectMany(item1 =>
pair.g2
.Where(item2 => item2.c != item1.c)
.Select(item2 => new {item1, item2})
)
);
Here's a simplified version if a,b pairs are unique in each list.
var dictionaryA = dataA
.ToDictionary(item => new {a = item.a, b = item.b}, item => item);
var dictionaryB = dataB
.ToDictionary(item => new {a = item.a, b = item.b}, item => item);
var results = dictionaryA
.Where(e1 => dictionaryB.ContainsKey(e1.Key))
.Select(e1 => new {i1 = e1.Value, i2 = dictionaryB[e1.Key]})
.Where(pair => pair.i1.c != pair.i2.c);

Categories

Resources