Linq GroupBy Max within Max - c#

var list = new[]
{
new { maker="Volvo", type=1, model=15},
new { maker="Volvo", type=8, model=10},
new { maker="Volvo", type=8, model=100},
new { maker="Volvo", type=8, model=40},
new { maker="Volvo", type=6, model=5},
new { maker="Volvo", type=2, model=0},
new { maker="Volvo", type=1, model=2},
new { maker="GM", type=1, model=0},
new { maker="GM", type=0, model=20},
new { maker="GM", type=9, model=5},
new { maker="GM", type=9, model=50},
new { maker="GM", type=9, model=25},
};
var results = list
.GroupBy(x => x.maker, (key, g) => g.OrderByDescending(e => e.type).First())
.ToList();
Returns
{ maker = Volvo, type = 8, model = 10 }
{ maker = GM, type = 9, model = 5 }
Which is closed to what I want and if I keep extending it to
var results = list
.GroupBy(x => x.maker, (key, g) => g.OrderByDescending(e => e.type).GroupBy(z => z.type, (key1, y) => y.OrderByDescending(u => u.model).First()))
.ToList();
Returns nothing
vs. Expected result
{ maker = Volvo, type = 8, model = 100 }
{ maker = GM, type = 9, model = 50 }
So how do I fix this?

You just need a ThenByDescending instead of another GroupBy:
var results = list
.GroupBy(x => x.maker,
(key, g) => g.OrderByDescending(e => e.type)
.ThenByDescending(e => e.model)
.First())
.ToList();

Related

Duplicated linq query

I have 2 almost identical linq queries and want to remove repeating code from it. The only difference is the extra property in the GroupBy depending on some true/false condition.
How can I conditionally group by in linq without repeating the code like below?
var allergensList = _context.RecipeAllergens
.Where(x => x.ParentId == Id && x.AllergenId != null)
.ToList();
var allergens = new List<AllergenInfo>();
if (isRecipe)
{
allergens = allergensList
.GroupBy(x => new { x.AllergenName, x.AllergenIcon, x.AllergenMaycontains })
.Select(a =>
{
var v = a.OrderBy(x => x.AllergenMaycontains).First();
return new AllergenInfo
{
AllergenName = v.AllergenName,
AllergenIcon = v.AllergenIcon,
AllergenMayContain = v.AllergenMaycontains ?? false
};
})
.ToList();
}
else
{
allergens = allergensList
.GroupBy(x => new { x.AllergenName, x.AllergenIcon })
.Select(a =>
{
var v = a.OrderBy(x => x.AllergenMaycontains).First();
return new AllergenInfo
{
AllergenName = v.AllergenName,
AllergenIcon = v.AllergenIcon,
AllergenMayContain = v.AllergenMaycontains ?? false
};
})
.ToList();
}
You can left grouping by x.AllergenMaycontains but under condition
allergens = allergensList
.GroupBy(x => new { x.AllergenName, x.AllergenIcon, AllergenMaycontains = isRecipe ? x.AllergenMaycontains : false })
.Select(a =>
{
var v = a.OrderBy(x => x.AllergenMaycontains).First();
return new AllergenInfo
{
AllergenName = v.AllergenName,
AllergenIcon = v.AllergenIcon,
AllergenMayContain = v.AllergenMaycontains ?? false
};
})
.ToList();

LINQ - sum of column in hierarchical data

