Use List.ToLookup() - c#

I have a List like the following:
var products = new List<Product>
{
new Product { Id = 1, Category = "Electronics", Value = 15.0 },
new Product { Id = 2, Category = "Groceries", Value = 40.0 },
new Product { Id = 3, Category = "Garden", Value = 210.3 },
new Product { Id = 4, Category = "Pets", Value = 2.1 },
new Product { Id = 5, Category = "Electronics", Value = 19.95 },
new Product { Id = 6, Category = "Pets", Value = 5.50 },
new Product { Id = 7, Category = "Electronics", Value = 250.0 },
};
I want to group by category and get the sum of 'Values' belonging to that category..
Example: Electronics: 284.95
While I can do this in some other way, I want to learn usage of Look-Up.
Is it possible to get these 2 values (category and Value) in a Look-Up? If yes, How can I do that?

When you retrieve by key from a Lookup, it behaves just like a grouping, so you can do things like this:
var productLookup = products.ToLookup(p => p.Category);
var electronicsTotalValue = productLookup["Electronics"].Sum(p => p.Value);
var petsTotalValue = productLookup["Pets"].Sum(p => p.Value);
//etc
var totalValue = products.Sum(p => p.Value);
// I wouldn't use the Lookup here, the line above makes more sense and would execute faster
var alsoTotalValue = productLookup.Sum(grp => grp.Sum(p => p.Value));

You probably want to use ToDictionary() instead of ToLookup
var dict = products
.GroupBy(p => p.Category)
.ToDictionary(grp => grp.Key, grp => grp.Sum(p => p.Value));
foreach(var item in dict)
{
Console.WriteLine("{0} = {1}", item.Key, item.Value);
}

You don't need a Lookup. You can do this with just a query:
var results =
from p in products
group p by p.Category into g
select new
{
Category = g.Key,
TotalValue = g.Sum(x => x.Value)
};

var rez = products.ToLookup(k => k.Category, v => v.Value).Select(k=>new KeyValuePair<string, double>(k.Key, k.Sum()));

Related

Linq query to return the cheapest product, only if it is unique

