Get list of objects whose field has maximum value - c#

Suppose I have this class
public class Person {
public string name;
public int age;
//...
}
Suppose I have an array of Person:
Person[] personArray;
How can I get the list of Person with the biggest age within personArray using Linq?
I'm trying this but I wish there was a one-liner to perform this task:
public List<Person> GetBiggestAgeList(){
var sortedPeople = personArray.OrderByDescending(person => person.age).ToList();
int maxAge = sortedPeople[0].age;
List<Person> answer = new List<Person>();
for(int i = 0; i < sortedPeople.Count; ++i){
if(sortedPeople[i].age == maxAge) answer.Add(sortedPeople[i]);
else break;
}
return answer;
}

One option would be
var opa = personArray.OrderByDescending(x=>x.age).FirstOrDefault();
to get all opas
var allOpas = personArray.Where(x=>x.age == opa.age);
One liner would be:
var allOpas2 = personArray.OrderByDescending(x=>x.age).GroupBy(x=>x.age).FirstOrDefault().ToList();

Several options to accomplish this:
Option 1
using Linq .Max() documentation
// structured
var max = personArray.Max(inner => inner.Age);
var list = personArray.Where(p => p.Age == max);
// ...or in an one-liner
var list = personArray.Where(p => p.Age == personArray.Max(inner => inner.Age));
Option 2
using Linq .GroupBy() + .FirstOrDefault() documentation
// this will first order your list
// then group by all the ages and take the first group because this is the group of the persons with the highest age.
var list = personArray.OrderByDescending(p => p.Age)
.GroupBy(p => p.Age)
.FirstOrDefault()
.ToList();
Here you can find a working example dotnet fiddle
I would recommand the Option 1 with the .Max() is more efficient and faster than Option 2 as you can see in the dotnet fiddle. To have it really fastest use Option 1 as two liner and resolve the .Max() first and then do the .Where(..).

If you're looking for a simple one-liner and don't mind adding an external dependency, MoreLINQ has an extension method (MaxBy) that will give you what you are looking for. Documentation
var people = MoreLinq.MoreEnumerable.MaxBy(personArray, x => x.Age).ToArray();
Otherwise, the following one-liner will do the job.
var people = personArray.Where(x => x.Age == personArray.Max(x => x.Age)).ToArray();
Another option is to split it into two queries.
var max = personArray.Max(x => x.Age); // Find maximum age
var people = personArray.Where(x => x.Age == max).ToArray(); // Find people with maximum age

Related

Find duplicates cards in a list of list using LINQ

This is what I did so far:
class CardDisplayer
{
public int CardSuit;
public int CardValue;
}
List<CardDisplayer> _playerHand;
// Group all cards by the same suit
var _handDuplicates = _playerHand.GroupBy(x => x.CardSuit)
.Select(g => g.ToList())
.ToList();
CardDisplayer _duplicateFound = null;
// And then find all cards with the same value number
for (int i = 0; i < _handDuplicates.Count; i++)
{
var _handReference = _handDuplicates[i];
var _temp = _handReference.GroupBy(x => x.CardValue)
.Where(g => g.Count() > 1)
.Select(g => g.ToList())
.ToList();
// If you find more than one card with the same number
if(_temp.Count > 0)
{
// Take it
_duplicateFound = _temp.First().First();
break;
}
}
What I'm trying to achieve is after get the player's hand I want to find if the player has duplicates in his hand by looking if there is cards with the same suit and the same value.
I tried a lot of things on the internet but I cannot figure out how to get the list of duplicates using LINQ instead write all these lines of code.
Can someone know how to do it please?
Thank you.
you can use the GroupBy method to create a complex key, then use the Any method to find if at least on group has more then 1 object, or Where / FirstOrDefault to find the duplicates
var grouped = _handReference.GroupBy(g => new {suit=g.CardSuit, value=g.CardValue});
var hasDuplicates=grouped.Any(g=>g.Count()>1);
var duplicateList=grouped.Where(g=>g.Count()>1);
var duplicate=grouped.FirstOrDefault(g=>g.Count()>1);
After a while, I found the perfect solution based also on the answers.
// Get the reference for the player hand
List<List<CardDisplayer>> _playerHand = playersSlots[_playerIndex];
// Find in the player's hand duplicates
var _duplicates = _playerHand.GroupBy(x => new { x.CardSuit, x.CardValue })
.Where(x => x.Skip(1).Any())
.SelectMany(g => g)
.Distinct(new CardEqualityComparer()) // Use this only if you want unique results
.ToList();
var _duplicateCard = _duplicates.FirstOrDefault();
If you want unique results you can use a custom CardEqualityComparer class and use it with the Distinct of LINQ
/// <summary>
/// Used to compare if two cards are equals.
/// </summary>
class CardEqualityComparer : IEqualityComparer<CardDisplayer>
{
public bool Equals(CardDisplayer x, CardDisplayer y)
{
// Two items are equal if their keys are equal.
return x.CardSuit == y.CardSuit && x.CardValue == y.CardValue;
}
public int GetHashCode(CardDisplayer obj)
{
return obj.CardSuit.GetHashCode() ^ obj.CardValue.GetHashCode();
}
}
You can find the reference on the web: StackOverflow and DotNetPerls
Thank you, everyone, for the help.

