9

I have a List of custom a datatype, simplified example (myMovies):

public class Movie
{
    public Int32 TVIndex;
    public string MovieName;
    public string MovieRating;
    public string MovieRuntime;
    public List<Actor> MovieActors;
    public List<MovieArt> MovieImages;
}



public class Actor
{
    public string ActorName;
    public string ActorRole;
}

public class MovieArt
{
    public string ImagePath;
}


List<Movie> myMovies = new List<Movie>(); 

Now I am trying to remove all duplicates from myMovies but ignoring TVIndex.

I have tried looking at

List<Movie> myDistinctMovies = myMovies.Distinct().ToList(); 

But cannot figure out how to ignore TvIndex. Is this possible?

Fred
  • 5,368
  • 4
  • 38
  • 67

3 Answers3

3

Based on @Grundy's answer, including implementation:

public class MovieEqualityComparer : IEqualityComparer<Movie>
{

    public bool Equals(Movie x, Movie y)
    {
        if ( x == null )
            return y == null;

        if ( y == null )
            return x == null;

        if ( object.ReferenceEquals(x, y) )
            return true;

        if ( !string.Equals(x.MovieName, y.MovieName) )
            return false;

        if ( !string.Equals(x.MovieRating, y.MovieRating) )
            return false;

        if ( !string.Equals(x.MovieRuntime, y.MovieRuntime) )
            return false;

        if ( !Enumerable.SequenceEqual(x.MovieActors, y.MovieActors) )
            return false;

        if ( !Enumerable.SequenceEqual(x.MovieImages, y.MovieImages) )
            return false;

        return true;
    }

    public int GetHashCode(Movie obj)
    {
        if ( obj == null )
            throw new ArgumentNullException();

        unchecked
        {
            int hash = 17;
            hash     = hash * 31 + ((obj.MovieName    == null) ? 0 : obj.MovieName.GetHashCode());
            hash     = hash * 31 + ((obj.MovieRating  == null) ? 0 : obj.MovieRating.GetHashCode());
            hash     = hash * 31 + ((obj.MovieRuntime == null) ? 0 : obj.MovieRuntime.GetHashCode());

            if ( obj.MovieActors != null )
            {
                foreach ( var actor in obj.MovieActors )
                    hash = hash * 31 + ((actor == null) ? 0 : actor.GetHashCode());
            }

            if ( obj.MovieImages != null )
            {
                foreach ( var image in obj.MovieImages )
                    hash = hash * 31 + ((image == null) ? 0 : image.GetHashCode());
            }

            return hash;
        }
    }
}

Usage is the same:

List<Movie> myMovies = new List<Movie>
    {
        // ...
    }; 

List<Movie> myDistinctMovies = myMovies.Distinct(new MovieEqualityComparer()).ToList(); 

EDIT

As pointed out by @Tim, you have to do something quite similar for your other custom types if you want to compare by anything other than reference equality.

public class Actor : IEquatable<Actor>
{
    public string ActorName;
    public string ActorRole;

    public override bool Equals(object obj)
    {
        return this.Equals(obj as Actor);
    }

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

        if ( object.ReferenceEquals(this, other) )
            return true;

        if ( !string.Equals(this.ActorName, other.ActorName) )
            return false;

        if ( !string.Equals(this.ActorRole, other.ActorRole) )
            return false;

        return true;
    }

    public override int GetHashCode()
    {
        unchecked
        {
            int hash = 17;
            hash     = hash * 31 + ((ActorName == null) ? 0 : ActorName.GetHashCode());
            hash     = hash * 31 + ((ActorRole == null) ? 0 : ActorRole.GetHashCode());

            return hash;
        }
    }
}

public class MovieArt : IEquatable<MovieArt>
{
    public string ImagePath;

    public override bool Equals(object obj)
    {
        return this.Equals(obj as MovieArt);
    }

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

        if ( object.ReferenceEquals(this, other) )
            return true;

