How to select students with repeated low scores? - c#

Scores are considered low if they are less than or equal to 5. I want to select students with repeated low scores.
The expected result is:
Andy
Bobby
Cindy
As each of them has repeated low scores.
Question
I got stuck in completing the last expression GroupBy in the Where clause.
Could you make it done?
class Student
{
public int Id { get; set; }
public string Name { get; set; } = null!;
public List<int> Scores { get; set; } = new List<int>();
public static List<Student> GetStudents()
{
return new List<Student>()
{
new Student
{
Id = 1,
Name="Andy",
Scores={1,1,2,2,3,4,5,6,7,8}
},
new Student
{
Id = 2,
Name="Bobby",
Scores={3,3,3,3,4,5}
},
new Student
{
Id = 3,
Name="Cindy",
Scores={1,1,2,2,3,4,5}
},
new Student
{
Id = 4,
Name="Dave",
Scores={1,2,3,4,5,6,7,8,9,10}
}
};
}
}
class Program
{
static void Main()
{
var query = Student.GetStudents()
.Where(s => s.Scores.GroupBy(i => i).????);
foreach (var x in query)
Console.WriteLine(x.Name);
Console.ReadLine();
}
}

I'd do something like this:
var query = Student.GetStudents()
.Where(s => s.Scores
.Where(x => x <= 5)
.GroupBy(i => i)
.Any(x => x.Count() > 1));

Try following :
var query = Student.GetStudents()
.Select(x => new { student = x.Name, scores = x.Scores.GroupBy(y => y).Select(y => new { score = y.Key, count = y.Count() }).ToList() }).ToList();
var lowScore = query.Where(x => x.scores.Any(y => (y.count > 1) && (y.score <= 5))).ToList();

Related

Optimization of custom rank function in C#

