Home > Enterprise >  Sorting list by range using LINQ in C#
Sorting list by range using LINQ in C#

Time:10-03

I have a list like this:

            List<Student> students = new List<Student>
            {
                new Student { Name = "M", Scores = new int[] { 94, 92, 91, 91 } },
                new Student { Name = "I", Scores = new int[] { 66, 87, 65, 93, 86} },
                new Student { Name = "C", Scores = new int[] { 76, 61, 73, 66, 54} },
                new Student { Name = "D", Scores = new int[] { 94, 55, 82, 62, 52} },
                new Student { Name = "P", Scores = new int[] { 91, 79, 58, 63, 55} },
                new Student { Name = "E", Scores = new int[] { 74, 85, 73, 75, 86} },
                new Student { Name = "P", Scores = new int[] { 73, 64, 53, 72, 68} },
            }

Is there any way that we calculate the average score of each student and display it by range. The result would be something like this:

Score > 90 and < 100
 M(student name) 92 (average score)
Score > 80 and < 90
 P 86.8
 I 83.4
 Y 82.4

I also need to calculate how many ranges. For example, with the above result, we have two ranges: (>90 and <100) and (>80 and <90).

I already know how to calculate the average score, however I am stuck at grouping them into range and count the number of ranges just using LINQ.

I would like to learn how to do.

CodePudding user response:

You can use a combination of LINQ's Average, Select and GroupBy, along with a little arithmetic:

var result = string.Join("\r\n",
    students.Select(s =>
        (s.Name, Average: s.Scores.Average(sc => (double)sc)))
    .GroupBy(s => (int)Math.Ceiling(s.Average / 10))
    .OrderByDescending(g => g.Key)
    .Select(g =>
        $"Score >= {g.Key * 10 - 10} and < {g.Key * 10}\r\n"
          string.Join("\r\n", g.Select(s => $" {s.Name} {s.Average:F1}"))
    );

Or slightly differently

var result = string.Join("\r\n",
    students.Select(s =>
        (s.Name, Average: s.Scores.Average(sc => (double)sc)))
    .GroupBy(s => (int)Math.Ceiling(s.Average / 10))
    .OrderByDescending(g => g.Key)
    .SelectMany(g =>
        g.Select(s => $" {s.Name} {s.Average:F1}")
         .Prepend($"Score >= {g.Key * 10 - 10} and < {g.Key * 10}\r\n"))
    );

CodePudding user response:

First, a note, you're going to need a case when an average score is exactly 90. I'm going to assume that will be handled by the higher bucket, though you can change the logic if you need the lower bucket.

It's best to compute and group by the "grade letter" for a score, because it's a letter, and letters can easily be alphabetized.

var studentsByGrade = students
    .Select(x => new {
        x.Name,
        AvgScore = x.Scores.Average()
    })
    .GroupBy(x => GetGradeLetter(x.AvgScore));

This is going to use a helper method.

private static string GetGradeLetter(double score)
{
    if (score is >= 90)
        return "A";
    
    if (score is >= 80)
        return "B";

    // add more as you'd like
    
    return "ZZZ";
}

It's worth noting that you don't need to display the letter here - it's just used because it's convenient to do ordering, and most likely that's what you'll end up using anyways. Typically you'll mark anything lower than "60" as one group, because (at least in the US school system) that means "F".

To display the results, use two foreaches.

foreach (var grade in studentsByGrade.OrderBy(x => x.Key))
{
    foreach (var student in grade.OrderByDescending(x => x.AvgScore))
    {
        Console.WriteLine($"{student.Name} {student.AvgScore}");
    }
    Console.WriteLine();
}
  • Related