0

i have an list of persons with basecode and an array of locations. i need to eliminate the persons in the list having different basecode with same locations and keep persons with differend locations.

i tried using IEqualityComparer, and group by in linq, but i didn't succeed. can you guys please advice me how to do it ? this is my class structure

public class Person
{
    public string Name { get; set; }

    public List<Location> Locations { get; set; }
}
public class Location
{
    public string Name { get; set; }
    public string BaseCode { get; set; }
}

data example

Person 1
Name : John

Locations :
      [0]  Name : India , BaseCode : "AA12"
      [1] Name : USA ,BaseCode : "AA14"
Person 2
Name : John

Locations :
      [0]  Name : India, BaseCode : "AA13"
      [1] Name : USA ,BaseCode : "AA14"
Person 3
Name : John

Locations :
      [0]  Name : India, BaseCode : "AA16"
      [1] Name : UK , BaseCode : "AA17"

I want to filter Person 2 from my list and keep person 1 and person 3. Please advice

Vilsad P P
  • 1,529
  • 14
  • 23

4 Answers4

1

Disclaimer: this solution doesn't specifically handle the same BaseCode with different/same locations; you didn't mention anything about this in your requirements.


IEqualityComparer<T> Route

The important parts here are the IEqualityComparer<T> implementations for both Person and Location:

class Program
{
    static void Main(string[] args)
    {
        var p1 = new Person {Name ="John", BaseCode="AA12", Locations = new List<Location>
        {
            new Location { Name = "India" },
            new Location { Name = "USA" }
        }};

        var p2 = new Person {Name ="John", BaseCode="AA13", Locations = new List<Location>
        {
            new Location { Name = "India" },
            new Location { Name = "USA" }
        }};

        var p3 = new Person {Name ="John", BaseCode="AA14", Locations = new List<Location>
        {
            new Location { Name = "India" },
            new Location { Name = "UK" }
        }};

        var persons = new List<Person> { p1, p2, p3 };

        // Will not return p2.
        var distinctPersons = persons.Distinct(new PersonComparer()).ToList();

        Console.ReadLine();
    }
}

public class PersonComparer : IEqualityComparer<Person>
{
    public bool Equals(Person x, Person y)
    {
        if (x == null || y == null)
            return false;

        bool samePerson = x.Name == y.Name;

        bool sameLocations = !x.Locations
            .Except(y.Locations, new LocationComparer())
            .Any();

        return samePerson && sameLocations;
    }

    public int GetHashCode(Person obj)
    {
        return obj.Name.GetHashCode();
    }
}

public class LocationComparer : IEqualityComparer<Location>
{
    public bool Equals(Location x, Location y)
    {
        if (x == null || y == null)
            return false;

        return x.Name == y.Name;
    }

    public int GetHashCode(Location obj)
    {
        return obj.Name.GetHashCode();
    }
}

The PersonComparer uses the linq Except extension supplying the LocationComparer to produce a list of differences between two lists of locations.

The PersonComparer then feeds into the linq Distinct method.


IEquatable<T> Route

If you need to work with BaseCode being different counting towards being a "match", I don't think this route would work because of GetHashCode not giving you an opportunity to distinguish values.

An alternative solution is to implement IEquatable<T> on the classes themselves and also override GetHashCode, Distinct and Except will then honour this implementation:

public class Person : IEquatable<Person>
{
    public string Name { get; set; }
    public string BaseCode { get; set; }
    public List<Location> Locations { get; set; }

    public bool Equals(Person other)
    {
        if (other == null)
            return false;

        bool samePerson = Name == other.Name;

        // This is simpler because of IEquatable<Location>
        bool sameLocations = !Locations.Except(other.Locations).Any();

        return samePerson && sameLocations;
    }

    public override int GetHashCode()
    {
        return Name.GetHashCode();
    }
}

public class Location : IEquatable<Location>
{
    public string Name { get; set; }

    public bool Equals(Location other)
    {
        if (other == null)
            return false;

        return Name == other.Name;
    }

    public override int GetHashCode()
    {
        return Name.GetHashCode();
    }
}

Which results in a simpler call:

var distinctPersons = persons.Distinct().ToList();
Adam Houldsworth
  • 63,413
  • 11
  • 150
  • 187
  • thanks for the reply, i edited my question, i made a big mistake in the structe, can you please check once more ? – Vilsad P P Jul 10 '12 at 13:46
  • The one thing you need to change is that `BaseCode` appears to be a member of `Location`, not `Person`. The question seems to imply that the base code should be ignored for comparing `Locations`, so the `LocationComparer` is still correct. – Joshua Honig Jul 10 '12 at 13:49
  • @jmh_gr When I made the answer, the `BaseCode` was on person. But my implementation doesn't do anything with `BaseCode` as the requirements don't specify what to do if `BaseCode` is equal. – Adam Houldsworth Jul 10 '12 at 13:50
  • @user783662 Other than my sample object code, the solution still stands as `BaseCode` isn't used directly. You didn't specify how to handle when `BaseCode` is equal. – Adam Houldsworth Jul 10 '12 at 13:53
0

You can make use of IEquatable Interface, and override Equal and GetHashCode method like this:

EDIT AFTER CHANGE IN QUESTION:

public class Location : IEquatable<Location>
{    
       public string Name { get; set; }     
       public string BaseCode { get; set; 

        public bool Equals(Location other)
        {
            if (Object.ReferenceEquals(other, null)) return false;

            if (Object.ReferenceEquals(this, other)) return true;
            return BaseCode.Equals(other.BaseCode);
        }

        public override int GetHashCode()
        {
            return BaseCode.GetHashCode();
        }


} 

So, now you can make use of Distinct on the List of Person, and it will only return distinct name and BaseCode.

 var distinctListPerson = PersonList.Distinct().ToList();

You can read about this interface from MSDN

Ebad Masood
  • 2,389
  • 28
  • 46
  • thanks for the reply, i edited my question, i made a big mistake in the structe, can you please check once more ? – Vilsad P P Jul 10 '12 at 13:45
0

Adam's solutions are more "proper" way of handling it. But if you want to do it with LINQ, then something like this shoud also do it (note that the code expects locations to be ordered and takes strings as identifiers):

persons
    .GroupBy(x => x.Name)
    .SelectMany(x => x)
        .GroupBy(y => string.Concat(y.Locations.Select(z => z.Name)))
    .SelectMany(x => x
        .GroupBy(y => string.Concat(y.Locations.Select(z => z.BaseCode)))
    .Select(x => x.First());
doblak
  • 3,036
  • 1
  • 27
  • 22
0

I'd be tempted to write something like the following. I've not checked that the y.Locations.Equals() works, but it should be simple to replace it for something that does the same job.

    List<Person> personList = new List<Person>();
    List<Person> deduplicatedPersonList = new List<Person>();
    personList.ForEach(x =>
    {
        Person existingPerson = personList.Find(y =>
        {
            if (y.Locations.Equals(x.Locations))
                return false;
            return true;
        });
        if (existingPerson == null)
            deduplicatedPersonList.Add(x);
    });
Rob Gough
  • 11
  • 4
  • thanks for the reply, i edited my question, i made a big mistake in the structe, can you please check once more ? In this case the locations.equal will not work. – Vilsad P P Jul 10 '12 at 13:46