I have written a code for students class to Rank them according to their marks. The code works accurately and gives the result as
Name:B Marks:30 Rank:1
Name:C Marks:30 Rank:1
Name:A Marks:20 Rank:3
Name:D Marks:10 Rank:4
But I need it to be optimized so it wont take too much of time to be processed. Below is the code
public List<Students> GetRanks(List<Students> students)
{
List<Students> rStudents = new List<Students>();
students = students.OrderByDescending(a => a.marks).ToList();
for (int i = 0; i < students.Count; i++)
{
Students stu = new Students();
stu.Id = students[i].Id;
stu.Name = students[i].Name;
stu.marks = students[i].marks;
if (i > 0 && students[i].marks == students[i - 1].marks)
{
stu.rank = rStudents.Select(a => a.rank).LastOrDefault();
}
else
{
stu.rank = i + 1;
}
rStudents.Add(stu);
}
return rStudents;
}
List<Students> students = new List<Students>() {
new Students() { Id = 1, Name = "A", marks = 20 },
new Students() { Id = 1, Name = "B", marks = 30 },
new Students() { Id = 1, Name = "C", marks = 30 },
new Students() { Id = 1, Name = "D", marks = 10 },
};
List<Students> rStudents = GetRanks(students);
public class Students
{
public int Id { get; set; }
public string Name { get; set; }
public double marks { get; set; }
public int rank { get; set; }
}
foreach (Students s in GetRanks(students))
{
Console.WriteLine($"Name:{s.Name}\tMarks:{s.marks}\tRank:{s.rank}");
}
Remove unnecessary resource.and using yield will have performance impact in large data. Fewer line and more readable.
If you are fetching data from DataBase it's better idea sort them there then fetch them.
public static IEnumerable<Students> GetRanks(List<Students> students)
{
List<Students> rStudents = students.OrderByDescending(a => a.marks).ToList();
for (int i = 0; i < rStudents.Count; i++)
{
if (i > 0 && rStudents[i].marks == rStudents[i - 1].marks)
{
rStudents[i].rank = rStudents[i - 1].rank;
}
else
{
rStudents[i].rank = i + 1;
}
yield return rStudents[i];
}
}
This should be pretty darn fast:
var rank = 1;
students
.GroupBy(x => x.marks)
.OrderByDescending(x => x.Key)
.ToList()
.ForEach(xs =>
{
xs.ToList().ForEach(x => x.rank = rank);
rank += xs.Count();
});
With your sample data I get:
Here's a non-destructive version of the code:
public static List<Students> GetRanks(List<Students> students)
{
var rank = 1;
return
students
.OrderByDescending(x => x.marks)
.GroupBy(x => x.marks)
.SelectMany(xs =>
{
var r = rank;
rank += xs.Count();
return xs.Select(x => new Students()
{
Id = x.Id,
Name = x.Name,
marks = x.marks,
rank = r,
});
})
.ToList();
}
I measured this using 100_000 randomly created students. My code completed in 110 milliseconds. The original code in the question took 64_119 milliseconds.
Yes, this code was nearly 600x faster.
More then 50% improvement if have large collect with a new method called GetRanksV1
namespace StudentPerformance
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
List<Students> students = new List<Students>();
Random _random = new Random();
for (int i = 1; i < 10001; i++)
{
students.Add(new Students() { Id = i, Name = "A" + i, marks = _random.Next(10, 50) });
}
var watch = System.Diagnostics.Stopwatch.StartNew();
var tmp = GetRanks(students);
watch.Stop();
Console.WriteLine($"Execution Time: {watch.ElapsedMilliseconds} ms");
watch = System.Diagnostics.Stopwatch.StartNew();
var tmp1 = GetRanksV1(students);
watch.Stop();
Console.WriteLine($"Execution Time V1: {watch.ElapsedMilliseconds} ms");
foreach (Students s in tmp1)
{
Console.WriteLine($"Name:{s.Name}\tMarks:{s.marks}\tRank:{s.rank}");
}
Console.ReadLine();
}
public static List<Students> GetRanks(List<Students> students)
{
List<Students> rStudents = new List<Students>();
students = students.OrderByDescending(a => a.marks).ToList();
for (int i = 0; i < students.Count; i++)
{
Students stu = new Students();
stu.Id = students[i].Id;
stu.Name = students[i].Name;
stu.marks = students[i].marks;
if (i > 0 && students[i].marks == students[i - 1].marks)
{
stu.rank = rStudents.Select(a => a.rank).LastOrDefault();
}
else
{
stu.rank = i + 1;
}
rStudents.Add(stu);
}
return rStudents;
}
public static List<Students> GetRanksV1(List<Students> students)
{
int Srno = 0;
var tmp = students.GroupBy(a => a.marks).OrderByDescending(o => o.Key).Select(s => new { Mark = s.Key, Rand = ++Srno, });
var values = from s in students
join t in tmp on s.marks equals t.Mark
select new Students { Id = s.Id, Name = s.Name, marks = s.marks, rank = t.Rand };
return values.ToList();
}
}
public class Students
{
public int Id { get; set; }
public string Name { get; set; }
public double marks { get; set; }
public int rank { get; set; }
}
}

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

Select a restricted subset of items based on child property

