LINQ nested collection - c#

I have a class that has a sub collection. Something like this:
public class Item {
public string Type {get;set}
public Subitem[] {get;set;}
}
I know that I can separate and count by Type this way:
var count = (from x in items
group x by x.Type into grouped
select new {
typename = grouped.Key,
count = grouped.Count()
}).ToDictionary<string, int>(x => x.typename, x => x.count);
This will return something like this:
{ type1, 13 }
{ type2, 26 }
and so on.
But how can I count by Subitem?
To return something like:
{ subitem1, 15 }
{ subitem2, 46 }

Your code sample is not legal C#, but suppose you have a collection called Subitems in your items - then you can use SelectMany() or in query syntax:
var count = (from i in items
from x in i.Subitems
group x by x into grouped
select new
{
typename = grouped.Key,
count = grouped.Count()
}).ToDictionary(x => x.typename, x => x.count);
Or alternatively in method syntax:
var countDict = items.SelectMany(x => x.Subitem)
.GroupBy(x => x)
.ToDictionary(g => g.Key, g => g.Count());

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());

LINQ group by sum not working as expected

I have this class:
public class tempClass
{
public int myKey { get; set; }
public int total { get; set; }
}
Code to group by and sum:
var list = new List<tempClass>();
list.Add(new tempClass { myKey = 1, total = 1 });
list.Add(new tempClass { myKey = 1, total = 2 });
list.Add(new tempClass { myKey = 2, total = 3 });
list.Add(new tempClass { myKey = 2, total = 4 });
list = list
.Select(w => new tempClass { myKey = w.myKey, total = w.total })
.GroupBy(x => new tempClass { myKey = x.myKey })
.Select(y => new tempClass { myKey = y.Key.myKey, total = y.Sum(z => z.total) })
.ToList();
The list count is still 4 after the GroupBy.
Same result for code below:
list = list
.GroupBy(x => new tempClass { myKey = x.myKey })
.Select(y => new tempClass { myKey = y.Key.myKey, total = y.Sum(z => z.total) })
.ToList();
The reason for this is that you group by a class which doesn't override Equals and GetHashCode. Then the implementation of System.Object is used which just compares references. Since all are different references you get one group for every instance.
You could group by this property or override Equals and GetHashCode to compare this property:
list = list
.Select(w => new tempClass { myKey = w.myKey, total = w.total })
.GroupBy(x => x.myKey)
.Select(y => new tempClass { myKey = y.Key, total = y.Sum(z => z.total) })
.ToList();
You don't need two Select lines, one is enough. And inside GroupBy, just select your key, don't create a new object of your class there:
list = list
.GroupBy(x => x.myKey)
.Select(y => new tempClass { myKey = y.Key, total = y.Sum(z => z.total) })
.ToList();
And here's the declarative-query-syntax version:
list = (from x in list
group x by x.myKey into g
select new tempClass { myKey = g.Key, total = g.Sum(z => z.total) }).ToList();
My, you are creating a lot of new TempClass objects in your LINQ statement, don't you?
The reason that you don't get the correct result is that your GroupBy doesn't make groups of TempClass objects with the equal TempClass.MyKey, but with equal TempClass.
The default EqualityComparer for TempClass declares two TempClass objects equal if they are the same object, thus making two TempClass objects unequal, even if they have the same values.
Your query should be:
var result = list
.GroupBy(listItem => listItem.MyKey) // make groups with equal MyKey
.Select(group => new // from every group make one new item
{
Key = group.Key, // with key the common MyKey in the group
GrandTotal = group.Sum(groupItem => groupItem.Total);
// and value the sum of all Total values in the group
});
I chose not to make the final resulting items a sequence of TempClasses, because I'm not sure if you would consider items with this GrandTotal as TempClass objects. But if you want, you could change the final select:
.Select(group => new TempKey()
{
Key = group.Key,
Total = group.Sum(groupItem => groupItem.Total);
});

How to rank a list with original order in c#

