Join three list using multiple columns c# linq lambda - c#

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.

Related

GroupBy bool and Select values for new list

I am trying to get into more complex Linq queries and right away catch a point I am feeling stuck. I have a following list in DB:
ID ELAPSED TIME APPISRUNNING
1 12 TRUE
2 54 TRUE
3 32 FALSE
Where ELAPSED TIME is TimeSpan and APPISRUNNING is a bool.
I would like to build a chart based on these values (https://github.com/beto-rodriguez/LiveCharts2). Chart build fine with this:
Title = "Analytics";
this.ActivityChartSeries = new ISeries[]
{
new PieSeries<double> { Values = new double[] { 2 }},
new PieSeries<double> { Values = new double[] { 2 }},
new PieSeries<double> { Values = new double[] { 2 }},
new PieSeries<double> { Values = new double[] { 2 }},
new PieSeries<double> { Values = new double[] { 2 }},
};
Now I somehow need to first GroupBy bool and then select a new List? I have tried following:
IEnumerable<DataRecord> dataRecords = await this.DataStore.GetItemsAsync();
this.ActivityChartSeries = dataRecords
.GroupBy(g => g.AppIsRunning)
.Select(m => new
{ // BELOW IS TOTALLY UNCLEAR FOR ME
Values = m.Select(r => r.EngineElapsed.Ticks),
Name = m.Select(r => r.Name),
})
.Select(x =>
new PieSeries<double>
{
Values = new List<double> { x.Values.FirstOrDefault() },
Name = x.Name.FirstOrDefault(),
});
Type of assigned variable:
public IEnumerable<ISeries> ActivityChartSeries
This part is totally unclear for me:
Values = m.Select(r => r.EngineElapsed.Ticks),
Name = m.Select(r => r.Name),
How after GroupBy I can create two types of data? Basically I need
"Application Running" and "Values"
"Application is not Running" and "Values"
EDIT:
Code provided by Somar Zein compiles fine:
var results = activityChartSeries
.GroupBy(a=> a.AppIsRunning)
.Select(item=> new PieSeries<double>{
Name = item.Key ? "Application is Running" : "Application is not Running",
Values = item.Select(x=> Convert.ToDouble(x.ElapsedTime.Ticks)).ToList()
});
However as a result I am getting something like this, why it is reloading in a loop?
Here is result:
enter image description here
EDIT2:
So I have created an example for testing purposes:
Class:
public class DataModel
{
public int Id { get; set; }
public TimeSpan ElapsedTime { get; set; }
public bool AppIsRunning { get; set; }
}
Code:
List<DataModel> records = new List<DataModel>();
records.Add(new DataModel { Id = 1, ElapsedTime = new TimeSpan(1, 20, 30), AppIsRunning = true });
records.Add(new DataModel { Id = 2, ElapsedTime = new TimeSpan(1, 20, 30), AppIsRunning = true });
records.Add(new DataModel { Id = 3, ElapsedTime = new TimeSpan(1, 20, 30), AppIsRunning = true });
records.Add(new DataModel { Id = 4, ElapsedTime = new TimeSpan(1, 20, 30), AppIsRunning = true });
records.Add(new DataModel { Id = 5, ElapsedTime = new TimeSpan(1, 20, 30), AppIsRunning = true });
this.ActivityChartSeries = records
.GroupBy(g => g.AppIsRunning)
.Select(item => new PieSeries<double>
{
Name = item.Key ? "Running" : "Not Running",
Values = new double[] { 2, 4 },
});
I get the same reloading effect, even thou originally provided Example from LiveCharts work fine.
you could try doing something like following:
var results = activityChartSeries
.GroupBy(a=> a.AppIsRunning)
.Select(item=> new PieSeries<double>{
Name = item.Key ? "Application is Running" : "Application is not Running",
Values = item.Select(x=> Convert.ToDouble(x.ElapsedTime.Ticks)).ToList()
});
hope that could be helpful!

C# select duplicates from sub-group

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

Combine duplicates in a list

In my current test project I'm looking to combine all objects in a list where one of their values is the same as in another object, I would then like to check the other values under these objects and combine them together, here's and example:
Object1
{
id = 111,
price1 = 10,
price 2 = 20
}
Object2
{
id = 222,
price1 = 10,
price 2 = 20
}
Object3
{
id = 111,
price1 = 30,
price 2 = 70
}
Object4
{
id = 444,
price1 = 15,
price 2 = 25
}
From the above Object1 and and Object3 would be combined based on their related 'id' value, their prices would then be combined and would result in the following object replacing Object1 and Object3 in a list:
NewObject
{
id = 111,
price1 = 40,
price 2 = 90
}
The end list would then look like this:
NewObject
{
id = 111,
price1 = 40,
price 2 = 90
}
Object2
{
id = 222,
price1 = 10,
price 2 = 20
}
Object4
{
id = 444,
price1 = 15,
price 2 = 25
}
So far I would go about obtaining the value using linq as follows:
Select all with the same id add thier values
Create new object with combined values for all obtained in step 1 and add to new list
Continue over list and if the 'id 'already exists in new list then ignore it as it's already been combined into the new list
Is there maybe a quicker easier way with a single LINQ statement?
var result = source
.GroupBy(x => x.id,
(key, values) => new {
id = key,
price1 = values.Sum(x => x.price1),
price2 = values.Sum(x => x.price2)
});
try group by
var combined = list.GroupBy(x => x.id, x => x).Select(x => new ListObj()
{
id = x.Key,
price1 = x.Sum(s => s.price1),
price2 = x.Sum(s => s.price2),
});
whole console app:
class Program
{
static void Main(string[] args)
{
var list = new List<ListObj>()
{
new ListObj()
{
id = 111,
price1 = 10,
price2 = 20
},
new ListObj()
{
id = 222,
price1 = 10,
price2 = 20
},
new ListObj()
{
id = 111,
price1 = 30,
price2 = 70
},
new ListObj()
{
id = 444,
price1 = 15,
price2 = 25
},
};
var combined = list
.GroupBy(x => x.id, x => x)
.Select(x => new ListObj()
{
id = x.Key,
price1 = x.Sum(s => s.price1),
price2 = x.Sum(s => s.price2),
});
Console.ReadKey();
}
}
public class ListObj
{
public int id { get; set; }
public int price1 { get; set; }
public int price2 { get; set; }
}

Linq query to do group by and min

I have a datastructure as shown below. For each (Person, Game) pairs, I need to find the lastest score in the past 24 hours. Is it possible to do that in LINQ? Something like (Person, Game, LatestScore)
+----------------+-----------------+---------------+-+------------+
| Person | Game | Score |EventTime |
+-----------------------------------------------------------------+
| | | | |
| | | | |
+----------------+-----------------+---------------+--------------+
Any hints would be very helpful.
Assuming you have a class like this:
class GameInfo
{
public DateTime EventTime { get; set; }
public String Game { get; set; }
public String Person { get; set; }
public double Score { get; set; }
}
You could do:
List<GameInfo> data = new List<GameInfo> {
new GameInfo { Game = "G1", Person = "A", Score = 10, EventTime = new DateTime(2014, 10, 10, 10, 10, 10) },
new GameInfo { Game = "G1", Person = "A", Score = 10, EventTime = new DateTime(2014, 10, 10, 10, 10, 10) },
new GameInfo { Game = "G2", Person = "B", Score = 11, EventTime = new DateTime(2014, 10, 10, 20, 10, 10) },
new GameInfo { Game = "G2", Person = "B", Score = 11, EventTime = new DateTime(2014, 10, 10, 20, 10, 10) }
};
var q = from game in data
group game by new { G = game.Game, P = game.Person } into g
select new {
Person = g.Key.P,
Game = g.Key.G,
Score = g.Aggregate((curmin, x) => (curmin == null || (x.EventTime) < curmin.EventTime ? x : curmin)).Score
};
foreach (var item in q)
{
Console.WriteLine("{0}, {1}, {2}", item.Game, item.Person, item.Score);
}
As #Rawling pointed out, getting the scoce can be quite costly if you have lage data sets. So doing that efficiently might save alot of time in getting the desired output.
var dt = DateTime.Now.AddDays(-1);
var results = context.Where(x=>x.EventTime >= dt)
.GroupBy(x=>new {x.Person,x.Game})
.Select(x=>new
{
x.Key.Person,
x.Key.Game,
LatestScore = x.Where(d=>d.EventTime == x.Max(l=>l.EventTime))
.Select(d=>d.Score)
.FirstOrDefault()
});

Group by using linq (range + count)

var data = new[] {
new { Id = 0, Cat = 1, Price = 2 },
new { Id = 1, Cat = 1, Price = 10 },
new { Id = 2, Cat = 1, Price = 30 },
new { Id = 3, Cat = 2, Price = 50 },
new { Id = 4, Cat = 2, Price = 120 },
new { Id = 5, Cat = 2, Price = 200 },
new { Id = 6, Cat = 2, Price = 1024 },
};
var ranges = new[] { 10, 50, 100, 500 };
Needed output is grouped price count by equal or greater than the range used according categories.
(in one linq statement)
cat range count
-------------------------------------
1 10 2 (In 1. categories there is 2 item that price >= 10(range) [10;30])
2 10 4 (In 2. categories there is 4 item that price >= 10(range) [50;120;200;1024])
2 50 4 ....
2 100 3 ....
2 500 1 (In 2. categories there is 1 item that price >= 500(range) [1024])
Try this:
var data = new[] {
new { Id = 0, Cat = 1, Price = 2 },
new { Id = 1, Cat = 1, Price = 10 },
new { Id = 2, Cat = 1, Price = 30 },
new { Id = 3, Cat = 2, Price = 50 },
new { Id = 4, Cat = 2, Price = 120 },
new { Id = 5, Cat = 2, Price = 200 },
new { Id = 6, Cat = 2, Price = 1024 },
};
var ranges = new[] { 10, 50, 100, 500 };
var result = from r in ranges
from g in data
where g.Price >= r
select new {g.Cat, Price=r};
var groupedData =
from d in result
group d by new{d.Cat, d.Price} into g
select new{Cat=g.Key.Cat, Price=g.Key.Price, TotalCount=g.Count()};
This should work:
var values =
data.SelectMany(x => ranges.Where(y => x.Price >= y)
.Select(y => new { Record = x, Range = y }))
.GroupBy(x => new { Cat = x.Record.Cat, Range = x.Range })
.Select(x => new { Cat = x.Key.Cat, Range = x.Key.Range, Count = x.Count()});
Results:
{ Cat = 1, Range = 10, Count = 2 }
{ Cat = 2, Range = 10, Count = 4 }
{ Cat = 2, Range = 50, Count = 4 }
{ Cat = 2, Range = 100, Count = 3 }
{ Cat = 2, Range = 500, Count = 1 }

Categories

Resources