Ordering list by 3 numbers - c#

I have a List<> with objects, that hold multiple fields, which are mostly numbers. I want to sort this list, by 3 of those numbers. I've tried this:
list = list.OrderBy(x => x.Val3).ThenBy(x => x.Val2)
.ThenBy(x => x.Val1).ToList();
which works fine, but only for the first two order/thenbys. The third one seems to not get run at all. I can sort for any combination of two of those values just fine, but the third on is always ignored.
I haven't tried the non LINQ approach yet, because I'm simply curious where the problem here is. Can't you sort for 3 values? What's the problem here? In case this matters in any way, 3 is a ushort, while 2 and 1 are uints.

I am not sure if there are any better solutions. If I were you, I would weight the value2 by multiply a big value. Just like:
list = list.OrderBy(x => x.Val3).ThenBy(x => x.Val2 * 10000 + x.Val1).ToList();

I think you can try this hack:
list = list.OrderBy(x => x.Val2).OrderBy(x => x.Val3).ThenBy(x => x.Val1).ToList();

This doesn't solve the problem because it just works. However, perhaps you can compare your code to this and see how it differs?
public class Foo
{
public string Name { get; set; }
public uint Val1 { get; set; }
public uint Val2 { get; set; }
public ushort Val3 { get; set; }
}
static void Main(string[] args)
{
OrderFoos();
Console.WriteLine("Press any key to end.");
Console.ReadKey();
}
private static void OrderFoos()
{
List<Foo> list = new List<Foo>();
list.Add(new Foo() { Name = "2nd", Val1 = 2, Val2 = 1, Val3 = 1 });
list.Add(new Foo() { Name = "1st", Val1 = 1, Val2 = 1, Val3 = 1 });
list.Add(new Foo() { Name = "3rd", Val1 = 1, Val2 = 2, Val3 = 1 });
list.Add(new Foo() { Name = "4th", Val1 = 2, Val2 = 1, Val3 = 2 });
list.Add(new Foo() { Name = "6th", Val1 = 4, Val2 = 1, Val3 = 3 });
list.Add(new Foo() { Name = "5th", Val1 = 3, Val2 = 1, Val3 = 3 });
list = list.OrderBy(x => x.Val3).ThenBy(x => x.Val2).ThenBy(x => x.Val1).ToList();
list.ForEach(x => Console.WriteLine(x.Name));
}

The documentation for ThenBy says
"This design enables you to specify multiple sort criteria by applying
any number of ThenBy or ThenByDescending methods."
So what you give as example code should work. In the absence of a specified comparator ThenBy uses the Default comparator. Are you sure that val1 is sortable using the default comparator?
EDIT: Doh! just saw "I can sort for any combination of two of those values just fine, but the third on is always ignored." so it seems that val1 is sortable by the default comparator.
The documentation and the answer from #Bob Horn suggests that your example should work. Can you add more details of the objects you're ordering?

Related

Sort descending by salary where teacher year = 3

