List union that depends / selects on property values - c#

I have a class "item":
public class Item
{
public int Id { get; set; }
public decimal Price { get; set; }
public override bool Equals(object obj)
{
if (obj is Item item)
return item.Id == Id;
return false;
}
// GetHashCode omitted...
}
And I have 2 lists that I need to union:
var items1 = new List<Item>
{
new Item { Id = 1, Price = 10 },
new Item { Id = 2, Price = 10 },
new Item { Id = 3, Price = 10 },
};
var items2 = new List<Item>
{
new Item { Id = 1, Price = 10 },
new Item { Id = 2, Price = 8 },
new Item { Id = 4, Price = 10 },
};
The union I get like this:
var union = items1.Union(items2).ToList();
But I need also the constraint that the items with the lowest price is in the union. So for example in the above lists Item.ID = 2 from "items2" must be in the union...so the result should be a list consisting of these 4 items:
Item { Id = 1, Price = 10 }
Item { Id = 2, Price = 8 } // Not the one with Price = 10
Item { Id = 3, Price = 10 }
Item { Id = 4, Price = 10 }
Is there an elegant way of doing this in C# (preferably using Linq)?

You can try using groupby, like below :
var result = items1.Union(items2).GroupBy(x => x.Id)
.Select(x => new Item
{
Id = x.Key,
Price = x.Min(i => i.Price)
});

Related

Linq query to return items of list with another list inside it

I'm new to Linq queries, and this problem encountered me (after the other dev was fired).
I'm consuming a freight API that returns a list of "n" freight prices for each product/item in the requisition. The response classes are:
public class FreightSimulation
{
public List<Item> Items { get; set; }
}
public class Item
{
public int Id { get; set; }
public List<ItemFreight> Freights { get; set; }
}
public class ItemFreight
{
public Company Company { get; set; }
public Type Type { get; set; }
public double Price { get; set; }
public int Days { get; set; }
public List<Error> Errors { get; set; }
}
public class Company
{
public int Id { get; set; }
public string Name { get; set; }
}
public class Type
{
public int Id { get; set; }
public string Description { get; set; }
}
public class Error
{
public string Message { get; set; }
}
I have a response model for 2 items (but it can also be "n" items), one of them have 5 freights possibilities, with no errors, but the other has two errors meaning that one company doesn't have a freight service for that item:
var response = new FreightSimulation
{
Items = new List<Item>
{
new Item
{
Id = 1,
Freights = new List<ItemFreight>
{
new ItemFreight
{
Errors = null,
Price = 10,
Days = 8,
Company = new Company
{
Id = 1
},
Type = new Type
{
Id = 3
}
},
new ItemFreight
{
Errors = null,
Price = 20,
Days = 3,
Company = new Company
{
Id = 1
},
Type = new Type
{
Id = 8
}
},
new ItemFreight
{
Errors = null,
Price = 20,
Days = 10,
Company = new Company
{
Id = 2
},
Type = new Type
{
Id = 1
}
},
new ItemFreight
{
Errors = null,
Price = 35,
Days = 4,
Company = new Company
{
Id = 2
},
Type = new Type
{
Id = 2
}
},
new ItemFreight
{
Errors = null,
Price = 15,
Days = 6,
Company = new Company
{
Id = 7468
},
Type = new Type
{
Id = 1
}
}
}
},
new Item
{
Id = 2,
Freights = new List<ItemFreight>
{
new ItemFreight
{
Errors = null,
Price = 10,
Days = 8,
Company = new Company
{
Id = 1
},
Type = new Type
{
Id = 3
}
},
new ItemFreight
{
Errors = null,
Price = 20,
Days = 3,
Company = new Company
{
Id = 1
},
Type = new Type
{
Id = 8
}
},
new ItemFreight
{
Errors = new List<Error>
{
new Error
{
Message = "Not found."
}
},
Company = new Company
{
Id = 2
},
Type = new Type
{
Id = 1
}
},
new ItemFreight
{
Errors = new List<Erro>
{
new Error
{
Message = "Not found."
}
},
Company = new Company
{
Id = 2
},
Type = new Type
{
Id = 2
}
},
new ItemFreight
{
Errors = null,
Price = 22,
Days = 4,
Company = new Company
{
Id = 7468
},
Type = new Type
{
Id = 1
}
}
}
}
}
};
What I need at first is a Linq Query that brings a List where the children List are common between all the Items and doesn't have errors in it.
In this case I have something like:
Company Id Type Id Error
Item 1 1 3 N
Item 1 1 8 N
Item 1 2 1 N
Item 1 2 2 N
Item 1 7468 1 N
Item 2 1 3 N
Item 2 1 8 N
Item 2 2 1 Y
Item 2 2 2 Y
Item 2 7468 1 N
So in this case I need a list where the combinations would be only the company 1 and 7468 (could be "n" companies). So in item 2 I have errors on my company 2 response, so it has to be eliminated. The result would be:
Company Id Type Id Error
Item 1 1 3 N
Item 1 1 8 N
Item 1 7468 1 N
Item 2 1 3 N
Item 2 1 8 N
Item 2 7468 1 N
It could happen with any of the combination companies/types, so it has to be some kind of a dynamic query, if it's possible.
Thanks in advance.
You can get the companies that return an error for any freight:
var errorCompanyIds = response.Items
.SelectMany(x => x.Freights)
.Where(y => y.Errors !=null && y.Errors.Any())
.Select(y => y.Company.Id)
.ToList();
Then you can create your new list and filter any freight from that comapny
var newList = new FreightSimulation
{
Items = response.Items.Select(x => new Item
{
Id = x.Id,
Freights = x.Freights.Where(y => !errorCompanyIds.Contains(y.Company.Id)).ToList()
}).ToList()
};
This should work:
var companyIds = new[] { 1, 7468 };
var result = response.Items.SelectMany(i => i.Freights.Select(f => new { itemId = i.Id, Freight = f }))
.Where(i => companyIds.Contains(i.itemId) && (!(i.Freight.Errors?.Any() ?? false)));
What you are doing here is creating pairs to preserve the ids and then filtering those pairs based on your desired criteria.
Alternatively, you can filter the original list of freights first, then pick by company:
var companyIds = new[] { 1, 7468 };
var result = response.Items.SelectMany(i => i.Freights.Where(f => (!(f.Errors?.Any() ?? false)))
.Select(f => new { itemId = i.Id, Freight = f }))
.Where(i => companyIds.Contains(i.itemId));
Just in case this part is a bit confusing:
(!(i.Freight.Errors?.Any() ?? false))
This essentially translates to:
i.Freight.Errors == null || !i.Freight.Errors.Any()
Meaning freights with null errors or no errors (empty collection).

