Intersect or union with a custom IEqualityComparer using Linq - linq

I have 2 set of coolection in memory and i want to return one set based on the 2. My object have the following suructure:
class Item
{
public string key {get,set;}
public int total1 {get;set;}
public int total2 {get ;set;}
}
I would like to "union" them so that when the key on item form set 1 is equal to the key of an item from the set 2 , my union should return an item as follow:
item_union.Key= item1.key==item2.key;
item_union.total1= item1.total1 + item2.total1;
item_union.total2= item1.total2 + item2.total2;
can someone show me how i should build my custom equality compararer to obtain this result?
many thanks in advance

It sounds like you might want a join, or you might just want to concatenate the collections, group by the key and then sum the properties:
// Property names changed to conform with normal naming conventions
var results = collection1.Concat(collection2)
.GroupBy(x => x.key)
.Select(g => new Item {
Key = g.Key,
Total1 = g.Sum(x => x.Total1),
Total2 = g.Sum(x => x.Total2)
});

Related

Having trouble grouping columns in Linq query with multiple joins

I have an MVC ViewModel that I'd like to pass through to a Razor view. In the controller, I've created a database context and joined tables together using Linq. Once summed and grouped, I'm getting an error:
Error CS1061 'decimal' does not contain a definition for 'GroupBy' and no accessible extension method 'GroupBy' accepting a first argument of type 'decimal' could be found (are you missing a using directive or an assembly reference?
I've gone through almost every example on stack overflow and google and couldn't find an example that matched the structure of my query. Also, the MS examples are very trivial and are not of much use.
Here is the action in the controller:
public IHttpActionResult GetEmployeeReleasedAllocatedBonus(int eid)
{
var employeeReleasedAllocatedBonus =
(from br in _context.BonusReleases
join emp in _context.Employees
on new
{
br.EmployeeID,
empID = br.EmployeeID
} equals new
{
emp.EmployeeID,
empID = eid
}
join job in _context.Jobs on br.JobID equals job.JobID
join bonus in _context.Bonus
on new
{
br.JobID,
empID = br.EmployeeID
}
equals new
{
bonus.JobID,
empID = bonus.EmployeeID
}
select new EmployeeAllocatedReleasedBonusViewModel()
{
AllocatedToEmployee = br.Amount, AllocatedPercentage = bonus.Amount * 100
,
JobNumber = job.JobNumber, JobDescription = job.JobDescription
})
.ToList()
.Sum(s => s.AllocatedToEmployee)
.GroupBy(g => new {g.JobNumber, g.JobDescription, g.AllocatedPercentage});
return Ok(employeeReleasedAllocatedBonus);
}
It's worth mentioning that the AllocatedPercentage datatype is a decimal. However, I've tried changing it to string but the error message stays.
Also tried using the group functionality right before .ToList() but that didn't work either.
After ToList() you have a List<EmployeeAllocatedReleasedBonusViewModel>.
In Sum(s => s.AllocatedToEmployee), every s is one EmployeeAllocatedReleasedBonusViewModel. Apparently a EmployeeAllocatedReleasedBonusViewModel has a property AllocatedToEmployee which is probably of type decimal. This can be summed into one decimal.
The result of the Sum (a decimal) is the input of your GroupBy. Does type decimal have a method GroupBy? Of course it doesn't!
Alas you forgot to tell us your requirements. It is difficult to extract them from code that doesn't do what you want.
It seems to me that you have two one-to-many relations:
Employees have zero or more BonusReleases. Every BonusRelease belongs to exactly one Employee using foreign key
Jobs have zero or more BonusReleases. Every BonusRelease belongs to exactly one Job.
Now what do you want: do you want all JobNumbers and JobDescriptions of all Jobs with the total of their AllocatedPercentage? I'm not sure what the Employees do within this query.
Whenever you want items with their sub-items, like Schools with their Students, Customers with their Orders, Orders with their OrderLines, use GroupJoin. If you want it the other way round: Student with the School that he attends, Order with the Customer who placed the Order, use Join.
var result = dbContext.Jobs.GroupJoin(dbContext.BonusReleases,
job => job.Id, // from every Job take the primary key
bonusRelease => bonusReleas.JobId, // from every BonusRelease take the foreign key
// parameter ResultSelector: take every Job with all its BonusReleases to make a new:
(job, bonusReleasesOfThisJob) => new
{
JobNumber = job.JobNumber,
JobDescription = job.JobDescription
// do you want the total of all allocated percentages?
TotalAllocatedPercentages = bonusReleasesOfThisJob
.Select(bonus => bonus.Amount)
.Sum(),
// do something to make it a percentage
// or do you want a sequence of allocated percentages?
TotalAllocatedPercentages = bonusReleasesOfThisJob
.Select(bonus => bonus.Amount)
.ToList(),
});
Or do you want the JobNumber / JobDescription / Total allocated bonus per Employee?
var result = dbContext.Employees.GroupJoin(dbContext.BonusReleases,
employee => employee.Id, // from every Employee take the primary key
bonus => bonus.EmployeeId, // from every BonusRelease take the foreign key
(employee, bonusesOfThisEmployee) => new
{
// Employee properties:
EmployeeId = employee.Id,
EmpoyeeName = employee.Name,
// for the jobs: Join the bonusesOfThisEmployee with the Jobs:
Jobs = dbContext.Jobs.GroupJoin(bonusOfThisEmployee,
job => job.Id,
bonusOfThisEmployee => bonusOfThisEmployee.JobId,
(job, bonusesOfThisJob) => new
{
Number = job.Id,
Description = job.Description,
TotalBonus = bonusOfThisJob.Select(bonus => bonus.Amount).Sum(),
}),
});
Harald's comment was key - after ToList(), I had a list of . Therefore I took a step back and said what if I put the results into an anonymous object first. Then do the group by and then the sum, putting the final result into the view model. It worked. Here is the answer.
var employeeReleasedAllocatedBonus =
(from br in _context.BonusReleases
join emp in _context.Employees
on new
{
br.EmployeeID,
empID = br.EmployeeID
} equals new
{
emp.EmployeeID,
empID = eid
}
join job in _context.Jobs on br.JobID equals job.JobID
join bonus in _context.Bonus
on new
{
br.JobID,
empID = br.EmployeeID
}
equals new
{
bonus.JobID,
empID = bonus.EmployeeID
}
select new
{
AllocatedToEmployee = br.Amount
,AllocatedPercentage = bonus.Amount * 100
,JobNumber = job.JobNumber
,JobDescription = job.JobDescription
})
.GroupBy(g => new {g.JobNumber, g.JobDescription, g.AllocatedPercentage})
.Select(t => new EmployeeAllocatedReleasedBonusViewModel
{
JobNumber = t.Key.JobNumber,
JobDescription = t.Key.JobDescription,
AllocatedPercentage = t.Key.AllocatedPercentage,
AllocatedToEmployee = t.Sum(ae => ae.AllocatedToEmployee)
});

Linq to EF in lightswitch WCF RIA Services

I have been trying for days now to solve the following problem in WCF RIA for lightswitch using linq:
the entity is:
moveDate Direction moveQuantity moveSignedQty
moveSignedQty is positive for In and negative for Out Direction
What I need to do is create a WCF Service in the form
tDate OBalance CBalance
CBalance is the sum of moveSignedQty for a particular moveDate
OBalance is the sum of mpveSignedQt for the previous moveDate; it is zero if there in no previous day value.
My approach below did not work:
Dim close = From c In Me.Context.StockMovements
Order By c.DateOfMovement
Group New With {c} By _
tranDate = CDate(c.DateOfMovement) _
Into g = Group
Let cBal = g.Sum(Function(s) s.c.SignedQuantity_KG)
Let fDate = g.OrderBy(Function(d) d.c.DateOfMovement).FirstOrDefault
Select New accStockBalance With {
.TransactionDate = tranDate, _
.ClosingBalance = cBal}
Dim sBal = close.GroupBy(Function(d) d.TransactionDate).Select( _
Function(b)
Dim subb = b.OrderBy(Function(t) t.TransactionDate)
Return subb.Select( _
Function(s, i) New With {
.TransactionDate = s.TransactionDate, _
.ClosingBalance = subb.ElementAt(i).ClosingBalance, _
.OpeningBalance = If(i = 0, 0, subb.ElementAt(i - 1).ClosingBalance)})
End Function)
Example:
moveDate Direction moveQuantity moveSignedQty
13/02/2013 In 30 30
13/02/2013 Out 4 -4
13/02/2013 Out 10 -10
14/02/2013 Out 4 -4
14/02/2013 Out 4 -4
14/02/2013 In 7 7
15/02/2013 In 15 15
Expected result:
tDate OBalance cBalance
13/02/2013 0 16
14/02/2013 16 15
15/02/2013 15 30
The last bit sBal threw up an error that lambda statements cannot be converted to expression trees.
Kindly guide me. I have read several Q and A in this and other forums help please.
Please forgive my terrible formatting couldnt figure out how to format the example to table format
The function below is composed of 2 statements.
Function(b)
Dim subb = ...
Return ...
This kind of function can't be used in LINQ query.
Moreover, the operators ElementAt and Select using index arguments aren't supported by EntityFramework.
I show you here a solution. It is written in C#. I do not think you'll have difficulty translating the code in VB. Furthermore, I do not use the fluent LINQ syntax that I do not find very understandable. Finally, for pedagogical concern I avoid using anonymous types.
The first thing to do is actually write a query that sums the movements by date. This request must allow to obtain a list of object (MovementSumItem class) each containing the date and the sum of the movements of the day.
class MovementSumItem
{
public DateTime Date { get; set; }
public int? TotalQty { get; set; }
}
The TotalQty property is declared as nullable. I'll explain why later.
I do not understand why your close query is so complicated!?! You just need to use the GroupBy operator once. And there is no interest in using the OrderBy operator.
IQueryable<MovementSumItem> movementSumItemsQuery =
context.StockMovements
.GroupBy(
// group key selector: the key is just the date
m => m.MoveDate,
// project each group to a MovementSumItem
(groupKey, items) => new MovementSumItem {
// date is the key of the group
Date = groupKey,
// items group sum
TotalQty = items.Sum(i => i.SignedQty),
});
Now, we must be able to determine for each item which is the previous item. And this of course without first execute the query below.
Here is the logical expression I propose:
Func<MovementSumItem,MovementSumItem> previousItemSelector = item =>
movementSumItemsQuery // from all items
.Where(b => b.Date < item.Date) // consider only those corresponding to a previous date
.OrderByDescending(b => b.Date) // ordering them from newest to oldest
.FirstOrDefault(); // take the first (so, the newest) or null if collection is empty
This expression does not use any index notion. It is therefore compatible with Entity Framework.
Combining this expression with the query above, we can write the complete query. This final request must allow to obtain a list of object (BalanceItem class) each containing the date, the opening balance (sum of movements from previous item) and the closing balance (sum of movements from current item).
class BalanceItem
{
public DateTime Date { get; set; }
public int OpeningBalance { get; set; }
public int ClosingBalance { get; set; }
}
Logically, the final query can be written:
IQueryable<BalanceItem> balanceItemsQuery =
movementSumItemsQuery
.Select(
item => new BalanceItem() {
Date = item.Date,
OpeningBalance = previousItemSelector(item).TotalQty ?? 0,
ClosingBalance = item.TotalQty ?? 0
});
Unfortunately Entity Framework does not support the invocation of the function previousItemSelector. So we must integrate the expression in the query.
IQueryable<BalanceItem> balanceItemsQuery =
movementSumItemsQuery
.Select(
item => new BalanceItem()
{
Date = item.Date,
OpeningBalance = movementSumItemsQuery
.Where(b => b.Date < item.Date)
.OrderByDescending(b => b.Date)
.FirstOrDefault().TotalQty ?? 0,
ClosingBalance = item.TotalQty ?? 0
});
Finally, to run the query, simply use (for example) the ToList operator.
List<BalanceItem> result = balanceItemsQuery.ToList();
Moreover, balanceItemsQuery being IQueryable, you can specify the query by adding, for example, a filter on the date:
IQueryable<BalanceItem> balanceItemsOfTheYearQuery = balanceItemsQuery
.Where(x => x.Date.Year == 2014);
Finally, you can verify that the query executes well via a single SQL query using the ToString function of the Query object.
Console.WriteLine(balanceItemsQuery.ToString());
Why TotalQty is declared nullable?
Otherwise, the OpeningBalance value expression should be written:
OpeningBalance = movementSumItemsQuery
.Where(b => b.Date < item.Date)
.OrderByDescending(b => b.Date)
.FirstOrDefault() == null ? 0 : movementSumItemsQuery
.Where(b => b.Date < item.Date)
.OrderByDescending(b => b.Date)
.FirstOrDefault().TotalQty
However, the comparison .FirstOrDefault() == null is not supported by Entity Framework.

LINQ query to return first item in an ordered grouping

Grouping is an area of LINQ that I haven't quite managed to get my head around yet. I have the following code:
var subTrips = tbTripData
.Where(t => selectedVehicleIds.Contains(t.VehicleID))
.Join(tbSubTripData,
t => t.TripID,
s => s.TripID,
(t, s) => new { t = t, s = s })
.Select(r =>
new SubTrip
{
VehicleID = r.t.VehicleID,
TripID = r.t.TripID,
Sequence = r.s.Sequence,
TripDistance = r.s.TripDistance,
Odometer = r.s.Odometer
})
.ToList();
I'm trying to figure out a LINQ query that will look at subTrips and for each VehicleID, find the first Odometer, i.e. the Odometer corresponding to the lowest TripID and Sequence values.
I've been poking at it for an hour but just can't figure it out. Can anyone offer some advice before I give up and write procedural code to do it?
UPDATE: To clarify, Sequence is the sequential number of each subtrip within a trip. So what I'm looking for is the Odometer from the first subtrip for each vehicle when the subtrips within each grouped VehicleID are ordered by TripID then by Sequence.
I'm not 100% what you're looking for. But this might get you started in the right direction.
var groupedList = (from s in subTrips
group s by s.VehicleID
into grp
select new
{
VehicleID = grp.Key,
Odometer = grp.OrderBy(ex => ex.TripID).Select(ex => ex.Odometer).First(),
TripID = grp.Min(ex => ex.TripID)
}
).ToList();
This will return the VehicleID, the Odometer corresponding to the lowest TripID, and the lowest TripID.

How to find all rows of items that have a part in common using LINQ?

I need to return all records (items) that has a part (X) so I can use that in a group or .GroupBy afterwards
Using this summary data:
ItemName PartName
1 A
1 B
2 A
3 C
So Item1 has two parts (A,B), etc...
I need a LINQ query that will
- find all items that have part A (i.e items 1 and 2)
- return all rows for all these items
1 A
1 B
2 A
Notice that the end result returned the row (1 B) because Item1 has PartA and so I need to get back all rows for Item1.
I was looking at something like:
let items = from data in summary where data.PartName == A select new { data.ItemName } // to get all the items I need
But then, now that I have that list I need to use it to get all the rows for all items listed, and I can't seem to figure it out ...
Actual Source Code (for reference):
NOTE:
Recipe = ITEM
Ingredient = PART
(I was just trying to make it simpler)
ViewFullRecipeGrouping = (
from data in ViewRecipeSummary
group data by data.RecipeName into recipeGroup
let fullIngredientGroups = recipeGroup.GroupBy(x => x.IngredientName)
select new ViewFullRecipe()
{
RecipeName = recipeGroup.Key,
RecipeIngredients = (
from ingredientGroup in fullIngredientGroups
select new GroupIngredient()
{
IngredientName = ingredientGroup.Key
}
).ToList(),
ViewGroupRecipes = (
from data in ViewRecipeSummary
// this is where I am looking to add the new logic to define something I can then use within the next select statement that has the right data based on the information I got earlier in this query.
let a = ViewRecipeSummary.GroupBy(x => x.RecipeName)
.Where(g => g.Any(x => x.IngredientName == recipeGroup.Key))
.Select(g => new ViewRecipe()
{
RecipeName = g.Key,
IngredientName = g.Select(x => x.IngredientName)
})
select new GroupRecipe()
{
// use the new stuff here
}).ToList(),
}).ToList();
Any help would be much appreciated.
Thanks,
I believe this does what you want:
var data = /* enumerable containing rows in your table */;
var part = "X";
var items = new HashSet<int>(data
.Where(x => x.PartName == part)
.Select(x => x.ItemName));
var query = data.Where(x => items.Contains(x.ItemName));
If I understand your comment at the end, I believe this also does what you want:
var query = data
.GroupBy(x => x.ItemName)
.Where(g => g.Any(x => x.PartName == part))
.Select(g => new
{
ItemName = g.Key,
PartNames = g.Select(x => x.PartName)
});

What's the LINQ to select the latest item from a number of versioned items?

I've got a class like the following:
public class Invoice
{
public int InvoiceId {get;set;}
public int VersionId {get;set;}
}
Each time an Invoice is modified, the VersionId gets incremented, but the InvoiceId remains the same. So given an IEnumerable<Invoice> which has the following results:
InvoiceId VersionId
1 1
1 2
1 3
2 1
2 2
How can I get just the results:
InvoiceId VersionId
1 3
2 2
I.e. I want just the Invoices from the results which have the latest VersionId. I can easily do this in T-SQL, but cannot for the life of me work out the correct LINQ syntax. I'm using Entity Framework 4 Code First.
Order by the VersionId, group them by InvoiceId, then take the first result of each group. Try this:
var query = list.OrderByDescending(i => i.VersionId)
.GroupBy(i => i.InvoiceId)
.Select(g => g.First());
EDIT: how about this approach using Max?
var query = list.GroupBy(i => i.InvoiceId)
.Select(g => g.Single(i => i.VersionId == g.Max(o => o.VersionId)));
Try using FirstOrDefault or SingleOrDefault in place of Single as well... it would give the same result although Single shows the intention better.
EDIT: I've tested both these queries with LINQ to Entities. They seem to work, so perhaps the issue is something else?
Option 1:
var latestInvoices = invoices.GroupBy(i => i.InvoiceId)
.Select(group => group.OrderByDescending(i => i.VersionId)
.FirstOrDefault());
EDIT: Changed 'Last' to 'FirstOrDefault', LINQ to Entities has issues with the 'Last' query operator.
Option 2:
var invoices = from invoice in dc.Invoices
group invoice by invoice.InvoiceId into invoiceGroup
let maxVersion = invoiceGroup.Max(i => i.VersionId)
from candidate in invoiceGroup
where candidate.VersionId == maxVersion
select candidate;
My version:
var h = from i in Invoices
group i.VersionId by i.InvoiceId into grouping
select new {InvoiceId = grouping.Key, VersionId = grouping.Max()};
Update
As was mentioned by Ahmad in the comments, the above query will return a projection. The version below will return a IQueryable<Invoice>. I use composition to build the query because I think it is more clear.
var maxVersions = from i in Invoices
group i.VersionId by i.InvoiceId into grouping
select new {InvoiceId = grouping.Key,
VersionId = grouping.Max()};
var latestInvoices = from i in Invoices
join m in maxVersions
on new {i.InvoiceId, i.VersionId} equals
new {m.InvoiceId, m.VersionId}
select i;

Resources