Remove List elements that are same - c#

I currently have two lists of Directory Info. Candidatelist & VersionList. VersionList being a sublist of candidate list. I'm trying to remove all the elements from candidate list that appear in version list. So if canadidate list has 177 elements and version list has 77 then we have 100 elements left in candidate list. To be more explicit of whats inside the list. Each element correspons to a directory folder name who has a name and a parent folder name. It is possible that the directory name has duplicates but diffeerent parents. I tried doing this but I'm not necesarily getting the correct result Take a look:
candidateList.RemoveAll(x => versionslist.Any(y => y.Name == x.Name) && versionslist.Any(y => y.Parent.Name == x.Parent.Name));
return candidateList;

Your current query is not constraining business requirements to a single version list item. It's making the two queries separately. It's saying:
Are there any versionList items where the Name matches the current candidateList item name
If yes:
Are there any versionList items where the Parent.Name matches the current candidateList item Parent.Name
If yes, remove the item from the candidate list. Instead, you should be querying for a versionList item that meets both requirements pieces at the same time.
candidateList.RemoveAll(x => versionslist.Any(y => y.Name == x.Name && y.Parent.Name == x.Parent.Name));
return candidateList;
This is now saying:
Are there any versionList items where the Name and Parent.Name matches the current candidateList item Name and Parent.Name, respectively.

You can use IEnumerable.Except() method with custom IEqualityComparer:
var differences = candidateList.Except(versionList, new DirectoryInfoComparer());
The EqualityComparer could look like this:
public class DirectoryInfoComparer : IEqualityComparer<DirectoryInfo>
{
bool IEqualityComparer<DirectoryInfo>.Equals(DirectoryInfo x, DirectoryInfo y)
{
return (x.Name == y.Name) && (x.Parent.Name == y.Parent.Name);
}
int IEqualityComparer<DirectoryInfo>.GetHashCode(DirectoryInfo obj)
{
if (Object.ReferenceEquals(obj, null))
return 0;
return obj.GetHashCode();
}
}

As you mentioned, you want to remove all the elements from version list that appear in canadidate list. I think then your syntax should be like:
VersionList.RemoveAll(x => candidateList.Any(y => y.Name == x.Name && y.Parent.Name == x.Parent.Name);
return VersionList;
if it is vice versa, then use
candidateList.RemoveAll(x => VersionList.Any(y => y.Name == x.Name && y.Parent.Name == x.Parent.Name);
return candidateList;

Related

Save part of Linq expression in Any and reuse

I have following Linq queries:
var leaders = _db.Context.Person.Where(p => p.PersonGroup.Any(pg => pg.IsActive && !pg.IsDeleted && pg.GroupType == 'leader'))
var staff = _db.Context.Person.Where(p => p.PersonGroup.Any(pg => pg.IsActive && !pg.IsDeleted && pg.GroupType == 'staff'))
How do I save
pg => pg.IsActive && !pg.IsDeleted part to a variable so my query can be simplified to something like
var staff = _db.Context.Person.Where(p => p.PersonGroup.Any(pg => pg.IsActiveAndNotDeleted && pg.GroupType == 'staff'))
Thanks
So you have a sequence of Persons, where every Person has a property PersonGroup. Apparently PersonGroup is a sequence of zero or more similar items.
We don't know what these items are. What we do know, is that each of these items have Boolean properties IsActive and IsDeleted and a property GroupType which gives an indication of the type of the item: is it a leader, or a staff, or maybe something else.
Be aware: GroupType does not say anything about PersonGroup, but about one item in the PersonGroup. You didn't specify that all items in one PersonGroup have the same GroupType. As far as I know, it can be that PersonGroup has two items, one has GroupType leader and one has GroupType staff.
Requirement: Give me all Persons that have at least one item in property PersonGroup that is Active AND not IsDeleted AND has a third condition.
In your example, the third condition is pg.GroupType == leader, or pg.GroupType == staff. But it could be any condition on the type of items that are in PersonGroup.
I don't know the type of items that are in PersonGroup. Let's say they are items of class Item. Please replace this with the actual type of the items that are in PersonGroup.
My advice would be to create an extension method that takes as input an IQueryable<Person> and the third condition, and returns as output the query for all Persons that have at least one Item in property PersonGroup that is Active, not Deleted and that match the third condition.
If you are not familiar with extension methods, read Extension methods demystified
public static IQueryable<Person> WhereAnyActiveGroup(
this IQueryable<Person> persons,
Expression<Func<Item,Boolean>> thirdCondition)
{
return persons.Where(person => person.PersonGroup
.Where(item => item.IsActive && !item.IsDeleted)
.Where(thirdCondition)
.Any());
}
TODO: invent a proper method name.
In words: from the input sequence of Persons, keep only those persons that have at least one Item in property PersonGroup that is Active and not Deleted and that matches the thirdCondition.
Usage:
using (var dbContext = new PersonelContext())
{
var leaders = dbContext.Persons
.WhereAnyActiveGroup(person => person.GroupType == 'leader'))
.ToList();
var staff = dbContext.Persons
.WhereAnyActiveGroup(person => person.GroupType == 'staff'))
.ToList();
}
You can even concatenate this with other LINQ methods:
var result = dbContext.Persons.Where(person => person.City == "New York")
.WheraAnyActiveGroup(person => person.GroupType == 'staff')
.GroupBy(person => person.Name)
.ToList();
I think the simplest route would be some variation on providing a method that returns the result of a where:
private IEnumerable<Person> GetActiveInRole(string role){
return _db.Context.Person.Where(p => p.PersonGroup.Any(pg => pg.IsActive && !pg.IsDeleted && pg.GroupType == role));
}
And then use that and build on it:
var staff = GetActiveInRole("staff");
var s = staff.Where(p => p.Name == "John");

