LINQ - sum of column in hierarchical data - c#

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

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 GroupBy Max within Max

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

How to correctly convert from IEnumerable<T> to List<T>?

I have this LINQ
var questions = _context.Questions
.Where(q => q.Level.Level == level)
.Select(q => new QuestionViewModel
{
Text = q.Text,
Id = q.Id,
IsMultiSelected = q.IsMultiSelected,
AnswerViewModels = q.Answers
.Select(
a => new AnswerViewModel
{
Checked = false,
Text = a.Text,
Id = a.Id
}) as List<AnswerViewModel>
});
return questions.ToList();
I get
Exception Details: System.NotSupportedException: The 'TypeAs' expression with an input of type 'System.Collections.Generic.IEnumerable`1' and a check of type 'System.Collections.Generic.List`1' is not supported. Only entity types and complex types are supported in LINQ to Entities queries.
in
return questions.ToList();
I don't use anonymous types in select. How to resolve this error ?
UPDATE
I coded some solution
List<QuestionViewModel> result = new List<QuestionViewModel>();
var questions = from q in _context.Questions
where q.Level.Level == level
select new QuestionViewModel()
{
Text = q.Text,
Id = q.Id,
IsMultiSelected = q.IsMultiSelected,
AnswerViewModels = from a in q.Answers
select new AnswerViewModel
{
Text = a.Text,
Id = a.Id,
Checked = false
}
};
var qList = questions.ToList();
for(int i = 0; i < questions.Count(); i++)
{
var q = qList[i]; //question
var a = q.AnswerViewModels.ToList(); //answers for question
var answers = new List<AnswerViewModel>(); //List answers
for(int j = 0; j < a.Count(); j++)
{
//add new Answer from IEnumerable<AnswerViewQuestion> to List<...>
answers.Add(new AnswerViewModel
{
Checked = false,
Id = a[j].Id,
Text = a[j].Text
});
}
result.Add(q);
}
How to optimize ?
The problem is with
.Select(a => new AnswerViewModel { ... }) as List<AnswerViewModel>
It should be
.Select(a => new AnswerViewModel { ... }).ToList()
The reason is that the correct way to convert a LINQ-generated IEnumerable to a List is of course by calling the ToList extension method, which you are already doing in the last line given.
Try something like this:
var questions = _context.Questions
.Where(q => q.Level.Level == level)
.Select(q => new QuestionViewModel
{
Text = q.Text,
Id = q.Id,
IsMultiSelected = q.IsMultiSelected,
AnswerViewModels = q.Answers
.Select(
a => new AnswerViewModel
{
Checked = false,
Text = a.Text,
Id = a.Id
})
}).AsEnumerable().Select(x => new QuestionViewModel
{
Text = x.Text,
Id = x.Id,
IsMultiSelected = x.IsMultiSelected,
AnswerViewModels = x.Answers.ToList()
});
return questions.ToList();

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

LINQ: Grouping By and Selecting from a List of objects based on max value

I have the MyItem class with 3 properties as below:
class MyItem
{
private string _name;
private int _value
private DateTime _TimeStamp;
public MyItem(string name, int value, string timeStamp)
{
this._name = name;
this._value = value;
this._timeStamp = DateTime.Parse(timeStamp);
}
public string Name
{ get {return this_name; } }
public int Value
{ get {return this._value; } }
public DateTime TimeStamp
{ get {return this._timeStamp; } }
// ...
}
also I have a list of MyItem as below:
var myItems = new List<MyItem>() {
new MyItem("A", 123, "23/02/2012"),
new MyItem("A", 323, "22/02/2012"),
new MyItem("B", 432, "23/02/2012"),
new MyItem("B", 356, "22/02/2012"),
// ...
}
how can I GROUP BY myList so that I am ONLY left with the items that have Maximum TimeStamp? ie the result below:
"A" 123 23/02/2012<br>
"B" 432 23/02/2012<br>
Thanks in advance.
myItems.GroupBy(item => item.Name)
.Select(grp => grp.Aggregate((max, cur) =>
(max == null || cur.Date > max.Date) ? cur : max))
This will select your results in the fastest time possible (at least that I can figure) without creating new objects and iterating over the collection the least amount of times.
Select the Max from the Group:
from item in MyItems
group item by item.Name into grouped
let maxTimeStamp = grouped.Max(i => i.TimeStamp)
select grouped.First(i => i.TimeStamp == maxTimeStamp)
var temp = myItems.Where(x => x.TimeStamp == myItems.Where(y => y.Name == x.Name).Max(z => z.TimeStamp)).Distinct().ToList();
var tmp = select i from myItems
group i by i.Name into g
select new MyItem
{
g.Name,
Value = g.OrderByDescending(x => x.Timestamp).First().Value,
Timestamp = g.Max(x => x.Timestamp)
};
OK, I changed your class MyItem for some LINQ convenience (hope this doesn't cause problems) by adding a blank constructor to the class:
public MyItem() { }
In a sample console program, this code will work:
static void Main(string[] args)
{
var myItems = new List<MyItem>()
{
new MyItem("A", 123, "23/02/2012"),
new MyItem("A", 323, "22/02/2012"),
new MyItem("B", 432, "23/02/2012"),
new MyItem("B", 356, "22/02/2012")
// ...
};
var grouped = from m in myItems
group m by m.Name into g
let maxTimestamp = g.Max(t => t.TimeStamp)
select new MyItem
{
Name = g.Key,
Value = g.First(f => f.TimeStamp == maxTimestamp).Value,
TimeStamp = maxTimestamp
};
foreach (var gItem in grouped)
{
Console.WriteLine(gItem.Name + ", " + gItem.Value + ", " + gItem.TimeStamp);
}
Console.ReadLine();
}
The output matches your expected results.

Categories

Resources