Reputation: 364
Given the following two classes:
public class Apple
{
public int Id { get; set; }
public string Name { get; set; }
}
public class Worm
{
public int AppleId { get; set; }
public int WormType { get; set; }
public int HungerValue { get; set; }
}
All instances of Worm are given an AppleId equal to a randomly existing Apple.Id
public void DoLINQ(List<Apple> apples, List<Worm> worms, string targetAppleName, List<int> wormTypes )
{
// Write LINQ Query here
}
How can we write a Linq query which finds all the elements in 'apples', whose 'Name' matches the 'targetAppleName' AND (does not "contain" the any worm with Wormtype given in Wormtypes OR only contains worms with Hungervalue equal to 500)?
Note that an instance of Apple does not actually 'contain' any elements of Worm, since the relation is the other way around. This is also what complicates things and why it is more difficult to figure out.
--Update 1--
My attempt which selects multiple apples with the same Id:
var query =
from a in apples
join w in worms
on a.Id equals w.AppleId
where (a.Name == targetAppleName) && (!wormTypes.Any(p => p == w.WormType) || w.HungerValue == 500)
select a;
--Update 2--
This is closer to a solution. Here we use two queries and then merge the results:
var query =
from a in apples
join w in worms
on a.Id equals w.AppleId
where (a.Name == targetAppleName) && !wormTypes.Any(p => p == w.WormType)
group a by a.Id into q
select q;
var query2 =
from a in apples
join w in worms
on a.Id equals w.AppleId
where (a.Name == targetAppleName) && wormTypes.Any(p => p == w.WormType) && w.HungerValue == 500
group a by a.Id into q
select q;
var merged = query.Concat(query2).Distinct();
--Update 3-- For the input we expect the LINQ query to use the parameters in the method, and those only. For the output we want all apples which satisfy the condition described above.
Upvotes: 1
Views: 140
Reputation: 205629
You were so close in your first attempt. But instead of a Join which multiplies the apples you really need GroupJoin which "Correlates the elements of two sequences based on key equality and groups the results". In query syntax it's represented by the join .. into
clause.
var query =
from apple in apples
join worm in worms on apple.Id equals worm.AppleId into appleWorms
where apple.Name == targetAppleName
&& (!appleWorms.Any(worm => wormTypes.Contains(worm.WormType))
|| appleWorms.All(worm => worm.HungerValue == 500))
select apple;
Upvotes: 1
Reputation: 10874
You can use a let construct to find the worms of a given apple if you want to use query syntax:
var q =
from a in apples
let ws = from w in worms where w.AppleId == a.Id select w
where
(ws.All(w => w.HungerValue == 500)
|| ws.All(w => !wormTypes.Any(wt => wt == w.WormType)))
&& a.Name == targetAppleName
select a;
In method chain syntax this is equivalent to introducing an intermediary anonymous object using Select:
var q =
apples.Select(a => new {a, ws = worms.Where(w => w.AppleId == a.Id)})
.Where(t => (t.ws.All(w => w.HungerValue == 500)
|| t.ws.All(w => wormTypes.All(wt => wt != w.WormType)))
&& t.a.Name == targetAppleName).Select(t => t.a);
I wouldn't exactly call this more readable, though :-)
Upvotes: 1
Reputation: 15794
Using lambda would look like this:
var result = apples.Where(a =>
a.Name == targetAppleName &&
(worms.Any(w => w.AppleId == a.Id && w.HungerValue >= 500)) ||
worms.All(w => w.AppleId != a.Id));
I think the lambda makes the code look a bit cleaner/easier to read, plus, the usage of.Any()
and .All()
is more efficient than a full on join IMHO... I haven't tested it with any heavy data so hard to speak with authority here (plus, there can't be that many apples...!)
BTW, this is the entire body of code. Kind of surprised it doesn't work for you. Maybe you missed something...?
public class Apple { public int Id { get; set; } public string Name { get; set; } } public class Worm { public int AppleId { get; set; } public int WormType { get; set; } public int HungerValue { get; set; } } void Main() { var apples = Enumerable.Range(1, 9).Select(e => new Apple { Id = e, Name = "Apple_" + e}).ToList(); var worms = Enumerable.Range(1, 9).SelectMany(a => Enumerable.Range(1, 5).Select((e, i) => new Worm { AppleId = a, WormType = e, HungerValue = i %2 == 0 ? a * e * 20 : 100 })).ToList(); DoLINQ(apples, worms, "Apple_4", new[] {4, 5}); } public void DoLINQ(IList apples, IList worms, string targetAppleName, IList wormTypes) { // Write LINQ Query here var result = apples.Where(a => a.Name == targetAppleName && (worms.All(w => w.AppleId != a.Id) || worms.Any(w => w.AppleId == a.Id && w.HungerValue >= 500))); result.Dump(); // remark this out if you're not using LINQPad apples.Dump(); // remark this out if you're not using LINQPad worms.Dump(); // remark this out if you're not using LINQPad }
Upvotes: 0
Reputation: 2742
var result = apples.Where(apple =>
{
var wormsInApple = worms.Where(worm => worm.AppleId == apple.Id);
return apple.Name == targetAppleName
&& (wormsInApple.Any(worm => wormTypes.Contains(worm.WormType)) == false
|| wormsInApple.All(worm => worm.HungerValue == 500));
});
For each apple, create a collection of worms in that apple. Return only apples that match the required name AND (contain no worms that are in WormType OR only contain worms with a HungerValue of 500).
Upvotes: 1
Reputation: 519
I have modify your query but didn't tested yet lets have a look and try it. Hopefully it will solve your problem.
var query =
from a in apples
join w in worms
on a.Id equals w.AppleId into pt
from w in pt.DefaultIfEmpty()
where (a.Name == targetAppleName) && (!wormTypes.Any(p => p == w.WormType) || (w.HungerValue == 500))
select a;
Thanks.
Upvotes: -2