Linq List Ranking in C# - c#

I have the below list which consist of a list of names and scores. The scores I got from using linq to get the sum of points for the list of peope:
Name Score
Ed 5
John 6
Sara 7
Dean 3
Nora 4
So i was able to group the list and make the sums, but now I am trying to rank these based on the score. So I am trying to get the below list:
Rank Name Score
1 Sara 7
2 John 6
3 Ed 5
4 Nora 4
5 Dean 3
But the linq code I am using doesn't seem to be getting the right ranks for me.
Here's my code for making the list and making the sums and ranking:
public class Person
{
public int Rank { get; set; }
public string Name { get; private set; }
public int Score { get; set; }
public Person(int rank, string name, int score)
{
Rank = rank;
Name = name;
Score = score;
}
}
public ObservableCollection<Person> Persons { get; set; }
public MainWindow()
{
Persons = new ObservableCollection<Person>();
var data = lines.Select(line => {
var column = line.Split(',');
var name = column[0];
int score = int.Parse(column[1]);
int rank =0;
return new { name, score, rank };
});
var groupedData = data.OrderByDescending(x => x.score).GroupBy(p => p.name).Select((g, i) => new { name = g.Key, rank=i+1, score = g.Sum(p => p.score)});
var persons = groupedData.Select(p => new Person(p.rank, p.name, p.score));
foreach (var person in persons) {
Persons.Add(person);
}
datagrid.ItemsSource = Persons;
}
I just would like to know how to include correct ranks in linq.

You have to OrderByDescending the groupedData (which has a total score) rather than the raw data containing non-summed, individual scores.
var persons = groupedData.Select(p => new Person(p.rank, p.name, p.score)).OrderByDescending(x => x.Score);
Edit: (putting the correct rank into Person.Rank)
var groupedData = data.GroupBy(p => p.name).Select(g => new { name = g.Key, score = g.Sum(p => p.score)}).OrderByDescending(x => x.score);
var persons = groupedData.Select((p,i) => new Person(i+1, p.name, p.score));

I asked a similar question to this a while back. My problem was that if people have the same score it will assign the wrong rank to the person. With your current code if John also had a score of 7 he would be ranked #2 as you are ordering by score and just using its position in the list when hes really tied for #1.
Here was my question and solution: Get the index of item in list based on value

How about an extension method doing ranking:
public static IEnumerable<TResult> SelectRank<TSource, TValue, TResult>(this IEnumerable<TSource> source,
Func<TSource, TValue> valueSelector,
Func<TSource, int, TResult> selector)
where TValue : struct
{
TValue? previousValue = default(TValue?);
var count = 0;
var rank = 0;
foreach (var item in source)
{
count++;
var value = valueSelector(item);
if (!value.Equals(previousValue))
{
rank = count;
previousValue = value;
}
yield return selector(item, rank);
}
}
Usage profiles.SelectRank(x => x.Score, (x, r) => new { x.Name, Rank = r }). This method do only 1 iteration of the source. I do recommend to use a sorted source parameter.

Adding code here for completion as tournament example:
var entriesByPoints = tournament.Entries.GroupBy(g=>g.Picks.Sum(s=>s.TournamentContestant.TotalScoreTargetAllocatedPoints)).Select((group)=> new {
Points = group.Key,
Entries = group.ToList()
});
var entriesByRankWithPoints = entriesByPoints.OrderByDescending(ob=>ob.Points).Select((p,i) => new {
Rank = 1 + (entriesByPoints.Where(w=>w.Points > p.Points).Sum(s=>s.Entries.Count())),
Points = p.Points,
Entries = p.Entries
});
Results:

Related

How to select students with repeated low scores?

