groupby in dictionary in C# - c#

var dict = new Dictionary<ClassA,int>();
I should add the Name and Seat number into the dict but I have to take it from separate classes,
eg
dict.Add(student.name)
dict.Add(class.studentlist.score)
I want it to get an output of
Student Score
Amy 78
Amy 89
Amy 45
Ben 34
.
.
.
as well as one that shows the total score if the student's name repeats more than once
Student Score
Amy (total score)
Ben (total score)
I'm not sure how to go about doing this, or if it is even possible? The name and score comes from different classes so I'm a bit confused.

You cannot have a dictionary with multiple Name keys, name should be unigue. So try to create a list
var students = new List<Student>();
students.Add(new Student { Name = "Amy", Score = 78 });
students.Add(new Student { Name = "Ben", Score = 34 });
students.Add(new Student { Name = "Amy", Score = 89 });
students.Add(new Student { Name = "Amy", Score = 45 });
List<string,int> scores = students.GroupBy(s => s.Name)
.Select(i => new Student { Name = i.Key, Score = i.Sum(x => x.Score) }).ToList();
public class Student
{
public string Name { get; set; }
public int Score { get; set; }
}
UPDATE
#Cleptus suggested to use a Dictionary<string, List> where string is a name, and list is to keep the score. It is a very interesting idea, but I like more a list, since it is more like an relational db and linq is ideal for list collections. I feel Dictionaries as too hierarchical , and the always need an extra step or code to get some information.
but dictionary could be used to keep the result information
Dictionary<string, int> result = students.GroupBy(s => s.Name)
.ToDictionary( i => i.Key, i=> i.Sum(x => x.Score));
var amyResult=result["Amy"];

Assuming your class student is:
public class Student
{
public string Name { get; set; }
}
You could use a Dictionary whose key would be a Student and whose content would be a list/array of scores.
List<Student, List<int>> results = new List<Student, List<int>>();
results.Add(new Student() { Name = "Amy"}, new List<int>() { 78, 89, 45 });
results.Add(new Student() { Name = "Ben"}, new List<int>() { 61 });
And to show the data, you just need to iterate the keys and show the data however you need (either aggregated or individually).
using System.Linq;
....
foreach (Student currentStudent in results.Keys) {
List<int> studentResults = results[currentStudent];
// This would show your first needed output (individual scores)
foreach(int result in studentResults) Console.WriteLine(currentStudent.Name + ": " + result.ToString());
// This would show your second needed output (total scores)
Console.WriteLine(currentStudent.Name + ": " + studentResults.Sum().ToString());
}
The second one takes advantage of IEnumerable.Sum()

Related

Sum of marks based on ID of a generic in List in C#

