简体   繁体   中英

How Can I Achieve this Using LINQ?

The best way I can describe what I'm trying to do is "Nested DistinctBy".

Let's say I have a collection of objects. Each object contains a collection of nicknames.

class Person
{
    public string Name { get; set; }
    public int Priority { get; set; }
    public string[] Nicknames { get; set; }
}

public class Program
{
    public static void Main()
    {
        var People = new List<Person>
        {
            new Person { Name = "Steve", Priority = 4, Nicknames = new string[] { "Stevo", "Lefty", "Slim" }},
            new Person { Name = "Karen", Priority = 6, Nicknames = new string[] { "Kary", "Birdie", "Snookie" }},
            new Person { Name = "Molly", Priority = 3, Nicknames = new string[] { "Mol", "Lefty", "Dixie" }},
            new Person { Name = "Greg", Priority = 5, Nicknames = new string[] { "G-man", "Chubs", "Skippy" }}
        };      
    }
}

I want to select all Persons but make sure nobody selected shares a nickname with another. Molly and Steve both share the nickname 'Lefty' so I want to filter one of them out. Only the one with highest priority should be included. If there is a highest priority tie between 2 or more then just pick the first one of them. So in this example I would want an IEnumerable of all people except Steve.

EDIT: Here's another example using music album instead of person, might make more sense.

class Album
{
   string Name {get; set;}
   int Priority {get;set;}
   string[] Aliases {get; set;}
{

class Program
{
var NeilYoungAlbums = new List<Album>
    {
        new Person{ Name = "Harvest (Remastered)", Priority = 4, Aliases = new string[] { "Harvest (1972)", "Harvest (2012)"}},
        new Person{ Name = "On The Beach", Priority = 6, Aliases = new string[] { "The Beach Album", "On The Beach (1974)"}},
        new Person{ Name = "Harvest", Priority = 3, Aliases = new string[] { "Harvest (1972)"}},
        new Person{ Name = "Freedom", Priority = 5, Aliases = new string[] { "Freedom (1989)"}}
    };
}

The idea here is we want to show his discography but we want to skip quasi-duplicates.

I would solve this using a custom IEqualityComparer<T> :

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

    public int Priority { get; set; }

    public string[] Nicknames { get; set; }
}

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

        return x.Nicknames.Any(i => y.Nicknames.Any(j => i == j));
    }

    // This is bad for performance, but if performance is not a
    // concern, it allows for more readability of the LINQ below
    // However you should check the Edit, if you want a truely 
    // LINQ only solution, without a wonky implementation of GetHashCode
    public int GetHashCode(Person obj) => 0;
}

// ...

var people = new List<Person>
{
    new Person { Name = "Steve", Priority = 4, Nicknames = new[] { "Stevo", "Lefty", "Slim" } },
    new Person { Name = "Karen", Priority = 6, Nicknames = new[] { "Kary", "Birdie", "Snookie" } },
    new Person { Name = "Molly", Priority = 3, Nicknames = new[] { "Mol", "Lefty", "Dixie" } },
    new Person { Name = "Greg", Priority = 5, Nicknames = new[] { "G-man", "Chubs", "Skippy" } }
};

var distinctPeople = people.OrderBy(i => i.Priority).Distinct(new PersonEqualityComparer());

EDIT:

Just for completeness, this could be a possible LINQ only approach:

var personNicknames = people.SelectMany(person => person.Nicknames
        .Select(nickname => new { person, nickname }));
var groupedPersonNicknames = personNicknames.GroupBy(i => i.nickname);
var duplicatePeople = groupedPersonNicknames.SelectMany(i => 
        i.OrderBy(j => j.person.Priority)
        .Skip(1).Select(j => j.person)
    );

var distinctPeople = people.Except(duplicatePeople);

A LINQ-only solution

var dupeQuery = people
    .SelectMany( p => p.Nicknames.Select( n => new { Nickname = n, Person = p } ) )
    .ToLookup( e => e.Nickname, e => e.Person )
    .SelectMany( e => e.OrderBy( p => p.Priority ).Skip( 1 ) );

var result = people.Except( dupeQuery ).ToList();

See .net fiddle sample

This works once, then you have to clear the set. Or store the results in a collection.

var uniqueNicknames = new HashSet<string>();

IEnumerable<Person> uniquePeople = people
    .OrderBy(T => T.Priority) // ByDescending?
    .Where(T => T.Nicknames.All(N => !uniqueNicknames.Contains(N)))
    .Where(T => T.Nicknames.All(N => uniqueNicknames.Add(N)));

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM