Sorting an ICollection descending by average including nullable values - asp.net-mvc-3

Given the following code:
class Book {
public int ID { get; set; }
public string Name { get; set; }
public ICollection<Review> Reviews { get; set; }
}
class Review {
public int ID { get; set; }
public int? Overall { get; set; }
public string Comment { get; set; }
}
And assuming I have a list of every book in the database:
var books; // List<Book>
I would like to show the user the books ordered by the average review. Ideally, the list of books would have the highest average review at the top and sort descending. Given I have the overall rating in Review as a null-able integer, some books will not have reviews, let alone an overall score. I'm having difficulty sorting this data, I've tried seemingly every combination of:
books.OrderByDescending(b => b.Reviews.Average(a => a.Overall));
Is it possible to sort my list without creating a second list to get something like:
Book A (4.5/5)
Book B (4.3/5)
Book C (3.9/5)
Book D (3.5/5)
Book E (-/5)
Book F (-/5)
Thanks in advance.

You should be able to use this:
books.OrderByDescending(b =>
(b.Reviews == null) ? 0:b.Reviews.Average(r => r.Overall??0));

Thank you TheCodeKing for point me in the right direction. Here is what fixed the problem:
books.OrderByDescending(b => b.Reviews.Count() == 0 ? 0 : b.Reviews.Average(r => r.Overall ?? 0));

Related

Returning Most Recent Records Based on Serial Number

I have the following model
public class EngineComplianceMetric
{
public int Id { get; set; }
public DateTime BuildDate { get; set; }
public string SerialNumber { get; set; }
public bool Complied { get; set; }
}
SerialNumber isn't unique to the table, as the same item could have been repaired multiple times over the years.
I'm trying to return a list that only includes each Serial Numbers most recent record that's before some set date.
So far I have
previousCompliedList = _dbContext.ComplianceMetrics
.Where(e => e.BuildDate < startDate)
.Distinct().ToList();
But this is just returning everything prior to the startDate.
You have to group your items by serial numbers, then you have to take the most recent item in each group (by ordering them and taking the first).
previousCompliedList = _dbContext.ComplianceMetrics
.Where(cm => cm.BuildDate < startDate).ToList()
.GroupBy(cm => cm.SerialNumber)
.Select(cm => cm.OrderByDescending(c => c.BuildDate).First());
Based on your error, you have to add the .ToList() after the where (before the GroupBy). But doing this will fetch all the items from the DB where the date is smaller and then run additional manipulations on it in memory.
If you don't want to fetch all the items from the DB, you will have to implement a solution similar to Astrid's suggestion in the comments.
I would recommend googling your error messages: https://stackoverflow.com/a/60874316/7175057

Linq Group By - Unable to group on custom classes?

I have noticed that when I do a LINQ - group by, it only seems to be working if I dont group on any of my custom classes.
I have a Product class (shown below) and I would like to group on Product.Id ,Product.Variant (object) and on Product.Options (ICollection) (because my source list contains multiple times the same Product but with different Variants and / or Options)
Product:
public class Product
{
public int Id { get; set; }
public Variant Variant { get; set; }
public ICollection<Option> Options{ get; set; }
public string Name { get; set; }
public int Amount { get; set; }
}
The code below will do a grouping, but only on my Product.ID, when I try to also group on Variant / Options, I get no grouping (well, not the grouping I am intending to make) as it will return just as much items as my source list.
IEnumerable<productAndSum> productsAndSums = unmappedProducts
.GroupBy(prod => new { Id = prod.Id})
.Select(group => new productAndSum()
{
Key = group.Key,
Sum = group.Sum(x => x.Amount)
});
If I am on the right track and the issue is related to the Objects, then it might be usefull to add that also Option and Variant have multiple objects and collections themselves, or is this too deep?
Extra information: I first started to just group by my Product class (not Id, Variant & Options separately), but this was unsuccessful. so I started eliminating properties and this is how I found out this issue. I think that solving this issue will result in killing two birds with one stone.
Warm regards

Count the occurrences of an object in a list where multiple criteria are matching using LINQ

I have a list of Cutdetails. I am trying to write a function using LINQ that will return the count of bars in the list where the CODE , BRAND, CODE and LENGTH all match. I want to be able to specify all these parameters and return a number for the number of matches.
I have tried using foreach statements which is fine but i'm sure there is an neater and smarter way to do it using LINQ. Any suggestions?
List<Bar> bars = new List<Bar>();
public class Bar
{
public string Brand { set; get; }
public string System { set; get; }
public string Code { set; get; }
public string Length { set; get; }
}
Thanks in advance!
Will
You can filter using the match and then do a count.
var occurences = bars.Where(x => x.Brand == "Brand" && x.Code == "code").Count();

How to dynamically add properties to a LINQ GroupBy clause

