I have two linq queries that I want to unionize on a common attribute:
One
{
Id,
Name,
Color
}
Two
{
Color,
Cost
}
I want to get the union of One and Two by unionizing on Color? If there is not a Two with a Color that corresponds to One, I want to set Cost to 0 in the output? How do I do this in LINQ?
Here is a sample using anonymous types on how to perform a left outer join:
var products = new[] {
new { Id = 1, Name = "Alpha", Color = "Red" },
new { Id = 2, Name = "Beta", Color = "Green" },
new { Id = 3, Name = "Gamma", Color = "Blue" }
};
var costs = new[] {
new { Color = "Red", Cost = 100 },
new { Color = "Blue", Cost = 200 },
new { Color = "Blue", Cost = 300 }
};
var query = products
.GroupJoin(
costs, p => p.Color, c => c.Color,
(p, c) => new { p.Id, p.Name, p.Color, Costs = c.DefaultIfEmpty() }
)
.SelectMany(
gj => gj.Costs,
(gj, c) => new { gj.Id, gj.Name, gj.Color, Cost = c == null ? 0 : c.Cost }
);
Query result:
Id Name Color Cost
-------------------
1 Alpha Red 100
2 Beta Green 0
3 Gamma Blue 200
3 Gamma Blue 300
This is called a join, not a union.
See the documentation.
You want a left outer join to keep the values appearing in the first list but are not present in the second.
Related
I'm trying to select some properties that are not in my group by. Is this possible?
var r = from w in drinks.ToList()
group w by new{
w.DrinkId
} into wGroup
select new DrinksValue(){
DrinkId = wGroup.DrinkId,
CatId = // trying to pull from wGroup???
Lasting = // trying to pull from wGroup???
DrinkTypes = wGroup.Select( x => new DrinkAttributes { Taste = wGroup.Key.Flavor, Benefit = wGroup.Sum(x => x.Value)
}).ToList()
};
How would I get CatId, and Lasting properties from the drinks.ToList()?
If I add them to the group like w.DrinkId, w.Flavor, w.Value, w.CatId, w.Lasting it messes up my results.
I can get other properties like Value using .Sum , but these properties aren't computed values.
Example of data:
DrinkId | CatId | Lasting | Flavor | Value
------------------------------------------
1 2 5.1 orange 11.0
1 2 5.1 blue 09.0
1 2 6.0 red 10.0
2 3 6.0 red 10.0
2 3 6.0 yellow 01.0
Want the results to be grouped like this:
{
"DrinkId": 1,
"CatId": 2,
"Lasting": 5.1,
"DrinkTypes": [{ Taste: orange, Benefit: 11.0}, {Taste: blue, Benefit: 09.0}, {Taste: red, Benefit: 10.0}]
}
This query should work:
var query =
from w in drinks
group w by new { w.DrinkId, w.CatId, w.Lasting } into wGroup
select new DrinksValue
{
DrinkId = wGroup.Key.DrinkId,
CatId = wGroup.Key.CatId,
Lasting = wGroup.Key.Lasting,
DrinkTypes = wGroup.Select(x => new DrinkAttributes
{ Taste = x.Flavor, Benefit = x.Value }
).ToList()
}
};
var r = query.ToList()
i have 2 table ,
produk table
id produk batch qty
1 AAA ADADAD 2
2 BBB ADADAD 2
3 BBB AAAAAA 2
...............
and move table,
id produk batch qty
1 BBB ADADAD 1
and i want showing table after qty from stok table minus qty from move table, what i want table
PRODUK BATCH QTY
AAA ADADAD 2
BBB ADADAD 1
BBB AAAAAA 2
and this my query
var obj = _db.produk
.Groupby(a=> new {a.code,a.batch})
.Select(a=> new {
produk = a.key.code,
batch = a.Key.batch,
qty = _db.move.Where(c => a.Any(p => p.code == a.code && p.batch == a.batch)).Sum(a=>a.qty)
}).tolist();
but not working
You have to do LEFT JOIN to grouped move table.
var moves =
from m in _db.move
group m by { m.code, m.batch } into g
select
{
g.Key.code,
g.Key.batch,
qty = g.Sum(x => x.qty)
};
var query =
from p in _db.produk
join m in moves on new { p.code, p.batch } equals { m.code, m.batch } into j
from m in j.DefaultIfEmpty()
select new
{
produk = p.code,
batch = p.batch.
qty = p.qty - (int?)m.qty ?? 0
};
If you prefer method syntax over query syntax then you can write your query as this:
var availableItems = repository
.GroupJoin(purchases,
stock => new { stock.Product, stock.Batch },
move => new { move.Product, move.Batch },
(stock, moves) => new { Stock = stock, Moves = moves })
.SelectMany(
stockAndRelatedMoves => stockAndRelatedMoves.Moves.DefaultIfEmpty(),
(stockAndRelatedMoves, relatedMove) => new
{
stockAndRelatedMoves.Stock.Product,
stockAndRelatedMoves.Stock.Batch,
Quantity = stockAndRelatedMoves.Stock.Quantity - (relatedMove?.Quantity ?? 0)
})
.ToList();
As you can see instead of GroupBy you need to use GroupJoin and instead of simple Select you need SelectMany to retrieve items from the joined records.
Some explanation:
stock => new { stock.Product, stock.Batch }: Anonymous type is used here because multiple fields are used in the join
stockAndRelatedMoves => stockAndRelatedMoves.Moves.DefaultIfEmpty(): it is needed because of left outer join
(relatedMove?.Quantity ?? 0): relatedMove can be null that's why we substitute it with 0
In the above code I've used the following collections:
var repository = new List<Stock>
{
new Stock { Id = 1, Product = "AAA", Batch = "ADADAD", Quantity = 2 },
new Stock { Id = 2, Product = "BBB", Batch = "ADADAD", Quantity = 2 },
new Stock { Id = 3, Product = "BBB", Batch = "AAAAAA", Quantity = 2 },
};
var purchases = new List<Move>
{
new Move { Id = 1, Product = "BBB", Batch = "ADADAD", Quantity = 1 }
};
You could also query the produck table, then, in the Select statement, filter the move table based on the produck's batch and produck properties, then calculate the qty.
Code as below:
List<Produk> produks = new List<Produk>()
{
new Produk(){ id = 1, produk= "AAA", batch="ADADAD", qty = 2},
new Produk(){ id = 2, produk= "BBB", batch="ADADAD", qty = 2},
new Produk(){ id = 3, produk= "BBB", batch="AAAAAA", qty = 2},
};
List<Remove> removes = new List<Remove>()
{
new Remove(){ id=1, produk="BBB", batch="ADADAD", qty=1}
};
var result = (from p in produks
select new Produk
{
id = p.id,
produk = p.produk,
batch = p.batch,
qty = p.qty - removes.Where(c => c.produk == p.produk && c.batch == p.batch).Sum(c => c.qty)
}).ToList();
The result like this:
I have the following two lists coming from two different warehouses.
var list1 = new List<Tshirt> {
new Tshirt(){ Color = "blue", size="M", qty=3 },
new Tshirt(){ Color = "red", size="M", qty=2 },
new Tshirt(){ Color = "green", size="M", qty=3 },
new Tshirt(){ Color = "blue", size="M", qty=3 },
}
var list2 = new List<Tshirt> {
new Tshirt(){ Color = "blue", size="M", qty=5 },
new Tshirt(){ Color = "red", size="M", qty=7 },
}
Using LINQ, how do I end up with a combined list like this.
var list3 = new List<Tshirt> {
new Tshirt(){ Color = "blue", size="M", qty=11 },
new Tshirt(){ Color = "red", size="M", qty=9 },
new Tshirt(){ Color = "green", size="M", qty=3 }
}
(I originally answered this question incorrectly, see the second heading below ("To combine all distinct Tshirt instances together") for my original, irrelevant, answer)
To combine all Tshirt instances and sum their qtys:
I see you're using a tuple of color + size to uniquely identify a type of t-shirt, which means if we combine all Tshirt instances together (Concat), then group them by color + size, then Sum the qty values, then return new Tshirt instances in a new list.
List<Tshirt> aggregatedShirts = uniqueShirts = Enumerable
.Empty<Tshirt>()
.Concat( list1 )
.Concat( list2 )
.GroupBy( shirt => new { shirt.Color, shirt.size } )
.Select( grp => new Tshirt()
{
Color = grp.Key.Color,
size = grp.Key.size,
qty = grp.Sum( shirt => shirt.qty )
} )
.ToList();
To combine all distinct Tshirt instances together
Assuming class Tshirt implements IEquatable<Tshirt> then just use Concat( ... ).Distinct().ToList():
I'd do it this way, others might prefer not to use Empty:
List<Tshirt> uniqueShirts = Enumerable
.Empty<Tshirt>()
.Concat( list1 )
.Concat( list2 )
.Distinct()
.ToList();
If Tshirt does not implement IEquatable then you can use the overload of Distinct that accepts an IEqualityComparer<TSource>:
class TshirtComparer : IEqualityComparer<Tshirt>
{
public static TshirtComparer Instance { get; } = new TshirtComparer();
public Boolean Equals(Tshirt x, Tshirt y)
{
if( ( x == null ) != ( y == null ) ) return false;
if( x == null ) return true;
return x.Color == y.Color && x.size == y.size && x.qty == y.qty;
}
public Int32 GetHashCode(Tshirt value)
{
if( value == null ) return 0;
// See https://stackoverflow.com/questions/263400/what-is-the-best-algorithm-for-an-overridden-system-object-gethashcode
Int32 hash = 17;
hash = hash * 23 + value.Color?.GetHashCode() ?? 0;
hash = hash * 23 + value.size?.GetHashCode() ?? 0;
hash = hash * 23 + value.qty;
return hash;
}
}
Usage:
List<Tshirt> uniqueShirts = Enumerable
.Empty<Tshirt>()
.Concat( list1 )
.Concat( list2 )
.Distinct( TshirtComparer.Instance )
.ToList();
Then to get the total quantity:
Int32 totalQuantity = uniqueShirts.Sum( shirt => shirt.qty );
var list3 = list1.Union(list2).GroupBy(o => new {o.Color, o.size})
.Select(o => new Tshirt()
{
Color = o.Key.Color,
size = o.Key.size,
qty = o.Sum(q => q.qty)
}).OrderByDescending(o => o.qty).ToList();
Suppose I have the items
{ // item 0
Color = yellow,
Priority = high,
},
{ // item 1
Color = yellow,
Priority = medium,
},
{ // item 2
Color = green,
Priority = high,
},
I want to be able to progressively build a GroupBy query as using var grouped = items.GroupBy(item => item.Color), what in this case I would have 2 groups and then group again using by the key item.Priority to have 3 groups.
Unfortunately using var regrouped = grouped.GroupBy(item => item.Priority) is not a valid solution.
The final result must be equivalent as doing
items.GroupBy(item => new {item.Color, item.Priority});
Since you want to regroup on a subkey as a new object, you can use ToLookup to create the IGroupings you need, and SelectMany to flatten them into the new IEnumerable:
var regrouped = grouped.SelectMany(cg => cg.GroupBy(i => i.Priority)
.ToLookup(pg => new { Color = cg.Key, Priority = pg.Key },
pg => pg.Select(i => i)
)
);
If I understood you correctly, you want to group each group of Color into Priority. If that's the case, this should work:
var items = new List<Item>
{
new Item { Color = "yellow", Priority = "high" },
new Item { Color = "red", Priority = "high" },
new Item { Color = "yellow", Priority = "medium" },
new Item { Color = "red", Priority = "medium" },
new Item { Color = "green", Priority = "high" },
new Item { Color = "green", Priority = "high" },
new Item { Color = "red", Priority = "high" },
};
var groupped = items.GroupBy(x => x.Color);
var regroupped = groupped.Select(x => new
{
Color = x.Key,
Priorities = x.GroupBy(y => y.Priority)
});
Notice that this returns the same result set than your query
var regroupped = items.GroupBy(item => new {item.Color, item.Priority});
Assume I have the following data:
var workers = new[]
{
new { Name = "John", Id = 1 },
new { Name = "Greg", Id = 2 },
new { Name = "Jack", Id = 3 },
new { Name = "Josh", Id = 4 },
new { Name = "Jill", Id = 5 },
new { Name = "Jane", Id = 6 }
};
var contracts = new[]
{
new { ContractNumber="1", WorkerId=1, ContractDate = new DateTime(2017,6,30) },
new { ContractNumber="2", WorkerId=2, ContractDate = new DateTime(2017,7,10) },
new { ContractNumber="3", WorkerId=2, ContractDate = new DateTime(2017,7,15) },
new { ContractNumber="4", WorkerId=5, ContractDate = new DateTime(2017,7,20) },
new { ContractNumber="5", WorkerId=1, ContractDate = new DateTime(2017,7,25) }
};
What I need to do is to select the first worker who has the minimum quantity of contracts where contract date greater or equals to:
var fromDate = new DateTime(2017, 7, 1);
excluding the workers with the following Id:
int[] exceptWorkerIds = new int[] {1, 4};
If several workers have a similar minimum quantity of contracts then select the worker with the first name in alphabetical order.
I resolved this task the following way.
Firstly, for each worker left join contracts. If contract exists my helper property ContractExists = 1, if not then 0.
var query =
from w in workers.Where(x => !exceptWorkerIds.Contains(x.Id))
join c in contracts.Where(x => x.ContractDate >= fromDate)
on w.Id equals c.WorkerId into workerContracts
from wc in workerContracts.DefaultIfEmpty()
select new {WorkerId = w.Id, WorkerName = w.Name, ContractExists = wc == null ? 0: 1};
This query gives me the following result:
Secondly, I group the obtained results by WorkerId, WorkerName getting the sum of contracts and order data by sum and worker name:
var result =
(from q in query
group q.ContractExists by new {q.WorkerId, q.WorkerName} into g
orderby g.Sum(), g.Key.WorkerName
select new
{
WorkerId = g.Key.WorkerId,
WorkerName = g.Key.WorkerName,
WorkerContractsCount = g.Sum()
}).ToList().Take(1);
Take(1) gives me the top 1 of resulted data:
The question: Is there a way to do it with the only query or any simpler or elegant manner then I did? If yes, does this help to boost productivity of query execution?
Rather than doing join (which multiplies the data) followed by group by you could use group join (what actually your query is using before you do from wc in workerContracts.DefaultIfEmpty()).
The other logic is pretty much the same - workerContracts.Count() gives you the desired quantity of contracts, so apply the desired order, take the first and you are done:
var result =
(from w in workers.Where(x => !exceptWorkerIds.Contains(x.Id))
join c in contracts.Where(x => x.ContractDate >= fromDate)
on w.Id equals c.WorkerId into workerContracts
let workerContractsCount = workerContracts.Count()
orderby workerContractsCount, w.Name
select new
{
WorkerId = w.Id,
WorkerName = w.Name,
WorkerContractsCount = workerContractsCount
})
.FirstOrDefault();
With maybe less Wheres and Join than Ivan Stoev's answer, here is a more compact version :
var result = workers
.Where(w => !exceptWorkerIds.Contains(w.Id))
.Select(w => new {
Name = w.Name,
Id = w.Id,
Nb = contracts
.Count(c => c.WorkerId == w.Id && c.ContractDate >= new DateTime(2017,7,1))
})
.OrderBy(w => w.Nb).ThenBy(w => w.Name).FirstOrDefault();
if(result != null)
Console.WriteLine(result.Name);
else
Console.WriteLine("Result not found");
Explanation : for each worker except the ones we don't want to check, we count the number of contract associated which date is later or equal to 2017,7,1, we then sort it by this number and by name, and take the first one.