I am trying to do aggregate functions in a list of generics in C# based on an id
class Student{
public int id { get; set; }
public int marks { get; set; }
}
class Main {
List<Student> StudentList = new List<Student>();
List.Add(new Student(1,55);
List.Add(new student(2,65);
List.Add(new student(4,75);
List.Add(new student(1,65);
List.Add(new student(2,45);
foreach(var st in StudentList) {
Console.WriteLine(st.id + " " + st.marks);
}
}
But this returns
1 55
2 65
4 75
1 65
2 45
I want to return the following result:
1 120
2 110
4 75
This can be done with the Linq GroupBy function:
var totals = StudentList
.GroupBy(sl => sl.id) //Group by ID
.Select(g => new Student(g.Key, g.Sum(s => s.marks)));
foreach (var st in totals)
{
Console.WriteLine(st.id + " " + st.marks);
}
You can also group with a Dictionary<int, int>.
With LINQ:
var Totals = StudentList
.GroupBy(x => x.Id)
.ToDictionary(x => x.Key, x => x.Select(i => i.Mark).Sum());
Without LINQ:
var Totals = new Dictionary<int, int>();
foreach (var student in StudentList)
{
var id = student.Id;
if (!Totals.ContainsKey(id))
{
Totals[id] = 0;
}
Totals[id] += student.Mark;
}
Then you can simply print out the results like this:
foreach (var result in Totals)
{
Console.WriteLine($"{result.Key} {result.Value}");
}
Which Outputs:
1 120
2 110
4 75

Group By Raw Data from DB

This is my raw data coming from DB:
PrimaryColumn StudentId StudentName CourseName CourseId CourseDuration
1 1 X Cse1 C1 2
2 1 X Cse2 C2 1
3 1 X Cse3 C3 3
4 2 Y Cse1 C1 2
5 2 Y Cse4 C4 5
Classes from C# end:
public class Student
{
public int StudentId {get; set;}
public string StudentName {get; set}
public List<Course> Courses {get; set;}
}
public class Course
{
public int CourseId {get; set;}
public string CourseName {get; set;}
public int CourseDuration {get; set; }
}
My goal is to fetch the data grouped by Students and the courses they would be taking which is done using List<Course> as a property of Student Class.
So, I thought the goal is pretty forward. So, I went ahead and used GroupBy on the raw data coming from DB to C# hoping to get the result but to no avail.
This is the best I've gotten, so far.
masterData.GroupBy(x => new { x.StudentId, x.StudentName }, (key, group) => new { StudentId = key.StudentId, StudentName = key.StudentName, Courses=group.ToList() }).ToList();
Although this doesn't get me what I hope to fetch. With some minor code workaround post this call, I'm able to achieve the what I need. But, it's irking me everytime that I'm unable to group the raw data properly.
It would be really helpful if anyone could show me the right way to further optimize the above code or do it different way altogether.
This is the way I need the result to be in a List<Student>:
Student:
StudentId: 1
StudentName: X
List<Course> Courses: [0] - { CourseId: C1, CourseName = Cse1, CourseDuration = 2}
[1] - { CourseId: C2, CourseName = Cse2, CourseDuration = 1}
[2] - { CourseId: C3, CourseName = Cse3, CourseDuration = 3}
Student:
StudentId: 2
StudentName: Y
List<Course> Courses: [0] - { CourseId: C1, CourseName = Cse1, CourseDuration = 2}
[1] - { CourseId: C4, CourseName = Cse4, CourseDuration = 5}
Cheers
You can do it like this:
Here full example: dotNetFiddle
List<Student> result = data.GroupBy(x => new { x.StudentID, x.StrudentName },
(key, group) => new Student{
StudentId = key.StudentID,
StudentName = key.StrudentName,
Courses = GetCourses(group)}).ToList();
//You can do this operation with Reflection if you want. If you don't want to write manually the property names.
public static List<Course> GetCourses(IEnumerable<RawData> data)
{
List<Course> course = new List<Course>();
foreach(var item in data)
{
Course c = new Course();
c.CourseDuration = item.CourseDuration;
c.CourseId = item.CourseID;
c.CourseName = item.CourseName;
course.Add(c);
}
return course;
}
What I'd do in your case (but I'm not sure you'll see it as 'properly grouping') is something along the lines of
var groupedStudents = masterData.GroupBy(x => x.StudentId);
foreach (var studentGroup in groupedStudents)
{
// new studentclass
var student = new Student(studentGroup.First().StudentId, studentGroup.First().StudentName);
foreach (var studentRecord in studentGroup)
{
student.Courses.Add(new course(studentRecord.CourseId, studentRecord.CourseName, studentRecord.CourseDuration);
}
// add the student-object to where you want to save them i.e.
this.MyStudentList.Add(student);
}
The problem here is that DB is poorly organized and stays far from normalized state. But you could deal with as if it were properly separated to different tables. This way you extracted Courses, Students and then aggregated them - the code below should give a clue
// get all courses
var courses = masterData.GroupBy(x => x.CourseId).Select(group => group.First())
.Select(x => new Course {CourseId = x.CourseId, CourseName = x.CourseName, ...})
.ToDictionary(x => x.CourseId);
// get all students (with empty courses for now)
var students = masterData.GroupBy(x => x.StudentId).Select(group => group.First())
.Select(x => new Student {StudentId = x.StudentId, ...})
.ToDictionary(x => x.StudentId);
// fill students with courses
foreach(var data in masterData)
{
student[data.StudentId].Courses.Add(courses[data.CourseId])
}
I suppose it is a clear way which could be reused after tables normalization. Alternatively you could try to write a complicated LINQ doing all this staff by a single query

How to get the very next element from the list after finding the key element

Scenario: I have a student list which comprise of name(distinct) and mark. Once user enters the name, I have to display the appropriate mark in console application. How to get the next element after fine the name.
Example:
// User enters like follows:
rice
50
john
60
pat
70
// If user enter "john" then--
60 // should display
Using LINQ I have found the key element in the list
var item = students.Find(x => x.Name == keyname);
Assuming that you have a class:
public class Student
{
public string Name {get;set;}
public int Mark {get;set;}
}
And you have a list that looks like:
List<Student> students = new List<Student>
{
new Student{Name = "Markus", Mark = 2},
new Student{Name = "Stephen", Mark = 1}
};
And you want to get the mark of Stephen its:
Student stephen = students.FirstOrDefault(s=> s.Name == "Stephen"); //if you have an unique property i you can use .SingleOrDefault in the same manner
if(stephen != null) //could be that the student is not even in the list
{
int markofStephen = stephen.Mark;
}
now to enter in this new DataType you can do:
List<Student> students = new List<Student>()
do
{
Console.Write("Please enter the Name of the Student(enter \"finish\" to exit):");
string name = Console.ReadLine();
if(name.ToLower() == "finish" )
break;
bool validMark = false;
do
{
Console.Write("Please enter the Mark of {0}:", name);
string markString = Console.ReadLine();
int mark;
validMark = int.TryParse(markString,out mark);
if(!validMark)
Console.WriteLine("Invalid Number Entered");
else
students.Add(new Student{Name=name, Mark = mark});
} while (!validMark)
} while (true)
Take a look at OOP
http://www.codeproject.com/Articles/22769/Introduction-to-Object-Oriented-Programming-Concep
Then you can create a Class or Struct that would define rice and 50.
i.e
public class Student
{
public string Name {get;set;}
public double Mark {get;set;}
}
Then via Console you can construct a new Student and then store it in a list
var item = students.Where(x => x.Name == keyname).Select(x=>x.Mark).FirstOrDefault();
Console.WriteLine(item);
//or
var item = students.Where(x => x.Name == keyname).FirstOrDefault();
if (item != null)
Console.WriteLine(item.Mark);
//or
var item = students.FirstOrDefault(s=> s.Name == keyname);
if (item != null)
Console.WriteLine(item.Mark);
Using classes or structures you can achieve better control that storing all values in a list or array
Do the below code:
var itemIndex = students.IndexOf(keyname);
And then the index of the value will be var value = students.ElementAt(itemIndex+1);.
You keep scores and names in the same list so I assume both are strings. In my implementation I take the list of names and scores, take each 2 items, create a tuple, and on a list of tuples I search for one where first item matches your key and return the second item (the score):
List<string> testList = new List<string> { "rice", "50", "john", "60", "pat", "70" };
string myKey = "rice";
var result =
testList.Zip(testList.Skip(1), (a, b) => Tuple.Create(a, b))
.Where(t => t.Item1 == myKey)
.First()
.Item2;
Or you can use anonymous types instead of a tuple:
var result =
testList.Zip(testList.Skip(1), (a, b) => new {Name = a, Score = b})
.Where(t => t.Name == myKey)
.First()
.Score;
Of course, the assumption is that each name always has a matching score next to it (as that was the condition described in your question).
This should work
var item = students.SkipWhile(x => x.Name != keyname).Skip(1).FirstOrDefault();

Where does "i" get its value in this LINQ statement?

I'm little perplexed by the behavior of this select LINQ statement. Just below the LOOK HERE comments you can see a select LINQ statement. That select statement is on the employees collection. So, it should accept only x as the input param. Out of curiosity I passed i to the delegate and it works. When it iterates through the select, it assigns 0 first and then it increments by 1. The result can be seen at the end of this post.
Where does the variable i get its value from? First of all, why does it allow me to use a variable i which is nowhere in the scope. It is not in the global scope neither in the local Main method. Any help is appreciated to understand this mystery.
namespace ConsoleApplication
{
using System;
using System.Collections.Generic;
using System.Linq;
public class Employee
{
public int EmployeedId { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
}
class Program
{
static void Main(string[] args)
{
var employees = new List<Employee>()
{
new Employee() { FirstName = "John", LastName = "Doe" },
new Employee() { FirstName = "Jacob", LastName = "Doe" }
};
// LOOK HERE...
var newEmployees = employees.Select((x, i) => new { id = i, name = x.FirstName + " " + x.LastName });
newEmployees.ToList().ForEach(x => { Console.Write(x.id); Console.Write(" "); Console.WriteLine(x.name); });
Console.ReadKey();
}
}
}
The result is
0 John Doe
1 Jacob Doe
Enumerable.Select has an overload that projects the current index of the element in the sequence. Also Enumerable.Where and Enumerable.SkipWhile/TakeWhile have it. You can use it like a loop variable in a for-loop which is sometimes handy.
One example which uses the index to create an anonymous type to group a long list into groups of 4:
var list = Enumerable.Range(1, 1000).ToList();
List<List<int>> groupsOf4 = list
.Select((num, index) => new { num, index })
.GroupBy(x => x.index / 4).Select(g => g.Select(x => x.num).ToList())
.ToList(); // 250 groups of 4
or one with Where which only selects even indices:
var evenIndices = list.Where((num, index) => index % 2 == 0);
It might also be important to mention that you can use these overloads that project the index only in method-syntax. LINQ query-syntax does not support it.

group items by range of values using linq (IGrouping)

Say I have a list of Person class
public class Person
{
public int Age { get; set; }
public string Name { get; set; }
}
How can I group by dynamic ranges? (For example starting from the youngest person I would like to group by ranges of 5 so if the youngest person is 12 the groups would be 12-17, 18-23 ....)
How can I determine the Key of IGrouping interface? (Set the Key of each group to be the ages average in that group for example)
To get the key to group by you can create a function:
String GetAgeInterval(Int32 age, Int32 minimumAge, Int32 intervalSize) {
var group = (age - minimumAge)/intervalSize;
var startAge = group*intervalSize + minimumAge;
var endAge = startAge + intervalSize - 1;
return String.Format("{0}-{1}", startAge, endAge);
}
Assuming that the minimum age is 12 and the interval size is 5 then for ages between 12 and 16 (inclusive) the function will return the string 12-16, for ages between 17 and 21 (inclusive) the function will return the string 17-21 etc. Or you can use an interval size of 6 to get the intervals 12-17, 18-23 etc.
You can then create the groups:
var minimumAge = persons.Min(person => person.Age);
var personsByAgeIntervals = persons
.GroupBy(person => GetAgeInterval(person.Age, minimumAge, 5));
To get the average age in each group you can do something like this:
var groups = personsByAgeIntervals.Select(
grouping => new {
AgeInterval = grouping.Key,
AverageAge = grouping.Average(person => person.Age),
Persons = grouping.ToList()
}
);
This will create a sequence of groups represented by an anonymous type with properties AgeInterval, AverageAge and Persons.
Using Linq but not IGrouping (I've never used this interface, so I didn't think helping you would be the best time to start). I added a configuration class to set the min/max age as well as a basic descriptor.
public class GroupConfiguration {
public int MinimumAge { get; set; }
public int MaximumAge { get; set; }
public string Description { get; set; }
}
I created a list of Person (people) and populated it with a few sample records.
List<Person> people = new List<Person>() {
new Person(12, "Joe"),
new Person(17, "Bob"),
new Person(21, "Sally"),
new Person(15, "Jim")
};
Then I created a list of GroupConfiguration (configurations) and populated it with 3 logical-for-me records.
List<GroupConfiguration> configurations = new List<GroupConfiguration>() {
new GroupConfiguration() {MinimumAge = 0, MaximumAge=17, Description="Minors"},
new GroupConfiguration() {MinimumAge = 18, MaximumAge=20, Description="Adult-No Alcohol"},
new GroupConfiguration() {MinimumAge = 21, MaximumAge=999, Description="Adult-Alcohol"},
};
I then load them to a dictionary, to maintain the relationship between the configuration and the results that match that configration. This uses Linq to find the records from people that match MinimumAge <= age <= MaximumAge. This would allow someone to be placed in multiple results, if there were MinimumAge and Maximum age overlaps.
Dictionary<GroupConfiguration, IEnumerable<Person>> groupingDictionary = configurations.ToDictionary(groupConfiguration => groupConfiguration, groupConfiguration
=> people.Where(x => x.Age >= groupConfiguration.MinimumAge && x.Age <= groupConfiguration.MaximumAge));
Throwing this in a console program, I validated that 3 people exist in the Minors group, 0 in the Adult-No Alcohol group, and 1 in the Adult-Alcohol group.
foreach (var kvp in groupingDictionary) {
Console.WriteLine(kvp.Key.Description + " " + kvp.Value.Count());
}
Console.ReadLine();

Categories