I have a list of teachers and I want to sort in descending order by salary teachers who have years of work experience = 3.
I want experience != 3 to keep their index (keep their position) and only sorting by salary teacher have experience = 3
Please help me to solve this problem.
class Teacher
{
public int id { get; set; }
public string name { get; set; }
public int year { get; set; }
public double salary { get; set; }
public Teacher()
{
}
public Teacher(int id, string name, int year, double salary)
{
this.id = id;
this.name = name;
this.year = year;
this.salary = salary;
}
}
List<Teacher> teacher = new List<Teacher>();
teacher.Add(new Teacher(1, "Teacher A", 4, 2000));
teacher.Add(new Teacher(2, "Teacher B", 3, 3000));
teacher.Add(new Teacher(3, "Teacher C", 5, 5000));
teacher.Add(new Teacher(4, "Teacher D", 3, 4000));
teacher.Add(new Teacher(5, "Teacher E", 3, 7000));
Output:
1, Teacher A, 4, 2000
5, Teacher E, 3, 7000
3, Teacher C, 5, 5000
4, Teacher D, 3, 4000
2, Teacher B, 3, 3000
Ugly Solution, but working:
Mind: Conversion to Array is not neccessary.
using System;
using System.Linq;
using System.Collections.Generic;
public class Program
{
public static void Main()
{
List<Teacher> teacher = new List<Teacher>();
teacher.Add(new Teacher(1, "Teacher A", 4, 2000));
teacher.Add(new Teacher(2, "Teacher B", 3, 3000));
teacher.Add(new Teacher(3, "Teacher C", 5, 5000));
teacher.Add(new Teacher(4, "Teacher D", 3, 4000));
teacher.Add(new Teacher(5, "Teacher E", 3, 7000));
var teachArr = teacher.ToArray();
// Create separate List of only those teacher, you want to re-order
// So, filter and sort.
var threeYearTeachArr = teacher
.Where(t => t.year == 3) // Filter
.OrderByDescending(t => t.salary) // Sort
.ToArray(); // Do it!
// Then replace all filtered items in the original collection
// with the sorted ones. => Only filtered will change places.
// We traverse 2 arrays, so we create two indexes and check both against their
// respective collection sizes, but we increment only the "original"
for( int i = 0, threes = 0; i < teachArr.Length && threes < threeYearTeachArr.Length; i++ )
{
// only if the current entry is one of those we sorted...
if( teachArr[i].year == 3 )
{
// ... replace it with the next entry in the sorted list.
// post-increment: use threes' value, then increment
teachArr[i] = threeYearTeachArr[threes++];
}
}
foreach( var t in teachArr )
{
Console.WriteLine($"{t.id} {t.name} | {t.year} | {t.salary}");
}
}
}
class Teacher
{
public int id { get; set; }
public string name { get; set; }
public int year { get; set; }
public double salary { get; set; }
public Teacher()
{
}
public Teacher(int id, string name, int year, double salary)
{
this.id = id;
this.name = name;
this.year = year;
this.salary = salary;
}
}
Output:
1 Teacher A | 4 | 2000
5 Teacher E | 3 | 7000
3 Teacher C | 5 | 5000
4 Teacher D | 3 | 4000
2 Teacher B | 3 | 3000
See in action: https://dotnetfiddle.net/AaIqzE
A simple and naive solution would be to just do a simple bubble sort where you only consider the year 3 teachers:
for (int i1 = 0; i1 < teacher.Count; i1++)
{
if (teacher[i1].year != 3)
continue;
for (int i2 = i1 + 1; i2 < teacher.Count; i2++)
{
if (teacher[i2].year != 3)
continue;
if (teacher[i1].salary > teacher[i2].salary)
(teacher[i1], teacher[i2]) = (teacher[i2], teacher[i1]);
}
}
This will have a performance characteristic of O(n^2) so it will perform badly if you have a lot of teachers. Fildor has a better solution, I'm just presenting an alternative.
Interesting puzzle.
My first thought is to pair the list with their indices, then split the list into pass/fail based on your filter criteria: teacher.year == 3. Then we can order the pass list, fix up the indices separately, and finally re-merge the pass and fail data back together.
Wow, sounds complex. Let's try it and see how it looks:
List<Teacher> SortYear3(IEnumerable<Teacher> source)
{
var indexed = source.Select((teacher, index) => (index, teacher)).ToArray();
var pass = indexed.Where(pair => pair.teacher.year == 3);
var passIndices = pass.Select(pair => pair.index).ToArray();
var passOrdered = pass.Select(pair => pair.teacher).OrderByDescending(teacher => teacher.salary).ToArray();
var reindex = Enumerable.Range(0, passIndices.Length).Select(i => (index: passIndices[i], teacher: passOrdered[i]));
var merged = indexed.Where(pair => pair.teacher.year != 3).Concat(reindex).OrderBy(p => p.index);
return merged.Select(pair => pair.teacher).ToList();
}
Well... it works, but mostly as an example of when LINQ is not the answer. And those intermediate arrays are a bit ugly, so let's not.
The next thought is to pull out the items you want to sort, sort them into an array, then feed them back in while adding items to a result list:
List<Teacher> SortYear3(List<Teacher> source)
{
var sorted = source.Where(t => t.year == 3).OrderByDescending(t => t.salary).ToArray();
var result = new List<Teacher>();
for (int i = 0, sortindex = 0; i < source.Count; i++)
{
var next = source[i];
if (next.year == 3)
result.Add(sorted[sortindex++]);
else
result.Add(next);
}
return result;
}
Down to one array allocation, but it still looks a little clunky. Let's copy the list to start with and just replace the ones that we sorted:
List<Teacher> SortYear3(List<Teacher> source)
{
var sorted = source.Where(t => t.year == 3).OrderByDescending(t => t.salary).ToArray();
var result = source.ToList();
for (int i = 0, sortindex = 0; i < result.Count; i++)
{
if (result[i].year == 3)
result[i] = sorted[sortindex++];
}
return result;
}
That looks much better... and is now almost exactly what #fildor wrote. Well, that's embarrassing. Let's spice it up a little: make it generic, give it some parameters to specify the filtering and sorting, etc.
IEnumerable<T> SortSelected<T, TKey>(IEnumerable<T> source, Func<T, bool> filter, Func<T, TKey> sortKey, bool descending = true)
{
var result = source.ToList();
var filtered = result.Where(filter);
var sorted = (descending ? filtered.OrderByDescending(sortKey) : filtered.OrderBy(sortKey)).ToArray();
for (int i = 0, j = 0; j < sorted.Count; i++)
{
if (filter(result[i]))
result[i] = sorted[j++];
}
return result;
}
List<Teacher> SortYear3(List<Teacher> source)
=> SortSelected(source, t => t.year == 3, t => t.salary, true).ToList();
(OK, so maybe I shouldn't answer these things when I've been up for more than 24 hours.)
Please check this answer, it is much more easier to understand and more optimised
using System;
using System.Xml.Serialization;
using System.Collections.Generic;
using System.Linq;
public class Program
{
public static void Main()
{
List<Teacher> teacher = new List<Teacher>();
teacher.Add(new Teacher(1, "Teacher A", 4, 2000));
teacher.Add(new Teacher(2, "Teacher B", 3, 3000));
teacher.Add(new Teacher(3, "Teacher C", 5, 5000));
teacher.Add(new Teacher(4, "Teacher D", 3, 4000));
teacher.Add(new Teacher(5, "Teacher E", 3, 7000));
var expTeacher=teacher.Where(x=>x.year==3).OrderByDescending(x=>x.salary).ToList();
for(int i=0,j=0;i<teacher.Count && j<expTeacher.Count;i++)
{
if(teacher[i].year==3)
{
teacher[i]= expTeacher[j];
j++;
}
}
foreach(var teach in teacher)
{
Console.WriteLine(teach.id+", "+teach.name+", "+teach.year+", "+teach.salary);
}
}
}
class Teacher
{
public int id { get; set; }
public string name { get; set; }
public int year { get; set; }
public double salary { get; set; }
public Teacher()
{
}
public Teacher(int id, string name, int year, double salary)
{
this.id = id;
this.name = name;
this.year = year;
this.salary = salary;
}
}
I'm just guessing with the answer because in general you question is not clear either in requirement as the output which I assume is that what you are already getting.
According to response, at first what came to my head was
var t2 = teachers.Where(t => t.year == 3).OrderByDescending(t => t.salary);
var t3 = teachers.Where(t => !t2.Select(ts => ts.id).Contains(t.id));
var final = t2.Concat(t3);
Yes, it is not optimal an probably there is a better way to achieve that, but it gives output as needed (?)
Teacher = 5 Teacher E 3 7000
Teacher = 4 Teacher D 3 4000
Teacher = 2 Teacher B 3 3000
Teacher = 1 Teacher A 4 2000
Teacher = 3 Teacher C 5 5000
I understood and solved it by my way. Fildor give me the idea
List<Coach> sorted = coaches.Where(x => x.YearOfExperience == 3).OrderByDescending(x => x.Salary).ToList();
List<Coach> originalList = coaches;
int index = 0;
for (int i = 0; i < originalList.Count; i++)
{
if (originalList[i].YearOfExperience == 3)
{
originalList[i] = sorted[index++];
}
}
foreach (var item in originalList)
{
item.show();
}
If you really want to filter your list for teachers having 3 years of experience then you can simply apply Where extension method using linq.
var requiredTeachers=teacher.Where(x=>x.year==3).OrderByDescending(x=>x.salary).ToList();