Scores are considered low if they are less than or equal to 5. I want to select students with repeated low scores.
The expected result is:
Andy
Bobby
Cindy
As each of them has repeated low scores.
Question
I got stuck in completing the last expression GroupBy in the Where clause.
Could you make it done?
class Student
{
public int Id { get; set; }
public string Name { get; set; } = null!;
public List<int> Scores { get; set; } = new List<int>();
public static List<Student> GetStudents()
{
return new List<Student>()
{
new Student
{
Id = 1,
Name="Andy",
Scores={1,1,2,2,3,4,5,6,7,8}
},
new Student
{
Id = 2,
Name="Bobby",
Scores={3,3,3,3,4,5}
},
new Student
{
Id = 3,
Name="Cindy",
Scores={1,1,2,2,3,4,5}
},
new Student
{
Id = 4,
Name="Dave",
Scores={1,2,3,4,5,6,7,8,9,10}
}
};
}
}
class Program
{
static void Main()
{
var query = Student.GetStudents()
.Where(s => s.Scores.GroupBy(i => i).????);
foreach (var x in query)
Console.WriteLine(x.Name);
Console.ReadLine();
}
}
I'd do something like this:
var query = Student.GetStudents()
.Where(s => s.Scores
.Where(x => x <= 5)
.GroupBy(i => i)
.Any(x => x.Count() > 1));
Try following :
var query = Student.GetStudents()
.Select(x => new { student = x.Name, scores = x.Scores.GroupBy(y => y).Select(y => new { score = y.Key, count = y.Count() }).ToList() }).ToList();
var lowScore = query.Where(x => x.scores.Any(y => (y.count > 1) && (y.score <= 5))).ToList();

Calculations on grouped rows using lambda functions in c#

I am trying to calculate Best and Worst body measurement changes for people who go on a fitness trip.
I have a database full of before and after body composition measurements for several people who go on various trips. Every participant and every trip has an Id. There are 3 types of readings, B(efore), M(iddle) and A(fter). Here is an example of the data:
ParticipantId TripId Type Weight BodyFatPct
1 2 B 195 22.8
1 2 B 189.6 24.1
1 2 A 186.6 21.2
1 2 A 187.6 23.8
2 3 B 199.2 23.7
2 3 B 198.4 25.1
2 3 A 193 22.4
Here is the class I'm using to represent the data:
public partial class Detail
{
public int ParticipantId { get; set; }
public int TripId { get; set; }
public string Type { get; set; }
public double? Weight { get; set; }
public double? BodyFatPct { get; set; }
}
Here is my highly inefficient C# code to calculate best and worst for weight and body fat.
List<Detail> result = new List<Detail>();
var _result = result.GroupBy(x => new { x.ParticipantId, x.TripId });
foreach(var res in _result)
{
var beforeHighWeight = res.Where(x => x.Type == "B").Max(x => x.Weight);
var beforeLowWeight = res.Where(x => x.Type == "B").Min(x => x.Weight);
var afterWeight = res.Where(x => x.Type == "A").Min(x => x.Weight);
var beforeHighFat = res.Where(x => x.Type == "B").Max(x => x.BodyFatPct);
var beforeLowFat = res.Where(x => x.Type == "B").Min(x => x.BodyFatPct);
var afterFat = res.Where(x => x.Type == "A").Min(x => x.BodyFatPct);
var BestWeightDiff = BeforeHighWeight - afterWeight;
var WorstWeightDiff = BeforeLowWeight - afterWeight;
var BestFatDiff = BeforeHighFat - afterFat;
var WorstFatDiff = BeforeLowFat - afterFat;
}
In actuality, I have about 15 fields to calculate, not just two. Is there a lambda function that does row-wise calculations on grouped data? Any help appreciated.
Performance optimizations normally come with the cost of less maintainable code. So if performance is almost sufficient you should probably follow the hint as commented by Prasad Telkikar and avoid filtering res more than once with the same predicate, i.e. you should assign the filtered lists and work with them:
var resA = res.Where(x => x.Type == "A")
var resB = res.Where(x => x.Type == "B")
If performance is an issue and affords work, you can enumerate the list once and maybe stream the values from the database by using Aggregate. You could e.g. create a class that holds all the values you want to calculate and has a method to update the values given a new entry. You could e.g. create the classes
public class Statistic
{
public int BeforeHighWeight { get; private set; }
public int BeforeLowWeight { get; private set; }
// Add the dimensions you are interested in
public Statistic(Detail detail)
{
// initialize with given Detail
}
public Statistic AddDetail(Detail detail)
{
// update Statistic with given Detail
return this;
}
}
public class Statistics
{
private readonly ConcurrentDictionary<(int, int), Statistic> _statistics = new ConcurrentDictionary<(int, int), Statistic>();
public Statistics AddDetail(Detail detail)
{
_statistics.AddOrUpdate(
(detail.ParticipantId, detail.TripId),
key => new Statistic(detail),
(key, statistic) => statistic.AddDetail(detail)
);
return this;
}
}
Then you could aggregate your values like so:
var rand = new Random();
var result = Enumerable.Range(1, 1000000)
.Select(i => new Detail {ParticipantId = i % 10000, TripId = rand.Next(100000) /*, ...*/})
.Aggregate(
new Statistics(),
(statistics, detail) => statistics.AddDetail(detail)
);

