LINQ suming nested groups in one statement - c#

I am converting some code to LINQ, at the same time exploring to what extent LINQ can accomplish.
Can the following code be condensed into a single LINQ query or method?
Dictionary<string, ItemPack> consolidated = new Dictionary<string, ItemPack>();
foreach (var product in Products)
{
foreach (var smallpack in product.ItemPacks)
{
ItemPack bigpack;
if (consolidated.TryGetValue(smallpack.ItemCode, out bigpack))
{
// the big pack quantity += quantity for making one product * the number of that product
bigpack.Quantity += smallpack.Quantity * product.Quantity;
// References: we make sure that the small pack is using the Item in the big pack.
// otherwise there will be 2 occurance of the same Item
smallpack.Item = bigpack.Item;
}
else
{
bigpack = new ItemPack(smallpack); // Copy constructor
bigpack.Quantity = smallpack.Quantity * product.Quantity;
consolidated.Add(smallpack.ItemCode, bigpack);
}
}
}
return consolidated;
In English, each product is made up of several items of different quantities. These items are grouped by item code and are packs into smallpacks. These smallpacks are shipped together as a unit product. There are many different products. A single item can be used in different product.
I now have a list of products and the quantity required for each for shipment. I want a LINQ statement to consolidate a flat list of items and their quantities.
I have gotten this far, but it looks like it does not work:
var packsQuery = from product in Products
from smallpack in product.ItemPacks
select new {Item = smallpack.Item, Quantity = smallpack.Quantity * product.Quantity};
foreach (var pack in packsQuery)
{
consolidated.Add(pack.Item.ItemCode, new ItemPack(pack.Item, pack.Quantity));
}
If I group first, then I cannot select item for its quantity. If I select first, then I lose the grouping. Chicken and egg story?
EDIT:
Useful note: smallpack is of type ItemPack which looks like this
public class ItemPack
{
Item { get; } // The item in this pack, which *must* be a shared reference across all objects that uses this Item. So that change in Item properties are updated everywhere it is used. e.g. Price.
ItemCode { get; } // The item code
Quantity { get; } // The number of such Item in this pack.
}

var query = (from product in Products
from smallPack in product.ItemPacks
select new
{
ItemCode = smallPack.ItemCode,
Item = smallPack.Item,
Quantity = smallPack.Quantity * product.Quantity,
})
.GroupBy(p => p.ItemCode)
.Select(p => new
{
ItemCode = p.Key,
Item = p.FirstOrDefault(),
Quantity = p.Sum(x=>x.Quantity)
})
.ToDictionary(p=>p.ItemCode);

Thanks for putting me in the right direction. I managed to work out the full query syntax version:
var query = from product in Products
from smallpack in product.ItemPacks
select new {
Item = smallpack.Item,
Quantity = smallpack.Quantity * product.Quantity
} into mediumpack
group mediumpack by mediumpack.Item.ItemCode into bigpack
select new {
Item = bigpack.First().Item, // shared reference
Quantity = bigpack.Sum(a => a.Quantity);
}
query.ToDictionary(...);
Any comments as to whether this is fine?

Related

How to simplify LINQ query just to filter out some special rows

I am very unfamiliar with Entity Framework and LINQ. I have a single entity set with some columns where I want to filter our some special rows.
4 of the rows are named Guid (string), Year (short), Month (short) and FileIndex (short). I want to get all rows which have the maximum FileIndex for each existing combination of Guid-Year-Month.
My current solution looks like this:
var maxFileIndexRecords = from item in context.Udps
group item by new { item.Guid, item.Year, item.Month }
into gcs
select new { gcs.Key.Guid, gcs.Key.Year, gcs.Key.Month,
gcs.OrderByDescending(x => x.FileIndex).FirstOrDefault().FileIndex };
var result = from item in context.Udps
join j in maxFileIndexRecords on
new
{
item.Guid,
item.Year,
item.Month,
item.FileIndex
}
equals
new
{
j.Guid,
j.Year,
j.Month,
j.FileIndex
}
select item;
I think there should be a shorter solution with more performance. Does anyone have a hint for me?
Thank you
You were close. It's not necessary to actually select the grouping key. You can simply select the first item of each group:
var maxFileIndexRecords =
from item in context.Udps
group item by new { item.Guid, item.Year, item.Month }
into gcs
select gcs.OrderByDescending(x => x.FileIndex).FirstOrDefault();

Compare two lists and add missing items by specific criteria