Merge multiple ObjectList into one

I have a object list like this:
using System;
using System.Collections.Generic;
using System.Linq;
public class Item
{
public int Id;
public int Price;
}
public class Program
{
public static void Main()
{
List<Item> food = new List<Item> {
new Item { Id = 1, Price = 1},
new Item { Id = 2, Price = 3},
new Item { Id = 4, Price = 9}
};
List<Item> drinks = new List<Item> {
new Item { Id = 1, Price = 1},
new Item { Id = 2, Price = 2},
new Item { Id = 3, Price = 0},
new Item { Id = 4, Price = 1},
new Item { Id = 6, Price = 1}
};
List<Item> magazines = new List<Item> {
new Item { Id = 3, Price = 1},
new Item { Id = 5, Price = 2},
};
var combined = food.Union(drinks).Union(magazines).Distinct().ToList();
}
}
What I want to do is, add all the prices into one list. Without any duplicates (Id). My goal is to have the total sum of the prices. So basically add all prices for the same ID together.
So the combined list should look like this:
List<Item> combined = new List<Item> {
new Item { Id = 1, Price = 2},
new Item { Id = 2, Price = 5},
new Item { Id = 3, Price = 1},
new Item { Id = 4, Price = 10},
new Item { Id = 5, Price = 2},
new Item { Id = 6, Price = 1}
};
Preferably using LINQ.
If you need to get a sum of prices for concatenated List<Item>, you should use GroupBy method to group the items by Id and then Sum of prices for every group
var combined = food.Concat(drinks).Concat(magazines)
.GroupBy(i => i.Id, i => i.Price, (i, prices) => new Item { Id = i, Price = prices.Sum() })
.OrderBy(i => i.Id).ToList();
You can also add OrderBy to sort the results by Id property, if it's important
var x =
// First, combine all lists
food.Concat(drinks).Concat(magazines)
// Group combined Items by Id
.GroupBy(item => item.Id)
// From all groups create final Items with Id and summed Price
.Select(g => new Item { Id = g.Key, Price = g.Sum(item => item.Price) });