Add duplicates together in List

First question :)
I have a List<Materiau> (where Materiau implements IComparable<Materiau>), and I would like to remove all duplicates and add them together
(if two Materiau is the same (using the comparator), merge it to the first and remove the second from the list)
A Materiau contains an ID and a quantity, when I merge two Materiau using += or +, it keeps the same ID, and the quantity is added
I cannot control the input of the list.
I would like something like this:
List<Materiau> materiaux = getList().mergeDuplicates();
Thank you for your time :)
Check out Linq! Specifically the GroupBy method.
I don't know how familiar you are with sql, but Linq lets you query collections similarly to how sql works.
It's a bit in depth to explain of you are totally unfamiliar, but Code Project has a wonderful example
To sum it up:
Imagine we have this
List<Product> prodList = new List<Product>
{
new Product
{
ID = 1,
Quantity = 1
},
new Product
{
ID = 2,
Quantity = 2
},
new Product
{
ID = 3,
Quantity = 7
},
new Product
{
ID = 4,
Quantity = 3
}
};
and we wanted to group all the duplicate products, and sum their quantities.
We can do this:
var groupedProducts = prodList.GroupBy(item => item.ID)
and then select the values out of the grouping, with the aggregates as needed
var results = groupedProducts.Select( i => new Product
{
ID = i.Key, // this is what we Grouped By above
Quantity = i.Sum(prod => prod.Quantity) // we want to sum up all the quantities in this grouping
});
and boom! we have a list of aggregated products
Lets say you have a class
class Foo
{
public int Id { get; set; }
public int Value { get; set; }
}
and a bunch of them inside a list
var foocollection = new List<Foo> {
new Foo { Id = 1, Value = 1, },
new Foo { Id = 2, Value = 1, },
new Foo { Id = 2, Value = 1, },
};
then you can group them and build the aggregate on each group
var foogrouped = foocollection
.GroupBy( f => f.Id )
.Select( g => new Foo { Id = g.Key, Value = g.Aggregate( 0, ( a, f ) => a + f.Value ) } )
.ToList();
List<Materiau> distinctList = getList().Distinct(EqualityComparer<Materiau>.Default).ToList();