I have a list of Products, with the Price. I would like to get the the cheapest one only if it is unique. If there are more than one Product with the same lowest price, it should not return any.
In the sample below, for the uniqProductList the query should return the BestOne while for the dupProductList, no product should be returned.
How do I write the Linq query ?
public class Product
{
public string Name { get; set; }
public decimal Price { get; set; }
public DateTime ExpiryDate { get; set; }
}
List<Product> uniqProductList = new List<Product>() {
new Product { Name = "GoodOne", Price = 12M },
new Product { Name = "NiceOne", Price = 12M },
new Product { Name = "ExpensiveOne", Price = 15M },
new Product { Name = "BestOne", Price = 9.99M }
};
List<Product> dupProductList = new List<Product>() {
new Product { Name = "GoodOne", Price = 12M },
new Product { Name = "NiceOne", Price = 12M },
new Product { Name = "ExpensiveOne", Price = 15M },
};
This is one way if you want to do it in a single query:
Product result = uniqProductList
.GroupBy(x => x.Price)
.OrderBy(x => x.Key)
.Take(1)
.FirstOrDefault(x => x.Count() == 1)?
.FirstOrDefault();
Group the results by price
Order by price so that the cheapest is the first result
Take the first result, since we aren't interested in the other ones
Return the grouping if there is only one result in the group
Return the value
There's almost certainly some other method that is faster, though.
You are looking for ArgMax which is not included into standard Linq but can be implemented manually with a help of Aggregate. Having a collection of the cheapest Products we can return null if we have more than 1 of them:
using System.Linq;
...
List<Product> source = ...
var bests = source
.Aggregate(new List<Product>(), (s, a) => {
if (s.Count <= 0 || s[0].Price == a.Price)
s.Add(a);
else if (a.Price <= s[0].Price) {
s.Clear();
s.Add(a);
}
return s;
});
Product best = bests.Count == 1 ? bests[1] : default(Product);
You could group by the elements by their price and getting the cheapest group:
var cheapestGrp = uniqProductList.GroupBy(i => i.Price).OrderBy(i => i.Key).First();
Then, based on the number of elements of the group, return the only element or return nothing:
if (cheapestGrp.Count() > 1)
return null;
else
return cheapestGrp.ToList().First();
result = Products.GroupBy(x => x.Price)
.Select(g => new { g.Name, Count = g.Count()
})
.Orderby(s.Count)
.Select(x.Name, x.Count).FirstOrDefault();
if(result.Count == 1){
return result;
}
else{
return null;
}
I would suggest this solution:
public Product? TryGetBestOne(IEnumerable<Product> products)
{
var bestProducts = products
.GroupBy(x => x.Price)
.OrderBy(x => x.Key)
.FirstOrDefault()?
.ToArray() ?? Array.Empty<Product>();
return bestProducts.Count() == 1 ? bestProducts.Single() : null;
}
You can use GroupBy and then use Where to get items where there is just one Count and then just sort in ascending order:
var result = uniqProductList
.GroupBy(u => u.Price)
.Select(grp => new { grp.Key, Count = grp.Count(), Items = grp.ToList() })
.Where(s => s.Count == 1)
.OrderBy(o=> o.Key)
.FirstOrDefault();
An example:
List<Product> uniqProductList = new List<Product>() {
new Product { Name = "GoodOne", Price = 12M },
new Product { Name = "NiceOne", Price = 12M },
new Product { Name = "ExpensiveOne", Price = 15M },
new Product { Name = "BestOne", Price = 9.99M }
};
List<Product> dupProductList = new List<Product>() {
new Product { Name = "GoodOne", Price = 12M },
new Product { Name = "NiceOne", Price = 12M },
new Product { Name = "ExpensiveOne", Price = 15M },
};
var result = uniqProductList
.GroupBy(u => u.Price)
.Select(grp => new { grp.Key, Count = grp.Count(), Items = grp.ToList() })
.Where(s => s.Count == 1)
.OrderBy(o=> o.Key)
.FirstOrDefault();
This is another solution :
var product = uniqProductList.OrderBy(a => a.Price)
.GroupBy(a => a.Price).FirstOrDefault()
.Aggregate(new List<Product>(), (result, item) =>
{
result.Add(item);
if (result.Count() > 1)
result = new List<Product>();
return result;
}).FirstOrDefault();
You can get the lowest price first, then you can group them.

Distinct by Category then Join all distinct values per Category in one column