Check the sum of a property of list items against another property

I have a list of items on an order, along with the order totals. I'm trying to find a way to add up all of the quantities that have shipped and compare that against the total order quantity, to see if there are any "backordered".
I get a list of PartInfo back, which includes all of the shipments of this product for an order.
public class PartInfo
{
public int OrderId { get; set; }
public string Name { get; set; }
public string TrackingId { get; set; }
public int ShippedQty { get; set; }
public int OrderTotal { get; set; }
}
If I use the following data:
List<PartInfo> PartList = new List<PartInfo>();
PartList.Add(new PartInfo() { OrderId = "1031",
Name = "Watch",
TrackingId = "1Z204E380338943508",
ShippedQty = 1,
OrderTotal = 4});
PartList.Add(new PartInfo() { OrderId = "1031",
Name = "Watch",
TrackingId = "1Z51062E6893884735",
ShippedQty = 2,
OrderTotal = 4});
How can I use LINQ to compare the total ShippedQty to the OrderTotal?
A direct answer could be something like this:
var backOrdered = partList.GroupBy(p => new { p.OrderId, p.OrderTotal })
.Select(g => new
{
g.Key.OrderId,
g.Key.OrderTotal,
TotalShipped = g.Sum(pi => pi.ShippedQty)
})
.Where(x => x.TotalShipped < x.OrderTotal);
Assuming that OrderId and OrderTotal are always linked, so you can group by them and always have one group per OrderId.
But as I said in a comment, if the data comes from a database there may be better ways to get the data, esp. when there is an Order with a collection navigation property containing PartInfos.
My reading is that the ShippedQty is for the single item (Name) for the order, so you want to group the items by the OrderId and count the quantity shipped. In that case, you can use group by LINQ:
var groupResults = PartList.GroupBy(
// Group by OrderId
x => x.OrderId,
// Select collection of PartInfo based on the OrderId
x => x,
// For each group based on the OrderId, create a new anonymous type
// and sum the total number of items shipped.
(x,y) => new {
OrderId = x,
ShippedTotal = y.Sum(z => z.ShippedQty),
OrderTotal = y.First().OrderTotal
});
For the sample data, this gives a single result, an anonymous type with three int properties (from C# interactive console):
f__AnonymousType0#3<int, int, int>> { { OrderId = 1031, ShippedTotal = 3, OrderTotal = 4 } }
You can then filter out the results to see where the order quantity is less than the order total
groupResults.Where(x => x.ShippedTotal < x.OrderTotal) ...

Remove duplicate from a list of type class

I have a class with a following properties
id (type: unique long), name (type: string), version major (VM) (type:long), version minor (Vm) (type: long)
I create a list of this class and the list looks as follows
ID Name VM Vm
1 ssim 2 1
2 SSim 3 1
3 Counter 5 1
4 Counter 6 2
5 Counter 6 5
I would like to remove duplicates from the list based on Version Major and then version minor. The final list should look as follows
ID Name VM Vm
2 SSim 3 1
5 Counter 6 5
Something like this, I think:
public class Product
{
public Product(long id, string name, int major, int minor)
{
this.Id = id;
this.Name = name;
this.Major = major;
this.Minor = minor;
}
public long Id { get; set; }
public int Major { get; set; }
public int Minor { get; set; }
public string Name { get; set; }
}
private static void Main()
{
IEnumerable<Product> products = new List<Product>
{
new Product(1, "ssim", 2, 1),
new Product(2, "SSim", 3, 1),
new Product(3, "Counter", 5, 1),
new Product(4, "Counter", 6, 2),
new Product(5, "Counter", 6, 5)
};
IEnumerable<Product> distinctProducts =
(from x in products group x by x.Name.ToLower() into g select g.OrderByDescending(y => y.Major).ThenByDescending(y => y.Minor).First()).OrderBy(x => x.Name).ToList();
}
So you want the maximum version of each name.
You can do it with linq like this:
void Main()
{
var versions = new List<Version>
{
new Version(1,2, "a"),
new Version(1,3, "a"),
new Version(1,3, "b"),
new Version(1,4, "b"),
new Version(1,1, "b"),
new Version(2,3, "c")
};
var distinctVersions = versions
.GroupBy(g => g.name.ToLowerInvariant())
.Select(g => g.ToList().OrderBy(x => x.major).ThenBy(x => x.minor).Last())
.ToList();
}
Say your class is ProgramEntry:
public class ProgramEntry {
public long Id;
public string Name;
public long VM;
public long Vm;
public ProgramEntry (long id, string name, long vM, long vm) {
Id = id;
Name = name;
VM = vM;
Vm = vm;
}
public override string ToString () {
return this.Id+":"+this.Name+"("+this.VM+"."+this.Vm+")";
}
}
(yes, using public fields is not good practice, but it simply a quick-and-dirty solution)
Now you can order them by version (first major, then minor):
List<ProgramEntry> programs = new List<ProgramEntry>();
//fill list with programs
var order = programs.OrderBy(x => -x.VM).ThenBy(x => -x.Vm);
This results in a IEnumerable<ProgramEntry> ordered with largest major first, and in case of equivalent major, largest minor first.
Next you can use this duplicate filter, to filter out elements with the same Name:
List<ProgramEntry> result = order.DistinctBy(x => x.Name).ToList();
The DistinctBy is by the way part of the MoreLINQ library. Or you can implement it yourself using an extension class:
public static class Foo {
public static IEnumerable<TSource> DistinctBy<TSource, TKey>
(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector) {
HashSet<TKey> seenKeys = new HashSet<TKey>();
foreach (TSource element in source) {
if (seenKeys.Add(keySelector(element))) {
yield return element;
}
}
}
}
Demo (using the csharp interactive shell):
$ csharp
Mono C# Shell, type "help;" for help
Enter statements below.
csharp> public class ProgramEntry {
>
> public long Id;
> public string Name;
> public long VM;
> public long Vm;
>
> public ProgramEntry (long id, string name, long vM, long vm) {
> Id = id;
> Name = name;
> VM = vM;
> Vm = vm;
> }
>
> public override string ToString () {
> return this.Id+":"+this.Name+"("+this.VM+"."+this.Vm+")";
> }
>
> }
csharp> List<ProgramEntry> programs = new List<ProgramEntry>();
csharp> programs.Add(new ProgramEntry(1,"ssim",2,1));
csharp> programs.Add(new ProgramEntry(2,"ssim",3,1));
csharp> programs.Add(new ProgramEntry(3,"Counter",5,1));
csharp> programs.Add(new ProgramEntry(4,"Counter",6,2));
csharp> programs.Add(new ProgramEntry(5,"Counter",6,5));
csharp> programs
{ 1:ssim(2.1), 2:ssim(3.1), 3:Counter(5.1), 4:Counter(6.2), 5:Counter(6.5) }
csharp> var order = programs.OrderBy(x => -x.VM).ThenBy(x => -x.Vm);
csharp> order
{ 5:Counter(6.5), 4:Counter(6.2), 3:Counter(5.1), 2:ssim(3.1), 1:ssim(2.1) }
csharp> List<ProgramEntry> result = order.DistinctBy(x => x.Name).ToList();
csharp> result
{ 5:Counter(6.5), 2:ssim(3.1) }
Is this the expected behavior?
Let's suppose this class resembles your data:
public class VerX
{
public int ID { get; set; }
public string Name { get; set; }
public int VerMajor { get; set; }
public int VerMinor { get; set; }
}
For your sample, this is how this data is filled:
var list = new List<VerX>
{
new VerX { ID = 1, Name = "ssim", VerMajor = 2, VerMinor = 1 },
new VerX { ID = 2, Name = "SSim", VerMajor = 3, VerMinor = 1 },
new VerX { ID = 3, Name = "Counter", VerMajor = 5, VerMinor = 1 },
new VerX { ID = 4, Name = "Counter", VerMajor = 6, VerMinor = 2 },
new VerX { ID = 5, Name = "Counter", VerMajor = 6, VerMinor = 5 },
};
Now, lets create a loop that would provide you with the desired result:
// First create new list that would hold the results
var listNew = new List<VerX>();
// Select distinct names from data (using ToLower, so casing does not matter)
var names = list.Select(t => t.Name.ToLower()).Distinct().ToList();
// Loop through each of distinct name
foreach (var name in names)
{
// With LINQ, select item whose name matches and sort list by VerMajor
// descending and VerMinor descending and take first item.
var item = list.Where(t => t.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase))
.OrderByDescending(t => t.VerMajor)
.ThenByDescending(t => t.VerMinor)
.FirstOrDefault();
// If item not found (although it should be found!), continue the loop
if (item == null)
continue;
// Add item to new list
listNew.Add(item);
}
// At the end of the loop, the listNew contains items as in your proposed result.
The same foreach loop can be obtained by more complex LINQ query:
// Select distinct names as in first case
var names = list.Select(t => t.Name.ToLower()).Distinct().ToList();
// Construct listNew from names based on same algorithm as before, but using LINQ this time.
var listNew = names
.Select(name => list.Where(t => t.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase))
.OrderByDescending(t => t.VerMajor)
.ThenByDescending(t => t.VerMinor)
.FirstOrDefault())
.Where(item => item != null)
.ToList();
// Here, listNew contains your desired result.
Based on your desired result, this provides you with results grouped by name, based on max VerMajor and max VerMinor.
I think that what you ask could be easily done with this code:
var groupsByName = myItems.GroupBy(x => x.Name.ToLower());
var distinctItems = groupsByName.Select(x => x.ToList()
.OrderByDescending(y => y.VM)
.ThenByDescending(z => z.Vm).First())
.OrderBy(k => k.Name).ToList();