How to use LINQ to sort custom items by using StartWith

I have the following code. which is based on using a temp container to select specific items and then add them at the end of the list.
var allRoles = roles.Table
.AsEnumerable().Select(p => new FirmRole
{
Code = p.Field<string>("RoleName"),
Name = p.Field<string>("RoleName")
})ToList();
var formRoles = allRoles.Where(p => p.Code.StartsWith("f")).ToList();
var otherRoles = allRoles.Except(formRoles).ToList();
otherRoles.AddRange(formRoles);
Would it be a better way to shorten this code and get rid of the temp list?
Something like
var allRoles = roles.Table
.AsEnumerable().Select(p => new FirmRole
{
Code = p.Field<string>("RoleName"),
Name = p.Field<string>("RoleName")
}).OrderBy(x=>x.Code.StartsWith("f")).ThenBy(a=>a);
On IEnumerable<T> (as is in this case) you are right, because OrderBy is a stable sorting (see Enumerable.OrderBy: This method performs a stable sort; that is, if the keys of two elements are equal, the order of the elements is preserved., so for elements with the same key, their previous ordering is maintained. On IQueryable<T> this isn't guaranteed.
var allRoles = roles.Table
.AsEnumerable().Select(p => new FirmRole
{
Code = p.Field<string>("RoleName"),
Name = p.Field<string>("RoleName")
}).Distinct()
.OrderBy(x => x.Item.Code.StartsWith("f"))
.ToList();
Note that you don't need a secondary ordering, because OrderBy is, as I've said, stable.
Speedwise: you'll have to benchmark it with small and big sets. An OrderBy should be O(nlogn), but an ordering by true/false (as in this case) is probably more similar to O(n)
The second example reads better.
Don't think you need the .ToList() after Distinct().
Hope that helps
You should use GroupBy and ToLookup to get the results that you are looking for.
var allRoles = roles.Table
.AsEnumerable().Select(p => new FirmRole
{
Code = p.Field<string>("RoleName"),
Name = p.Field<string>("RoleName")
}).GroupBy(x => x.StartsWith("f")).ToLookup(g => g.Key);;
var formRoles = allRoles[true].ToList();
var otherRoles = allRoles[false].ToList();
I believe that you can implement a custom sort comparison class based on the IComparer<T> interface for providing the required custom sorting:
public class CustomSortComparer : IComparer<string>
{
public int Compare(string x, string y)
{
StringComparer sc = StringComparer.CurrentCultureIgnoreCase;
if (string.IsNullOrEmpty(x) || string.IsNullOrEmpty(y))
{
return sc.Compare(x, y);
}
if (x.StartsWith("f", StringComparison.CurrentCultureIgnoreCase) &&
!y.StartsWith("f", StringComparison.CurrentCultureIgnoreCase))
{
return 1;
}
if (y.StartsWith("f", StringComparison.CurrentCultureIgnoreCase) &&
!x.StartsWith("f", StringComparison.CurrentCultureIgnoreCase))
{
return -1;
}
return sc.Compare(x, y);
}
}
And then call:
var allRoles = roles.Table
.AsEnumerable().Select(p => new FirmRole
{
Code = p.Field<string>("RoleName"),
Name = p.Field<string>("RoleName")
}).OrderBy(x => x.Code, new CustomSortComparer()).ToList();

LINQ: Efficient way to get sum of all child items

I have the following for each loop to get sum of all child objects. Is there a better way using LINQ?
Purchase p1 = db.Purchases.FirstOrDefault(p => p.PurchaseId == 1);
int total = 0;
foreach (SellingItem item in p1.SellingItems)
{
total = total + Convert.ToInt32(item.Price);
}
REFERENCE:
How to get the sum of the volume of highest and lowest priced items in linq
Using the ALL operator in linq to filter child items of EntitySet
Sounds like you just want:
// Any reason for FirstOrDefault rather than SingleOrDefault?
var purchase = db.Purchases.FirstOrDefault(p => p.PurchaseId == 1);
if (purchase != null)
{
var total = purchase.SellingItems
.Sum(x => Convert.ToInt32(x.Price));
...
}
// TODO: Work out what to do if there aren't any such purchases
Why do you need the conversion of the price though? What type is Price, and why isn't it already the right type? (And does that really want to be int rather than decimal?)
p1.SellingItems.Sum(p => p.Price)
Try using the Linq Sum method:
Purchase p1 = db.Purchases.FirstOrDefault(p => p.PurchaseId == 1);
int total = p1.SellingItems.Sum(item => Convert.ToInt32(item.Price));
It is not more efficient, in that it will not be any faster. But it is more concise.

Linq to Objects ordering by arbitrary number of parameters

I have a list of Func defining an ordering:
var ordering = new List<Func<Person, IComparable>>
{ x => x.Surname, x => x.FirstName };
I can order the results with something like...
people = people.OrderBy(ordering[0]).ThenBy(ordering[1]);
I'm trying to figure how to do the above when the list can contain any number of sequential orderings. Is it possible?
people = people.OrderBy(ordering[0]).ThenBy(ordering[1]).ThenBy(ordering[2]);
is the same as
var orderedPeople = people.OrderBy(ordering[0]);
orderedPeople = orderedPeople.ThenBy(ordering[1]);
orderedPeople = orderedPeople.ThenBy(ordering[2]);
people = orderedPeople;
so you simply write a loop like this:
if (ordering.Count != 0)
{
var orderedPeople = people.OrderBy(ordering[0]);
for (int i = 1; i < ordering.Count; i++)
{
orderedPeople = orderedPeople.ThenBy(ordering[i]);
}
people = orderedPeople;
}
As others have mentioned, you can use a loop to do this.
If you prefer, you can also use the Aggregate operator:
// Requires a non-empty ordering sequence.
var result2 = ordering.Skip(1)
.Aggregate(people.OrderBy(ordering.First()), Enumerable.ThenBy);
(or)
// Shorter and more "symmetric" but potentially more inefficient.
// x => true should work because OrderBy is a stable sort.
var result = ordering.Aggregate(people.OrderBy(x => true), Enumerable.ThenBy);
You should be able to do something similar to this
people = people.OrderBy(ordering[0])
foreach(var order in ordering.Skip(1))
{
people = people.ThenBy(order);
}
Alternately
for(i = 0; i < ordering.Count; i++)
{
people = i == 0 ? people.OrderBy(ordering[i]) : people.ThenBy(ordering[i]);
}
Remember that LINQ execution is deferred. You can build up the expression sequentially before accessing the results, doing something like:
var ordered = unordered.OrderBy(ordering.First());
foreach (var orderingItem in ordering.Skip(1))
{
ordered = ordered.ThenBy(orderingItem);
}
You might want to do this with dynamically building up you're expression. More info here: Dynamic LINQ and Dynamic Lambda expressions?

LINQ how to query if a value is between a list of ranges?

Let's say I have a Person record in a database, and there's an Age field for the person.
Now I have a page that allows me to filter for people in certain age ranges.
For example, I can choose multiple range selections, such as "0-10", "11-20", "31-40".
So in this case, I'd get back a list of people between 0 and 20, as well as 30 to 40, but not 21-30.
I've taken the age ranges and populated a List of ranges that looks like this:
class AgeRange
{
int Min { get; set; }
int Max { get; set; }
}
List<AgeRange> ageRanges = GetAgeRanges();
I am using LINQ to SQL for my database access and queries, but I can't figure out how query the ranges.
I want to do something like this, but of course, this won't work since I can't query my local values against the SQL values:
var query = from person in db.People
where ageRanges.Where(ages => person.Age >= ages.Min && person.Age <= ages.Max).Any())
select person;
You could build the predicate dynamically with PredicateBuilder:
static Expression<Func<Person, bool>> BuildAgePredicate(IEnumerable<AgeRange> ranges)
{
var predicate = PredicateBuilder.False<Person>();
foreach (var r in ranges)
{
// To avoid capturing the loop variable
var r2 = r;
predicate = predicate.Or (p => p.Age >= r2.Min && p.Age <= r2.Max);
}
return predicate;
}
You can then use this method as follows:
var agePredicate = BuildAgePredicate(ageRanges);
var query = db.People.Where(agePredicate);
As one of your errors mentioned you can only use a local sequence with the 'Contains' method. One option would then be to create a list of all allowed ages like so:
var ages = ageRanges
.Aggregate(new List<int>() as IEnumerable<int>, (acc, x) =>
acc.Union(Enumerable.Range(x.Min,x.Max - (x.Min - 1)))
);
Then you can call:
People.Where(x => ages.Contains(x.Age))
A word of caution to this tale, should your ranges be large, then this will FAIL!
(This will work well for small ranges (your max number of accepted ages will probably never exceed 100), but any more than this and both of the above commands will become VERY expensive!)
Thanks to Thomas' answer, I was able to create this more generic version that seems to be working:
static IQueryable<T> Between<T>(this IQueryable<T> query, Expression<Func<T, decimal>> predicate, IEnumerable<NumberRange> ranges)
{
var exp = PredicateBuilder.False<T>();
foreach (var range in ranges)
{
exp = exp.Or(
Expression.Lambda<Func<T, bool>>(Expression.GreaterThanOrEqual(predicate.Body, Expression.Constant(range.Min)), predicate.Parameters))
.And(Expression.Lambda<Func<T, bool>>(Expression.LessThanOrEqual(predicate.Body, Expression.Constant(range.Max)), predicate.Parameters));
}
return query.Where(exp);
}
Much simpler implementation is to use Age.CompareTo()
I had a similar problem and solved it using CompareTo
In a database of houses, I want to find houses within the range max and min
from s in db.Homes.AsEnumerable()
select s;
houses = houses.Where( s=>s.Price.CompareTo(max) <= 0 && s.Price.CompareTo(min) >= 0 ) ;

Categories