C# Linq query for matching lists exactly - c#

I have a list of IDs and collection of objects, each with its own child list of IDs. I want a linq query that will return any of the objects for which its child list exactly matches the list of IDs. Here is what I have, and I think it works -- but it's ugly, and requires two steps. Is there a better way to do this?
var inputIDs = new List<int> {1, 5, 8, 10, 12};
var object1 = new {name = "object1", IDs = new List<int> {9, 10, 11, 12}};
var object2 = new {name = "object2", IDs = new List<int> {1, 5, 8, 12}};
var object3 = new {name = "object3", IDs = new List<int> {1, 5, 8, 10, 12}};
var objects = new List<object> {object1, object2, object3};
var candidateObjects = objects.Where(o => o.IDs.All(i => inputIDs.Contains(i)));
var exactMatches = candidateObjects.Where(o => inputIDs.All(i => o.IDs.Contains(i)));
// exactMatches should only contain object3

I think what you want is Enumerable.SequenceEqual:
var inputIDs = new List<int> { 1, 5, 8, 10, 12 };
var object1 = new { name = "object1", IDs = new List<int> { 9, 10, 11, 12 } };
var object2 = new { name = "object2", IDs = new List<int> { 1, 5, 8, 12 } };
var object3 = new { name = "object3", IDs = new List<int> { 1, 5, 8, 10, 12 } };
var objects = new[] { object1, object2, object3 }.ToList();
var exactMatches = objects.Where(o => o.IDs.SequenceEqual(inputIDs));
Per #AnupSharma, you will need to sort if your sequences could be out of order (and now performance rears its head):
var inputIDs = new[] { 1, 5, 8, 10, 12 }.OrderBy(i => i).ToList();
//...
var exactMatches = objects.Where(o => o.IDs.OrderBy(i => i).SequenceEqual(inputIDs));
A (slight) performance improvement can be had by testing for Count since we know the sources are Lists:
var exactMatches = objects.Where(o => o.IDs.Count == inputIDs.Count && o.IDs.OrderBy(i => i).SequenceEqual(inputIDs));

A similar but faster solution would be using Except with a count check:
objects.Where(o => !o.IDs.Except(inputIDs).Any() && o.IDs.Count == inputIDs.Count);

I think adding a check for count of elements will be enough for your example
var result = objects.Where(o => o.IDs.Count == inputIDs.Count)
.Where(o => o.IDs.All(id => inputIDs.Contains(id)));
You can do some optiomization by using HashSet
var inputs =new HashSet<int>(inputsIDs);
var result = objects.Where(o => o.IDs.Count == inputIDs.Count)
.Where(o => o.IDs.All(id => inputs.Contains(id)));

Related

Check if a set exactly includes a subset using Linq taking into account duplicates

var subset = new[] { 9, 3, 9 };
var superset = new[] { 9, 10, 5, 3, 3, 3 };
subset.All(s => superset.Contains(s))
This code would return true, because 9 is included in the superset,but only once, I want an implementation that would take into account the duplicates, so it would return false
My thought was that you could group both sets by count, then test that the super group list contained every key from the sub group list and, in each case, the super count was greater than or equal to the corresponding subcount. I think that I've achieved that with the following:
var subset = new[] { 9, 3, 9 };
var superset = new[] { 9, 10, 5, 3, 3, 3 };
var subGroups = subset.GroupBy(n => n).ToArray();
var superGroups = superset.GroupBy(n => n).ToArray();
var basicResult = subset.All(n => superset.Contains(n));
var advancedResult = subGroups.All(subg => superGroups.Any(supg => subg.Key == supg.Key && subg.Count() <= supg.Count()));
Console.WriteLine(basicResult);
Console.WriteLine(advancedResult);
I did a few extra tests and it seemed to work but you can test some additional data sets to be sure.
Here is another solution :
var subset = new[] { 9, 3, 9 };
var superset = new[] { 9, 10, 5, 3, 3, 3 };
var subsetGroup = subset.GroupBy(x => x).Select(x => new { key = x.Key, count = x.Count() });
var supersetDict = superset.GroupBy(x => x).ToDictionary(x => x.Key, y => y.Count());
Boolean results = subsetGroup.All(x => supersetDict[x.key] >= x.count);
This works for me:
var subsetLookup = subset.ToLookup(x => x);
var supersetLookup = superset.ToLookup(x => x);
bool flag =
subsetLookup
.All(x => supersetLookup[x.Key].Count() >= subsetLookup[x.Key].Count());
That's not how sets and set operations work. Sets cannot contain duplicates.
You should treat the two arrays not as sets, but as (unordered) sequences. A possible algorithm would be: make a list from the sequence superset, then remove one by one each element of the sequence subset from the list until you are unable to find such an element in the list.
bool IsSubList(IEnumerable<int> sub, IEnumerable<int> super)
{
var list = super.ToList();
foreach (var item in sub)
{
if (!list.Remove(item))
return false; // not found in list, so sub is not a "sub-list" of super
}
return true; // all elements of sub were found in super
}
var subset = new[] { 9, 3 };
var superset = new[] { 9, 10, 5, 3,1, 3, 3 };
var isSubSet = IsSubList(subset, superset);

