Linq group the columns combination to a two list - c#

I have question about how I could use Linq grouping the same combination that the list has then relate to a two list.
Example:
I have theses classes.
public class PetCategoryOwner
{
public string PetCategory { get; set; }
public string Owner { get; set; }
}
public class PetCategoriesOwners
{
public IEnumerable<string> PetCategories { get; set; }
public IEnumerable<string> Owners { get; set; }
}
The example data.
Owner
Pet Category
Higa
Terry
Higa
Charlotte
Oliver
Terry
Oliver
Charlotte
Oliver
Chausie
Price
Chausie
Liam
Terry
Liam
Chartreux
var petCategoryOwner = new List<PetCategoryOwner>()
{
new PetCategoryOwner { Owner = "Higa", PetCategory = "Terry"},
new PetCategoryOwner { Owner = "Higa", PetCategory = "Charlotte"},
new PetCategoryOwner { Owner = "Oliver", PetCategory = "Terry"},
new PetCategoryOwner { Owner = "Oliver", PetCategory = "Charlotte"},
new PetCategoryOwner { Owner = "Oliver", PetCategory = "Chausie"},
new PetCategoryOwner { Owner = "Price", PetCategory = "Chausie"},
new PetCategoryOwner { Owner = "Liam", PetCategory = "Terry"},
new PetCategoryOwner { Owner = "Liam", PetCategory = "Chartreux"}
};
Expected values
Owner
Pet Category
Group
Higa
Terry
A
Higa
Charlotte
A
Oliver
Terry
A
Oliver
Charlotte
A
Oliver
Chausie
B
Price
Chausie
B
Liam
Terry
C
Liam
Chartreux
C
var petCategoriesOwners = new List<PetCategoriesOwners>()
{
new PetCategoriesOwners()
{
PetCategories = new List<string>() { "Terry", "Charlotte" },
Owners = new List<string>() { "Oliver", "Higa" }
},
new PetCategoriesOwners()
{
PetCategories = new List<string>() { "Chausie" },
Owners = new List<string>() { "Oliver", "Price" }
},
new PetCategoriesOwners()
{
PetCategories = new List<string>() { "Chartreux", "Terry" },
Owners = new List<string>() { "Liam" }
}
}

In order to solve your problem you need to do two steps: group by owners and merge owners based on the fact other group's set is a subset of current owner. You can try to achieve it by running below LINQ query:
public class PetCategoriesOwners
{
public List<string> PetCategories { get; set; }
public List<string> Owners { get; set; }
}
var petCategoriesOwners = petCategoryOwner
.GroupBy(x => x.Owner)
.Select(x => new
{
Owner = x.Key,
Categories = x.Select(y => y.PetCategory)
})
.OrderBy(x => x.Categories.Count())
.Aggregate(new List<PetCategoriesOwners>(), (acc, current) =>
{
var currentCategories = current.Categories.ToList();
var matches = acc.Where(group => group.PetCategories.All(x => currentCategories.Contains(x)));
foreach(var match in matches)
{
match.Owners.Add(current.Owner);
currentCategories = currentCategories.Except(match.PetCategories).ToList();
}
if (currentCategories.Any())
{
acc.Add(
new PetCategoriesOwners() {
Owners = new List<string>() { current.Owner },
PetCategories = currentCategories
});
}
return acc;
});
So it's important to group by Owner, process groups in ascending order in terms of length. The Aggregate method basically tries to find if previosly entered item overlaps with currently processed one. If it happens then we take those intersecting elements, add owner there and remove those from current element. If any element is left then we create own group for such owner.
Edit: .NET Fiddle

Note: This answer is based on a false interpretation of the question post. In it, I have assumed that petCategoriesOwners is an input and that the content of the table containing the Group column is the expected output.
You could achieve what you want to do by using a combination of SelectMany(), .Select(), .Where() and .Contains() from System.Linq.
Slightly simplified, by using ints for the group value rather than char, this is a possible implemenation:
public class GroupedOwnerAndPetCategory
{
public string Owner { get; set; }
public string PetCategory { get; set; }
public int Group { get; set; }
}
var grouped = petCategoriesOwners
.SelectMany((ownerPetMix, index) => petCategoryOwner
.Where(ownerPetPair =>
ownerPetMix.Owners.Contains(ownerPetPair.Owner) &&
ownerPetMix.PetCategories.Contains(ownerPetPair.PetCategory))
.Select(ownerPetPair => new GroupedOwnerAndPetCategory
{
Owner = ownerPetPair.Owner,
PetCategory = ownerPetPair.PetCategory,
Group = index
}))
.ToList();
Using your example input, grouped contains the following entries:
Higa Terry 0
Higa Charlotte 0
Oliver Terry 0
Oliver Charlotte 0
Oliver Chausie 1
Price Chausie 1
Liam Terry 2
Liam Chartreux 2
Example fiddle here.