How use Reflection to condition multiple properties to check for equality in a LINQ .Where statement, depending on what class is passed?

I'm trying to generalize a duplicate checker function, which depending on which type of object, checks the properties said class has (provided in a configuration) are equal to those in another list.
I have decided to create a Dictionary, which will accept a type string for the key (Book, Author, Shop, etc.) and an array of properties that need to be equal.
Example of Dictionary enties:
"Book", ["Title", "CoverImage", "NumberOfPages"]
"Author", ["Name", "Address", "SomethingElse"]
Then, I pass an object to the function and use Reflection to get the name of the type...
obj.GetType().Name;
... which I then use to fetch the right KVP from the Dictionary, meaning that if I pass a Book object, I get "Book". We then use that to get the configuration via ...
configDictionary["obj.GetType().Name"]
... which gives us the array of strings that are the properties that we need to check equality on.
I've gotten to the part where I need something along the lines of
list.Where(x => --> for each of the strings in the array - x.GetType.GetProperty(string) && --> same for next string && same for next string
... and then I need to top it off with an...
x.Id != obj.Id
To make sure we check for duplicates based on our logic (different id's and matches on all properties but has different Id's thus - a duplicate).
The end query should look like
Books:
someList.Where(x =>
x.Title == obj.Title
&& x.CoverImage == obj.CoverImage
&& x.NumberOfPages == obj.NumberOfPages
&& x.Id != obj.Id)
.FirstOrDefault();
Authors:
someList.Where(x => x.Name == obj.Name
&& x.Address == obj.Address
&& x.SomethingElse == obj.SomethingElse
&& x.Id != obj.Id)FirstOrDefault();
Try to avoid reflection because it can slow down your application. As an alternative you can create a dictionary and put all comparators into it:
var configDictionary = new Dictionary<string, List<Func<object, object, bool>>>
{
{
"Book",
new List<Func<object, object, bool>>
{
(b1, b2) => ((Book)b1).Title == ((Book)b2).Title,
(b1, b2) => ((Book)b1).CoverImage == ((Book)b2).CoverImage,
(b1, b2) => ((Book)b1).NumberOfPages == ((Book)b2).NumberOfPages,
(b1, b2) => ((Book)b1).Id != ((Book)b2).Id,
}
},
// same for Authors
};
Now you can use it in Where method:
var typeName = obj.GetType().Name; // here we using Reflection but once per collection, not per each item
var first = someList.Where(x => configDictionary[typeName].All(f => f(x, obj))).FirstOrDefault();
Also, because FirstOrDefault also has overload that accept predicate last line can be rewritten to:
var first = someList.FirstOrDefault(x => configDictionary[typeName].All(f => f(x, obj)));
A better solution will be creating custom attribute which will tag property. Then in class override default method Equals which will get all properties with this attribute and return equality.

C# Distinct with ability to choose which object to save, which ones to remove

I implemented this comparer which works OK.
class ReservationDatesDistinctComparer : IEqualityComparer<ReservationModel>
{
public bool Equals(ReservationModel x, ReservationModel y)
{
return x.FromDate.Date== y.FromDate.Date && x.ToDate.Date == y.ToDate.Date && x.UnitId == x.UnitId;
}
public int GetHashCode(ReservationModel product)
{
int hashProductCode = 1;
return hashProductCode;
}
}
But on ReservationModel I have some other property let's call it ReservationType and I would like to filter out with distinct same dates but keep only ReservationModel who has Type A not Type B.
How it is posible to affect on Distinct which model it will choose?
Distinct will keep the elements it encounters first, a possible solution would be to order those which have ReservationType A first:
reservatonModels.OrderByDescending(m => m.ReservationType == ReservationType.A)
.Distinct(new ReservationDatesDistinctComparer());
I don't think you can use Distinct for this. (Unless you want to rely on undocumented implementation details, as per Lukazoid's answer.)
Something similar to this might do the trick. (Group the elements that your comparer deems to be equal, then order each group so that Type A is prioritised, then take the first element from each group.)
var result = source.GroupBy(x => x, new ReservationDatesDistinctComparer())
.Select(g => g.OrderBy(x => (x.ReservationType == "Type A") ? 1 : 2)
.First());

