Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How Can I Achieve this Using LINQ?

Tags:

c#

.net

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.

like image 863
Sean O'Neil Avatar asked Jul 07 '19 08:07

Sean O'Neil


People also ask

What types of objects can you query using LINQ?

You can use LINQ to query any enumerable collections such as List<T>, Array, or Dictionary<TKey,TValue>. The collection may be user-defined or may be returned by a . NET API.

How do I run a SQL query in LINQ?

Add a LINQ to SQL class file. Drag and drop the respective table. Now, copy this code in the main method. We are creating an instance of sample datacontext class and then we are using this ExecuteQuery method to execute the SQL query.


2 Answers

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);
like image 168
Tobias Tengler Avatar answered Oct 01 '22 07:10

Tobias Tengler


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

like image 27
Sir Rufo Avatar answered Oct 01 '22 05:10

Sir Rufo