Linq GroupBy into new object - c#

First of all I am new to Linq.
I have a list of objects named Lba that have different properties to be grouped by. My object properties are :
int aId,
int bId,
int max,
int min,
CustomObject1? Co1,
CustomObject2? Co2,
CustomObject3? Co3
I want to group my objects into a new object LbaGroup that have the following properties
int aId,
int bId,
int max,
int min,
IList<CustomObject1> Co1,
IList<CustomObject2> Co2,
IList<CustomObject3> Co3
I grouped my objects with the following query but it returns a List>
var query = myLbaList
.GroupBy(a => new {a.aId, a.bId, a.max, a.min})
.Select(a => a.ToList())
.ToList();
I am able to build my new objects with 2 foreach loops but I was wondering if there is a direct way to achieve that with Linq.
Thank you.
Edit: My loops look like :
foreach(List<Lba> LbaList in query){
LbaGroup myNewObject = new LbaGroup{
Co1 = new List<CustomObject1>(),
Co2 = new List<CustomObject2>(),
Co3 = new List<CustomObject3>(),
}
foreach(Lba oldObject in LbaList){
myNewObject.aId = oldObject.aId;
myNewObject.bId = oldObject.bId;
myNewObject.max = oldObject.min;
if oldObject.Co1 != null
myNewObject.Co1.Add(oldObject.Co1);
if oldObject.Co2 != null
myNewObject.Co2.Add(oldObject.Co2);
if oldObject.Co3 != null
myNewObject.Co3.Add(oldObject.Co3);
}
}

Something like this:
var result = myLbaList
.GroupBy(
a => new {a.aId, a.bId, a.max, a.min}, // Key
a => new {a.Col1, a.Col2, a.Col3}) // Values
.Select(chunk => new {
// From Key
aId = chunk.Key.aId,
bId = chunk.Key.bId,
max = chunk.Key.max,
min = chunk.Key.min,
// From Values
Col1 = chunk
.Where(item => item.Col1.HasValue) // not null items
.Select(item => item.Col1.Value) // CustomObject1? to CustomObject1
.ToList(),
Col2 = chunk
.Where(item => item.Col2.HasValue)
.Select(item => item.Col2.Value)
.ToList(),
Col3 = chunk
.Where(item => item.Col3.HasValue)
.Select(item => item.Col3.Value)
.ToList(),
})
.ToList();

Related

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

How to filter a list based on 2 properties?

I have a list in my code that I need to filter through and return specific rows based on two criteria. The List in question is a list of models from a database. There are two ID properties on each model, one is the ID from the data table and is unique, the other is an ID we use to identify groups and can repeat. We'll call them ID and GroupID. Basically, I want the resulting list to have only one of each GroupID, and it should be the one with the highest (numerically speaking) ID. For example:
Input:
List<MyModel> modelList = new List<MyModel>
modelList[0].ID = 1 modelList[0].GroupID = 5
modelList[1].ID = 2 modelList[1].GroupID = 5
modelList[2].ID = 3 modelList[2].GroupID = 6
modelList[3].ID = 4 modelList[3].GroupID = 6
Desired Output:
Models at indexes 1 and 3.
Using LINQ:
var items = (from model in modelList
group model by model.GroupID into modelGroup
select modelGroup.Max(i => i.ID)).ToList();
What you have to do here is first order the modelList by ID and then GroupBy the list items by GroupID, then pull the item with max Id value.
var result = modelList.OrderByDescending(x => x.ID).GroupBy(x => x.GroupID).Select(x => x.First());
the above query will give you the result.
This is your solution:
var myData = models.GroupBy(model => model.GroupId)
.Select(group => group.OrderByDescending(model => model.Id).First());
Or you could also do this:
var myData = models.GroupBy(model => model.GroupId)
.Select(group => group.First(model => model.Id == group.Max(model1 => model1.Id)));
For fun, here's a fiddle.
You can try to use GroupBy.
var q = modelList.GroupBy(x => x.GroupID, x => x,
(key, g) => new {
GroupID = key,
Id = g.Max(c => c.ID)
});
This should group all your elements by GroupId and select Max ID in one of that groups.
Try this code:
List<MyModel> modelList = new List<MyModel>();
modelList.Add(new MyModel());
modelList.Add(new MyModel());
modelList.Add(new MyModel());
modelList.Add(new MyModel());
modelList[0].ID = 1; modelList[0].GroupID = 5;
modelList[1].ID = 2; modelList[1].GroupID = 5;
modelList[2].ID = 3; modelList[2].GroupID = 6;
modelList[3].ID = 4; modelList[3].GroupID = 6;
var list = from ml in modelList group ml by ml.ID into r select new { ID = r.Key, MaxGroupID = r.Max() };
this might help you
modelList.GroupBy(model => model.GroupId, g => g.Id).Select(item => item.Max())
var newModelList = modelList.GroupBy(ml => ml.GroupID)
.Select(g => new MyModel
{
ID = g.OrderByDescending(x => x.ID).First().ID,
GroupID = g.Key
}).ToList();
Details
1) GroupBy then Select to get distinct items over GroupID.
2) First() after OrderByDescending to get highest ID.
3) new MyModel in Select is just to be explicit about the projection.

Preserve order with linq after groupby and selectmany

Is there a way to preserve the order after this linq expression?
var results =
DateList
.GroupBy(x => x.Date.Subtract(firstDay).Days / 7 + 1)
.SelectMany(gx => gx, (gx, x) => new {Week = gx.Key,DateTime =x,Count = gx.Count(),});
I found this Preserving order with LINQ , but I'm not sure if its the GroupBy or SelectMany casing the issues
Yes, if you first select your DateList and combine it with an index, using an overload of .Select that uses a delegate with a second (int) parameter that is called with the index of the items from the sequence :
DateList
.Select((dateTime, idx) => new {dateTime, idx})
.GroupBy(x => x.dateTime.Date.Subtract(firstDay).Days / 7 + 1)
...and persist the value through the linq chain
.SelectMany(gx => gx, (gx, x) => new {Week = gx.Key,
DateTime = x.dateTime,
Count = gx.Count(),
x.idx})
...then use it to re-order the output
.OrderBy(x => x.idx)
...and strip it from your final selection
.Select(x => new {x.Week, x.DateTime, x.Count});
then you can maintain the same order as the original list.
Solution of #spender is good, but can it be done without OrderBy? It can, because we can use the index for direct indexing into array, but it would not be one linq query:
var resultsTmp =
DateList.Select((d, i) => new { d, i })
.GroupBy(x => x.d.Date.Subtract(firstDay).Days / 7 + 1)
.SelectMany(gx => gx, (gx, x) => new { Week = gx.Key, DateTime = x.d, Count = gx.Count(), x.i })
.ToArray();
var resultsTmp2 = resultsTmp.ToArray();
foreach (var r in resultsTmp) { resultsTmp2[r.i] = r; };
var results = resultsTmp2.Select(r => new { r.Week, r.DateTime, r.Count });
It looks a bit complex. I would probably do something more straightforward like:
var DateList2 = DateList.Select(d => new { DateTime = d, Week = d.Subtract(firstDay).Days / 7 + 1 }).ToArray();
var weeks = DateList2.GroupBy(d => d.Week).ToDictionary(k => k.Key, v => v.Count());
var results = DateList2.Select(d2 => new { d2.Week, d2.DateTime, Count = weeks[d2.Week] });

create n lists of list linq

I have a simple list
var list =
AppUtils.db.GetDataTable("dbo.RankSelectChart", view) // stopred procedure and getting datatable
.AsEnumerable()
.Select(i => new
{
Date = i.Field<DateTime>("lastDatetime"),
P1 = i.Field<decimal>("p1"),
P2 = i.Field<decimal>("P2"),
P3..... P(n)
}
)
.ToList()
.OrderBy(x => x.Date);
than i want to get a list of lists or dictionary like List<Dictionary<Datetime, Decimal>> means Dictionary<Date, P1> .... Dictionary<Date, P(n)>
how to write algorithm which is not depend how many P we have
As it stands, you will need to use reflection to access the properties:
var result = new[]{"P1", "P2", "P3", ...}.Select(p => list.ToDictionary(
i => i.Date,
i => i.GetType().GetProperty(p).GetValue(i)));
However, if you could avoid creating your list in the first place and just pull from the data table directly, it may be easier.
var dt = AppUtils.db.GetDataTable("dbo.RankSelectChart", view); // stopred procedure and getting datatable
var pColumns = dt.Columns.Cast<DataColumn>()
.Where(c => c.ColumnName.StartsWith("p"));
var result = pColumns
.Select(p => dt.AsEnumerable().ToDictionary(
i => i.Field<DateTime>("lastDatetime"),
i => i.Field<DateTime>(p.ColumnName)))
.ToList();
If 'Date' is unique for all records in 'list', then you can use reflection to get the P(i) value for a record in list. Like so:
// build sample data
var list = Enumerable.Range(0, 10)
.ToList()
.Select(x => new {
Date = DateTime.Now.AddMinutes(x),
P1 = new Decimal(x),
P2 = new Decimal(x + 1),
P3 = new Decimal(x + 2)
})
.ToList();
// list partionned by date; assumes that Date is unique in list
List<Dictionary<DateTime, Decimal>> partitionedList;
if (list.Count == 0) {
partitionedList = new List<Dictionary<DateTime, Decimal>>();
} else {
var n = 3;
var listElementType = list[0].GetType();
partitionedList = Enumerable.Range(1, n)
.Select(x => {
var prop = listElementType.GetProperty("P" + x);
var pList = list.ToDictionary(
ll => ll.Date,
ll => (Decimal)prop.GetValue(ll));
return pList;
})
.ToList();
}
If 'Date' is not unique, then it cannot be the key to a dictionary and the desired data structure is not achievable.

Categories

Resources