I have written a code like below:
foreach (var itemA in itm)
{
foreach (var itemB in filteredList)
{
if (itemA.ItemID != itemB.ItemID)
{
missingList.Add(itemB);
ListToUpdate.Add(itemB);
}
else
{
if (itemA.QuantitySold != itemB.QuantitySold)
{
ListToUpdate.Add(itemB);
}
}
}
}
So as you can see i have two lists here which are identical in their structure and they are:
List #1 is "itm" list - which contains old records from DB
List #2 is "filteredList" - which has all items from DB and + new ones
I'm trying to add items to missingList and ListToUpdate on next criteria:
All items that are "new" in filteredList - meaning their ItemID doens't exists in "itm" list should be added to missingList.
And all items that are new in filteredList- filteredList - meaning their ItemID doens't exists in "itm" list should be added to .ListToUpdate
And final criteria to add items to ListToUpdate should be those items that exist in both lists - and if the quantitysold in "itm" list is different - add them to ListToUpdate
The code above that I written gives me completely wrong results, I end up having more than 50000 items extra in both lists...
I'd like to change this code in a manner that it works like I wrote above and to possibly use parallel loops or PLINQ to speed things up...
Can someone help me out ?
Let's use Parallel.ForEach, which is available in C# 4.0:
Parallel.ForEach(filteredList, (f) =>
{
var conditionMatchCount = itm.AsParallel().Max(i =>
// One point if ID matches
((i.ItemID == f.ItemID) ? 1 : 0) +
// One point if ID and QuantitySold match
((i.ItemID == f.ItemID && i.QuantitySold == f.QuantitySold) ? 1 : 0)
);
// Item is missing
if (conditionMatchCount == 0)
{
listToUpdate.Add(f);
missingList.Add(f);
}
// Item quantity is different
else if (conditionMatchCount == 1)
{
listToUpdate.Add(f);
}
});
The above code uses two nested parallelised list iterators.
Following is an example to compare two lists which will give you list of new IDs.
Class I used to hold the data
public class ItemList
{
public int ID { get; set; }
}
Function to get new IDs
private static void GetNewIdList()
{
List<ItemList> lstItm = new List<ItemList>();
List<ItemList> lstFiltered = new List<ItemList>();
ItemList oItemList = new ItemList();
oItemList.ID = 1;
lstItm.Add(oItemList);
lstFiltered.Add(oItemList);
oItemList = new ItemList();
oItemList.ID = 2;
lstItm.Add(oItemList);
lstFiltered.Add(oItemList);
oItemList = new ItemList();
oItemList.ID = 3;
lstFiltered.Add(oItemList);
var lstListToUpdate = lstFiltered.Except(lstItm);
Console.WriteLine(lstListToUpdate);
}
For getting the list of common IDs use following
var CommonList = from p in lstItm
join q in lstFiltered
on p.ID equals q.ID
select p;
UPDATE 2
For getting the list of new IDs from filtered list based on ID
var lstListToUpdate2 = lstFiltered.Where(a => !lstItm.Select(b => b.ID).Contains(a.ID));

Random with condition

I have the following code to extract records from a dbcontext randomly using Guid class:
var CategoryList = {1,5};
var generatedQues = new List<Question>();
//Algorithm 1 :)
if (ColNum > 0)
{
generatedQues = db.Questions
.Where(q => CategoryList.Contains(q.CategoryId))
.OrderBy(q => Guid.NewGuid()).Take(ColNum).ToList();
}
First, I have a list of CategoryId stored in CategoryList as a condition to be fulfilled when getting records from the db. However, I would like to achieve an even distribution among the questions based on the CategoryId.
For example:
If the ColNum is 10, and the CategoryId obtained are {1,5}, I would like to achieve by getting 5 records that are from CategoryId = 1 and another set of 5 records from CategoryId = 5. If the ColNum is an odd number like 11, I would also like to achieve an even distribution as much as possible like maybe getting 5 records from CategoryId 1 and 6 records from CategoryId 2.
How do I do this?
This is a two step process,
Determine how many you want for each category
Select that many items from each category in a random order
For the first part, define a class to represent the category and how many items are required
public class CategoryLookup
{
public CategoryLookup(int catId)
{
this.CategoryId = catId;
}
public int CategoryId
{
get; private set;
}
public int RequiredAmount
{
get; private set;
}
public void Increment()
{
this.RequiredAmount++;
}
}
And then, given your inputs of the required categories and the total number of items required, work out how many are required for each category
var categoryList = new []{1,5};
var colNum = 7;
var categoryLookup = categoryList.Select(x => new CategoryLookup(x)).ToArray();
for(var i = 0;i<colNum;i++){
categoryLookup[i%categoryList.Length].Increment();
}
The second part is really easy, just use a SelectMany to get the list of questions (Ive used a straight linq to objects to test, should work fine for database query. questions in my code would just be db.Questions in yours)
var result = categoryLookup.SelectMany(
c => questions.Where(q => q.CategoryId == c.CategoryId)
.OrderBy(x => Guid.NewGuid())
.Take(c.RequiredAmount)
);
Live example: http://rextester.com/RHF33878
You could try something like this:
var CategoryList = {1,5};
var generatedQues = new List<Question>();
//Algorithm 1 :)
if (ColNum > 0 && CategoryList.Count > 0)
{
var take = // Calculate how many of each
// First category
var query = db.Questions
.Where(q => q.CategoryId == CategoryList[0])
.OrderBy(q => Guid.NewGuid()).Take(take);
// For all remaining categories
for(int i = 1; i < CategoryList.Count; i++)
{
// Calculate how many you want
take = // Calculate how many of each
// Union the questions for that category to query
query = query.Union(
query
.Where(q => q.CategoryId == CategoryList[i])
.OrderBy(q => Guid.NewGuid()).Take(take));
}
// Randomize again and execute query
generatedQues = query.OrderBy(q => Guid.NewGuid()).ToList()
}
The idea is to just get a random list for each category and add them all together. Then you randomize that again and create your list. I do not know if it will do all this on the database or in memory, but it should be database I think. The resulting SQL will look horrible though.