I'm just wondering if there's a better way to write this code, basically the source object contains a mix of items with a boolean property however the destination object has two lists which should contain the true/false items independently.
I've written it in Linq and it works just fine but it feels as though there's a better way. Any suggestions?
void Main()
{
var s = new ResponseObject()
{
Results = new List<GroupedObject>()
{
new GroupedObject()
{
Name = "List A",
List=new List<DetailObject>()
{
new DetailObject{ Name = "Allowed", AllowedAccess = true},
new DetailObject{ Name = "Restricted", AllowedAccess = false}
}
},
new GroupedObject()
{
Name = "List B",
List=new List<DetailObject>()
{
new DetailObject{ Name = "Allowed", AllowedAccess = true},
new DetailObject{ Name = "Restricted", AllowedAccess = false}
}
}
}
};
var d = new ResponseViewModel();
d.AllowedResults = FilterObjectsByAccess(s.Results, true);
d.RestrictedResults = FilterObjectsByAccess(s.Results, false);
// Other stuff
}
public IEnumerable<GroupedObject> FilterObjectsByAccess(IEnumerable<GroupedObject> source, bool allowAccess)
{
return source.Where(i => i.List.Any(c => c.AllowedAccess == allowAccess))
.Select(i => new GroupedObject()
{
Name = i.Name,
List = i.List.Where(c => c.AllowedAccess == allowAccess)
});
}
public class ResponseObject
{
public IEnumerable<GroupedObject> Results { get; set; }
}
public class ResponseViewModel
{
public IEnumerable<GroupedObject> AllowedResults { get; set; }
public IEnumerable<GroupedObject> RestrictedResults { get; set; }
}
public class GroupedObject
{
public string Name { get; set; }
public IEnumerable<DetailObject> List { get; set; }
}
public class DetailObject
{
public string Name { get; set; }
public bool AllowedAccess { get; set; }
}
One change that may be worth benchmarking would be changing:
public IEnumerable<GroupedObject> FilterObjectsByAccess(IEnumerable<GroupedObject> source, bool allowAccess)
{
return source.Where(i => i.List.Any(c => c.AllowedAccess == allowAccess))
.Select(i => new GroupedObject()
{
Name = i.Name,
List = i.List.Where(c => c.AllowedAccess == allowAccess)
});
}
to:
public IEnumerable<GroupedObject> FilterObjectsByAccess(IEnumerable<GroupedObject> source, bool allowAccess)
{
return source
.Select(i => new GroupedObject()
{
Name = i.Name,
List = i.List.Where(c => c.AllowedAccess == allowAccess).ToList() // `ToList` here is optional - it is a trade-off between RAM and CPU
})
.Where(z => z.List.Any());
}
Your original code, with the use of Any then Where would enumerate i.List twice. The above change would likely improve that.
Another approach, which would likely involve even higher memory consumption could be to switch to using ToLookup:
var d = new ResponseViewModel
{
AllowedResults =
FilterObjectsByAccess(s.Results)
.Select(z => new GroupedObject() { Name = z.Name, List = z.GroupedList[false] })
.Where(z => z.List.Any()),
RestrictedResults =
FilterObjectsByAccess(s.Results)
.Select(z => new GroupedObject() { Name = z.Name, List = z.GroupedList[true] })
.Where(z => z.List.Any())
};
// Other stuff
}
public List<SpecialGroupedObject> FilterObjectsByAccess(IEnumerable<GroupedObject> source)
{
return source.Select(i => new SpecialGroupedObject()
{
Name = i.Name,
GroupedList = i.List.ToLookup(c => c.AllowedAccess)
}).ToList();
}
I can suggest you to use ToDictionary() like this:
var result = new[] {true, false}.ToDictionary(k => k,
v =>
s.Results.Where(w => w.List.Any(x => x.AllowedAccess == v))
.Select(c => new GroupedObject {Name = c.Name, List = c.List.Where(l => l.AllowedAccess == v)}));
var allowedResults = result[true];
var restrictedResults = result[false];
Or this:
var result = s.Results
.SelectMany(c => c.List, (b, c) => new {b.Name, DObj = c})
.GroupBy(g => g.DObj.AllowedAccess)
.ToDictionary(k=> k.Key,
c =>
new {
c.Key,
List =
c.GroupBy(cg => cg.Name)
.Select(
x => new GroupedObject {Name = x.Key, List = x.Select(l => l.DObj).ToList()})
.ToList()
});

Group By using Linq

