LINQ group by sum not working as expected - c#

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

Related

Is there a LINQ-oriented method to quicky evaluate nested lists in dictionaries

I have a dictionary constructed like this:
Dictionary<string, List<MyObject>>
And my object has an integer value as one of its properties:
public class MyObject
{
public int number {get; set;}
}
How would I construct a LINQ-oriented query to evaluate the average number for each Key in the dictionary?
Depending on your needs
var results = dict.Select(
x => new
{
x.Key,
avg = x.Value.Average(y => y.number)
});
Or if you want your results in a dictionary
var results = dict.ToDictionary(x => x.Key, x => x.Value.Average(y => y.number));
Because dictionaries are enumerables, your request can be accomplished in a straightforward and natural manner
var averages = dictionary
.ToDictionary(pair => pair.Key, pair => pair.Value.Average(e => e.number));
Demo on dotnet fiddle
You can use Average to achieve it.
var data = new Dictionary<string, List<MyObject>>();
data.Add("1", new List<MyObject> { new MyObject { number = 1 }, new MyObject { number = 2 }, new MyObject { number = 3 }});
data.Add("2", new List<MyObject> { new MyObject { number = 4 }, new MyObject { number = 5 }, new MyObject { number = 6 }});
var result = data.Select(p => new { p.Key, Average = p.Value.Average(n => n.number) });

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

C# Linq Group elements from list and count sum of values

i have a List of Lists of objects each containing a string and a float value.
I need to group those elements by the string value (name) and order the groups by the sum of float value.
public class Element
{
public string Name;
public float Value;
public Element(string name,float value) {
Name = name;
Value = value;
}
}
List<List<Element>> elementslist = new List<List<Element>>();
elementslist.Add(new List<Element>() { new Element("Apple", 1.2f), new Element("Banana", 0) });
elementslist.Add(new List<Element>() { new Element("Apple", 2.1f), new Element("Banana", 1.4f) });
elementslist.Add(new List<Element>() { new Element("Apple", 0), new Element("Banana", 0) });
p.s.: is there any smarter aggregation algorythm to obtain thise result ? Maybe could be also considered of "close" these values are in List order...
Thank you very much
First flat (SelectMany) the list, then GroupBy by name and OrderBy by Sum of values:
var groups = elements.SelectMany(l => l)
.GroupBy(e => e.Name)
.OrderBy(g => g.Sum(x => x.Value))
This will give you a IEnumerable<Element> with one element by name and the sum of Values in the Value member. Hope it helps.
elementslist.SelectMany(n => n).GroupBy(n => n.Name).Select(n => new Element(n.First().Name, n.Sum(p => p.Value))).OrderBy(n => n.Value)
Or OrderByDescending.

How to group and merge/flatten a list of anonymous objects in LINQ

I have a list of anonymous objects generated by a LINQ query that I do not have access to modify.
The objects have the following properties:
OrderId, RepId, FirstName, LastName, Address
Each "Rep" often places multiple orders, so there are a lot of rows where the only difference is the OrderId. There is a requirement that if the same Rep has placed multiple orders, to batch these together in groups of 6 with a new structure:
OrderId1, OrderId2, ..., OrderId6, RepId, FirstName, LastName, Address
But if the rep has placed say 8 orders, there would be a batch of 6 and a batch of 2. So the new objects don't always have the same number of properties.
I've started by grouping the initial result set by RepId, but I have no clue where to go next.
Is this possible using LINQ?
As your output have anonymous objects with different schema, that make the thing a little more complicate.
Ideally you should design your entity class to use list for orders instead of property like "OrderId1", "OrderId2"... That is not extensible and error prone. But for that specific question, we can combine LINQ and ExpandoObject to achieve this.
orders.GroupBy(order => order.RepId)
.SelectMany(orderGroup => orderGroup.Select((order, i) => new {
Order = order,
ReqId = orderGroup.Key,
SubGroupId = i / 6
}))
.GroupBy(h => new {
ReqId = h.ReqId,
SubGroupId = h.SubGroupId,
FirstName = h.Order.FirstName,
LastName = h.Order.LastName,
Address = h.Order.Address
})
.Select(orderWithRichInfo => {
dynamic dynamicObject = new ExpandoObject();
int i = 1;
foreach(var o in orderWithRichInfo)
{
((IDictionary<string, object>)dynamicObject).Add("OrderId" + i, o.Order.OrderId);
i++;
}
((IDictionary<string, object>)dynamicObject).Add("FirstName", orderWithRichInfo.Key.FirstName);
((IDictionary<string, object>)dynamicObject).Add("LastName", orderWithRichInfo.Key.LastName);
((IDictionary<string, object>)dynamicObject).Add("Address", orderWithRichInfo.Key.Address);
return dynamicObject;
});
Hope it helps.
First option.
If you want to get 6 OrderId-s as a list, you can create
class OrderBundle
{
public int RepId { get; set; }
public List<int> OrderIds { get; set; }
}
Group your items:
var orderBundels = orderList
.GroupBy(m => m.RepId)
.Select(g => new OrderBundle
{
RepId = g.Key,
OrderIds = g.Select(m => m.OrderId).ToList()
});
And then split them into groups:
List<OrderBundle> dividedOrderBundels = new List<OrderBundle>();
foreach (OrderBundle orderBundle in orderBundels)
{
int bundelCount = (int)Math.Ceiling(orderBundle.OrderIds.Count() / 6.0);
for (int i = 0; i < bundelCount; i++)
{
OrderBundle divided = new OrderBundle
{
RepId = orderBundle.RepId,
OrderIds = orderBundle.OrderIds.Skip(i * 6).Take(6).ToList()
};
dividedOrderBundels.Add(divided);
}
}
Second option:
You can achieve the same result without creating model like below:
var result = orderList
.GroupBy(m => m.RepId)
.SelectMany(g => g.Select((m, i) => new
{
RepId = g.Key,
FirstName = m.FirstName,
LastName = m.LastName,
Address = m.Address,
OrderId = m.OrderId,
BunleIndex = i / 6
}))
.GroupBy(m => m.BunleIndex)
.Select(g => new
{
RepId = g.Select(m => m.RepId).First(),
FirstName = g.Select(m => m.FirstName).First(),
LastName = g.Select(m => m.LastName).First(),
Address = g.Select(m => m.Address).First(),
OrderIds = g.Select(m => m.OrderId).ToList()
})
.ToList()

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

Categories

Resources