Related

Remove from a list that has a list within it based on integer list

I have a list that basically look like this...
public class Area
{
public int Id { get; set; }
public string Name { get; set; }
public List<ZipCodeAdresses> ListOfIncludedDestinations { get; set; }
}
public class ZipCodeAdresses
{
public int AreaId { get; set; }
public List<Person> AdressList { get; set; }
}
public class Person
{
public string MottagarNamn { get; set; }
public string Street { get; set; }
}
var intListToRemove = new List<int>(){2,3};
var list = new List<Area>();
var subList = new List<ZipCodeAdresses>();
var personList = new List<Person>
{
new Person() {MottagarNamn = "User 1"},
new Person() {MottagarNamn = "User 2"}
};
subList.Add(new ZipCodeAdresses(){AdressList = personList , AreaId = 1});
personList = new List<Person>
{
new Person() {MottagarNamn = "User 3"},
new Person() {MottagarNamn = "User 4"}
};
subList.Add(new ZipCodeAdresses() { AdressList = personList, AreaId = 2 });
list.Add(new Area(){Name = "List A", ListOfIncludedDestinations = subList});
subList = new List<ZipCodeAdresses>();
personList = new List<Person>
{
new Person() {MottagarNamn = "User 5"},
new Person() {MottagarNamn = "User 6"}
};
subList.Add(new ZipCodeAdresses() { AdressList = personList, AreaId = 3 });
personList = new List<Person>
{
new Person() {MottagarNamn = "User 7"},
new Person() {MottagarNamn = "User 8"}
};
subList.Add(new ZipCodeAdresses() { AdressList = personList, AreaId = 4 });
list.Add(new Area() { Name = "List B", ListOfIncludedDestinations = subList });
I need to be able to remove from the list ListOfIncludedDestinations where AreaId is equal to any integer in intListToRemove which in this example is 2 and 3?
List<T> contains a method RemoveAll, that removes all entries that fulfill a certain condition. In your case it is:
foreach(var entry in list)
{
entry.ListOfIncludedDestinations.RemoveAll(x => intListToRemove.Contains(x.AreaId));
}
This loops through your list, and for every entry it removes all entries in ListOfIncludedDestinations that have an AreadId which is in intListToRemove.
Online demo: https://dotnetfiddle.net/ialnPb
You should add this sample code to remove them from the list :
foreach (var i in list)
i.ListOfIncludedDestinations.RemoveAll(o => intListToRemove.Contains(o.AreaId));

how to grab all the owners of a name

I have one plain class data like,
var data = new List<PlainData>
{
new PlainData {Name = "A", Owner = "X"},
new PlainData {Name = "A", Owner = "Y"},
new PlainData {Name = "B", Owner = "X"}
};
Here for same Name I have one or more than one owner.
Now I want to transform this data into list based owners like below class,
public class ListBasedData
{
public string Name { get; set; }
public List<string> Owners { get; set; }
}
And here I am trying to do, how to grab all the owners of a name?
List<ListBasedData> listBasedDatas = new List<ListBasedData>();
var groups = data.GroupBy(a => a.Name);
foreach (var group in groups)
{
var a = group.Key;
var b = group.ToList();
listBasedDatas.Add(new ListBasedData{Name = group.Key, Owners = });
}
List<ListBasedData> listBasedDatas = data
.GroupBy(a => a.Name)
.Select(grp => new ListBasedData
{
Name = grp.Key,
Owners = grp.Select(x => x.Owner).ToList()
})
.ToList();
The key is to use Select to perform a projection on the owners in each group, from PlainData to string.

Visual Studio 2019 Intellisense for Linq Joined To Class Not Appearing