I need to present a list of Ads grouped by category, including the Ads Count for each category.
Categories are grouped by a Parent Category like Cars that include the Categories Saloon, Cabriolet and Sports.
Models:
public class Ad
{
[Key]
public int Id { get; set; }
public Category Category { get; set; }
}
public class Category
{
public int Id { get; set; }
[ForeignKey("CategoryParent")]
public int? CategoryParent_Id { get; set; }
public string Name { get; set; }
public virtual ICollection<Ad> Ads { get; set; }
}
The result as to be:
Cars - Count: 100 (where 100 is the sum of for example 20 Saloon's Ads, 80 Cabrilet's)
At the moment, I'm only able to present the list of all Categories, and not grouped by Parent Category.
var adIds = {1,2,4,5}
var result =
from c in categoryQuery
let searchCount = c.Ads.Count(a => adIds.Contains(a.Id))
where searchCount > 0
select new CategoryGetAllBySearchDto
{
Id = c.CategoryParent_Id,
Name = c.CategoryParent.Name,
SearchCount = searchCount,
Ids = c.Ads.Where(a => adIds.Contains(a.Id)).Select(a => a.Id)
};
GroupBy in memory:
var adIds = { 1, 2, 4, 5 };
var result = categoryQuery.Where(c => c.Ads.Any(a => adIds.Contains(a.Id)))
.Select(c => new
{
c.CategoryParent_Id,
c.CategoryParent.Name,
Ids = c.Ads.Where(a => adIds.Contains(a.Id)).Select(a => a.Id).AsEnumerable()
})
.ToList()
.GroupBy(c => new {c.CategoryParent_Id, c.Name})
.Select(g => new CategoryGetAllBySearchDto
{
Id = g.Key.CategoryParent_Id,
Name = g.Key.Name,
Ids = g.SelectMany(u => u.Ids).AsEnumerable()
})
.ToList();
i think you need this:
var adIds = { 1, 2, 4, 5 };
var result = from c in categoryQuery
where c.Ads.Any(a => adIds.Contains(a.Id))
group c by new {c.CategoryParent_Id, c.CategoryParent.Name} into g
select new CategoryGetAllBySearchDto
{
Id = g.Key.CategoryParent_Id,
Name = g.Key.Name,
SearchCount = g.SelectMany(u => u.Ads)
.Where(a => adIds.Contains(a.Id))
.Count(),
Ids = g.SelectMany(u => u.Ads)
.Where(a => adIds.Contains(a.Id))
.Select(a => a.Id)
};
you can get out the SearchCount an add the AsEnumerable to Ids to get query just once
public class CategoryGetAllBySearchDto
{
public int? Id { get; set; }
public string Name { get; set; }
public int SearchCount { get { return this.Ids.Count() } }
public IEnumerable<int> Ids { get; set; }
}
and the query :
var adIds = { 1, 2, 4, 5 };
var result = from c in categoryQuery
where c.Ads.Any(a => adIds.Contains(a.Id))
group c by new {c.CategoryParent_Id, c.CategoryParent.Name} into g
select new CategoryGetAllBySearchDto
{
Id = g.Key.CategoryParent_Id,
Name = g.Key.Name,
Ids = g.SelectMany(u => u.Ads)
.Where(a => adIds.Contains(a.Id))
.Select(a => a.Id)
.AsEnumerable()
};

LINQ Lambda Group By with Sum

