Using Custom IComparer to Order by Pointer value - c#

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

Related

c# cross join two same type of lists

So here I have some code, which works ok. But I want to change the select part to something else, I am not sure what other methods I can use any help would be appreciated.
using System;
using System.Linq;
using System.Collections.Generic;
public class Program
{
public static void Main()
{
var all = new List<People>{new People{Id = 1, Name = "andy1", Age = null}, new People{Id = 2, Name = "andy2", Age = null}, new People{Id = 3, Name = "andy3", Age = null}, new People{Id = 4, Name = "andy4", Age = null}, };
var someOfThem = new List<People>{new People{Id = 1, Name = null, Age = 1}, new People{Id = 2, Name = null, Age = 1},new People{Id = 3, Name = null, Age = 1}};
var test = someOfThem.Select(c =>
{
c.Name = all.Find(a => a.Id == c.Id).Name;
return c;
});
foreach (var item in test)
Console.WriteLine("{0}={1}={2}", item.Id, item.Name, item.Age);
}
}
public class People
{
public int Id
{
get;
set;
}
public int? Age
{
get;
set;
}
public string Name
{
get;
set;
}
}
And here is the result.
1=andy1=1
2=andy2=1
3=andy3=1
I am just wondering is there another way to achieve the same result but a more elegant way? or an easier way?
var test = someOfThem.Select(c =>
{
c.Name = all.Find(a => a.Id == c.Id).Name;
return c;
});
Update
Sorry I did not show my problem properly at first, I have updated my quesiton. Please have a look again.
You can use C#'s LINQ keywords and more specifically, the join keyword assosciated with it:
using System;
using System.Linq;
using System.Collections.Generic;
public class Program
{
public static void Main()
{
var all = new List<People> { new People { Id = 1, Name = "andy1", }, new People { Id = 2, Name = "andy2", }, new People { Id = 3, Name = "andy3", }, new People { Id = 4, Name = "andy4", }, };
var someOfThem = new List<People> { new People { Id = 1, Name = null, }, new People { Id = 2, Name = null, } };
var test = from item in someOfThem
join element in all on item.Id equals element.Id
select element;
foreach (var item in test)
Console.WriteLine("{0}={1}", item.Id, item.Name);
}
}
public class People
{
public int Id
{
get;
set;
}
public string Name
{
get;
set;
}
}
The code version would be
var test = someOfThem.Join(all, item => item.Id, element => element.Id, (item, element) => element);
as shown in Robert's comment
You can use the Join (you can also use a dictionary, but I'm not going to show it):
Here's the syntax for join:
var test = someOfThem.Join(all, item => item.Id, element => element.Id,
(item, element) => new Person {
Id = item.Id ?? element.Id,
Name = item.Name ?? element.Name,
Age = item.Age ?? element.Age
});
You can implement Equals and GetHashCode in your People class and use Intersect.
Or, create an EqualityComparer, that way your comparison logic is decoupled:
class Program
{
public static void Main()
{
var all = new List<People> { new People { Id = 1, Name = "andy1", }, new People { Id = 2, Name = "andy2", }, new People { Id = 3, Name = "andy3", }, new People { Id = 4, Name = "andy4", }, };
var someOfThem = new List<People> { new People { Id = 1, Name = null, }, new People { Id = 2, Name = null, } };
var test = all.Intersect(someOfThem, new PeopleIdComparer()).ToList();
foreach (var item in test)
Console.WriteLine("{0}={1}", item.Id, item.Name);
}
}
public class PeopleIdComparer : IEqualityComparer<People>
{
public bool Equals(People x, People y)
{
return x.Id == y.Id;
}
public int GetHashCode(People obj)
{
return HashCode.Combine(obj.Id);
}
}
public class People
{
public int Id
{
get;
set;
}
public string Name
{
get;
set;
}
}

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

Code review of this portion of Linq Filters

I am a newbie of c #, I would like to know if I can remove the for each and do a single operation with Linq. I would like to return an IEnumerable with already filtered. is it possible to do this? Every suggestion is welcome, thank you very much
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Linq
{
class Oggetto
{
public int Id { get; set; }
public string MyProperty { get; set; }
public int Deleted { get; set; }
}
class Program
{
static void Main(string[] args)
{
IEnumerable<Oggetto> lista = new List<Oggetto> {
new Oggetto(){ Id = 1, MyProperty = "Propr1", Deleted = 0 },
new Oggetto(){ Id = 1, MyProperty = "Propr2", Deleted = 1 },
new Oggetto(){ Id = 2, MyProperty = "Prop3", Deleted = 0 },
new Oggetto(){ Id = 3, MyProperty = "Propr4", Deleted = 0 },
new Oggetto(){ Id = 3, MyProperty = "Prop5", Deleted = 1 }
};
foreach (var item in lista.Where(x => x.Deleted == 1).GroupBy(x => x.Id).Select(g => g.First()))
{
item.MyProperty = string.Join(",", lista.Where(t => t.Id == item.Id).Select(x => x.MyProperty).ToArray());
Console.WriteLine(item.Id);
Console.WriteLine(item.MyProperty);
}
Console.ReadLine();
}
}
}
You can use projection for this.
var orderedList = lista.GroupBy(x => x.Id)
.Where(x => x.Any(y => y.Deleted == 1))
.Select(x => new Oggetto
{
Id = x.Key, MyProperty = string.Join(",", x.Select(v => v.MyProperty))
});
foreach (var item in orderedList)
{
Console.WriteLine(item.Id);
Console.WriteLine(item.MyProperty);
}
Anyway, as #Alex said you shoud replace Deleted field type to bool and as said by #Marco Salerno start programming in English you'll not regret.
First of all I would avoid the groupBy statement. This is a lot of unneded overhead. You can use distinct instead. This will give you all the IDs you need to know.
var ids = lista.Where(x => x.Deleted).Select(x => x.Id).Distinct();
You can then select all the elements that you need with:
var items = ids.Select(i => lista.Where(x => x.Id == i));
which results in a List of Lists. For the ease of use I would convert this to a Dictionary<K, V> (int this case it's Dictionary<long, List<string>> as a final step:
var dictionary = items.ToDictionary(l => l.First().Id, l => l.Select(o => o.MyProperty).ToList());
You now got a "nice and filtered" collection you can use any way you like (or just output it)
foreach (var item in dictionary)
{
Console.WriteLine($"Id: {item.Key}");
Console.WriteLine($"Properties: {string.Join(", ", item.Value)}");
}
I also changed your class a little bit to:
class Oggetto
{
public int Id { get; set; }
public string MyProperty { get; set; }
// bool instead of int - Deleted has only 2 states
public bool Deleted { get; set; }
}
First of all STOP programming in Italian, start doing it in English.
Anyway, this should be a better approach:
class Program
{
static void Main(string[] args)
{
List<Item> items = new List<Item> {
new Item{ Id = 1, MyProperty = "Propr1", Deleted = 0 },
new Item{ Id = 1, MyProperty = "Propr2", Deleted = 1 },
new Item{ Id = 2, MyProperty = "Prop3", Deleted = 0 },
new Item{ Id = 3, MyProperty = "Propr4", Deleted = 0 },
new Item{ Id = 3, MyProperty = "Prop5", Deleted = 1}
};
foreach (IGrouping<int,Item> group in items.GroupBy(x => x.Id).ToList())
{
List<Item> groupItems = group.ToList();
Item deletedItem = groupItems.Where(x => x.Deleted == 1).FirstOrDefault();
if(deletedItem != null)
{
deletedItem.MyProperty = string.Join(",", groupItems.Select(x => x.MyProperty).ToArray());
Console.WriteLine(deletedItem.Id);
Console.WriteLine(deletedItem.MyProperty);
}
}
}
}
class Item
{
public int Id { get; set; }
public string MyProperty { get; set; }
public int Deleted { get; set; }
}

Remove elements in a list considering duplicated subelements

I need to remove elements in a single list considering one or more duplicated subelement
Classes
public class Person
{
public int id { get; set; }
public string name { get; set; }
public List<IdentificationDocument> documents { get; set; }
public Person()
{
documents = new List<IdentificationDocument>();
}
}
public class IdentificationDocument
{
public string number { get; set; }
}
Code:
var person1 = new Person() {id = 1, name = "Bob" };
var person2 = new Person() {id = 2, name = "Ted" };
var person3 = new Person() {id = 3, name = "Will_1" };
var person4 = new Person() {id = 4, name = "Will_2" };
person1.documents.Add(new IdentificationDocument() { number = "123" });
person2.documents.Add(new IdentificationDocument() { number = "456" });
person3.documents.Add(new IdentificationDocument() { number = "789" });
person4.documents.Add(new IdentificationDocument() { number = "789" }); //duplicate
var personList1 = new List<Person>();
personList1.Add(person1);
personList1.Add(person2);
personList1.Add(person3);
personList1.Add(person4);
//more data for performance test
for (int i = 0; i < 20000; i++)
{
var personx = new Person() { id = i, name = Guid.NewGuid().ToString() };
personx.documents.Add(new IdentificationDocument() { number = Guid.NewGuid().ToString() });
personx.documents.Add(new IdentificationDocument() { number = Guid.NewGuid().ToString() });
personList1.Add(personx);
}
var result = //Here comes the linq query
result.ForEach(r => Console.WriteLine(r.id + " " +r.name));
Expected result:
1 Bob
2 Ted
3 Will_1
Example
https://dotnetfiddle.net/LbPLcP
Thank you!
You can use the Enumerable.Distinct<TSource> method from LINQ. You'll need to create a custom comparer to compare using the subelement.
See How do I use a custom comparer with the Linq Distinct method?
Well, yes, you could use a custom comparer. But that's going to be lots more code than your specific example requires. If your specific example is all you need, this this will work fine:
var personDocumentPairs = personList1
.SelectMany(e => e.documents.Select(t => new {person = e, document = t}))
.GroupBy(e => e.document.number).Select(e => e.First());
var result = personDocumentPairs.Select(e => e.person).Distinct();
along the lines of Adam's solution the trick is to iterate persons and group them by associated document numbers.
// persons with already assigned documents
// Will_2
var duplicate = from person in personList1
from document in person.documents
group person by document.number into groupings
let counter = groupings.Count()
where counter > 1
from person in groupings
.OrderBy(p => p.id)
.Skip(1)
select person;
// persons without already assigned documents
// Bob
// Ted
// Will_1
var distinct = from person in personList1
from document in person.documents
group person by document.number into groupings
from person in groupings
.OrderBy(p => p.id)
.Take(1)
select person;
the orderby is a made up rule for the already assigned documents persons, but your mileage may vary

Query values in a Dictionary<ObjectId, Class> using LINQ?

Consider the following simple example of Students and Teachers;
// person
public class Person
{
public ObjectId Id { get; set; }
public string Name { get; set; }
public Person() {
Id = ObjectId.GenerateNewId(DateTime.Now);
}
}
// student has a Classroom
public class Student : Person
{
public string Classroom { get; set; }
}
// teacher has a Dictionary<ObjectId, Student> Students
public class Teacher : Person
{
[BsonDictionaryOptions(DictionaryRepresentation.ArrayOfDocuments)]
public Dictionary<ObjectId, Student> Students { get; set; }
public Teacher() {
Students = new Dictionary<ObjectId, Student>();
}
}
class Program
{
static void Main(string[] args)
{
var server = MongoServer.Create("mongodb://localhost/database?safe=true");
var database = server.GetDatabase("sandbox");
var collection = database.GetCollection<Teacher>("teachers");
collection.Drop();
// create students
var s1 = new Student() { Name = "s1", Classroom = "foo" };
var s2 = new Student() { Name = "s2", Classroom = "foo" };
var s3 = new Student() { Name = "s3", Classroom = "baz" };
var s4 = new Student() { Name = "s4", Classroom = "foo" };
// teacher 1
var t1 = new Teacher() { Name = "t1" };
t1.Students.Add(s1.Id, s1);
t1.Students.Add(s2.Id, s2);
collection.Insert(t1);
// teacher 2
var t2 = new Teacher {Name = "t2"};
t2.Students.Add(s3.Id, s3);
collection.Insert(t2);
// add teacher 3
var t3 = new Teacher() {Name = "t3"};
t3.Students.Add(s4.Id, s4);
collection.Insert(t3);
// select via key
var onlyt1 = collection.AsQueryable().Where(t => t.Students.ContainsKey(s1.Id)).ToList();
Console.WriteLine("onlyt1 : {0}", onlyt1.ToJson());
Console.ReadLine();
}
}
I can select via the key (shown above), but how do I find all the teachers who have students with classroom of "foo"? I want to write something like;
// select via value
var shouldBeJustT1andT3 = collection.AsQueryable().Where(t => t.Students.Values.Where(s => s.Classroom == "foo")).ToList();
You can use Any to get any teacher for whom there are students in a given classroom "foo":
List<Teacher> shouldBeJustT1andT3 = collection.Where(
teacher => teacher.Students.Any(student => student.Classroom == "foo")
).ToList();
Edit
Since Mongo's IQueryable isn't supporting Any by default, maybe you could just use Where and Count instead of Any:
List<Teacher> shouldBeJustT1andT3 = collection.Where(
teacher => teacher.Students.Where(student => student.Classroom == "foo").Count() > 0
).ToList();
Can't you have Students of type just ICollection<Person>?
Then you don't need query dictionary's values but flat objects' list, i.e. where s.ID == x && s.Classroom == "blah".
Dictionary makes sense to find object by key only, i.e. t.Students[studentId].
To find teachers: see dbaseman's answer, he's correct.

Categories

Resources