Get list of entities whose fk related entity does not contain id in list of ids

I have two tables - Products and SKUs, related one to many.
I have a third table, rental, which contains sku ids that have been rented.
Products have a number of related SKUs - so product 1 may have sku 1, 2, and 3 for instance.
Customers rent products by SKU. So SKU 1 may be out of inventory, but there are still 2 SKUs left in inventory, meaning the product is still available.
If all 3 SKUs are rented (are in the rental table), the product is unavailable.
I need to query the context for Products that have SKUs remaining in inventory.
I'm trying to do this by getting a list of products, a list of unavailable SKUs, and returning those products whose SKUs are not in the unavailable SKU list.
I have an iQueryable of products...
IQueryable<products> p = (from r in db.products from pc in r.productcategories
where pc.categories.categoryname == n select r)
.OrderByDescending(s => s.productname).Take(18);
and a list of unavailable skus...
List<int> rentedskus = new List<int>();
rental = rental.Where(t => sd < t.enddate && t.startdate < ed);
foreach (var r in rental)
{
rentedskus.Add(r.skuid);
}
Things get sloppy here, but I get a list of product skus from my product list...
List<products> plist = p.ToList();
List<int> productskus = new List<int>();
foreach(var pr in plist)
{
foreach(var sk in pr.sku)
{
productskus.Add(sk.skuid);
}
}
Now, assuming this is an acceptable way to achieve my goal, I need to select only those products whose related SKUs are not in rentedskus. I've tried a few variations of...
var productlistresult = productskus.Except(rentedskus).ToList();
p = p.Where(i => i.productlistresult.Contains(product.skus(n => n.skuid));
You were pretty much there with your last sample code, you just have some logic errors. This is what you need:
p = p.Where(i => productlistresult.Contains(i.skuid));
Also, you can clean up your code considerably, by using Select. For example, instead of creating a list, querying the rented skus, then iterating over the skus and adding them to the list, just do:
var rentedskus = rental.Where(t => sd < t.enddate && t.startdate < ed).Select(t => t.skuid);

Grouping items in Linq query

I'm trying to group a result set of ektron search results items and output them on screen, I use the following code
var groupdResults =
from result in response.Results
orderby result[SearchSmartFormProperty.GetDateProperty("/root/FundClosingDate")]
group result by result[SearchSmartFormProperty.GetStringProperty("/root/DeadlineAltText")]
into statusGroup
select new
{
closingDate =statusGroup.Key,
count= statusGroup.Count
};
I then add these to a listview: uxSearchResultView.DataSource = groupdResults;
The problem I'm having is that i need to output all the data from the resultset e.g. title,closingdate, etc.., It currently only outputs, e.g.
Closing 2
open 1
really appreciate any help anyone can offer
-----------------------Updated-------------------------------------
I think i have a working solution now, but its kind of messy
var groupdResults = from result in response.Results
orderby result[SearchSmartFormProperty.GetDateProperty("/root/FundClosingDate")]
group result by result[SearchSmartFormProperty.GetStringProperty("/root/DeadlineAltText")]
into statusGroup
select new
{
closingDate = statusGroup.Key,
count = statusGroup.Count(),
items = statusGroup.ToList()
};
List<Ektron.Cms.Search.SearchResultData> SRDATA = new List<Ektron.Cms.Search.SearchResultData>();
foreach (var result in groupdResults)
{
for (int i = 0; i < result.items.Count; i++)
{
SRDATA.Add(result.items[i]);
}
}
Any input as to a cleaner implementation?
thanks
You can do following:
select new
{
closingDate =statusGroup.Key,
count= statusGroup.Count(),
items = statusGroup.ToList()
};
items property will contain items that were assigned to that group in a List.
(Modified)
I think you need a class to capture your results:
class GroupResult
{
public string Status { get; set; }
public ICollection<SearchResultData> Items { get; set; }
}
The grouping statusGroup is an IEnumerable of your items, so you can do:
var groupdResults =
from result in response.Results
group result by result[SearchSmartFormProperty.GetStringProperty("/root/DeadlineAltText")]
into statusGroup
select new GroupResult
{
Status =statusGroup.Key,
Items = statusGroup.ToList()
}.ToList();
After that you can display the Items in any way you wish, like sorted by Status and the Items sorted by ClosingDate.
But maybe it is enough to just sort response.Results by status and then by closing date.
Rather than using anonymous types I would put that into a new object.
var groupdResults =
from result in response.Results
orderby result[SearchSmartFormProperty.GetDateProperty("/root/FundClosingDate")]
group result by result[SearchSmartFormProperty.GetStringProperty("/root/DeadlineAltText")]
into statusGroup
select new ResultObject
{
closingDate = statusGroup.First().ClosingDate,
....
};

Categories

Resources