My VS intellisense does not work when i do lambda queries, like Join, GroupJoin, etc. The properties of the second model never appear in the suggestions. I'm sorry for my english :)
See the images:
As #JeroenMostert said, this is a known bug. If you really want the intellisense, you can specify your types; with result2 you'll get intellisense.
You just have to decide if having to explicitly set your types is worth it, especially as it means you can't really return an anonymous object.
Personally, I don't think making your code more verbose is worth it as not having intellisense won't prevent you from setting up your lambda.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ConsoleApp1
{
class Program
{
static void Main(string[] args)
{
var people = new List<Person>();
var employees = new List<Employee>();
var result = employees.Join(people, x => x.Id, y => y.Id, (x, y) => new JoinedItem{ Id = x.Id, Name = y.Name });
var result2 = employees.Join<Employee, Person, int, JoinedItem>(people, x => x.Id, y => y.Id, (x, y) => new JoinedItem { Id = x.Id, Name = y.Name });
}
}
public class Person
{
public int Id { get; set; }
public string Name { get; set; }
}
public class Employee
{
public int Id { get; set; }
public string Name { get; set; }
}
public class JoinedItem
{
public int Id { get; set; }
public string Name { get; set; }
}
}
Apparently there is a workaround, by placing the second key selector and the result selector between brackets ().
class Person
{
public string Name { get; set; }
}
class Pet
{
public string Name { get; set; }
public Person Owner { get; set; }
}
public static void JoinEx1()
{
Person magnus = new Person { Name = "Hedlund, Magnus" };
Person terry = new Person { Name = "Adams, Terry" };
Person charlotte = new Person { Name = "Weiss, Charlotte" };
Pet barley = new Pet { Name = "Barley", Owner = terry };
Pet boots = new Pet { Name = "Boots", Owner = terry };
Pet whiskers = new Pet { Name = "Whiskers", Owner = charlotte };
Pet daisy = new Pet { Name = "Daisy", Owner = magnus };
List<Person> people = new List<Person> { magnus, terry, charlotte };
List<Pet> pets = new List<Pet> { barley, boots, whiskers, daisy };
var query =
people.Join(pets,
person => person,
(pet => pet.Owner), // intellisense here
((person, pet) => // intellisense here
new { OwnerName = person.Name, Pet = pet.Name }));
Afterwards the brackets can be removed, but intellisense helps a lot on complicated object structures.

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)));

get elements from list based on another list

I got two classes, like:
public class Person
{
public long Id { get; set; }
public string Name { get; set; }
}
public class Vampire
{
public long Id { get; set; }
}
Then, I have two lists, a list of persons and a list of vampires. All vampires are persons.
What I need is two children lists of persons, infected and notInfected. I'm building the two lists with a for, but I know it's possible using linq or something.
Any help?
Something like this:
var vampireIds = new HashSet<long>(vampireList.Select(x => x.Id));
var infectedPersons = personList.Where(x => vampireIds.Contains(x.Id));
var regularPersons = personList.Where(x => !vampireIds.Contains(x.Id));
I would go with something like the following:
void Main()
{
var list = new List<Person>(){ new Person(){ Id = 1 }, new Vampire(){ Id = 2 } };
var infected = list.Where (x => x is Vampire);
var notInfected = list.Except(infected);
}
public class Person
{
public long Id { get; set; }
public string Name { get; set; }
}
public class Vampire : Person
{
}
If only a person can be a Vapire, you could inherit Vampire from Person and then iterate through all persons and see if they are Vampires; if yes -> add to Vampire list, otherwise to non-Vampire list.
Try this:
var people = new List<Person>
{
new Person {Id = 1, Name = "John"},
new Person {Name = "Dave", Id = 2},
new Person {Id = 3, Name = "Sarah"}
};
var vamps = new List<Vampire> {new Vampire {Id = 1}};
var theInfected = people.Where(p => vamps.Select(v => v.Id).Contains(p.Id));
var theAfraid = people.Except(theInfected);
foreach (var person in theInfected)
{
System.Console.WriteLine(person.Name + " Is Infected!");
}
foreach (var person in theAfraid)
{
System.Console.WriteLine(person.Name + " Is Afraid!");
}
Hope it's helpful.

Categories

Resources