Get list of matching objects from 3 different lists - c#

I'm exercising on one task. Basically i got 3 different lists in C# code side. Class employee is containing the skills list. And also have list of employees as well.
for example :
class Employee
{
List<Skills> Skills;
}
class Skills
{
int Id;
int Name;
}
class Where_Im_Working_Currently
{
List<Employee> employees ;
}
What I'm trying to achieve is to get list of common skills from every individual. suppose we have 3 employees and all 3 employees have JAVA skill in common such that { id = x , name = JAVA }.
so all 3 employees have skills similar with id and name needs to be fetched out.
NOTE : I'm trying to get all matching skills and not just subset of skills
for e.g.
Case 1: Perfect match. (Get List having a, b, c)
list 1 => a, b, c
list 2 => a, b, c
list 3 => a, b, c
Case 1: No match. (Get Null list)
list 1 => a, b, c
list 2 => a, b,
list 3 => b, c
following is the query i have come up with :
var skills= employees.Select(x => x.Skills.Select(p => p.Id== x.Skill[0].Id && p.Name == x.Skill[0].Name));
but this will give IEnumerable that's where its getting wrong and im unable to form LINQ.
Any pointers or help are welcomed.

This might not be the most optimized way of doing this, but here's a solution that outputs the skills that are common for all employees.
The key is to use SelectMany to get to the child lists.
public class Employee
{
public List<Skills> Skills { get; set; }
}
public class Skills
{
public int Id { get; set; }
public string Name { get; set; }
}
[Test]
public void GetSomeSkills()
{
var employees = new List<Employee>
{
new Employee { Skills = new List<Skills> { new Skills { Id = 1, Name = "Java" }, new Skills { Id = 2, Name = "C#" } } },
new Employee { Skills = new List<Skills> { new Skills { Id = 1, Name = "Java" }, new Skills { Id = 3, Name = "Cooking" } } },
new Employee { Skills = new List<Skills> { new Skills { Id = 1, Name = "Java" } } },
//new Employee { Skills = new List<Skills> { new Skills { Id = 4, Name = "C++" } } }
};
var allSkills = employees.SelectMany(x => x.Skills).ToList();
Console.WriteLine(string.Join(", ", allSkills.Select(x => x.Name)));
// Output: Java, C#, Java, Cooking, Java
var commonSkills = employees.SelectMany(e =>
e.Skills.Where(s => employees.All(e2 => e2.Skills.Select(x => x.Id).Contains(s.Id)))).ToList();
Console.WriteLine(string.Join(", ", commonSkills.Select(x => x.Name)));
// Output: Java, Java, Java
}
If you uncomment the last Employee you would have a zero result, as there would no longer be a skill that is common for all employees.
Also you probably want to get distinct result, but it sounds like you already know how to do that.
Edit after original question was modified:
The below outputs only the skills that everyone has, if you uncomment the last Employee you would have null as result.
[Test]
public void GetSomeSkills()
{
var employees = new List<Employee>
{
new Employee { Skills = new List<Skills> { new Skills { Id = 1, Name = "Java" }, new Skills { Id = 2, Name = "C#" } } },
new Employee { Skills = new List<Skills> { new Skills { Id = 1, Name = "Java" }, new Skills { Id = 2, Name = "C#" } } },
// new Employee { Skills = new List<Skills> { new Skills { Id = 1, Name = "Java" } } },
};
bool HasSameSkills(Employee first, Employee second)
{
var firstIds = first.Skills.Select(x => x.Id).OrderBy(x => x).ToList();
var secondIds = second.Skills.Select(x => x.Id).OrderBy(x => x).ToList();
return firstIds.SequenceEqual(secondIds);
}
var commonSkills = employees.FirstOrDefault(x => employees.All(y => HasSameSkills(x, y)))?.Skills;
Console.WriteLine(string.Join(", ", (commonSkills ?? new List<Skills>()).Select(x => x.Name)));
// Output: Java, C#
}

Related

Group of objects with similar names