I want to make a ranking from a list and output it on original order.
This is my code so far:
var data = new[] { 7.806468478, 7.806468478, 7.806468478, 7.173501754, 7.173501754, 7.173501754, 3.40877696, 3.40877696, 3.40877696,
4.097010736, 4.097010736, 4.097010736, 4.036494085, 4.036494085, 4.036494085, 38.94333318, 38.94333318, 38.94333318, 14.43588131, 14.43588131, 14.43588131 };
var rankings = data.OrderByDescending(x => x)
.GroupBy(x => x)
.SelectMany((g, i) =>
g.Select(e => new { Col1 = e, Rank = i + 1 }))
.ToList();
However, the result will be order it from descending:
What I want is to display by its original order.
e.g.: Rank = 3, Rank = 3, Rank = 3, Rank = 4, Rank = 4, Rank = 4, etc...
Thank You.
Using what you have, one method would be to keep track of the original order and sort a second time (ugly and potentially slow):
var rankings = data.Select((x, i) => new {Item = x, Index = i})
.OrderByDescending(x => x.Item)
.GroupBy(x => x.Item)
.SelectMany((g, i) =>
g.Select(e => new {
Index = e.Index,
Item = new { Col1 = e.Item, Rank = i + 1 }
}))
.OrderBy(x => x.Index)
.Select(x => x.Item)
.ToList();
I would instead suggest creating a dictionary with your rankings and joining this back with your list:
var rankings = data.Distinct()
.OrderByDescending(x => x)
.Select((g, i) => new { Key = g, Rank = i + 1 })
.ToDictionary(x => x.Key, x => x.Rank);
var output = data.Select(x => new { Col1 = x, Rank = rankings[x] })
.ToList();
As #AntonínLejsek kindly pointed out, replacing the above GroupBy call with Distinct() is the way to go.
Note doubles are not a precise type and thus are really not a good candidate for values in a lookup table, nor would I recommend using GroupBy/Distinct with a floating-point value as a key. Be mindful of your precision and consider using an appropriate string conversion. In light of this, you may want to define an epsilon value and forgo LINQ's GroupBy entirely, opting instead to encapsulate each data point into a (non-anonymous) reference type, then loop through a sorted list and assign ranks. For example (disclaimer: untested):
class DataPoint
{
decimal Value { get; set; }
int Rank { get; set; }
}
var dataPointsPreservingOrder = data.Select(x => new DataPoint {Value = x}).ToList();
var sortedDescending = dataPointsPreservingOrder.OrderByDescending(x => x.Value).ToList();
var epsilon = 1E-15; //use a value that makes sense here
int rank = 0;
double? currentValue = null;
foreach(var x in sortedDescending)
{
if(currentValue == null || Math.Abs(x.Value - currentValue.Value) > epsilon)
{
currentValue = x.Value;
++rank;
}
x.Rank = rank;
}
From review of the data you will need to iterate twice over the result set.
The first iteration will be to capture the rankings as.
var sorted = data
.OrderByDescending(x => x)
.GroupBy(x => x)
.Select((g, i) => new { Col1 = g.First(), Rank = i + 1 })
.ToList();
Now we have a ranking of highest to lowest with the correct rank value. Next we iterate the data again to find where the value exists in the overall ranks as:
var rankings = (from i in data
let rank = sorted.First(x => x.Col1 == i)
select new
{
Col1 = i,
Rank = rank.Rank
}).ToList();
This results in a ranked list in the original order of the data.
A bit shorter:
var L = data.Distinct().ToList(); // because SortedSet<T> doesn't have BinarySearch :[
L.Sort();
var rankings = Array.ConvertAll(data,
x => new { Col1 = x, Rank = L.Count - L.BinarySearch(x) });

List c# distinct by item and sum other items