I have a class Product:
class Product
{
int Id { get; set; }
string Name { get; set; }
int CategoryId { get; set; }
int PlantId { get; set; }
DateTime ProductionDate { get; set; }
}
I would like to use the LINQ GroupBy on multiple properties but I do not know in advance how many and which properties. For instance I might want to group by just CategoryId, just PlantId or both. I found an article on the net that describes how to use LINQ GrouBy dinamically.
This might work good indeed but if I want to perform the Group By on ProductionDate.Year and ProductionDate.Month without knowing the granularity in advance? As granularity I mean whether I want to group all the Products produced in a specific year or narrow the group by to the month.
The only logical solution that I found is:
public ProductInfo GetGroupedProducts(int? year, int? month, int? selectedCategoryId, int? selectedPlantId)
{
List<Product> products = GetProducts();
var groupedProducts = products.GroupBy(p => new { (year == null ? p.ProductionDate.Year : string.Empty),
(month == null ? p.ProductionDate.Month : string.Empty),
(selectedCategoryId == null ? p.CategoryId : string.Empty),
(selectedPlantId == null ? p.PlantId : string.Empty)
});
//perform some additional filtering and assignments
}
But I guess there could be a cleaner and more proper solution. With the old style way of building queries, based on strings, this task was much easier to accomplish. If there is no other way, I really think this is a part of LINQ that needs to be improved.
The cleaner solution is to use this extension method:
public static TResult With<TInput, TResult>(this TInput? o, Func<TInput, TResult>
selector, TResult defaultResult = default(TResult)) where TInput : struct
{
return o.HasValue ? selector(o.Value) : defaultResult;
}
Like this:
string result = year.With(T => p.ProductionDate.Year, string.Empty);
of this, if nulls are okay:
string result = year.With(T => p.ProductionDate.Year);
or something with T which is int in case the int? has value.
But, you know, the better solution is out there, so feel free to expand your code so I could analyze it.
If I understand what you are asking, I had a similar issue Reversing typeof to use Linq Field<T>
I would do something like
public static IEnumerable<IGrouping<string, TElement>> GroupMany<TElement>(
this IEnumerable<TElement> elements,
params Func<TElement, object>[] groupSelectors)
{
return elements.GroupBy(e => string.Join(":", groupSelectors.Select(s => s(e))));
}
then you can call your function like
var groupedProducts = products.GroupMany(p => p.CategoryId , p => p.ProductionDate.Month);
The function groups via a string of the properties divided by a colon. The reason why I did this is because the hashcode for strings are guaranteed to be the same as opposed to a class.

how to fill missing values from a list

I have an object containing a date and a count.
public class Stat
{
public DateTime Stamp {get; set;}
public int Count {get; set ;}
}
I have a Serie object that holds a list of thoses Stat plus some more info such as name and so on...
public class Serie
{
public string Name { get; set; }
public List<Stat> Data { get; set; }
...
}
Consider that I have a List of Serie but the series don't all contain the same Stamps.
I need to fill in the missing stamps in all series with a default value.
I thought of an extension method with signature like this (please provide better name if you find one :) ) :
public static IEnumerable<Serie> Equalize(this IEnumerable<ChartSerie> series, int defaultCount)
this question seems to treat the same problem, but when querying directly the DB. of course I could loop through the dates and create another list. But is there any more elegant way to achieve this?
i.e.:
Serie A:
01.05.2010 1
03.05.2010 3
Serie B:
01.05.2010 5
02.05.2010 6
I should get :
Serie A :
01.05.2010 1
02.05.2010 0
03.05.2010 3
Serie B:
01.05.2010 5
02.05.2010 6
03.05.2010 0
Not sure if this is elegant enough for you ;-) but since I like Linq, this is what I would have done (using your naming scheme):
public static IEnumerable<Serie> Equalize(
this IEnumerable<Serie> series,
int defaultCount)
{
var allStamps = series
.SelectMany(s => s.Data.Select(d => d.Stamp))
.Distinct()
.OrderBy(d => d)
.ToList();
return series.Select(serie => new Serie(
serie.Name,
allStamps.Select(d =>
serie.Data.FirstOrDefault(stat => stat.Stamp == d)
??
new Stat(d, defaultCount))
));
}
For this code to compile, your classes needs a couple of constructors:
public class Stat
{
public Stat() {}
public Stat(DateTime stamp, int count)
{
Stamp = stamp;
Count = count;
}
public DateTime Stamp { get; set; }
public int Count { get; set; }
}
public class Serie
{
public Serie() {}
public Serie(string name, IEnumerable<Stat> data)
{
Name = name;
Data = new List<Stat>(data);
}
public string Name { get; set; }
public List<Stat> Data { get; set; }
}
When calling series.Equalize(0) the code above will leave the original instances intact, and return a sequence of newly created Serie-instances with their Data padded with defaults.
Nothing magic about it. Just the sweetness of Linq... (and the null coalescing operator!)
I haven't tried this with loads and loads of data, so your milage may vary.

Resources