Cannot group data in LINQ

I have a question about a LINQ grouping.
I thought that grouping would be a simple matter of using the GroupBy function on the result set and specifying what to group it by. However my items appear to not be grouping together and instead are displaying as if the GroupBy function wasn't there. I want to group by the itemPk, but I'm can't seem to do it. I have tried grouping by both category.ItemFk and Item.Itempk, but no luck. Could someone give me a pointer on this?
var itemIds = items.Select(i => i.ItemId).ToList();
var itemAndCatJoin =
from item in Context.SCS_Items
join category in Context.SCS_ItemCategories
on item.ItemPk equals category.ItemFk
into temp
from category in temp.DefaultIfEmpty()
select new ExportItemTable
{
Category = category,
Item = item
};
return itemAndCatJoin.Where(i => itemIds.Contains(i.Item.ItemPk))
.GroupBy(n => new {n.Item, n.Category})
.Select(i => new ExportableItem
{
ItemPk = i.Key.Item.ItemPk,
Name = i.Key.Item.Name,
Description = i.Key.Item.Description,
Price = i.Key.Item.Price,
Category = i.Key.Category.Category.Category_Name,
GLDepartment = i.Key.Category.GL_Department.Name ?? "",
GLName = i.Key.Category.GL_Name.Name ?? "",
StartDate = i.Key.Item.StartDate,
EndDate = i.Key.Item.EndDate,
FiscalYear = i.Key.Item.SCS_FiscalYear.Name,
School = i.Key.Item.School != null ? i.Key.Item.School.School_Name : i.Key.Item.Board.Board_Name,
Beneficiary = i.Key.Item.SCS_Beneficiary.Name,
Quantity = i.Key.Item.MaxQuantity,
Deleted = i.Key.Item.DeletedFlag,
OptionalStudents = i.Key.Item.SCS_Attachments.Where(a => !a.IsRequired).SelectMany(a => a.SCS_StudentAttachments).Where(s => !s.DeletedFlag).Select(s => s.StudentFk).Distinct().Count(),
RequiredStudents = i.Key.Item.SCS_Attachments.Where(a => a.IsRequired).SelectMany(a => a.SCS_StudentAttachments).Where(s => !s.DeletedFlag).Select(s => s.StudentFk).Distinct().Count(),
IsPublic = i.Key.Item.IsPublic,
AllowRecurring = i.Key.Item.AllowRecurringPayments,
EffectiveCutoff = i.Key.Item.SCS_Attachments.Where(a => !a.DeletedFlag && a.CourseDropCutoff.HasValue).Select(a => a.CourseDropCutoff).OrderBy(a => a).FirstOrDefault(),
CreatedDate = i.Key.Item.CreatedDate
}).OrderBy(i => i.ItemPk).ToList();
}
your groupbyy is indeed doing nothing for you, you need to tell the groupby what to group by....
like
.GroupBy(n => n.Category)
Here is a simple example to your grouping question:
class Program
{
static void Main()
{
var allItems = GetAllItems();
var groups = from item in allItems
group item by item.Category
into newGroup
select newGroup;
foreach (var group in groups)
{
Console.WriteLine($"\nCategory: {group.Key}");
foreach (var item in group)
{
Console.WriteLine($"{item.Name}: {item.Price}");
}
}
Console.ReadLine();
}
static List<Category> GetAllCategories()
{
return new List<Category>()
{
new Category() { Id = 1, Name = "Programming Books" },
new Category() { Id = 2, Name = "Fiction Books" }
};
}
static List<Item> GetAllItems()
{
return new List<Item>()
{
new Item() { Id = 1, Name = "Embedded Linux", Category = 1, Price = 9.9 },
new Item() { Id = 2, Name = "LINQ In Action", Category = 1, Price = 36.19 },
new Item() { Id = 3, Name = "C# 6.0 and the .NET 4.6 Framework", Category = 1, Price = 40.99 },
new Item() { Id = 4, Name = "Thinking in LINQ", Category = 1, Price = 36.99 },
new Item() { Id = 5, Name = "The Book Thief", Category = 2, Price = 7.99 },
new Item() { Id = 6, Name = "All the Light We Cannot See", Category = 2, Price = 16.99 },
new Item() { Id = 7, Name = "The Life We Bury", Category = 2, Price = 8.96 }
};
}
}
public class Item
{
public int Id { get; set; }
public string Name { get; set; }
public double Price { get; set; }
public int Category { get; set; }
}
public class Category
{
public int Id { get; set; }
public string Name { get; set; }
}
This example is simple enough for anyone new to LINQ. I am sure you can make some adjustment to make it work for your specific issue. Hope this will help.