Hi I can do this in method syntax but I'm trying to improve my lambda skills how can I do:
SELECT SUM([job_group_quota]) as 'SUM'
FROM [dbo].[tbl_job_session]
WHERE [job_group_job_number] = #jobnum
and [job_group_ID] like #sess
GROUP BY [job_group_job_number]
I've been messing around with it but can't get it right.
lnq.tbl_job_sessions.GroupBy(a => a.job_group_job_number == jnum)
.Select(b => new { b.job_group_quota}).Sum();
A general example:
query
.GroupBy(item => item.GroupKey)
.Select(group => group.Sum(item => item.Aggregate));
Few Group by Examples
public void GroupBy1()
{
var personList = dbEntities.People.GroupBy(m => m.PersonType).Select(m => new { PersonType = m.Key, Count = m.Count() });
}
public void GroupBy2()
{
var personList = dbEntities.People.GroupBy(m => new { m.PersonType, m.FirstName }).Select(m => new { PersonType = m.Key, Count = m.Count() });
}
public void GroupBy3()
{
var personList = dbEntities.People.Where(m => m.EmailPromotion != 0).GroupBy(m => new { m.PersonType, m.FirstName }).Select(m => new { PersonType = m.Key, Count = m.Count() });
}
public void GroupBy4()
{
var personList = dbEntities.People.GroupBy(m => new { m.PersonType, m.FirstName }).Where(m => m.Count() > 70).Select(m => new { PersonType = m.Key, Count = m.Count() });
}
public void GroupBy5()
{
var personList = dbEntities.People
.GroupBy(m =>
new
{
m.PersonType
}).Where(m => m.Count() > 70)
.Select(m =>
new
{
PersonType = m.Key,
Count = m.Count()
});
var list1 = dbEntities.People.
GroupBy(m => new { m.PersonType }).
Select(m =>
new
{
Type = m.Key,
Count = m.Count()
})
.Where(
m => m.Count > 70
&& m.Type.PersonType.Equals("EM")
|| m.Type.PersonType.Equals("GC"));
}
public void GroupBy6()
{
var list1 = dbEntities.People.
GroupBy(m => new { m.PersonType, m.EmailPromotion }).Select(m =>
new
{
Type = m.Key,
Count = m.Count()
})
.Where
(
m => m.Count > 70 && m.Type.EmailPromotion.Equals(0) &&
(
m.Type.PersonType.Equals("EM") ||
m.Type.PersonType.Equals("GC")
));
}
public void GroupBy7()
{
var list1 = dbEntities.People.
GroupBy(m => m.PersonType).
Select(c =>
new
{
Type = c.Key,
Total = c.Sum(p => p.BusinessEntityID)
});
}
public void GroupBy8()
{
var list1 = dbEntities.People.
GroupBy(m => m.PersonType).
Select(c =>
new
{
Type = c.Key,
Count = c.Count(),
Total = c.Sum(p => p.BusinessEntityID)
});
}
public void GroupBy9()
{
var list1 = dbEntities.People.
GroupBy(m => m.PersonType).
Select(c =>
new
{
Type = c.Key,
Max = c.Max(),
});
}
If you want to get a key-to-sum Dictionary result.
var allJobQuota = jobSessions.GroupBy(s => s.jobNumber)
.ToDictionary(g => g.Key, g => g.Sum(s => s.quota));
This example shows how to iterate the grouped values getting the key and totals, and how to get totals directly (like previous). Both using only lambda operator.
using System;
using System.Collections.Generic;
using System.Linq;
public class Person
{
public string Name { get; set; }
public string City { get; set; }
public string ZipCode { get; set; }
public int SomeValue { get; set; }
}
class Program
{
static void Main(string[] args)
{
List<Person> data = GetPopulatedData();
var totals = data.GroupBy(x =>
new { x.Name, x.City, x.ZipCode }).Select(y =>
y.Sum(i => i.SomeValue));
var groupsForIterate = data.GroupBy(x =>
new { x.Name, x.City, x.ZipCode });
Console.WriteLine("Totals: ");
foreach (var total in totals)
{
Console.WriteLine(total);
}
Console.WriteLine("Categories: ");
foreach (var categ in groupsForIterate)
{
// You can refer to one field like this: categ.Key.Ciduad
Console.WriteLine("Group" + categ.Key);
Console.WriteLine(categ.Sum(x => x.SomeValue));
}
//Output:
//Totals:
//1
//2
//1
//Categories:
//Group{ Name = Mark, City = BCN, ZipCode = 00000 }
//1
//Group{ Name = Mark, City = BCN, ZipCode = 000000 }
//2
//Group{ Name = John, City = NYC, ZipCode = 000000 }
//1
}
private static List<Person> GetPopulatedData()
{
List<Person> datos = new List<Person>()
{
new Person(){Name="Mark", City = "BCN",
ZipCode = "00000", SomeValue = 1}, // group A
new Person(){Name="Mark", City = "BCN",
ZipCode = "000000", SomeValue = 1}, // group B
new Person(){Name="Mark", City = "BCN",
ZipCode = "000000", SomeValue = 1}, // group B
new Person(){Name="John", City = "NYC",
ZipCode = "000000", SomeValue = 1}, // group C
};
return datos;
}
}
Sum Ficha_Venda and Entrada from Movimento:
var query = from bd in db.Movimento
where (movimento.Data != null ? bd.Data == movimento.Data : bd.Data == movimento.Data)
&& (bd.Loja == Loja)
group bd by bd.Data into t
select new {entrada = t.Sum(bd=> bd.Entrada), ficha = t.Sum(bd=> bd.Ficha_Venda)};

Categories

Resources