Sorting Multiple Lists

I have 3 List containing : Index, Name, Age
Example:
List<int> indexList = new List<int>();
indexList.Add(3);
indexList.Add(1);
indexList.Add(2);
List<string> nameList = new List<string>();
nameList.Add("John");
nameList.Add("Mary");
nameList.Add("Jane");
List<int> ageList = new List<int>();
ageList.Add(16);
ageList.Add(17);
ageList.Add(18);
I now have to sort all 3 list based on the indexList.
How do I use .sort() for indexList while sorting the other 2 list as well
You are looking at it the wrong way. Create a custom class:
class Person
{
public int Index { get; set; }
public string Name{ get; set; }
public int Age{ get; set; }
}
Then, sort the List<Person> with the help of the OrderBy method from the System.Linq namespace:
List<Person> myList = new List<Person>() {
new Person { Index = 1, Name = "John", Age = 16 };
new Person { Index = 2, Name = "James", Age = 19 };
}
...
var ordered = myList.OrderBy(x => x.Index);
Also, you can read Jon Skeet article about your anti-pattern.
Farhad's answer is correct and should be accepted. But if you really have to sort three related lists in that way you could use Enumerable.Zip and OrderBy:
var joined = indexList.Zip(
nameList.Zip(ageList, (n, a) => new { Name = n, Age = a }),
(ix, x) => new { Index = ix, x.Age, x.Name })
.OrderBy(x => x.Index);
indexList = joined.Select(x => x.Index).ToList();
nameList = joined.Select(x => x.Name).ToList();
ageList = joined.Select(x => x.Age).ToList();
All are ordered by the value in the index-list afterwards.

Categories

Resources