Find duplicates cards in a list of list using LINQ - c#

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.

Related

Get list of objects whose field has maximum value

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

How to get ONLY unique values from list C#

For example, I have a list of excel cells
List<Cell> cells = new List<Cell>
{
new Cell("4"),
new Cell("Hez"),
new Cell("Method"),
new Cell("4"),
new Cell("Val"),
new Cell("Method"),
}
I need to get the only unique cell (in this case Cell("Val"), Cell("Hez")) so Distinct() is not for me.
I found this solution but it doesn't return any data at all
var uniqueTest = allData.GroupBy(cell => cell)
.Where(group => group.ToString().Count() == 1)
.Select(group => group.Key);
I think the problem is Cell object doesn't contain any comparison methods (This is IronXl lib) so this is why I'm using ToString() here.
But I don't quite understand linq yet, so any explanation or advice is appreciated
Remarks:
I need to get a list of cells back, but with unique values
Step 1: Group the cells by their value.
Step 2: Keep only the groups of size 1.
Step 3: Get the only item from each group.
var uniqueCells =
allData.GroupBy(cell => cell.Value) //Step 1
.Where(g => g.Count() == 1) //Step 2
.Select(g => g.Single()) //Step 3
This should be easy.
Lets count the value of Number 4 is Key.. then your linq should look like this
var uniqueTest = allData.GroupBy(x=> x.Key).Select(x=> x.First()).Select(x=> x.Key);
If I understand you correctly, and i'm not sure if I do, you can use .First() or the more robust .Single()
var uniqueTest = allData.First(c => c.ToString() == "Val");
In this sample i'm assuming c.ToString() will give you the cell's value. Otherwise it will likely be something like c.Value or something.
There are als the OrDefault variants.
Check out this article for the differences;
https://www.c-sharpcorner.com/blogs/singleordefault-vs-firstordefault-in-linq-query1
if you class Cell is something like this.
public class Cell
{
public Cell(string mystring)
{
MyString = mystring;
}
public string MyString { get; set; }
then this will work to get a unique list:
List<Cell> UniqueCells = new List<Cell>();
foreach (var cell in cells)
{
if(!UniqueCells.Any(c=>c.MyString == cell.MyString))
{
UniqueCells.Add(cell);
}
}
In this case only the first cell containing 'Method' and '4' will be added. the '!' and '.Any' are the essential parts.

Compare two List elements and replace if id is equals

I have two lists with Classes
public class Product
{
int id;
string url;
ect.
}
I need compare in the old list (10k+ elements) a new list(10 elements) by ID
and if an id is same just replace data from new List to old list
I think it will be good using LINQ.
Can you help me how can I use LINQ or there are batter library?
Do you need to modify the collection in place or return a new collection?
If you are returning a new collection you could
var query = from x in oldItems
join y in newItems on y.Id equals x.Id into g
from z in g.DefaultIfEmpty()
select z ?? x;
var new List = query.ToList();
This method will ignore entries in newItems that do not exist in old items.
If you are going to be modifying the collection in place you would be better off working with a dictionary and referencing that everywhere.
You can create a dictionary from the list by doing
var collection = items.ToDictionary(x => x.Id, x => x);
Note modifying the dictionary doesn't alter the source collection, the idea is to replace your collection with the dictionary object.
If you are using the dictionary you can then iterate over new collection and check the key.
foreach (var item in newItems.Where(x => collection.ContainsKey(x.Id))) {
collection[item.Id] = item;
}
Dictionaries are iterable so you can loop over the Values collection if you need to. Adds and removes are fast because you can reference by key. The only problem I can think you may run into is if you rely on the ordering of the collection.
If you are stuck needing to use the original collection type then you could use the ToDictionary message on your newItems collection. This makes your update code look like this.
var converted = newItems.ToDictionary(x => x.Id, x => x);
for (var i = 0; i < oldItems.Count(); i++) {
if (converted.ContainsKey(oldItems[i].Id)) {
oldItems[i] = converted[oldItems[i].Id];
}
}
This has the advantage the you only need to loop the newitems collection once, from then on it's key lookups, so it's less cpu intensive. The downside is you've created an new collection of keys for newitems so it consumes more memory.
Send you a sample function that joins the two list by id property of both lists and then update original Product.url with the newer one
void ChangeItems(IList<Product> original, IList<Product> newer){
original.Join(newer, o => o.id, n => n.id, (o, n) => new { original = o, newer = n })
.ToList()
.ForEach(j => j.original.Url = j.newer.Url);
}
Solution :- : The LINQ solution you're look for will be something like this
oldList = oldList.Select(ele => { return (newList.Any(i => i.id == ele.id) ? newList.FirstOrDefault(newObj => newObj.id == ele.id) : ele); }).ToList();
Note :- Here we are creating the OldList based on NewList & OldList i.e we are replacing OldList object with NewList object.If you only want some of the new List properties you can create a copy Method in your class
EG for copy constructor
oldList = oldList.Select(ele => { return (newList.Any(i => i.id == ele.id) ? ele.Copy(newList.FirstOrDefault(newObj => newObj.id == ele.id)) : ele); }).ToList();
//Changes in your class
public void Copy(Product prod)
{
//use req. property of prod. to be replaced the old class
this.id = prod.id;
}
Read
It is not a good idea to iterate over 10k+ elements even using linq as such it will still affect your CPU performance*
Online sample for 1st solution
As you have class
public class Product
{
public int id;
public string url;
public string otherData;
public Product(int id, string url, string otherData)
{
this.id = id;
this.url = url;
this.otherData = otherData;
}
public Product ChangeProp(Product newProd)
{
this.url = newProd.url;
this.otherData = newProd.otherData;
return this;
}
}
Note that, now we have ChangeProp method in data class, this method will accept new class and modify old class with properties of new class and return modified new class (as you want your old class be replaced with new classes property (data). So at the end Linq will be readable and clean.
and you already have oldList with lots of entries, and have to replace data of oldList by data of newList if id is same, you can do it like below.
suppose they are having data like below,
List<Product> oldList = new List<Product>();
for (int i = 0; i < 10000; i++)
{
oldList.Add(new Product(i, "OldData" + i.ToString(), "OldData" + i.ToString() + "-other"));
}
List<Product> newList = new List<Product>();
for (int i = 0; i < 5; i++)
{
newList.Add(new Product(i, "NewData" + i.ToString(), "NewData" + i.ToString() + "-other"));
}
this Linq will do your work.
oldList.Where(x => newList.Any(y => y.id == x.id))
.Select(z => oldList[oldList.IndexOf(z)].ChangeProp(newList.Where(a => a.id == z.id).FirstOrDefault())).ToList();
foreach(var product in newList)
{
int index = oldList.FindIndex(x => x.id == product.id);
if (index != -1)
{
oldList[index].url = product.url;
}
}
This will work and i think it's a better solution too.
All the above solution are creating new object in memory and creating new list with 10k+
records is definitely a bad idea.
Please make fields in product as it won't be accessible.

Using linq to group and sum List<int> (Zip?)

I have a list of objects I want to group.
Objects have a List parameter, and during grouping I want to make the sum of the lists like this :
for(int i=0;i<MyList1.Count();i++)
{
StatutOperations[i]=StatutOperations1[i]+StatutOperations2[i]...
}
For now using linq I have the following :
liste_rep = liste_rep.GroupBy(l => l.Nom)
.Select(cl => new Repere
{
Quantite = cl.Sum(c => c.Quantite),
IdAff = cl.First().IdAff,
ID = 0,
ListeOperations = cl.First().ListeOperations,
StatutOperations = cl.Zip(StatutOperations)//First().StatutOperations
}).ToList();
The line making problem is the last one, I found how to use Zip function to summ two tables, but what if I want to use it grouping Lists?
Edit : StatusOperations is a list of integers, concretely liste_rep is a list of details, details have a list of n operations, and StatusOperations determines how much details have been operated for each operation.
Example :
ListOperations = CUT, DRILL, PAINT
StatusOperations = 20,20,10
This means 20 details are cut, 20 are drilled and 10 are painted
I want to group the list of details getting totals for each operation.
Edit 2 :
For now I only could manage to do it making myself the grouping :
liste_rep = liste_rep.OrderBy(p => p.Nom).ToList();
if (liste_rep.Count()>1)
{
totalStatut = liste_rep[0].StatutOperations.ConvertAll(s => s = 0);
string oldRep = "";
Repere repere = new Repere();
foreach (Repere rep in liste_rep)
{
if (rep.Nom!=oldRep)
{
newListRep.Add(repere);
repere = new Repere();
repere.Nom = rep.Nom;
repere.StatutOperations = rep.StatutOperations;
}
else
{
repere.StatutOperations=repere.StatutOperations.Zip(rep.StatutOperations, (x, y) => x + y).ToList();
}
oldRep = rep.Nom;
}
}
You can use this
if StatutOperations is a list of int).
Use this at last line.
StatutOperations= cl.Aggregate((opl1, opl2) =>
{ return opl1.StatutOperations.Zip(opl2.StatutOperations, (opin1,opin2)=>opin1+opin2).ToList(); });
in above code Aggregate runs through two elements and aggregate as sum (op1+op2).
Note : Remember use aggregate if and only if list contains more than one element
.
Edit:
Sorry the above code is incorrect as this is applying aggregate on repere type object and hence the expected return value would be of Repere type.
Edited my code now it should work fine now.
liste_rep.GroupBy(l => l.Nom)
.Select(cl => new Repere
{
Quantite = cl.Sum(c => c.Quantite),
IdAff = cl.First().IdAff,
ID = 0,
ListeOperations = cl.First().ListeOperations,
StatutOperations = cl
.Select(x=>x.StatutOperations)
.Aggregate((x,y)=> x.Zip(y,(p,q)=>p+q).ToList());
}).ToList();

Use LINQ to group a sequence by date with no gaps

I'm trying to select a subgroup of a list where items have contiguous dates, e.g.
ID StaffID Title ActivityDate
-- ------- ----------------- ------------
1 41 Meeting with John 03/06/2010
2 41 Meeting with John 08/06/2010
3 41 Meeting Continues 09/06/2010
4 41 Meeting Continues 10/06/2010
5 41 Meeting with Kay 14/06/2010
6 41 Meeting Continues 15/06/2010
I'm using a pivot point each time, so take the example pivot item as 3, I'd like to get the following resulting contiguous events around the pivot:
ID StaffID Title ActivityDate
-- ------- ----------------- ------------
2 41 Meeting with John 08/06/2010
3 41 Meeting Continues 09/06/2010
4 41 Meeting Continues 10/06/2010
My current implementation is a laborious "walk" into the past, then into the future, to build the list:
var activity = // item number 3: Meeting Continues (09/06/2010)
var orderedEvents = activities.OrderBy(a => a.ActivityDate).ToArray();
// Walk into the past until a gap is found
var preceedingEvents = orderedEvents.TakeWhile(a => a.ID != activity.ID);
DateTime dayBefore;
var previousEvent = activity;
while (previousEvent != null)
{
dayBefore = previousEvent.ActivityDate.AddDays(-1).Date;
previousEvent = preceedingEvents.TakeWhile(a => a.ID != previousEvent.ID).LastOrDefault();
if (previousEvent != null)
{
if (previousEvent.ActivityDate.Date == dayBefore)
relatedActivities.Insert(0, previousEvent);
else
previousEvent = null;
}
}
// Walk into the future until a gap is found
var followingEvents = orderedEvents.SkipWhile(a => a.ID != activity.ID);
DateTime dayAfter;
var nextEvent = activity;
while (nextEvent != null)
{
dayAfter = nextEvent.ActivityDate.AddDays(1).Date;
nextEvent = followingEvents.SkipWhile(a => a.ID != nextEvent.ID).Skip(1).FirstOrDefault();
if (nextEvent != null)
{
if (nextEvent.ActivityDate.Date == dayAfter)
relatedActivities.Add(nextEvent);
else
nextEvent = null;
}
}
The list relatedActivities should then contain the contiguous events, in order.
Is there a better way (maybe using LINQ) for this?
I had an idea of using .Aggregate() but couldn't think how to get the aggregate to break out when it finds a gap in the sequence.
Here's an implementation:
public static IEnumerable<IGrouping<int, T>> GroupByContiguous(
this IEnumerable<T> source,
Func<T, int> keySelector
)
{
int keyGroup = Int32.MinValue;
int currentGroupValue = Int32.MinValue;
return source
.Select(t => new {obj = t, key = keySelector(t))
.OrderBy(x => x.key)
.GroupBy(x => {
if (currentGroupValue + 1 < x.key)
{
keyGroup = x.key;
}
currentGroupValue = x.key;
return keyGroup;
}, x => x.obj);
}
You can either convert the dates to ints by means of subtraction, or imagine a DateTime version (easily).
In this case I think that a standard foreach loop is probably more readable than a LINQ query:
var relatedActivities = new List<TActivity>();
bool found = false;
foreach (var item in activities.OrderBy(a => a.ActivityDate))
{
int count = relatedActivities.Count;
if ((count > 0) && (relatedActivities[count - 1].ActivityDate.Date.AddDays(1) != item.ActivityDate.Date))
{
if (found)
break;
relatedActivities.Clear();
}
relatedActivities.Add(item);
if (item.ID == activity.ID)
found = true;
}
if (!found)
relatedActivities.Clear();
For what it's worth, here's a roughly equivalent -- and far less readable -- LINQ query:
var relatedActivities = activities
.OrderBy(x => x.ActivityDate)
.Aggregate
(
new { List = new List<TActivity>(), Found = false, ShortCircuit = false },
(a, x) =>
{
if (a.ShortCircuit)
return a;
int count = a.List.Count;
if ((count > 0) && (a.List[count - 1].ActivityDate.Date.AddDays(1) != x.ActivityDate.Date))
{
if (a.Found)
return new { a.List, a.Found, ShortCircuit = true };
a.List.Clear();
}
a.List.Add(x);
return new { a.List, Found = a.Found || (x.ID == activity.ID), a.ShortCircuit };
},
a => a.Found ? a.List : new List<TActivity>()
);
Somehow, I don't think LINQ was truly meant to be used for bidirectional-one-dimensional-depth-first-searches, but I constructed a working LINQ using Aggregate. For this example I'm going to use a List instead of an array. Also, I'm going to use Activity to refer to whatever class you are storing the data in. Replace it with whatever is appropriate for your code.
Before we even start, we need a small function to handle something. List.Add(T) returns null, but we want to be able to accumulate in a list and return the new list for this aggregate function. So all you need is a simple function like the following.
private List<T> ListWithAdd<T>(List<T> src, T obj)
{
src.Add(obj);
return src;
}
First, we get the sorted list of all activities, and then initialize the list of related activities. This initial list will contain the target activity only, to start.
List<Activity> orderedEvents = activities.OrderBy(a => a.ActivityDate).ToList();
List<Activity> relatedActivities = new List<Activity>();
relatedActivities.Add(activity);
We have to break this into two lists, the past and the future just like you currently do it.
We'll start with the past, the construction should look mostly familiar. Then we'll aggregate all of it into relatedActivities. This uses the ListWithAdd function we wrote earlier. You could condense it into one line and skip declaring previousEvents as its own variable, but I kept it separate for this example.
var previousEvents = orderedEvents.TakeWhile(a => a.ID != activity.ID).Reverse();
relatedActivities = previousEvents.Aggregate<Activity, List<Activity>>(relatedActivities, (items, prevItem) => items.OrderBy(a => a.ActivityDate).First().ActivityDate.Subtract(prevItem.ActivityDate).Days.Equals(1) ? ListWithAdd(items, prevItem) : items).ToList();
Next, we'll build the following events in a similar fashion, and likewise aggregate it.
var nextEvents = orderedEvents.SkipWhile(a => a.ID != activity.ID);
relatedActivities = nextEvents.Aggregate<Activity, List<Activity>>(relatedActivities, (items, nextItem) => nextItem.ActivityDate.Subtract(items.OrderBy(a => a.ActivityDate).Last().ActivityDate).Days.Equals(1) ? ListWithAdd(items, nextItem) : items).ToList();
You can properly sort the result afterwards, as now relatedActivities should contain all activities with no gaps. It won't immediately break when it hits the first gap, no, but I don't think you can literally break out of a LINQ. So it instead just ignores anything which it finds past a gap.
Note that this example code only operates on the actual difference in time. Your example output seems to imply that you need some other comparison factors, but this should be enough to get you started. Just add the necessary logic to the date subtraction comparison in both entries.

Categories