I would like to filter out "" names then select each unique location where there are duplicate IDs regardless of name:
Data Setup
var list = new[]
{
new { id = 3, Name = "", Location = "LocationA" },
new { id = 2, Name = "", Location = "LocationA" },
new { id = 1, Name = "T", Location = "LocationB" },
new { id = 2, Name = "H", Location = "LocationB" },
new { id = 3, Name = "E", Location = "LocationB" },
new { id = 3, Name = "R", Location = "LocationB" },
new { id = 5, Name = "U", Location = "LocationC" },
new { id = 5, Name = "S", Location = "LocationC" },
new { id = 5, Name = "S", Location = "LocationD" },
new { id = 4, Name = "O", Location = "LocationD" },
new { id = 4, Name = "Z", Location = "LocationE" },
};
Query
var query1 = list
.Where(s => s.Name != "")
.GroupBy(g => g.Location)
.Where(w => w.Select(s => s.Location).Count() > 1)
.SelectMany(s => s)
.GroupBy(g => g.id)
.Where(w => w.Select(s => s.id).Count() > 1)
.SelectMany(s => s)
.ToList();
Console.WriteLine("output\n" + string.Join("\n", query1));
Returns
{ id = 3, Name = E, Location = LocationB }
{ id = 3, Name = R, Location = LocationB }
{ id = 5, Name = U, Location = LocationC }
{ id = 5, Name = S, Location = LocationC }
{ id = 5, Name = S, Location = LocationD }
vs What I actually wanted
{ id = 3, Name = E, Location = LocationB }
{ id = 3, Name = R, Location = LocationB }
{ id = 5, Name = U, Location = LocationC }
{ id = 5, Name = S, Location = LocationC }
LocationD has IDs 4 & 5 so it should've been filtered out, I wasn't able to do so. What am I doing wrong? How do I correct it?
Given
var list = new[]
{
new { id = 3, Name = "", Location = "LocationA" },
new { id = 2, Name = "", Location = "LocationA" },
new { id = 1, Name = "T", Location = "LocationB" },
new { id = 2, Name = "H", Location = "LocationB" },
new { id = 3, Name = "E", Location = "LocationB" },
new { id = 3, Name = "R", Location = "LocationB" },
new { id = 5, Name = "U", Location = "LocationC" },
new { id = 5, Name = "S", Location = "LocationC" },
new { id = 5, Name = "S", Location = "LocationD" },
new { id = 4, Name = "O", Location = "LocationD" },
new { id = 4, Name = "Z", Location = "LocationE" },
};
Example
var results = list
.Where(s => s.Name != "")
.GroupBy(x => new {x.id, x.Location})
.Where(g => g.Count() > 1)
.SelectMany(y => y);
foreach (var result in results)
Console.WriteLine($"{result.id}, {result.Name}, {result.Location}");
Output
3, E, LocationB
3, R, LocationB
5, U, LocationC
5, S, LocationC
Group by id and Location. And get .Count() more than 1.
var query1 = list
.Where(s => s.Name != "")
.GroupBy(g => new { g.Location, g.id })
.Where(g => g.Count() > 1)
.SelectMany(g => g)
.ToList();
Sample demo
Related
List<empl> lstSource = new List<empl>();
lstSource.Add(new empl { departmentId = 2, Id = 101, Name = "S1" });
lstSource.Add(new empl { departmentId = 2, Id = 109, Name = "S9" });
lstSource.Add(new empl { departmentId = 2, Id = 102, Name = "S2" });
lstSource.Add(new empl { departmentId = 4, Id = 101, Name = "S1" });
lstSource.Add(new empl { departmentId = 4, Id = 102, Name = "S2" });
lstSource.Add(new empl { departmentId = 4, Id = 108, Name = "S8" });
lstSource.Add(new empl { departmentId = 3, Id = 105, Name = "S5" });
lstSource.Add(new empl { departmentId = 3, Id = 103, Name = "S3" });
lstSource.Add(new empl { departmentId = 3, Id = 102, Name = "S2" });
should result {Id = 102, Name = "S2"}
if I add
lstSource.Add(new empl { departmentId = 3, Id = 101, Name = "S1" });
should result {Id = 102, Name = "S2"} {Id = 101, Name = "S1"}
Hint : we can group with departmentId and find common Id in 3 group.
Based on your comments and example above, I take it that the Name associated with any given Id is always the same. In that case, you could split the Ids registered on each department into separate lists, then intersect those lists to find the common Ids, and then find the associated Name for each common Id.
You have done something similar in your own example. By rewriting the code (e.g. by replacing the foreach loop with an Aggregate() function) you could achieve a more straight forward approach:
var idsPerDepartment = lstSource
.GroupBy(item => item.departmentId)
.Select(gr => gr.Select(item => item.Id));
var commonIds = idsPerDepartment
.Aggregate((common, next) => common.Intersect(next));
var commonItems = commonIds
.Select(id => lstSource.First(item => item.Id == id))
.Select(commonItem => new { commonItem.Id, commonItem.Name })
.OrderByDescending(commonItem => commonItem.Name);
commonItems is empty (not null) if there are no common items.
It could all be written as one single expression, but I've spilt it into several variables to clarify what is happening along the way.
var groups = lstSource.GroupBy(t1=> t1.departmentId)
.ToList();
var validIds = groups.First().Select(t1 => t1.Id).ToList();
foreach (var g in groups.Skip(0))
{
var otherGroupItemIds = g.Select(t1 => t1.Id).ToList();
validIds = validIds.Intersect(otherGroupItemIds).ToList();
}
if (validSRIds.Count > 0)
return lstSource.FindAll(t1 => validSRIds.Contains(t1.Id)).GroupBy(t2 => new { t2.Id, t2.Name }).Select(g => g.First()).OrderByDescending(t => t.Name).ToList();
you will get all common id,name which belongs to all group
I have two lists of elements that differ only on the IsTemporalPaymentTerm boolean field.
I want to use LINQ to compare the two lists, entry by entry, and produce a third list that has, for each entry, the one where IsTemporalPaymentTerm = true, and if there isn't one, then I want the one where IsTemporalPaymentTerm = false.
Here's some sample code:
var allResults = db.PaymentTerms
.AsQueryable()
.Where(y => y.WorkDate == date
&& y.ProjectID == ProjectID
&& y.CompanyID == CompanyID
&& y.PayeeID == PayeeID);
//TABLE WITH ONLY TEMPORAL PAYMENT TERMS
var onlyTemporalResults = allResults.Where(x => x.IsTemporalPaymentTerm);
//TABLE WITH ONLY NON-TEMPORAL PAYMENT TERMS
var nonTemporalResults = allResults.Where(x => !x.IsTemporalPaymentTerm);
So, basically what I want is to compare onlyTemporalResults against nonTemporalResults , and get a final list that has either the temporal payment term, OR the non-temporal payment term if no temporal payment term could be found.
Pseudo code example:
List<PaymentTerms> TemporalPaymentTerms = new List<PaymentTerms>();
PaymentTerm unnaprovedPT1 = new PaymentTerm { PayeeID = 1, CompanyID = 2, ProjectID = 3, IsTemporalPaymentTerm = false };
PaymentTerm unnaprovedPT2 = new PaymentTerm { PayeeID = 2, CompanyID = 2, ProjectID = 3, IsTemporalPaymentTerm = false };
TemporalPaymentTerms.Add(unnaprovedPT1);
TemporalPaymentTerms.Add(unnaprovedPT2);
List<PaymentTerms> NonTemporalPaymentTerms = new List<PaymentTerms>();
PaymentTerm approvedPT1 = new PaymentTerm { PayeeID = 2, CompanyID = 2, ProjectID = 3, IsTemporalPaymentTerm = true};
PaymentTerm approvedPT1 = new PaymentTerm { PayeeID = 3, CompanyID = 2, ProjectID = 3, IsTemporalPaymentTerm = true};
//LINQ query that merges both lists goes here.
//FINAL EXPECTED RESULT:
List<PaymentTerms> FinalList = [
{PayeeID = 1, CompanyID = 2, ProjectID = 3, IsTemporalPaymentTerm = false},
{PayeeID = 2, CompanyID = 2, ProjectID = 3, IsTemporalPaymentTerm = false},
{PayeeID = 3, CompanyID = 2, ProjectID = 3, IsTemporalPaymentTerm = true}
];
I know this can be done iterating over the two lists (temporal and non-temporal Payment Terms), and then comparing them, but I guess my question is:
Can this be done, more efficiently and in a more elegant way, using a single LINQ query? Maybe a certain form of join that I am missing? Conditional Where clauses?
So far I have failed to see the answer. Thanks in advance!
Is this what you're looking for?
static void Main(string[] args)
{
RunPaymentTermsTest();
Console.WriteLine("Done!");
Console.ReadLine();
}
private static void RunPaymentTermsTest()
{
var temporalPaymentTerms = new List<PaymentTerm>
{
new PaymentTerm { PayeeID = 1, CompanyID = 2, ProjectID = 3, IsTemporalPaymentTerm = false },
new PaymentTerm { PayeeID = 2, CompanyID = 2, ProjectID = 3, IsTemporalPaymentTerm = false }
};
var nonTemporalPaymentTerms = new List<PaymentTerm>()
{
new PaymentTerm { PayeeID = 2, CompanyID = 2, ProjectID = 3, IsTemporalPaymentTerm = true },
new PaymentTerm { PayeeID = 3, CompanyID = 2, ProjectID = 3, IsTemporalPaymentTerm = true }
};
var toAdd = temporalPaymentTerms
.Where(x =>
!nonTemporalPaymentTerms.Any(y =>
y.CompanyID == x.CompanyID &&
y.PayeeID == x.PayeeID &&
y.ProjectID == x.ProjectID))
.ToList();
var results = nonTemporalPaymentTerms;
results.AddRange(toAdd);
foreach (var result in results.OrderBy(x => x.PayeeID).ThenBy(x => x.CompanyID).ThenBy(x => x.ProjectID))
{
Console.WriteLine(
$"PayeeID: {result.PayeeID}, CompanyID: {result.CompanyID}, ProjectID: {result.ProjectID}, IsTemporalPaymentTerm: {result.IsTemporalPaymentTerm}");
}
}
I have a model class which looks something like this:
public class Employee
{
public int Id {get;set;}
public int ParentId {get;set;}
public string Name{get;set;}
public string Designation {get;set;}
}
using which I simulated a list:
new List<Employee> employees
{
new Employee{Id = 1, ParentId = 0, Name = "A", Designation = "CEO" },
new Employee{Id = 2, ParentId = 1, Name = "B", Designation = "Manager" },
new Employee{Id = 3, ParentId = 1, Name = "C", Designation = "Manager" },
new Employee{Id = 4, ParentId = 2, Name = "D", Designation = "Lead" },
new Employee{Id = 5, ParentId = 3, Name = "E", Designation = "Lead" },
new Employee{Id = 6, ParentId = 4, Name = "F", Designation = "Developer" },
new Employee{Id = 7, ParentId = 4, Name = "G", Designation = "Developer" },
new Employee{Id = 8, ParentId = 5, Name = "H", Designation = "Developer" }
};
Well I need to write a LINQ query to filter the above list so that even the parent objects(if present) are retained during the filtering. I could not quiet wrap my head around the retainment of the parent part where I always end up getting it wrong.
To make it more clear this is what is the expected filtered list in case the filter search criteria are the Ids 6 and 7:
{
new Employee{Id = 1, ParentId = 0, Name = "A", Designation = "CEO" },
new Employee{Id = 2, ParentId = 1, Name = "B", Designation = "Manager" },
new Employee{Id = 4, ParentId = 2, Name = "D", Designation = "Lead" }
new Employee{Id = 6, ParentId = 4, Name = "F", Designation = "Developer" },
new Employee{Id = 7, ParentId = 4, Name = "G", Designation = "Developer" }
}
and if the Id to filter is 8:
{
new Employee{Id = 1, ParentId = 0, Name = "A", Designation = "CEO" },
new Employee{Id = 3, ParentId = 1, Name = "C", Designation = "Manager" },
new Employee{Id = 5, ParentId = 3, Name = "E", Designation = "Lead" },
new Employee{Id = 8, ParentId = 5, Name = "H", Designation = "Developer" }
}
and if the Id to filter is 2:
{
new Employee{Id = 1, ParentId = 0, Name = "A", Designation = "CEO" },
new Employee{Id = 2, ParentId = 1, Name = "B", Designation = "Manager" }
}
You can implement a help method, EmployeeAndBosses which returns given employee and all the parents:
private static IEnumerable<Employee> EmployeeAndBosses(Employee value,
IEnumerable<Employee> collection) {
for (Employee item = value;
item != null;
item = collection.FirstOrDefault(x => x.ParentId == item.Id))
yield return item;
}
then you can filter topmost employee in the hierarchy, and add their bosses then:
HashSet<int> ids = new HashSet<int>() {
6, 7
};
var result = employees
.Where(item => ids.Contains(item.Id)) // topmost
.SelectMany(item => EmployeeAndBosses(item, employees)) // topmost and parents
.GroupBy(item => item.Id) // Duplicates (e.g. CEO) removing
.Select(group => group.First()); //
Edit: If you have a huge collection(s) and that's why FirstOrDefault and GroupBy are bad choice, you can implement Bread First Search:
private static IEnumerable<Employee> MyFilter(IEnumerable<Employee> list,
IEnumerable<int> idsToFind) {
Dictionary<int, Employee> stuff = list
.ToDictionary(item => item.Id, item => item);
HashSet<int> ids = new HashSet<int>(idsToFind);
HashSet<int> completed = new HashSet<int>();
Queue<Employee> agenda = new Queue<Employee>(list.Where(item => ids.Contains(item.Id)));
while (agenda.Count > 0) {
Employee current = agenda.Dequeue();
if (null != current && completed.Add(current.Id)) {
yield return current;
if (stuff.TryGetValue(current.ParentId, out current))
agenda.Enqueue(current);
}
}
}
As some comments seem to be quite... Subjective... Here is a simple (but somewhat inefficient) extension that handles your requirements like a charm:
(assuming you'll never hire an employee as a boss to another employee that in turn is their boss, but such madness would probably break the company quicker than it breaks the query)
public static IEnumerable<Employee> FindByIdsAndIncludeParents(this IEnumerable<Employee> employees, params int[] targetIds)
=> employees
.Where(r => targetIds.Contains(r.Id))
.SelectMany(r => employees.FindByIdsAndIncludeParents(r.ParentId).Append(r))
.Distinct();
As some are not quite as keen of exchanging this quite expensive operation for the mere beauty of it, we could trade in some beauty for speed using a dictionary as entry point for instant access to the appended boss-search:
public static IEnumerable<Employee> FindFriendsFaster(this IEnumerable<Employee> employees, params int[] targetIds)
=> employees
.ToDictionary(e => e.Id, e => e)
.FindWithBossFriend(targetIds)
.Distinct();
public static IEnumerable<Employee> FindWithBossFriend(this IDictionary<int, Employee> hierarchy, params int[] targetIds)
=> targetIds
.Where(eId => hierarchy.ContainsKey(eId))
.Select(eId => hierarchy[eId])
.SelectMany(e => hierarchy.FindWithBossFriend(e.ParentId).Append(e));
As you might be able to spot, I personally can't seem to be able to trade in any more of my dignity for the possible removal of that last .Distinct(), but there are rumors going around some would be.
(Edited)
This is my code with examples of the collections I am working with:
That's what I meant when I wrote that myProducts1 and myProducts1 are nested collections
List<MyProducts> myProducts1= {
new MyProducts{id = 1, Name = "product1", isExcl= true},
new MyProducts{id = 2, Name = "product2", isExcl= false},
new MyProducts{id = 3, Name = "product3", isExcl= true},
new MyProducts{id = 4, Name = "product4", isExcl= false}
}
List<MyProducts> myProducts2= {
new MyProducts{id = 5, Name = "product5", isExcl= true},
new MyProducts{id = 6, Name = "product6", isExcl= false}
}
IEnumerable<SelectedProductRequest> selectedProducts = {
new SelectedProductRequest {id = 1, Name = "product1", Price = 23},
new SelectedProductRequest {id = 1, Name = "product1", Price = 44},
new SelectedProductRequest {id = 2, Name = "product2", Price = 11},
new SelectedProductRequest {id = 6, Name = "product6", Price = 34},
}
List<CategoryProduct> productsWithCategories = {
{CategoryName= "Category1", categoryProduct = myProducts1 },
{CategoryName= "Category2", categoryProduct = myProducts2 }
}
Here is my first code:
IEnumerable<SelectedProductViewModel> products1 =
.GroupBy(categoryProduct => categoryProduct .CategoryName)
.Select(categoryProduct => new ProductCategoryViewModel(
categoryProduct .Key,
categoryProduct
.Select(product => new SelectedProductViewModel(
product.Name,
selectedProducts.FirstOrDefault(selectedProduct => selectedProduct.id== product.id)?.Price ?? 0,
product.IsExclusive))
.OrderByDescending(product => product.id)))
Here is my second code:
IEnumerable<SelectedProductViewModel> products2 =
.GroupBy(categoryProduct => categoryProduct .CategoryName)
.Select(categoryProduct => new ProductCategoryViewModel(
categoryProduct .Key,
categoryProduct
.Join(contractSelectedProducts, product => product.id, selected => selected.id, (product, selected) =>
new SelectedProductViewModel(
product.Name,
selected?.Price ?? 0,
product.IsExclusive))
The results I get with those pieces of code:
products1= {"Category1",{
new SelectedProductRequest {id = 1, Name = "product1", Price = 23, IsExclusive = true},
new SelectedProductRequest {id = 2, Name = "product2", Price = 11, IsExclusive = false},
new SelectedProductRequest {id = 3, Name = "product3", Price = 0, IsExclusive = true},
new SelectedProductRequest {id = 4, Name = "product4", Price = 0, IsExclusive = false}
},
"Category2", {new SelectedProductRequest {id = 5, Name = "product5", Price = 0, IsExclusive = true},
new SelectedProductRequest {id = 6, Name = "product6", Price = 34, IsExclusive = false}}
products2= {"Category1",{
new SelectedProductRequest {id = 1, Name = "product1", Price = 23, IsExclusive = true},
new SelectedProductRequest {id = 1, Name = "product1", Price = 44, IsExclusive = true},
new SelectedProductRequest {id = 2, Name = "product2", Price = 11, IsExclusive = false}
}},
"Category2", {new SelectedProductRequest {id = 6, Name = "product6", Price = 34, IsExclusive = false}
But the result I want to achieve is this:
products= {"Category1",{
new SelectedProductRequest {id = 1, Name = "product1", Price = 23, IsExclusive = true},
new SelectedProductRequest {id = 1, Name = "product1", Price = 44, IsExclusive = true},
new SelectedProductRequest {id = 2, Name = "product2", Price = 11, IsExclusive = false},
new SelectedProductRequest {id = 3, Name = "product3", Price = 0, IsExclusive = true},
new SelectedProductRequest {id = 4, Name = "product4", Price = 0, IsExclusive = false}
}},
"Category2",{
{new SelectedProductRequest {id = 5, Name = "product5", Price = 0, IsExclusive = true},
{new SelectedProductRequest {id = 6, Name = "product6", Price = 34, IsExclusive = false}
}
(edited code)
My question: How can I achieve this result without using products1 and products2? Or how can i do it in a better way?
updated as per comment
Solution 1
I changed the last Linq query by :
List<ProductCategory> mergedList = products1
// concat products2 with products1
.Concat(products2)
// group by category name
.GroupBy(x => x.CategoryName)
//dictionay(categoryName, list of SelectedProductRequests)
.ToDictionary(key => key, value => value.SelectMany(x => x.SelectedProductRequests)
.GroupBy(x => new { x.id, x.Name, x.Price, x.IsExclusive })
// take the first in grouped element
.Select(x => x.First())
//convert to SelectedProductRequest
.Select(x => new SelectedProductRequest { id = x.id, Name = x.Name, Price = x.Price, IsExclusive = x.IsExclusive })
.OrderBy(x => x.id)//order by id
.ToList())
.Select(x => new ProductCategory { CategoryName = x.Key.Key, SelectedProductRequests = x.Value })
.ToList();
Solution 2
if i understand your demand, you need to build productsWithCategories to get Expected result with one linq request, then check the following proposition by using GroupJoin instead Join :
IEnumerable<ProductCategoryViewModel> result = productsWithCategories
.Select(x => new ProductCategoryViewModel
{
CategoryName = x.CategoryName,
SelectedProductViewModels = x.categoryProduct
.GroupJoin(selectedProducts, prd => prd.id, sel => sel.id,
(prd, sel) => sel != null && sel.Any() ?
sel.Select(y =>
new SelectedProductViewModel
{
id = prd.id,
isExcl = prd.isExcl,
Name = prd.Name,
Price = y.Price
}).ToList() :
new List<SelectedProductViewModel>
{
new SelectedProductViewModel
{
id = prd.id,
isExcl = prd.isExcl,
Name = prd.Name,
Price = 0
}
})
.SelectMany(z => z)
.ToList()
});
i hope that will help you fix the issue
You can use Concat method to join arrays:
var selectedProductsMapped = selectedProducts.Select(s => new SelectedProductRequest
{
id = s.id,
Name = s.Name,
Price = s.Price,
isExclusive = myProducts.Where(p => p.id == s.id).FirstOrDefault().isExclusive
});
var exlcludedProducts = myProducts.Where(p => !selectedProducts.Any(sp => sp.id == p.id))
.Select(s => new SelectedProductRequest
{
id = s.id,
Name = s.Name,
Price = 0
});
var result = selectedProductsMapped.Concat(exlcludedProducts);
An example:
var selectedProducts = new List<SelectedProductRequest> {
new SelectedProductRequest {id = 1, Name = "product1", Price = 23},
new SelectedProductRequest {id = 1, Name = "product1", Price = 44},
new SelectedProductRequest {id = 2, Name = "product2", Price = 11}
};
var myProducts = new List<MyProducts> {
new MyProducts{id = 1, Name = "product1", isExclusive= true},
new MyProducts{id = 2, Name = "product2", isExclusive= false},
new MyProducts{id = 3, Name = "product3", isExclusive= true},
new MyProducts{id = 4, Name = "product4", isExclusive= false}
};
var selectedProductsMapped = selectedProducts.Select(s => new SelectedProductRequest
{
id = s.id,
Name = s.Name,
Price = s.Price,
isExclusive = myProducts.Where(p => p.id == s.id).FirstOrDefault().isExclusive
});
var exlcludedProducts = myProducts.Where(p => !selectedProducts.Any(sp => sp.id == p.id))
.Select(s => new SelectedProductRequest
{
id = s.id,
Name = s.Name,
Price = 0
});
var result = selectedProductsMapped.Concat(exlcludedProducts);
You can produce the set of products1 and products2 with Linq's Union(). You will need to implement SelectedProductRequest's GetHashCode().
products = products1.Union(products2);
Try it Online!
Add this to SelectedProductRequest:
public class SelectedProductRequest : IEquatable<SelectedProductRequest>
{
public int id {get;set;}
public string Name {get;set;}
public int Price {get;set;}
public bool IsExclusive {get;set;}
public bool Equals(SelectedProductRequest other) => other is null
&& this.id == other.id
&& this.Name == other.Name
&& this.Price == other.Price
&& this.IsExclusive == other.IsExclusive;
public override bool Equals(object obj) => Equals(obj as SelectedProductRequest);
public override int GetHashCode() => (id, Name, Price, IsExclusive).GetHashCode();
}
I have these lists:
var subjects = new List<SubjectModel>
{
new SubjectModel { subjId = 1, subjName = "Math" },
new SubjectModel { subjId = 2, subjName = "Science" },
new SubjectModel { subjId = 3, subjName = "History" },
new SubjectModel { subjId = 4, subjName = "Language" }
};
var quizzes = new List<QuizModel>
{
new QuizModel { quizId = 1, quizDate = DateTime.Parse("2016-11-25"), quizScore = 10, subjectId = 1 },
new QuizModel { quizId = 2, quizDate = DateTime.Parse("2016-11-25"), quizScore = 15, subjectId = 1 },
new QuizModel { quizId = 3, quizDate = DateTime.Parse("2016-11-25"), quizScore = 8, subjectId = 2 },
new QuizModel { quizId = 4, quizDate = DateTime.Parse("2016-11-26"), quizScore = 13, subjectId = 1 },
new QuizModel { quizId = 5, quizDate = DateTime.Parse("2016-11-26"), quizScore = 20, subjectId = 2 }
};
var exams = new List<ExamModel>
{
new ExamModel { examId = 1, examDate = DateTime.Parse("2016-11-25"), examScore = 90, subjectId = 1 },
new ExamModel { examId = 2, examDate = DateTime.Parse("2016-11-25"), examScore = 88, subjectId = 2 },
new ExamModel { examId = 3, examDate = DateTime.Parse("2016-11-25"), examScore = 92, subjectId = 4 },
new ExamModel { examId = , examDate = DateTime.Parse("2016-11-26"), examScore = 84, subjectId = 1 },
};
var exercises = new List<ExerciseModel>
{
new ExerciseModel { exerciseId = 1, exerciseDate = DateTime.Parse("2016-11-25"), exerciseScore = 17, subjectId = 1 },
new ExerciseModel { exerciseId = 2, exerciseDate = DateTime.Parse("2016-11-25"), exerciseScore = 15, subjectId = 2 },
new ExerciseModel { exerciseId = 3, exerciseDate = DateTime.Parse("2016-11-26"), exerciseScore = 15, subjectId = 1 },
new ExerciseModel { exerciseId = 4, exerciseDate = DateTime.Parse("2016-11-26"), exerciseScore = 12, subjectId = 4 },
new ExerciseModel { exerciseId = 5, exerciseDate = DateTime.Parse("2016-11-26"), exerciseScore = 10, subjectId = 1 },
};
I was able to successfully group each of them by date and by subject.
var allQuizzes = quizzes.GroupBy(qz => qz.quizDate, (q, values) =>
new
{
Date = q,
Quizzes = values.GroupBy(v => v.subjectId, (c, values2) =>
new {
SubjectId = c,
QuizSum = values2.Sum(v2 => v2.quizScore)
})
});
var allExercises = exercises.GroupBy(ex => ex.exerciseDate, (e, values) =>
new {
Date = e,
Exercises = values.GroupBy(x => x.subjectId, (z, values2) =>
new {
SubjectId = z,
ExerSum = values2.Sum(r => r.exerciseScore)
})
});
var allExams = exams.GroupBy(ex => ex.examDate, (e, values) =>
new
{
Date = e,
Exercises = values.GroupBy(x => x.subjectId, (z, values2) =>
new
{
SubjectId = z,
ExamSum = values2.Sum(r => r.examScore)
})
});
However, I need to join all three of them to get the sum of all scores. The final table should display like this.
-----------------------------------------------------------------
| Date | Math | Science | History | Language |
| 11/25/2016 | 132 | 111 | 0 | 92 |
| 11/26/2016 | 122 | 20 | 0 | 12 |
-----------------------------------------------------------------
I tried to join them, but it can't seem to join by multiple columns.
I select from all 3 collections results in form of the same anonymous class (the same Idea had Andrei in first answer), that allows me just to collect all results together in all list, without mapping and converting.
var allQuiz = quizzes.GroupBy(x => new { x.subjectId, x.quizDate })
.Select(x => new {
Date = x.Key.quizDate,
Subj = x.Key.subjectId,
Sum = x.Sum(r=>r.quizScore)});
var allExam= exams.GroupBy(x => new { x.subjectId, x.examDate })
.Select(x => new {
Date = x.Key.examDate,
Subj = x.Key.subjectId,
Sum = x.Sum(r=>r.examScore)});
var allExc = exercises.GroupBy(x => new { x.subjectId, x.exerciseDate })
.Select(x => new {
Date = x.Key.exerciseDate,
Subj = x.Key.subjectId,
Sum = x.Sum(r=>r.exerciseScore)});
Combining of all results together:
var all = allQuiz.ToList();
all.AddRange(allExam.ToList());
all.AddRange(allExc.ToList());
var result = all.GroupBy(x => new { x.Date, x.Subj })
.Select(x => new { x.Key.Date, x.Key.Subj, Sum = x.Sum(s => s.Sum)});
var list = result.GroupBy(r => r.Date).Select(x => new {
Date = x.Key,
Math = x.SingleOrDefault(t=>t.Subj==1)?.Sum ?? 0,
Science = x.SingleOrDefault(t=>t.Subj==2)?.Sum ?? 0,
History = x.SingleOrDefault(t=>t.Subj==3)?.Sum ?? 0,
Language = x.SingleOrDefault(t=>t.Subj==4)?.Sum ?? 0,
});
Output in LinqPad:
Here is an idea. Instead of keeping the distinction while grouping, you could convert all three to the same structure. For instance:
var allQuizzes = quizzes.GroupBy(qz => qz.quizDate, (q, values) =>
new
{
Date = q,
Results = values.GroupBy(v => v.subjectId, (c, values2) =>
new {
SubjectId = c,
Sum = values2.Sum(v2 => v2.quizScore)
})
});
Notice names "Results" and "Sum" - you can use the same for the other two objects. And now you have three collections, all of the same structure:
{
Date:
Results: [
{SubjectId, Sum}
{SubjectId, Sum}
...
]
}
Since they are all the same now, you can stop treating them differently, use UNION to merge all three, group them by date and within that by subject. Then you could probably iterate through subject list to get necessary info, depends on what you mean by "final table".
This is what i came up with.
It may not be best optimized, but might be enough for you.
I rendered the results into a StringBuilder in my test.
var result =
quizzes.Select(q => new {SubjectId = q.subjectId, Date = q.quizDate, Score = q.quizScore})
.Union(exams.Select(e => new {SubjectId = e.subjectId, Date = e.examDate, Score = e.examScore}))
.Union(exercises.Select(e => new {SubjectId = e.subjectId, Date = e.exerciseDate, Score = e.exerciseScore}))
.GroupBy(arg => arg.Date,
(key, values)=>
new
{
Key = key,
Scores = values.GroupBy(v => v.SubjectId, (s, values2) => new { SubjectId = s, SumScore = values2.Sum(v2 => v2.Score) })
});
StringBuilder sb = new StringBuilder("Date\t\t");
foreach (SubjectModel subject in subjects)
{
sb.Append($"{subject.subjName}\t");
}
sb.AppendLine();
foreach (var record in result)
{
sb.Append($"{record.Key.ToShortDateString()}\t");
foreach (SubjectModel subject in subjects)
{
int sum = record.Scores.Where(s => s.SubjectId == subject.subjId).Select(s => s.SumScore).DefaultIfEmpty(0).Single();
sb.Append($"{sum}\t");
}
sb.AppendLine();
}
string finalTable = sb.ToString();
Instead of using three different anonymous objects to hold the results, make your own class:
public enum TestType
{
Quiz,
Exam,
Exercise,
}
public class TestScore
{
public TestType Type { get; set; }
public DateTime Date { get; set; }
public int Score { get; set; }
public int SubjectId { get; set; }
// Constructors - make a TestScore object
public TestScore(QuizModel q)
{
Type = TestType.Quiz;
Date = q.quizDate;
Score = q.quizScore;
SubjectId = q.SubjectId;
}
public TestScore(ExamModel e)
{
Type = TestType.Exam;
Date = e.examDate;
Score = e.examScore;
SubjectId = e.SubjectId;
}
public TestScore(ExerciseModel e)
{
Type = TestType.Exercise;
Date = e.exerciseDate;
Score = e.exerciseScore;
SubjectId = e.SubjectId;
}
}
Convert to TestScore:
List<TestScore> scores = new List<TestScore>();
scores.AddRange(quizzes.Select(q => new TestScore(q));
scores.AddRange(exams.Select(e => new TestScore(e));
scores.AddRange(exercises.Select(e => new TestScore(e));
Now you have one datasource instead of three, displaying the results becomes easy.