LINQ for removing elements that are started with other element from list

I have a list List<string> with some paths.
C:\Dir\Test\
C:\MyDir\
C:\YourDir\
C:\Dir\
I want to go through all the elements (using LINQ) and remove entries that are started with other element from my list.
In my example C:\Dir\Test\ starts with C:\Dir\ - so I want to remove C:\Dir\Test\.
Use List<T>.RemoveAll() method:
sourceList.RemoveAll(x => sourceList.Any(y => x != y && x.StartsWith(y)));
Try this:
myInitialList.RemoveAll(x =>myInitialList.Any(q => q != x && q.StartsWith(x)));
Or if you want to keep the original list, this is a way to get all the records that do not match your criteria:
List<string> resultList = myInitialList.Except(x => myInitialList.Any(q => q != x && q.StartsWith(x)));
How about
mylist = mylist.Where(a => mylist.All(b => b == a || !a.StartsWith(b)))
.Distinct()
.ToList();
This will return a new list where there isn't another item in the list that it starts with.
It has the extra check to allow returning the value where there string is the same, otherwise all items would be removed from the list.
Finally the distinct call means that two occurrences of the same string are removed.
Building on nsinreal's comment and solution you could do something like
myList = myList.OrderBy(d => d)
.Aggregate(new List<string>(),
(list, item) => {
if (!list.Any(x => item.StartsWith(x)))
list.Add(item);
return list;
}).ToList();
This reduces the complexity of the solution by reducing the size of the search list for each test. It still requires an initial sort.
Personally I find this alternative solution harder to read and my first answer is more expressive the problem to solve.
The most efficient way is IMO to sort the paths, then iterate them and return only the ones not starting as one of the previous, i.e. :
public static IEnumerable<string>
GetRootPathsOfSet(this IEnumerable<string> paths)
{
var sortedSet = new SortedSet<string>(paths,
StringComparer.CurrentCultureIgnoreCase);
string currRoot = null;
foreach (var p in sortedSet)
{
if (currRoot == null ||
!p.StartsWith(currRoot, StringComparison.InvariantCultureIgnoreCase))
{
currRoot = p;
yield return currRoot;
}
}
}
Some notes:
All the paths MUST terminate with a trailing back-slash, otherwise the StartsWith approach is not safe (e.g. C:\Dir and C:\Directory)
This code uses case-insensitive comparison
I'm not using pure LINQ here, but it's an extension method

