1

I have 2 Lists: List<Car> newCars and List<Car> oldCars.

I need to use the List<Car> newCars as the source of truth and return both Present and Absent cars in the List<Car>oldCars list.

How can I do this?

My approach:

var present = newCars.Where(c => oldCars.All(w => w.Id != c.Id));
var absent = newCars.Where(c => oldCars.All(w => w.Id == c.Id));

I am quite new to LINQ, and I am not sure of the logic that I have used above.

  1. Can someone help me out to get this working in a better and optimized way?

  2. Can this be done in one single query and return 2 result sets (as tuples)?

I realize that running the same code with equals and not equals as above can be a costly approach.

Kit
  • 20,354
  • 4
  • 60
  • 103
Illep
  • 16,375
  • 46
  • 171
  • 302
  • [Equi-join](https://learn.microsoft.com/en-us/dotnet/framework/data/adonet/method-based-query-syntax-examples-join-linq-to-dataset#join) and [left join](https://learn.microsoft.com/en-us/dotnet/csharp/linq/perform-left-outer-joins). – Kenneth K. Apr 24 '19 at 18:22
  • 1
    If you need two resulting lists, use a normal for/foreach loop. Fill both in one pass. – Alexander Petrov Apr 24 '19 at 18:31
  • look into the Intersect method of the List Class. this will return common elements in 2 lists and return the results in a new list. – Kevbo Apr 24 '19 at 18:34
  • First I would get a list of all `Car` objects which match using LINQ. Then you can use `List.Contains(item) == false` in a loop – TheBatman Apr 24 '19 at 18:52
  • Should'nt absent be utilizing `Any` instead of `All` – CSharpie Apr 24 '19 at 19:03
  • Your statement of the present and absent seems ambiguous, based on your code I assume you want to split the `newCars` list into cars present in `oldCars` and cars absent from `oldCars`, in which case you should have `Any` instead of `All` - obviously `oldCars.All` will only be true if `oldCars` contains one car. – NetMage Apr 24 '19 at 23:42

4 Answers4

0

You can create custom IEqualityComparer:

public class CarEqualityComparer : IEqualityComparer<Car>
{
    public bool Equals(Car x, Car y)
    {
        return x.Id == y.Id;
    }

    public int GetHashCode(Car obj)
    {
        return obj.Id.GetHashCode();
    }
}

And use it like this:

var present = newCars.Intersect(oldCars, new CarEqualityComparer());
var absent = newCars.Except(oldCars, new CarEqualityComparer());
koryakinp
  • 3,989
  • 6
  • 26
  • 56
0

You can utilize an anonymous variable containing the two lists:

var res = new
{
    present = newCars.Where(n => oldCars.Select(o => o.Id).Contains(n.Id)), // present if the old cars list contains the id from the new list
    absent = newCars.Where(n => !oldCars.Select(o => o.Id).Contains(n.Id)) // absent if the old cars list does not contain the id from the new list
};
Amir Molaei
  • 3,700
  • 1
  • 17
  • 20
  • What if I need to Check not only the Id of the car, but also the registrationNumber. Is there a way to have 2 conditions in your Select ? – Illep Apr 25 '19 at 00:54
0

When you are saying that new cars are the source of truth, that means you want them always in the list, but you may not have an equivalent old car. You are describing a left-join.

This should get you most of what you want.

var results = from n in newCars
    join o in oldCars
    on n.Id == o.Id
    group into cars 
    from oc in cars.DefaultIfEmpty()
    select (NewCar: n, Present: oc != null)

The result is a tuple containing the new car and a boolean Present that is true or false based on whether there is an old car or not.

That gives you a single result set. You wanted two result sets as a tuple. To get this, just extend the above query, grouping by Present. This gives you a result set with exactly two items

var results = (from r in (from n in newCars
        join o in oldCars
        on n.Id equals o.Id
        into cars
        from oc in cars.DefaultIfEmpty()
        select (NewCar: n, Present: oc != null))
    group r by r.Present into g
    orderby g.Key descending 
    select g.Select(x => x.NewCar).ToList()).ToList();

from which you can get a single tuple containing both lists

var finalResult = (Present: results[0], Absent: results[1]);
Kit
  • 20,354
  • 4
  • 60
  • 103
  • I would suggest putting a `ToList` on the final `select` so that `g.Select` is processed once during the query. Also, I think your first query is missing something: doesn't `from oc.DefaultIfEmpty()` needs a range variable? And you use `n` and `newcar` as the same range variable. – NetMage Apr 24 '19 at 19:20
  • What about the `from oc.DefaultIfEmpty()`? It doesn't need a range variable? – NetMage Apr 24 '19 at 20:05
  • I think you were looking at a prior edit (it *was* wrong at one point). `cars.DefaultIfEmpty()` is what is needed for correct results. – Kit Apr 24 '19 at 21:05
  • No, first query see the phrase? – NetMage Apr 24 '19 at 21:09
  • Oops. I'm finally getting what you're trying to say. I changed it in one code block but not the other; that's what I get for not using a compiler. Thanks for being diligent! – Kit Apr 24 '19 at 22:46
0

Using an extension method to treat adding to a list (a tiny bit) more functionally, you can use LINQ Aggregate to do this in one pass:

var idsFromOldCars = oldCars.Select(c => c.Id).ToHashSet();
var(present, absent) = newCars.Aggregate((p:new List<Car>(),a:new List<Car>()), (pa,nc) => idsFromOldCars.Contains(nc.Id) ? (pa.p.AfterAdd(nc),pa.a) : (pa.p,pa.a.AfterAdd(nc)));

Where AfterAdd is defined in a static class as:

public static class ListExt {
    public static List<T> AfterAdd<T>(this List<T> head, params T[] tail) {
        head.AddRange(tail);
        return head;
    }
}

But it is much clearer to just use a foreach loop:

    var idsFromOldCars = oldCars.Select(c => c.Id).ToHashSet();
    var present = new List<Car>();
    var absent = new List<Car>();
    foreach (var nc in newCars) {
        if (idsFromOldCars.Contains(nc.Id))
            present.Add(nc);
        else
            absent.Add(nc);
    }

In both cases, creating the HashSet ensures you don't have to scan through the oldCars list to find each newCar. If you knew the lists were ordered by Id, you could do a more complicated one-pass solution traveling down the lists in parallel, but that hardly seems worth the complexity even then.

NOTE: A Join effectively does something similar, converting the second List into a Lookup (like a Dictionary) that can be used to find the matches with the first List.

NetMage
  • 26,163
  • 3
  • 34
  • 55
  • Can I use ANY instead ? I may have more than 1 condition to match in order to categories it to present or absent ? Help. – Illep Apr 25 '19 at 00:57
  • @Illep Perhaps you can update your question to be clearer? You seem to have conflicting ideas about what you want... – NetMage Apr 25 '19 at 17:26