Where does "i" get its value in this LINQ statement?

I'm little perplexed by the behavior of this select LINQ statement. Just below the LOOK HERE comments you can see a select LINQ statement. That select statement is on the employees collection. So, it should accept only x as the input param. Out of curiosity I passed i to the delegate and it works. When it iterates through the select, it assigns 0 first and then it increments by 1. The result can be seen at the end of this post.
Where does the variable i get its value from? First of all, why does it allow me to use a variable i which is nowhere in the scope. It is not in the global scope neither in the local Main method. Any help is appreciated to understand this mystery.
namespace ConsoleApplication
{
using System;
using System.Collections.Generic;
using System.Linq;
public class Employee
{
public int EmployeedId { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
}
class Program
{
static void Main(string[] args)
{
var employees = new List<Employee>()
{
new Employee() { FirstName = "John", LastName = "Doe" },
new Employee() { FirstName = "Jacob", LastName = "Doe" }
};
// LOOK HERE...
var newEmployees = employees.Select((x, i) => new { id = i, name = x.FirstName + " " + x.LastName });
newEmployees.ToList().ForEach(x => { Console.Write(x.id); Console.Write(" "); Console.WriteLine(x.name); });
Console.ReadKey();
}
}
}
The result is
0 John Doe
1 Jacob Doe
Enumerable.Select has an overload that projects the current index of the element in the sequence. Also Enumerable.Where and Enumerable.SkipWhile/TakeWhile have it. You can use it like a loop variable in a for-loop which is sometimes handy.
One example which uses the index to create an anonymous type to group a long list into groups of 4:
var list = Enumerable.Range(1, 1000).ToList();
List<List<int>> groupsOf4 = list
.Select((num, index) => new { num, index })
.GroupBy(x => x.index / 4).Select(g => g.Select(x => x.num).ToList())
.ToList(); // 250 groups of 4
or one with Where which only selects even indices:
var evenIndices = list.Where((num, index) => index % 2 == 0);
It might also be important to mention that you can use these overloads that project the index only in method-syntax. LINQ query-syntax does not support it.

Group by in LINQ on a property in an array

I have an array for which I want to group the items based on a property. I tried the below code, but it is not grouping correctyly. MyArray is the array and Id is the property on which I want to do the grouping.
var docGroup = (from x in MyArray
group x by x.Id).Select(grp => new
{
Id = grp.Key,
Results = grp.ToList(),
})
.Results
.ToList());
To keep it simple if I just make it
var docGroup = from x in MyArray group x by x.Id;
where Id is a string "123" in the array and MyArray[2] has both the same Id. When I check the docGroup it has two entries and both have the 123 key instead of just one entry with the 123 key.
Here's a very simple example:
class Program
{
static void Main(string[] args)
{
Test[] tArray = new Test[3];
Test t = new Test() { Id = "123", Val="First" };
Test t1 = new Test() { Id = "123", Val="Second" };
Test t2 = new Test() { Id = "1234", Val="Third" };
tArray[0] = t;
tArray[1] = t1;
tArray[2] = t2;
var g = from x in tArray group x by x.Id;
}
}
class Test
{
public string Id { get; set; }
public string Val { get; set; }
}
Now if I look at g it has count 2 of which one is the Id 123 and the second is the Id 1234. I am not sure what is going wrong with my array. So this seems to work, but I am not sure what is going on with my array. I'll do some research on it.
Sorry guys, I found the issue. The Id was in a value property in MyArray which I was not using and so it was not grouping correctly. Thanks for the help everyone.
Everything works as expected.
GroupBy produces an enumerable of IGrouping. Since you have two distinct keys ("123" and "1234") you will get an enumerable of two elements. These grouping have a uniqe key and they're by themself enumerables.
So
g.Where(x => x.Key == "123").ToList();
will contain two elements (First, Second) and
g.Where(x => x.Key == "1233").ToList();
will contain one element (Third).