Linq lambda entities , does not contain definition

I have this query code here :
//Get all records based on ActivityID and TaskID.
public IList<Model.questionhint> GetRecords1(int listTask, int listActivity)
{
IList<Model.questionhint> lstRecords = context.questionhints.ToList();
return lstRecords.GroupBy(x => new { x.QuestionNo, x.ActivityID, x.TaskID }).Where(a => a.TaskID == listTask && a.ActivityID == listActivity).ToList();
}
The error lies in the .Where statement, it says does not contain definition for ActivityID and TaskID.
Full error :
'System.Linq.IGrouping' does not contain a definition for 'ActivityID' and no extension method 'ActivityID' accepting a first argument of type 'System.Linq.IGrouping' could be found (are you missing a using directive or an assembly reference?)
I am weak in query statements, basically I want to retrieve records from database where activity id = something and task id = something and group them by questionNo, activityId and Task ID .
The simplest fix here is: filter (where) before you group; this will also reduce the work that the grouping has to do:
return context.questionhints
.Where(a => a.TaskID == listTask && a.ActivityID == listActivity)
.GroupBy(x => new { x.QuestionNo, x.ActivityID, x.TaskID })
.ToList();
The reason it isn't working in your original code is, as already mentioned, that GroupBy returns a sequence of groups - each of which has a .Key (your anonymous type) and is itself an IEnumerable<T> sequence of the items in that group.
However! Your method claims to return IList<Model.questionhint>; your grouped data is not, and will never be, an IList<Model.questionhint> - it will be an IList<IGrouping<{some anonymous type, Model.questionhint>>. So: you cannot group like that if you are claiming that it is an IList<Model.questionhint> - and since the grouping is an anonymous type, you can't change the return type to match. You have two choices:
don't group
group by something declarable (a custom type, or a Tuple<...>), and change the return type to match
For example:
public IList<IGrouping<Tuple<int,int,int>,Model.questionhint>>
GetRecords1(int listTask, int listActivity)
{
return context.questionhints
.Where(a => a.TaskID == listTask && a.ActivityID == listActivity)
.GroupBy(x => Tuple.Create(x.QuestionNo, x.ActivityID, x.TaskID))
.ToList();
}
change it to Where(a => a.Key.TaskID == listTask && a.Key.ActivityID == listActivity)
You are dealing with an IGrouping, of which the Key property is the anonymous object new { x.QuestionNo, x.ActivityID, x.TaskID }
This solves the original error, but now we are trying to return IGroupings, which is not the correct return type. A cleaner way of doing it would be
var groups = lstRecords.GroupBy(x => new { x.QuestionNo, x.ActivityID, x.TaskID }).Where(a => a.Key.TaskID == listTask && a.Key.ActivityID == listActivity);
IList<Model.questionhint> questionHints = new List<Model.questionhint>();
foreach(var group in groups)
{
questionHints.AddRange(group);
}
return questionHints;
Note, this code is untested. You can do all this in one linq line (as im sure Mark will), however i tend to split it for readability
Alternative
If your goal is to get all the questionHints that match the criteria, what is wrong with a simple Where clause?
lstRecords.Where(a=>a.TaskID == listTask && a.ActivityID == listActivity).ToList();
References
http://msdn.microsoft.com/en-us/library/bb344977.aspx

Categories