My data looks like this:
I am trying to create a JSON ouput (using JSON .NET) which will have the sum of the Value column by levels.
For example:
{
'id': 'AB',
'sum': '53',
'level2' : [
{
'id' : 'CD',
'sum' : '23',
'level3' : [
{
'id' : 'd1',
'sum' : '12'
},
{
'id' : 'd2',
'sum' : '11'
}
]
...
I am trying to use LINQ to create this. So far I have the following code:
var query = reader.SelectRows(r => new
{
level1 = r["level1"].ToString(),
sum = r["sum"] != DBNull.Value ? Convert.ToDouble(r["sum"]) : 0,
level2 = new
{
level2 = r["level2"].ToString(),
sum = r["sum "] != DBNull.Value ? Convert.ToDouble(r["sum"]) : 0,
level3 = new
{
level3 = r["level3 "].ToString(),
sum = r["sum"] != DBNull.Value ? Convert.ToDouble(r["sum"]) : 0
}
}
})
.GroupBy(r => new { r.level1 })
.Select(g => new
{
id = g.Key.level1,
sum = g.Sum(x => x.sum),
level2 = g.GroupBy(l => new { l.level2.level2 })
.Select(l => new
{
id = l.Key.level2,
sum = g.Sum(y => y.sum),
level3 = l.GroupBy(m => new { m.level2.level3.level3 })
.Select(m => new
{
id = m.Key.level3,
sum = g.Sum(z => z.sum),
})
})
});
retJSON = JsonConvert.SerializeObject(new { data = query }, Formatting.Indented);
The SelectRows function is like this:
// Adapted from this answer https://stackoverflow.com/a/1202973
// To https://stackoverflow.com/questions/1202935/convert-rows-from-a-data-reader-into-typed-results
// By https://stackoverflow.com/users/3043/joel-coehoorn
public static IEnumerable<T> SelectRows<T>(this IDataReader reader, Func<IDataRecord, T> select)
{
while (reader.Read())
{
yield return select(reader);
}
}
However, I am getting sum repeated at every level, i.e. the same value. Any direction on how to achieve this will be greatly appreciated.
I think your query is nearly there just 2 changes (shown by comments below).
.GroupBy(r => new { r.level1 })
.Select(g => new
{
id = g.Key.level1,
sum = g.Sum(x => x.sum),
level2 = g.GroupBy(l => new { l.level2.level2 })
.Select(l => new
{
id = l.Key.level2,
sum = l.Sum(y => y.sum), //l not g
level3 = l.GroupBy(m => new { m.level2.level3.level3 })
.Select(m => new
{
id = m.Key.level3,
sum = m.Sum(z => z.sum), //m not g
})
})
});

How to split List but keep groupBy

I have a list of key/value pairs in the following form:
[{John:6},{Alex:100},{Peter:4},{Peter,John:5},{Alex,Kati:1}]
I wonder if there is a simple linq expression I can use to translate the list into
[{John:11},{Alex:101},{Peter:9},{Kati:1}]
ie split string by comma and adjust counts.
the list above is coming from following LINQ
var list = people.Where(a => !string.IsNullOrWhiteSpace(a.Name))
.GroupBy(a => a.Name.Trim()).Select(a => new User { Name = a.Key, Items= a.Count() });
With
var list = new[]
{
new KeyValuePair<string, int>("John", 6),
new KeyValuePair<string, int>("Alex", 100),
new KeyValuePair<string, int>("Peter", 4),
new KeyValuePair<string, int>("Peter,John", 5),
new KeyValuePair<string, int>("Alex,Kati", 1)
};
this grouping
var modifiedList = list.SelectMany(p => p.Key.Split(',').Select(n => new {Name = n, Number = p.Value}))
.GroupBy(p => p.Name).Select(g => new KeyValuePair<string, int>(g.Key, g.Sum(r => r.Number)));
gives you the output
{[John, 11]}
{[Alex, 101]}
{[Peter, 9]}
{[Kati, 1]}
Try this:
var list = new List<KeyValuePair<string, int>>
{
new KeyValuePair<string,int>("John", 6),
new KeyValuePair<string,int>("Alex", 100),
new KeyValuePair<string,int>("Peter", 4),
new KeyValuePair<string,int>("Peter,John", 5),
new KeyValuePair<string,int>("Alex,Kati", 1)
};
var result = list.SelectMany(x => x.Key.Split(','),
(x, y) => new KeyValuePair<string, int>(y, x.Value))
.GroupBy(x => x.Key)
.ToDictionary(key => key.Key, value => value.Sum(x => x.Value));
Dictionary<string, int> result = keyVals
.SelectMany(kv => kv.Key.Split(',').Select(name => new{ name, kv.Value }))
.GroupBy(x => x.name)
.ToDictionary(xg => xg.Key, xg => xg.Sum(x => x.Value));
Result:
{[John, 11]}
{[Alex, 101]}
{[Peter, 9]}
{[Kati, 1]}
Demo
So you have classes like this
public class Person
{
public string Name { get; set; }
}
public class User
{
public string Name { get; set; }
public int Items { get; set; }
}
If I understand correctly you want to count how many times each name occurs
var people = new[]
{
new Person { Name = "John" },
new Person { Name = "John,Alex" },
new Person { Name = "Alex" },
new Person { Name ="Peter,John" }
};
var list = people.SelectMany(p => p.Name.Split(','))
.GroupBy(n => n)
.Select(g => new User { Name = g.Key, Items = g.Count() });

How to reuse a data into an anonymous type decalaration?

This is my code where I declare a anonymous type into a select :
var projectsByManagersAndMonths = projectDates
.Where(pd => pd.project.isEnabled)
.GroupBy(pd => new { pd.project.manager, pd.project.dtEnd.Value.Month })
.Select(group => new
{
Manager = group.Key.manager.displayName,
Month = group.Key.Month,
Projects = group.Select(pd => new
{
Count = group.Count(),
CostToComplete = group.Sum(pdd => pdd.totals.costToComplete),
TimeWorkable = new UserBillingRate(WS, group.Key.manager, new Period(start, end)).TimeWorkable,
StatusOfCharge = CostToComplete / TimeWorkable //IMPOSSIBLE
})
})
.ToList();
If you look the line where I added //IMPOSSIBLE, I try to reuse 2 data just calculated above into the same anonymous type.
How can I achieve this?
You would have to create an intermediate object which you can then reference the values of.
var projectsByManagersAndMonths = projectDates
.Where(pd => pd.project.isEnabled)
.GroupBy(pd => new { pd.project.manager, pd.project.dtEnd.Value.Month })
.Select(group => new
{
Manager = group.Key.manager.displayName,
Month = group.Key.Month,
Projects = group.Select(pd => new
{
Count = group.Count(),
CostToComplete = group.Sum(pdd => pdd.totals.costToComplete),
TimeWorkable = new UserBillingRate(WS, group.Key.manager, new Period(start, end)).TimeWorkable,
}).Select(pd => new
{
Count = pd.Count,
CostToComplete = pd.CostToComplete,
TimeWorkable = pd.TimeWorkable,
StatusOfCharge = pd.CostToComplete / pd.TimeWorkable //POSSIBLE
})
})
.ToList();
You can do it by adding a second level of Select:
Projects = group.Select(pd => new
{ // This level deals with the first level of computation
Count = group.Count(),
CostToComplete = group.Sum(pdd => pdd.totals.costToComplete),
TimeWorkable = new UserBillingRate(WS, group.Key.manager, new Period(start, end)).TimeWorkable
// This level takes the computed results, and add derived computations
}).Select(pd => new {
pd.Count,
pd.CostToComplete,
pd.TimeWorkable,
// Now that pd is an anonymous class created by the level above,
// both pd.CostToComplete and pd.TimeWorkable are defined.
StatusOfCharge = pd.CostToComplete / pd.TimeWorkable
})

Joining & Transposing multiple Lists using LINQ

Hi I have the following code which returns me the right data but it seems there must be a better way to combine 3 lists, based on their common field(s) and transpose the results out into a new list of a given type using LINQ, instead of resorting to the foreach at the end. Any ideas?
public IEnumerable<StagSummaryByCflHistoricalItem> GetSummaryByCflHistorical(DateTime currentDate)
{
var allRecords =
this.preGrantSummaryHistoricalRepository
.AllWithFetch(this.preGrantSummaryHistoricalRepository.All, x => x.CaseFileLocation)
.Where(
x => x.Date >= currentDate.FirstDayOfQuarterFromDateTime()
&& x.Date <= currentDate.LastDayOfQuarterFromDateTime())
.ToList();
var summaryForQuarter =
allRecords.GroupBy(x => new { x.CaseFileLocation.Id, x.CaseFileLocation.Name }).Select(
x =>
new
{
CaseFileLocationId = x.Key.Id,
Description = x.Key.Name,
TotalCasesEnteredCfl = x.Sum(y => y.TotalCasesEntered),
TotalNetFeeEnteredCfl = x.Sum(y => y.TotalNetFeeEntered),
TotalCasesLeftCfl = x.Sum(y => y.TotalCasesLeft),
TotalNetFeeLeftCfl = x.Sum(y => y.TotalNetFeeLeft)
})
.OrderBy(x => x.CaseFileLocationId)
.ToList();
var summaryForMonth =
allRecords.Where(x => x.Date >= currentDate.FirstDayOfMonthFromDateTime())
.GroupBy(x => new { x.CaseFileLocation.Id, x.CaseFileLocation.Name }).Select(
x =>
new
{
CaseFileLocationId = x.Key.Id,
Description = x.Key.Name,
TotalCasesEnteredCfl = x.Sum(y => y.TotalCasesEntered),
TotalNetFeeEnteredCfl = x.Sum(y => y.TotalNetFeeEntered),
TotalCasesLeftCfl = x.Sum(y => y.TotalCasesLeft),
TotalNetFeeLeftCfl = x.Sum(y => y.TotalNetFeeLeft)
})
.OrderBy(x => x.CaseFileLocationId)
.ToList();
var summaryForWeek =
allRecords.Where(x => x.Date >= currentDate.FirstDayOfWeekFromDateTime(DayOfWeek.Monday)).GroupBy(
x => new { x.CaseFileLocation.Id, x.CaseFileLocation.Name }).Select(
x =>
new
{
CaseFileLocationId = x.Key.Id,
Description = x.Key.Name,
TotalCasesEnteredCfl = x.Sum(y => y.TotalCasesEntered),
TotalNetFeeEnteredCfl = x.Sum(y => y.TotalNetFeeEntered),
TotalCasesLeftCfl = x.Sum(y => y.TotalCasesLeft),
TotalNetFeeLeftCfl = x.Sum(y => y.TotalNetFeeLeft)
})
.OrderBy(x => x.CaseFileLocationId)
.ToList();
var finalList = summaryForQuarter
.Select(x => new StagSummaryByCflHistoricalItem()
{
CaseFileLocationId = x.CaseFileLocationId,
Description = x.Description,
QuarterTotalCasesEnteredCfl = x.TotalCasesEnteredCfl,
QuarterTotalCasesLeftCfl = x.TotalCasesLeftCfl,
QuarterTotalNetFeeEnteredCfl = x.TotalNetFeeEnteredCfl,
QuarterTotalNetFeeLeftCfl = x.TotalNetFeeLeftCfl
})
.OrderBy(x => x.CaseFileLocationId)
.ToList();
foreach (var qrt in finalList)
{
var mnthData = summaryForMonth.FirstOrDefault(x => x.CaseFileLocationId == qrt.CaseFileLocationId);
if (mnthData != null)
{
qrt.MonthTotalCasesEnteredCfl = mnthData.TotalCasesEnteredCfl;
qrt.MonthTotalCasesLeftCfl = mnthData.TotalCasesLeftCfl;
qrt.MonthTotalNetFeeEnteredCfl = mnthData.TotalNetFeeEnteredCfl;
qrt.MonthTotalNetFeeLeftCfl = mnthData.TotalNetFeeLeftCfl;
}
var weekData = summaryForWeek.FirstOrDefault(x => x.CaseFileLocationId == qrt.CaseFileLocationId);
if (weekData == null)
{
continue;
}
qrt.WeekTotalCasesEnteredCfl = weekData.TotalCasesEnteredCfl;
qrt.WeekTotalCasesLeftCfl = weekData.TotalCasesLeftCfl;
qrt.WeekTotalNetFeeEnteredCfl = weekData.TotalNetFeeEnteredCfl;
qrt.WeekTotalNetFeeLeftCfl = weekData.TotalNetFeeLeftCfl;
}
return finalList;
}
Note: I am intentionally getting the entire quarter's worth of data first as a list and then operating on it to do the month & quarter totals but this is mainly because I cannot fathom a way to get the end result from a combined LINQ IQuerable.
I am using NHibernate, LINQ method syntax, the repository pattern and SQL Server 2008
If I'm reading your code correctly, you're projecting your results to anonymous types with the same resulting members, which makes it very easy to join your results together. I did something similar recently with Union. Here's a simplified example I just wrote to demonstrate:
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
namespace ConsoleApplication1 {
public class Month {
public int MonthID { get; set; }
public string MonthName { get; set; }
public int NoDays { get; set; }
}
internal class Program {
private static void Main(string[] args) {
// Build up months
var months = new List<Month>();
for (var i = 1; i <= 12; i++) {
months.Add(new Month {
MonthID = i,
MonthName = DateTimeFormatInfo.CurrentInfo.GetMonthName(i),
NoDays = DateTime.DaysInMonth(2012, i)
});
}
var w = months.Select(m => new {
m.MonthName
});
var x = months.Select(m => new {
m.MonthName
});
var y = months.Select(m => new {
m.MonthName
});
var z = w.Union(x).Union(y);
foreach (var m in z) {
Console.WriteLine(m.MonthName);
}
Console.Read();
}
}
}
Bear in mind that "Union" (like the SQL UNION clause) will remove any duplicates from your list. If you don't want to remove duplicates (i.e. perform a "union all"), use "Concat" as follows:
var z = w.Concat(x).Concat(y);

Categories

Resources