List.Contains(item) with generic list of objects

If you have a List how do you return the item if a specified property or collection of properties exists?
public class Testing
{
public string value1 { get; set; }
public string value2 { get; set; }
public int value3 { get; set; }
}
public class TestingList
{
public void TestingNewList()
{
var testList = new List<Testing>
{
new Testing {value1 = "Value1 - 1", value2 = "Value2 - 1", value3 = 3},
new Testing {value1 = "Value1 - 2", value2 = "Value2 - 2", value3 = 2},
new Testing {value1 = "Value1 - 3", value2 = "Value2 - 3", value3 = 3},
new Testing {value1 = "Value1 - 4", value2 = "Value2 - 4", value3 = 4},
new Testing {value1 = "Value1 - 5", value2 = "Value2 - 5", value3 = 5},
new Testing {value1 = "Value1 - 6", value2 = "Value2 - 6", value3 = 6},
new Testing {value1 = "Value1 - 7", value2 = "Value2 - 7", value3 = 7}
};
//use testList.Contains to see if value3 = 3
//use testList.Contains to see if value3 = 2 and value1 = "Value1 - 2"
}
}
You could use
testList.Exists(x=>x.value3 == 3)
If you're using .NET 3.5 or better, LINQ is the answer to this one:
testList.Where(t => t.value3 == 3);
testList.Where(t => t.value3 == 2 && t.value1 == "Value1 - 2");
If not using .NET 3.5 then you can just loop through and pick out the ones you want.
Look at the Find or FindAll method of the List<T> class.
If you want to use the class's implementation of equality, you can use the Contains method. Depending on how you define equality (by default it'll be referential, which won't be any help), you may be able to run one of those tests. You could also create multiple IEqualityComparer<T>s for each test you want to perform.
Alternatively, for tests that don't rely just on the class's equality, you can use the Exists method and pass in a delegate to test against (or Find if you want a reference to the matching instance).
For example, you could define equality in the Testing class like so:
public class Testing: IEquatable<Testing>
{
// getters, setters, other code
...
public bool Equals(Testing other)
{
return other != null && other.value3 == this.value3;
}
}
Then you would test if the list contains an item with value3 == 3 with this code:
Testing testValue = new Testing();
testValue.value3 = 3;
return testList.Contains(testValue);
To use Exists, you could do the following (first with delegate, second with lambda):
return testList.Exists(delegate(testValue) { return testValue.value3 == 3 });
return testList.Exists(testValue => testValue.value3 == 2 && testValue.value1 == "Value1 - 2");
A LINQ query would probably be the easiest way to code this.
Testing result = (from t in testList where t.value3 == 3 select t).FirstOrDefault();

Categories