Combine two list of lists in LINQ

I have two lists of the following class:
public class KeyValues
{
public int Key { get;set;}
public List<double> DoubleList { get;set;}
}
var list1 = new List<KeyValues>();
list1.Add (new KeyValues(){Key = 33, DoubleList = new List<double>(){2.3,2.4,2.5}});
list1.Add (new KeyValues(){Key = 34, DoubleList = new List<double>(){3.3,3.4,3.5}});
list1.Add (new KeyValues(){Key = 35, DoubleList = new List<double>(){4.3,4.4,4.5}});
var list2 = new List<KeyValues>();
list2.Add (new KeyValues(){Key = 33, DoubleList = new List<double>(){20.3,20.4}});
list2.Add (new KeyValues(){Key = 34, DoubleList = new List<double>(){30.3,30.4}});
list2.Add (new KeyValues(){Key = 35, DoubleList = new List<double>(){40.3,40.4}});
I would like to combine those into a new list by mathing the keys and combining the sub lists. So the result should be:
list3 = [
[33, {2.3,2.4,2.5,20.3,20.4}]
[34, {3.3,3.4,3.5,30.3,30.4}]
[35, {4.3,4.4,4.5,40.3,40.4}]
]
Of course I can iterate over the keys, but isn't there a much better (faster) linq solution for this?
Using Concat and GroupBy methods can produce the expected result (after making a code compile, of course)
var result = list1.Concat(list2).GroupBy(kv => kv.Key, kv => kv.DoubleList)
.Select(g => new KeyValues { Key = g.Key, DoubleList = g.SelectMany(i => i).ToList() });
You can concatenate the two lists together and then use GroupBy, and then Select to project the output into the desired format. For example:
var list3 = list1.Concat(list2)
.GroupBy(x => x.Key)
.Select(x => new KeyValue
{
Key = x.Key,
DoubleList = x.SelectMany(x => x.DoubleList).ToList()
})
.ToList();
You're example is not syntactically correct:
Should be like this:
var list1 = new List<KeyValue>();
list1.Add(new KeyValue { Key = 33, DoubleList = new List<double>() { 2.3, 2.4, 2.5 } });
list1.Add(new KeyValue { Key = 34, DoubleList = new List<double>() { 3.3, 3.4, 3.5 } });
list1.Add(new KeyValue { Key = 35, DoubleList = new List<double>() { 4.3, 4.4, 4.5 } });
var list2 = new List<KeyValue>();
list2.Add(new KeyValue { Key = 33, DoubleList = new List<double>() { 20.3, 20.4 } });
list2.Add(new KeyValue { Key = 34, DoubleList = new List<double>() { 30.3, 30.4 } });
list2.Add(new KeyValue { Key = 35, DoubleList = new List<double>() { 40.3, 40.4 } });
list1.Zip(list2, (first, second) => (first.Key, first.DoubleList.Concat(second.DoubleList));
If the lists have the same element keys in the same order you can do this:
list1.Zip(list2, (first, second) =>
(first.Key, first.DoubleList.Concat(second.DoubleList)));
My first suggestion here is to use Dicitionary<int, List<double>> instead of creating a class KeyValues or if you want specifically to use Key-Value pair, then .net have that too, it's called: KeyValuePair.
Just in case if you are interested, here is the way to convert your class to Dictionary:
var list3 = list1.Concat(list2)
.ToDictionary(item => item.Key);
Note: this will actually fail because of key duplications, so in case if you don't want that to happen, make sure that Keys are different.
Update:
Or you can use Lookup<TKey, TElement> to make it work even with duplicate keys:
var list3 = list1
.Concat(list2)
.ToLookup(group => group.Key, group => group.DoubleList)
.ToDictionary(group => group.Key, group => group.First());
One suggestion: you can use a collection initializer instead of adding elements one by one:
var list1 = new List<KeyValues>
{
new KeyValues {Key = 33, DoubleList = new List<double>() {2.3, 2.4, 2.5}},
new KeyValues {Key = 34, DoubleList = new List<double>() {3.3, 3.4, 3.5}},
new KeyValues {Key = 35, DoubleList = new List<double>() {4.3, 4.4, 4.5}}
};
More information:
Dictionary<TKey,TValue> (official documentation here)
KeyValuePair VS DictionaryEntry (great post here)

Lambda where id does not exist in another list

I have to exclude items whose ids exist in another list.
List<Int64> Listofid;
var filteredlist = curProjArea.Project.ForEach(x => {x.ProjectId = (where
project id does not exist in Listofid) });
Is this possible?
You can filter projects in Where clause:
List<Int64> Listofid;
var filteredlist = curProjArea.Project.Where(x => !Listofid.Contains(x.ProjectId));
List<Int64> Listofid;
var filteredlist = curProjArea.Project.Where(x => !Listofid.Contains(x.ProjectId)).ToList();
Try this out once
int[] values1 = { 1, 2, 3, 4 };
// Contains three values (1 and 2 also found in values1).
int[] values2 = { 1, 2, 5 };
// Remove all values2 from values1.
var result = values1.Except(values2);
// Show.
foreach (var element in result)
{
Console.WriteLine(element);
}
From:
https://www.dotnetperls.com/except
I think , It's useful for you
List<Int64> Listofid = new List<Int64>() { 5, 3, 9, 7, 5, 9, 3, 7 };
List<Int64> filteredlist = new List<Int64>() { 8, 3, 6, 4, 4, 9, 1, 0 };
List<Int64> Except = filteredlist.Except(Listofid).ToList();
Console.WriteLine("Except Result");
foreach (int num in Except)
{
Console.WriteLine("{0} ", num); //Result = 8,6,4,1,0
}
Just negate the .Contains on the list for which you want to exclude it.
var filteredList = curProjArea.Project.Where(a => !Listofid.Contains(a.ProjectId));
Demo in Dotnet fiddle

name of assoc-diff in C#

What do you call this method, (is it available in .net?)
var list1 = new List<int>() { 1, 2, 2, 3, 4 };
var list2 = new List<int>() { 1, 2, 3};
var results = list1.diff(list2);
results:
{ 2, 4 }
The closest thing built in is the Except LINQ operator.
Produces the set difference of two sequences.
Though with your example it will result in:
{ 4 }
I don't believe there is a direct analogue to what you want.
You actually need a multiset implementation. Although there is no multiset out of the box in BCL, there are some ideas here and in the linked question.
Or you can actually implement one by yourself, it's not so complicated:
class Multiset<K> // maybe implement IEnumerable?
{
Dictionary<K, int> arities = new Dictionary<K, int>();
...
Multiset<K> Except(Multiset<K> other)
{
foreach (var k in arities.keys)
{
int arity = arities[k];
if (other.Contains(k))
arity -= other.Arity(k);
if (arity > 0)
result.Add(k, arity);
}
return result;
}
}
This exactly return what you want, You can refactor it in a Extension Method:
var results = list1.GroupBy(p => p).Select(p => new { item = p.Key, count = p.Count() })
.Concat(list2.GroupBy(p => p).Select(p => new { item = p.Key, count = -p.Count() }))
.GroupBy(p => p.item).Select(p => new { item = p.Key, count = p.Sum(q => q.count) })
.Where(p => p.count > 0)
.SelectMany(p => Enumerable.Repeat(p.item, p.count));
Like this: (see oded's post for a linq to msdn)
int[] numbersA = { 0, 2, 4, 5, 6, 8, 9 };
int[] numbersB = { 1, 3, 5, 7, 8 };
IEnumerable<int> aOnlyNumbers = numbersA.Except(numbersB);

Possible to group by Count in LINQ?

This might be either impossible or so obvious I keep passing over it.
I have a list of objects(let's say ints for this example):
List<int> list = new List<int>() { 1, 2, 3, 4, 5, 6 };
I'd like to be able to group by pairs with no regard to order or any other comparison, returning a new IGrouping object.
ie,
list.GroupBy(i => someLogicToProductPairs);
There's the very real possibility I may be approaching this problem from the wrong angle, however, the goal is to group a set of objects by a constant capacity. Any help is greatly appreciated.
Do you mean like this:
List<int> list = new List<int>() { 1, 2, 3, 4, 5, 6 };
IEnumerable<IGrouping<int,int>> groups =
list
.Select((n, i) => new { Group = i / 2, Value = n })
.GroupBy(g => g.Group, g => g.Value);
foreach (IGrouping<int, int> group in groups) {
Console.WriteLine(String.Join(", ", group.Select(n=>n.ToString()).ToArray()));
}
Output
1, 2
3, 4
5, 6
you can do something like this...
List<int> integers = new List<int>() { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
var p = integers.Select((x, index) => new { Num = index / 2, Val = x })
.GroupBy(y => y.Num);
int counter = 0;
// this function returns the keys for our groups.
Func<int> keyGenerator =
() =>
{
int keyValue = counter / 2;
counter += 1;
return keyValue;
};
var groups = list.GroupBy(i => {return keyGenerator()});

Categories