I have this Data:
CategoryId Value
1 val1
1 val2
1 val2
2 test1
2 test1
3 data1
3 data2
3 data2
the output that i want is like this:
CategoryId Value
1 val1. val2.
2 test1.
3 data1. data2.
output should be Distinct in CategoryId and only distinct values per category should be displayed and joined together in 1 column value. (Assume that the values are string values which are 1 to 3 sentences long).
How do i query this in LINQ? or how do i group it with the output that i wanted?
GroupBy CategoryId and Join the Distinct Values
var distinctCategory = categoryList.GroupBy(x => x.CategoryId)
.Select(x => new Category()
{
CategoryId = x.Key,
Value = string.Join(". ", x.Select(y => y.Value).Distinct())
});
https://dotnetfiddle.net/0Z04AY
This is tested and working solution.
List<Data> data = new List<Data>();
data.Add(new Data(){CategoryId = 1,Value = "val1"});
data.Add(new Data(){CategoryId = 1,Value = "val2"});
data.Add(new Data(){CategoryId = 1,Value = "val2"});
data.Add(new Data(){CategoryId = 2,Value = "test1"});
data.Add(new Data(){CategoryId = 2,Value = "test1"});
data.Add(new Data(){CategoryId = 3,Value = "data1"});
data.Add(new Data(){CategoryId = 3,Value = "data2"});
data.Add(new Data(){CategoryId = 3,Value = "data2"});
var result = data.Distinct()
.GroupBy(d => d.CategoryId, d=>d.Value, (k,v) => new { Key=k, Values = v.Distinct() } )
.Select(d => new Data()
{
CategoryId = d.Key,
Value = string.Join(". ", d.Values.ToList())
}).ToList();
It sounds like you want to group the values by category ID, and then take the distinct values within the group.
That can all be done with a single GroupBy call:
var query = input.GroupBy(
item => item.CategoryId, // Key projection
item => item.Value, // Element projection
// Result projection. (You may want to add ToList within the lambda to materialize.)
(key, values) => new { Key = key, Values = values.Distinct() });
Here's a complete program with your example data:
using System;
using System.Linq;
class Program
{
static void Main()
{
var input = new[]
{
new { CategoryId = 1, Value = "val1" },
new { CategoryId = 1, Value = "val2" },
new { CategoryId = 1, Value = "val2" },
new { CategoryId = 2, Value = "test1" },
new { CategoryId = 2, Value = "test1" },
new { CategoryId = 3, Value = "data1" },
new { CategoryId = 3, Value = "data2" },
new { CategoryId = 3, Value = "data2" },
};
var query = input.GroupBy(
item => item.CategoryId, // Key
item => item.Value, // Element
// Projection of results
(key, values) => new { Key = key, Values = values.Distinct() });
foreach (var element in query)
{
Console.WriteLine($"{element.Key}: {string.Join(", ", element.Values)}");
}
}
}
Alternatively, you could just do the grouping of value by category ID, then perform the Distinct() part when you consume:
var query = from item in input
group item.Value by item.CategoryId;
foreach (var group in query)
{
var values = group.Distinct();
Console.WriteLine($"{group.Key}: {string.Join(", ", values)}");
}
You can use string.Join within the query, of course - but I'd normally wait until the last possible moment to convert the logical data into a display representation. If you wanted to do that with the first example, you'd just use:
var query = input.GroupBy(
item => item.CategoryId, // Key projection
item => item.Value, // Element projection
// Result projection. (You may want to add ToList within the lambda to materialize.)
(key, values) => new { Key = key, Values = string.Join(", ", values.Distinct()) });
Note that I've joined the values with a comma rather than a period. Your original expected output wouldn't be achievable with just string.Join and a period anyway, as you have a trailing period too. You could get that exact output with a projection then a string.Concat call, e.g.
foreach (var element in query)
{
var periodValues = element.Values.Select(x => x + ". ");
Console.WriteLine($"{element.Key}: {string.Concat(periodValues)}");
}

How to intersect results after GroupBy

To illustrate my problem I have created this simple snippet. I have a class Item
public class Item
{
public int GroupID { get; set; }
public int StrategyID { get; set; }
public List<Item> SeedData()
{
return new List<Item>
{
new Item {GroupID = 1, StrategyID = 1 },
new Item {GroupID = 2, StrategyID = 1 },
new Item {GroupID = 3, StrategyID = 2 },
new Item {GroupID = 4, StrategyID = 2 },
new Item {GroupID = 5, StrategyID = 3 },
new Item {GroupID = 1, StrategyID = 3 },
};
}
}
And what I want to check is that this SeedData method is not returning any duplicated GroupID/StrategyID pairs.
So in my Main method I have this:
Item item = new Item();
var data = item.SeedData();
var groupByStrategyIdData = data.GroupBy(g => g.StrategyID).Select(v => v.Select(gr => gr.GroupID)).ToList();
for (var i = 0; i < groupByStrategyIdData.Count; i++)
{
for (var j = i + 1; j < groupByStrategyIdData.Count; j++)
{
Console.WriteLine(groupByStrategyIdData[i].Intersect(groupByStrategyIdData[j]).Any());
}
}
which is working fine but one of the problems is that I have lost the StrategyID so in my real-case scenario I won't be able to say for which StrategyID/GroupID pair I have duplication so I was wondering is it possible to cut-off the LINQ to here:
var groupByStrategyIdData = data.GroupBy(g => g.StrategyID)
and somehow perform the check on this result?
One of the very easy ways would be to do grouping using some identity for your Item. You can override Equals/GetHashCode for your Item or instead write something like:
Item item = new Item();
var data = item.SeedData();
var duplicates = data.GroupBy(x => string.Format("{0}-{1}", x.GroupID, x.StrategyID))
.Where(group => group.Count() > 1)
.Select(group => group.Key)
.ToList();
Please note, that using a string for identity inside of group by is probably not the best way to do grouping.
As of your question about "cutting" the query, you should also be able to do the following:
var groupQuery = data.GroupBy(g => g.StrategyID);
var groupList = groupQuery.Select(grp => grp.ToList()).ToList();
var groupByStrategyIdData = groupQuery.Select(v => v.Select(gr => gr.GroupID)).ToList();
You may be able to do it another way, as follows:
// Check for duplicates
if (data != null)
{
var grp =
data.GroupBy(
g =>
new
{
g.GroupID,
g.StrategyID
},
(key, group) => new
{
GroupID = key.GroupID,
StrategyId = key.StrategyID,
Count = group.Count()
});
if (grp.Any(c => c.Count > 1))
{
Console.WriteLine("Duplicate exists");
// inside the grp object, you can find which GroupID/StrategyID combo have a count > 1
}
}