        if ( !string.Equals(this.ImagePath, other.ImagePath) )
            return false;

        return true;
    }

    public override int GetHashCode()
    {
        unchecked
        {
            int hash = 17;
            hash     = hash * 31 + ((ImagePath == null) ? 0 : ImagePath.GetHashCode());

            return hash;
        }
    }
}
Steven Liekens
  • 10,761
  • 4
  • 50
  • 77
  • in `GetHashCode` possibly need add check for `null` property value – Grundy Feb 10 '14 at 12:35
  • @Grundy I changed the algorithm :) – Steven Liekens Feb 10 '14 at 12:51
  • yep! that what i mean. But what the magic numbers `17` and `31`? – Grundy Feb 10 '14 at 12:52
  • It's a trick that I pulled off the internet that reduces the chance of hashcode collisions. I believe that you can take any set of prime numbers instead of 17 and 31. The algorithm remains the same. – Steven Liekens Feb 10 '14 at 12:57
  • 1
    This won't work because `MovieActors` and `MovieImages` contain custom types which also need to override `Equals` and `GetHashCode`. The `GethashCode` implementation should also accumulate the `GetHashCode` of every image and actor instead of the list's. – Tim Schmelter Feb 10 '14 at 13:02
  • @StevenLiekens you are not joking! I was hoping for an "Ignore this field" flag ;) – Fred Feb 10 '14 at 13:47
  • @StevenLiekens I have tried this but its not working and I cannot see why. If I put a break point in the GetHashCode code it is hit, however the breakpoint in the Equals code is not. Should I be able to break the code here? – Fred Feb 10 '14 at 16:02
  • When the hash codes for `x` and `y` are identical, the framework skips the ´Equals()´ call. Is that what's going on? If so, then `x` and `y` should indeed be identical. – Steven Liekens Feb 10 '14 at 16:40
  • Equals will be used only if GetHashCode was the same, so GetHashCode is a quick pre-check. – Tim Schmelter Feb 10 '14 at 18:07
  • Woops, I said it the other way around :x – Steven Liekens Feb 10 '14 at 18:31
  • @StevenLiekens This works perfectly! Thanks for everyones help – Fred Feb 11 '14 at 09:05
2

you can use Distinct with EqualityComparer something like this

public class MoviesEqualityComparer : IEqualityComparer<Movie>
{
    public bool Equals(Movie x, Movie y)
    {
        return ..../* check all needed fields */

    }

    public int GetHashCode(Movie obj)
    {
        return .... /* get hashcode for movie objec*/
    }
}

and use it like

List<Movie> myDistinctMovies = myMovies.Distinct(new MoviesEqualityComparer()).ToList(); 
Grundy
  • 13,060
  • 3
  • 33
  • 51
-1

you could use a LINQ Query may be ...

List<Movie> myDistinctMovies  = (myMovies .GroupBy(p =>p.MovieName,
                                                   p =>p.MovieRating,p=>p.MovieRuntime)
                                          .Select(g => g.First())
                                          .ToList());

Thanks..

rawatdeepesh
  • 584
  • 8
  • 26
  • This is my personal preference because it doesn't require creating an implementation of IEqualityComparer (yuck). – Hezi Feb 10 '14 at 12:52
  • Thanks , i feel it is easier and precise too! – rawatdeepesh Feb 10 '14 at 12:57
  • 1
    But this ignores the rating, runtime, actors and images. -1 – Tim Schmelter Feb 10 '14 at 13:00
  • @Tim Schmelter: add the other fields to your group by. I remember that the syntax for multi-key grouping was a bit quirky, but you can find examples. See: [link](http://stackoverflow.com/questions/847066/group-by-multiple-columns) – Hezi Feb 10 '14 at 13:07
  • 1
    @Hezi: that would work if it was a single object but the actors and images are lists with custom types. – Tim Schmelter Feb 10 '14 at 13:15