Godspark
Godspark

Reputation: 364

What is the appropriate LINQ query to this specific case?

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

Answers (5)

Ivan Stoev
Ivan Stoev

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

Jonas H&#248;gh
Jonas H&#248;gh

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

code4life
code4life

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

TVOHM
TVOHM

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

Nauman Khan
Nauman Khan

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

Related Questions