C# LINQ Take limited results per grouped

I have list that includes class named 'ID', 'Name' and 'Category'. There are 6 item in list.
List<MyData> list =
{
{0, "John", "Police"},
{1,"Michael", "Police"},
{2,"Alice", "Police"},
{3, "Ferdinand", "Thief"},
{4, "Jocas", "Thief"},
{5, "Connor", "Thief"}
};
I wanna list them with limited quantity per group by 'Category' with LINQ.
Example : I want list 2 item for each 'Cateogory'. Listed should be below :
John Police
Michael Police
Ferdinand Thief
Jocas Thief
Use combination of Take and SelectMany:
var results = list.GroupBy(x => x.Category).SelectMany(g => g.Take(2)).ToList();
I've tested it on following Item class:
public class Item
{
public int ID { get; set; }
public string Name { get; set; }
public string Category { get; set; }
}
And query:
List<Item> list = new List<Item>
{
new Item { ID = 0, Name = "John", Category = "Police"},
new Item { ID = 1, Name = "Michael", Category = "Police"},
new Item { ID = 2, Name = "Alice", Category = "Police"},
new Item { ID = 3, Name = "Ferdinand", Category = "Thief"},
new Item { ID = 4, Name = "Jocas", Category = "Thief"},
new Item { ID = 5, Name = "Connor", Category = "Thief"}
};
var results = list.GroupBy(x => x.Category).SelectMany(g => g.Take(2)).ToList();
Returns 4 elements, right as you want.

LINQ - GroupBy a key and then put each grouped item into separate 'buckets'