How to write a LINQ query combining group by and aggregates?

Given the following input, how do I write a LINQ query or expression to return an aggregated result set for the quantity?
Input:
var foo = new[] { new { PO = "1", Line = 2, QTY = 0.5000 },
new { PO = "1", Line = 2, QTY = 0.2500 },
new { PO = "1", Line = 2, QTY = 0.1000 },
new { PO = "1", Line = 2, QTY = -0.1000 }
}.ToList();
Desired result:
Something along the lines of
new { PO = "1", Line = 2, QTY = 0.7500 } // .5 + .25 + .1 + -.1
How would I write it for multiple lines as well (see the object model in foo)?
How about this:
var result = foo.GroupBy(x => x.Line)
.Select(g => new { PO = g.First().PO,
Line = g.Key,
QTY = g.Sum(x => x.QTY) });
In the case you just have one Line, just add a .Single() - result is an IEnumerable of the anonymous type defined when you set up foo.
Edit:
If both PO and Line should designate different groups (PO can have different values), they both have to be part of the group key:
var result = foo.GroupBy(x => new { x.PO, x.Line})
.Select(g => new {
PO = g.Key.PO,
Line = g.Key.Line,
QTY = g.Sum(x => x.QTY)
});
var query = (from t in foo
group t by new {t.PO, t.Line}
into grp
select new
{
grp.Key.PO,
grp.Key.Line,
QTY = grp.Sum(t => t.QTY)
}).ToList()

In a LINQ query, when calling ToDictionary, what is assigned the key and value?

var products =
(
from p in products
where !p.parentID.HasValue
select new ProductList
{
Name = p.Name,
Id = p.ID,
IsInStock = products.Any(...)
}
)
.ToArray();
I want to call ToDictionary() and have the Id as the Key, and IsInStock as the value.
Is this possible?
Here is an example of using ToDictionary() that might help
var x = new[]
{
new { Name = "Alice", Score = 50 },
new { Name = "Bob", Score = 40 },
new { Name = "Cathy", Score = 45 }
};
var y = x.ToDictionary(i => i.Name);
The value of y is (from LinqPad):
In any case, I think if you add ToDictionary(a => a.ID, b => b.IsInStock) you should get what you're looking for.
Try this:
var products =
(
from p in products
where !p.parentID.HasValue
select new ProductList
{
Name = p.Name,
Id = p.ID,
IsInStock = products.Any( ... )
}
)
.ToDictionary(y => y.ID, x => x.IsInStock);
Did you even try to write it??
I think little look at InteliSense would tell you there is simple override to ToDictinary, that takes selectors for both key and value : http://msdn.microsoft.com/en-us/library/bb548657.aspx

Categories

Resources