Imagine a list of objects:
public class Foo
{
public string Name {get; set;}
public int Total {get; set;}
}
And now, my list contains 3 objects:
new Foo {Name="object", Total=3};
new Foo {Name="object", Total=7};
new Foo {Name="object", Total=5};
How to distinct by the name and sum the totals? i.e. The List will have just one object:
new Foo {Name="object", Total=15};
Thanks in advance!
You can do this:
var newTotalList = yourList.GroupBy(x => x.Name)
.Select(x => new
{
Name = x.Key,
Total = x.Sum(y => y.Total)
})
.ToList();
What this code does is, simply, first group the elements by name, and then sum the Total fields of all the elements inside the group.
You should be able to use grouping.
var groupedList = (from ol in objectList
group ol by ol.Name
into grp
select new foo
{
Name = grp.Key,
Total= grp.Sum(ex => ex.Total),
City = grp.Select(ex => ex.City).FirstOrDefault(),
Country = grp.Select(ex => ex.Country ).FirstOrDefault(),
Phone = grp.Select(ex => ex.Phone).FirstOrDefault()
}
).ToList();
You can group by Name and then just sum up by Total:
var l = new List<Object>
{
new Object {Name="object", Total=3},
new Object {Name="object", Total=7},
new Object {Name="object", Total=5}
};
var result = l.GroupBy(o => o.Name)
.Select (grp => new Object
{
Name = grp.Key,
Total = grp.Sum(o => o.Total)
});
result is now:
Try this:
List<Object> list = ... // make a list somehow
var totalsPerName = list
.GroupBy(o => o.Name)
.Select(group => new Object { Name = group.Key, Total = group.Sum(o => o.Total) })
Group your elements by Name, and then for each group, select a new object whose name is name value grouped by for this group, and Total is the sum of all Totals in the group:
var groupdObject =list
.GroupBy(o =>o.Name)
.Select(g =>
new
{
Name = g.Key,
Total = g.Sum(o=>o.Total)
});

Selecting Items with the same subcollection using LINQ

Here is the pseudo case:
class parent{
string name; //Some Property
List<int> myValues;
.......
}
........
//Initialize some parent classes
List<parent> parentList = new List<parent>();
parentList.add(parent123); //parent123.myValues == {1,2,3}
parentList.add(parent456); //parent456.myValues == {4,5,6}
parentList.add(parentMatch); //parentMatch.myValues == {1,2,3}
What I am aiming for is a query which retrieves a List of parent objects where their
myValues Lists are equivalent. In this case it would return parent123 and parentMatch.
So you can wrap the logic up and just use GroupBy if you implement an IEqualityComparer:
class IntegerListComparer : IEqualityComparer<List<int>>
{
#region IEqualityComparer<List<int>> Members
public bool Equals(List<int> x, List<int> y)
{
//bool xContainsY = y.All(i => x.Contains(i));
//bool yContainsX = x.All(i => y.Contains(i));
//return xContainsY && yContainsX;
return x.SequenceEqual(y);
}
public int GetHashCode(List<int> obj)
{
return 0;
}
#endregion
}
Call it like so:
var results = list
.GroupBy(p => p.MyValues, new IntegerListComparer())
.Where(g => g.Count() > 1)
.SelectMany(g => g);
Very silly solution:
var groups = list.GroupBy(p => string.Join(",", p.list.Select(i => i.ToString()).ToArray()))
.Where(x => x.Count() > 1).ToList();
Result:
an IEnumerable of groups containing parent objects having list with same int (in the same order).
If you need to match list of elements in any order (i.e. 1,2,3 == 3,1,2), just change p.list to p.list.OrderBy(x => x).
Plus, if you're targeting framework 4.0, you can avoid ToArray and ToString
EDIT:
added a where to filter single-occurrence groups.
Now if you have these parents:
parent A 1,2,3
parent B 1,2,3
parent C 1,2,3
parent D 4,5,6
parent E 4,5,6
parent F 7,8,9
it returns:
(A,B,C) - (D,E)
Try this:
var matches = (from p1 in parentList
from p2 in parentList
let c1 = p1.myValues
let c2 = p2.myValues
where p1 != p2 &&
c1.All(child => c2.Contains(child)) &&
c2.All(child => c1.Contains(child))
select p1).Distinct();

Categories

Resources