I have a list of items as such:
public class Item
{
public int ItemId { get; set; }
public string ItemName { get; set; }
public int ListId { get; set; }
}
1 Test1 1
2 Test2 1
3 Test3 1
4 List 2
5 List2 2
6 Testing 3
7 Testing2 3
8 Testing3 3
Is there a way for me to group by the ListId and put them into each separate buckets, i.e, ListId1 bucket will have all items with ListId == 1. The list is dynamically returned from SQL, so I don't know before hand how many ListId there will be.
You can use GroupBy:
var groups = items.GroupBy(item => item.ListId);
foreach(var group in groups)
{
Console.WriteLine("List with ID == {0}", group.Key);
foreach(var item in group)
Console.WriteLine(" Item: {0}", item.ItemName);
}
Let's create your list of items:
List<Item> items = new List<Item>();
items.Add(new Item() { ItemId = 1, ItemName = "Test1", ListId = 1 });
items.Add(new Item() { ItemId = 2, ItemName = "Test2", ListId = 1 });
items.Add(new Item() { ItemId = 3, ItemName = "Test3", ListId = 1 });
items.Add(new Item() { ItemId = 4, ItemName = "List", ListId = 2 });
items.Add(new Item() { ItemId = 5, ItemName = "List2", ListId = 2 });
items.Add(new Item() { ItemId = 6, ItemName = "Testing", ListId = 3 });
items.Add(new Item() { ItemId = 7, ItemName = "Testing2", ListId = 3 });
items.Add(new Item() { ItemId = 8, ItemName = "Testing3", ListId = 3 });
var groupByResult = items.GroupBy(i => i.ListId);
After this GroupBy call, groupByResult is a variable of type IEnumerable<IGrouping<int, Item>> which is basically a collection of objects that implement IGrouping interface. This allows you to iterate through all items as IGrouping is derived from IEnumerable<> and has an extra field named Key:
public interface IGrouping<out TKey, out TElement> : IEnumerable<TElement>, IEnumerable
{
TKey Key { get; }
}
Briefly said, a GroupBy method call returns a list of lists. An outer list corresponds to 'buckets' as you mentioned in your question. Then each 'bucket' contains items corresponding to that 'bucket'. To be specific to your example, the value of groupByResult is depicted in this screenshot. As we can see there, your initial collection was grouped into three different buckets that have 3, 2 and 3 items, respectively.
As for accessing items in these groups, you can use simple LINQ:
List<Item> firstBucketItems = groupByResult.First(i => i.Key == 1).ToList();
List<Item> secondBucketItems = groupByResult.First(i => i.Key == 2).ToList();
List<Item> thirdBucketItems = groupByResult.First(i => i.Key == 3).ToList();
Or you can just iterate through all items:
foreach (var itemGroup in groupByResult)
{
int groupKey = itemGroup.Key;
foreach (Item item in itemGroup)
{
// Do whatever...
}
}
IList<Student> studentList = new List<Student>()
{
new Student() { StudentID = 1, StudentName = "John", Age = 18 } ,
new Student() { StudentID = 2, StudentName = "Steve", Age = 21 } ,
new Student() { StudentID = 3, StudentName = "Bill", Age = 18 } ,
new Student() { StudentID = 4, StudentName = "Ram" , Age = 20 } ,
new Student() { StudentID = 5, StudentName = "Abram" , Age = 21 }
};
var groupedResult = from s in studentList group s by s.Age;
//iterate each group
foreach (var ageGroup in groupedResult)
{
Console.WriteLine("Age Group: {0}", ageGroup.Key); //Each group has a key
foreach(Student s in ageGroup) // Each group has inner collection
Console.WriteLine("Student Name: {0}", s.StudentName);
}
Basic format
var res = (from i in items
group i by i.ListId into g
select );
You can also do this by following
using System;
using System.Collections.Generic;
using System.Linq;
public class Program
{
public static void Main()
{
List<Item> items = new List<Item>();
items.Add(new Item() { ItemId = 1, ItemName = "Test1", ListId = 1 });
items.Add(new Item() { ItemId = 2, ItemName = "Test2", ListId = 1 });
items.Add(new Item() { ItemId = 3, ItemName = "Test3", ListId = 1 });
items.Add(new Item() { ItemId = 4, ItemName = "List", ListId = 2 });
items.Add(new Item() { ItemId = 5, ItemName = "List2", ListId = 2 });
items.Add(new Item() { ItemId = 6, ItemName = "Testing", ListId = 3 });
items.Add(new Item() { ItemId = 7, ItemName = "Testing2", ListId = 3 });
items.Add(new Item() { ItemId = 8, ItemName = "Testing3", ListId = 3 });
var groupByResult = items.GroupBy(i => i.ListId);
List<GroupedItems> betterGroups = new List<GroupedItems>();
foreach (var groupedItem in groupByResult)
{
betterGroups.Add(new GroupedItems() { ListId = groupedItem.Key, Items = groupedItem.ToList() });
// ListId = 1 , List<Item> 3 records
// ListId = 2 , List<Item> 2 records
// ListId = 3 , List<Item> 3 records
// You can iteratre throught ListId
}
}
public class Item
{
public int ItemId { get; set; }
public string ItemName { get; set; }
public int ListId { get; set; }
}
public class GroupedItems
{
public int ListId {get; set;}
public List<Item> Items {get; set;}
}
}
donetFiddle example Link

Categories