I have an object:
public class Student
{
public int Id { get; set; }
public string Name { get; set; }
}
I return a list that may look like the following:
var students = new List<Student>() {
new Student(){ Id = 1, Name="Bill85"},
new Student(){ Id = 2, Name="Bill66"},
new Student(){ Id = 3, Name="Ram7895"},
new Student(){ Id = 4, Name="Ram5566"},
new Student(){ Id = 5, Name="Join1230"}
};
I want to group them together if they have similar names, to get the result like below, is there any quick way to solve this?
GroupStudentList
UserList
Id = 1, Name="Bill85"
Id = 2, Name="Bill66"
UserList
Id = 3, Name="Ram7895"
Id = 4, Name="Ram5566"
UserList
Id = 3, Name="Join1230"
public class Student
{
public List<Student> ListGroupStudent {get; set;}
//add one list property...
}
var listGroup = listStudent.GroupBy(
x => x.Name,
(key, y) => new { ListGroupStudent = y.ToList() });
Making the assumption that similar names refers to identical names when all digits are removed from each name (as suggested by #klaus-gütter), a simple implementation could be the following:
Group the students based on their Name, but stripped for digits (using Regex)
For each group of similarily named students, select the students in that group to constitute a sublist
resulting in a nested List of Students:
List<List<Student>> groupStudentList = students
.GroupBy(
s => Regex.Replace(s.Name, "[0-9]*", string.Empty),
(similarName, students) => students.ToList())
.ToList();
The Regex expression above basically says "Replace all (if any) digits between 0 and 9 that exist in s.Name with an empty string". This means that if you have two students named "Tom1" and "T3o4m", they will also be grouped together -- because both names stripped for digits will be "Tom".
Optionally, you could create a UserList class:
public class UserList
{
public List<Student> Students { get; set; }
}
and create a UserList object for each grouping of students based on their name similarity:
List<UserList> groupStudentList = students
.GroupBy(
s => Regex.Replace(s.Name, "[0-9]*", string.Empty),
(similarName, students) => new UserList { Students = students.ToList() })
.ToList();

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

combining two LINQ expressions into one

In this case, I have two different LINQ expressions to get count from Products for two different conditions. I was just curious if there could be anyway of retrieving these two counts from one LINQ expression?
class Program
{
static void Main(string[] args)
{
List<Product> Products = new List<Product>()
{
new Product() { ID = 1 },
new Product() { ID = 2 },
new Product() { ID = 3 },
new Product() { ID = 4 },
new Product() { ID = 5 },
new Product() { ID = 6 }
};
int all = Products.Count();
int some = Products.Where(x => x.ID < 2).Count();
}
}
public class Product
{
public int ID { get; set; }
}
Using Aggregate you can avoid iterating through your collection twice:
var result = Products.Aggregate(new {a=0, s=0},(p,c) =>
{
return new { a = p.a + 1, s = c.ID < 2 ? p.s + 1 : p.s };
});
Now result.a == 6 and result.s == 2
You can, of course, create a class to hold your result if you want instead of using an anonymous type, and it works much the same way. That maybe easier to deal with if you have to return it from a function, for example.
So you could do something like:
public class CountResult
{
public int All { get; set; }
public int Some { get; set; }
}
public CountResult GetMyCount(IEnumerable<Product> products)
{
return products.Aggregate(new CountResult(), (p,c) =>
{
p.All++;
if (c.ID < 2) // or whatever you condition might be
{
p.Some++;
}
return p;
});
}
You can do this using a Tuple<int, int>:
var result = new Tuple<int, int>(Products.Count, Products.Where(x => x.ID < 2).Count());
And plese remark the use of Products.Count property which has O(1) complexity instead of O(N), so you don't have to worry at all about the performance of this implementation. For further reading you can check this post.

Using Custom IComparer to Order by Pointer value

I have a class that defines a student, and has a property (FollowedBy) that is immediately behind that student. I am looking for a way to order the students based on this linkage.
class Student
{
public int StudentID { get; set; }
public string Name { get; set; }
public int? FollowedBy { get; set; }
}
var lstStudents = new List<Student>()
{ new Student() { StudentID = 2, Name = "Mark", FollowedBy =4 },
new Student() { StudentID = 1, Name = "Sam", FollowedBy = 2},
new Student() { StudentID = 4, Name = "Fred", FollowedBy =null } ,
new Student() { StudentID = 3, Name = "Janice", FollowedBy = 1}};
for (var s in lstStudents.OrderBy(x => ????))
{
console.Write(s.Name);
}
// The output I'm looking for
// Janice
// Sam
// Mark
// Fred
public List<Student> GetOrderedStudents(List<Student> students)
{
Student[] reverseOrder = new Student[students.Count];
Student last = students.Single(s => s.FollowedBy == null);
reverseOrder[0] = last;
Student next = last;
for (var i = 1; i < students.Count; i++)
{
next = students.Single(s => s.FollowedBy == next.StudentID);
reverseOrder[i] = next;
}
return reverseOrder.Reverse().ToList();
}
What you're trying to do isn't strictly sorting, and it won't support certain sort algorithms that rely on comparitive principles like A > B > C => A > C without making the implementation of IComparer aware of the entire set. Such an IComparer is likely to run much slower than simply sorting using a search.
It seems it would be easier to make this into a helper method (extension method if you want to use linq-like syntax) which used its own mechanics in order to search the set for each FollowedBy/StudentID combo.
You can find the root and then follow FollowedBy:
Dictionary<int, Student> dict = lstStudents
.ToDictionary(item => item.StudentID);
// root
Student s = dict[lstStudents
.Select(item => item.StudentID)
.Except(lstStudents
.Where(item => item.FollowedBy.HasValue)
.Select(item => item.FollowedBy.Value))
.First()];
for (; s != null; s = s.FollowedBy == null? null : dict[s.FollowedBy.Value]) {
Console.WriteLine(s.Name);
}

LinQ nested lists and nested selects

Consider these two tables:
ClassID Name
1 C1
2 C2
ClassID List<CourseSession>
1 [Object that has value "A"], [Object that has value "B"]
2 [Object that has value "B"], [Object that has value "C"]
When I join these two tables in Linq, I get:
ID Name List
1 C1 [A, B]
2 C2 [A, B]
Wheras I need to expand them:
ID Name List
1 C1 A
1 C1 B
2 C2 A
2 C2 B
Linq code:
var classes = from row in t.AsEnumerable()
select new
{
ClassID = row.Field<Guid>("ClassID"),
ClassName = row.Field<string>("Name"),
};
var classCourses = from row in classes.AsEnumerable()
select new
{
ID = row.ID,
CourseSessionList = GetAllCoursesByID(row.ID).AsEnumerable()
};
//Attempt to join
var expandedClassCourse = from classRow in classes
join ccRow in classCourses
on classRow.ID equals ccRow.ID
into filteredExpandedClasses
select filteredExpandedClasses;
I'm not sure how to achieve this. Any ideas?
Something like (not sure what your model looks like):
context.CouseSessions.Where(cs => /* condition goes here */)
.Select(cs =>
new
{
Name = cs.Name,
Class = cs.Class.Name
});
or
context.Classes.Where(c => /* condition goes here */)
.SelectMany(c => c.Courses)
.Select(cs =>
new
{
Name = cs.Name,
Class = cs.Class.Name
});
I created two models based on assumption. I hope this helps.
class Info
{
public int Id { get; set; }
public string Name { get; set; }
public List<string> List { get; set; }
}
class MyClass
{
public int Id { get; set; }
public string Name { get; set; }
public string s { get; set; }
}
static void Main(string[] args)
{
var infos = new List<Info> { new Info { Id = 1, Name = "c1", List = new List<string> { "A", "B" } }, new Info { Id = 2, Name = "c2", List = new List<string> { "A", "B" } } };
var myClasses = new List<MyClass>();
foreach (var info in infos)
{
myClasses.AddRange(info.List.Select(a => new MyClass { Id = info.Id, Name = info.Name, s = a }));
}
}
(from c in classList
join s in sessionList on c.ClassID equals s.ClassID
select new
{
ID = c.ClassID,
Name = c.Name,
SessionList = s.SessionList
})
.SelectMany(e => e.SessionList.Select(s => new
{
ID = e.ClassID,
Name = e.Name,
Session = s
}))

Categories

Resources