LINQ filtering on a Dictionary<string, IList<string>> - linq

I have code similar to this:
var dict = new Dictionary<string, IList<string>>();
dict.Add("A", new List<string>{"1","2","3"});
dict.Add("B", new List<string>{"2","4"});
dict.Add("C", new List<string>{"3","5","7"});
dict.Add("D", new List<string>{"8","5","7", "2"});
var categories = new List<string>{"A", "B"};
//This gives me categories and their items matching the category list
var result = dict.Where(x => categories.Contains(x.Key));
Key Value
A 1, 2, 3
B 2, 4
What I would like to get is this:
A 2
B 2
So the keys and just the values that are in both lists. Is there a way to do this in LINQ?
Thanks.

Easy peasy:
string key1 = "A";
string key2 = "B";
var intersection = dict[key1].Intersect(dict[key2]);
In general:
var intersection =
categories.Select(c => dict[c])
.Aggregate((s1, s2) => s1.Intersect(s2));
Here, I'm utilizing Enumerable.Intersect.

A somewhat dirty way of doing it...
var results = from c in categories
join d in dict on c equals d.Key
select d.Value;
//Get the limited intersections
IEnumerable<string> intersections = results.First();
foreach(var valueSet in results)
{
intersections = intersections.Intersect(valueSet);
}
var final = from c in categories
join i in intersections on 1 equals 1
select new {Category = c, Intersections = i};
Assuming we have 2 and 3 common to both lists, this will do the following:
A 2
A 3
B 2
B 3

Related

Default values for empty groups in Linq GroupBy query

I have a data set of values that I want to summarise in groups. For each group, I want to create an array big enough to contain the values of the largest group. When a group contains less than this maximum number, I want to insert a default value of zero for the empty key values.
Dataset
Col1 Col2 Value
--------------------
A X 10
A Z 15
B X 9
B Y 12
B Z 6
Desired result
X, [10, 9]
Y, [0, 12]
Z, [15, 6]
Note that value "A" in Col1 in the dataset has no value for "Y" in Col2. Value "A" is first group in the outer series, therefore it is the first element that is missing.
The following query creates the result dataset, but does not insert the default zero values for the Y group.
result = data.GroupBy(item => item.Col2)
.Select(group => new
{
name = group.Key,
data = group.Select(item => item.Value)
.ToArray()
})
Actual result
X, [10, 9]
Y, [12]
Z, [15, 6]
What do I need to do to insert a zero as the missing group value?
Here is how I understand it.
Let say we have this
class Data
{
public string Col1, Col2;
public decimal Value;
}
Data[] source =
{
new Data { Col1="A", Col2 = "X", Value = 10 },
new Data { Col1="A", Col2 = "Z", Value = 15 },
new Data { Col1="B", Col2 = "X", Value = 9 },
new Data { Col1="B", Col2 = "Y", Value = 12 },
new Data { Col1="B", Col2 = "Z", Value = 6 },
};
First we need to determine the "fixed" part
var columns = source.Select(e => e.Col1).Distinct().OrderBy(c => c).ToList();
Then we can process with the normal grouping, but inside the group we will left join the columns with group elements which will allow us to achieve the desired behavior
var result = source.GroupBy(e => e.Col2, (key, elements) => new
{
Key = key,
Elements = (from c in columns
join e in elements on c equals e.Col1 into g
from e in g.DefaultIfEmpty()
select e != null ? e.Value : 0).ToList()
})
.OrderBy(e => e.Key)
.ToList();
It won't be pretty, but you can do something like this:
var groups = data.GroupBy(d => d.Col2, d => d.Value)
.Select(g => new { g, count = g.Count() })
.ToList();
int maxG = groups.Max(p => p.count);
var paddedGroups = groups.Select(p => new {
name = p.g.Key,
data = p.g.Concat(Enumerable.Repeat(0, maxG - p.count)).ToArray() });
You can do it like this:-
int maxCount = 0;
var result = data.GroupBy(x => x.Col2)
.OrderByDescending(x => x.Count())
.Select(x =>
{
if (maxCount == 0)
maxCount = x.Count();
var Value = x.Select(z => z.Value);
return new
{
name = x.Key,
data = maxCount == x.Count() ? Value.ToArray() :
Value.Concat(new int[maxCount - Value.Count()]).ToArray()
};
});
Code Explanation:-
Since you need to append default zeros in case when you have less items in any group, I am storing the maxCount (which any group can produce in a variable maxCount) for this I am ordering the items in descending order. Next I am storing the maximum count which the item can producr in maxCount variable. While projecting I am simply checking if number of items in the group is not equal to maxCount then create an integer array of size (maxCount - x.Count) i.e. maximum count minus number of items in current group and appending it to the array.
Working Fiddle.

How can I intersect more than two sets/lists of values?

Here is an example that works in Linqpad. The problem is that I need it to work for more than two words, e.g. searchString = "headboard bed railing". This is a query against an index and instead of "Match Any Word" which I've done, I need it to "Match All Words", where it finds common key values for each of the searched words.
//Match ALL words for categories in index
string searchString = "headboard bed";
List<string> searchList = new List<string>(searchString.Split(' '));
string word1 = searchList[0];
string word2 = searchList[1];
var List1 = (from i in index
where i.word.ToUpper().Contains(word1)
select i.category.ID).ToList();
var List2 = (from i in index
where i.word.ToUpper().Contains(word2)
select i.category.ID).ToList();
//How can I make this work for more than two Lists?
var commonCats = List1.Intersect(List2).ToList();
var category = (from i in index
from s in commonCats
where commonCats.Contains(i.category.ID)
select new
{
MajorCategory = i.category.category1.description,
MinorCategory = i.category.description,
Taxable = i.category.taxable,
Life = i.category.life,
ID = i.category.ID
}).Distinct().OrderBy(i => i.MinorCategory);
category.Dump();
Thanks!
Intersection of an intersection is commutative and associative. This means that (A ∩ B ∩ C) = (A ∩ (B ∩ C)) = ((A ∩ B) ∩ C), and rearranging the order of the lists will not change the result. So just apply .Intersect() multiple times:
var commonCats = List1.Intersect(List2).Intersect(List3).ToList();
So, to make your code more general:
var searchList = searchString.Split(' ');
// Change "int" if this is not the type of i.category.ID in the query below.
IEnumerable<int> result = null;
foreach (string word in searchList)
{
var query = from i in index
where i.word.ToUpper().Contains(word1)
select i.category.ID;
result = (result == null) ? query : result.Intersect(query);
}
if (result == null)
throw new InvalidOperationException("No words supplied.");
var commonCats = result.ToList();
To build on #cdhowie's answer, why use Intersect? I would think you could make it more efficient by building your query in multiple steps. Something like...
if(string.IsNullOrWhitespace(search))
{
throw new InvalidOperationException("No word supplied.");
}
var query = index.AsQueryable();
var searchList = searchString.Split(' ');
foreach (string word in searchList)
{
query = query.Where(i => i.word.ToUpper().Contains(word));
}
var commonCats = query.Select(i => i.category.ID).ToList();

Linq select subgroup

I have the following in my db table:
- MailingId | GroupName | ServiceId
- 1 | group1 | 3
- 2 | group1 | 5
- 3 | group1 | 8
- 4 | group2 | null
- 5 | group3 | null
...
In my view i have 2 groups of checkboxes:
1) (services) with id's 3,5,8 (serviceId).
2) and a list of checkboxes for mailing groups (group1, group2, group3)
I need to select the following using LINQ:
Select rows that I have selected in ServiceId checkbox list PLUS any other. For example if I check off ServiceId's (3 and 5) and group "Group3" then my output would be rows MailingId: 1, 3 and 5. HOWEVER, if I select ANY service from (first group of checkboxes) AND DO NOT select "Group1" from mailing group checkboxes then rows with Group1 SHOULD NOT be in the output.
I'm using EF4. Please help.
Thanks
The 2 arrays would be the selections that are posted from your view
int[] selectedservices = {3,5};
string[] selectedgroups = {"group3"};
using (Model model = new Model())
{
bool b = selectedservices.Contains(1);
var mailinglists = from m in model.MalingSet
where selectedgroups.Contains(m.GroupName)
&& ((m.ServiceId.HasValue && selectedservices.Contains(m.ServiceId.Value)) || m.ServiceId.HasValue == false)
select m.MailingId;
}
Try something like this.
var mailingGroup =
from m in Mailings
where m.ServiceId != null // or whatever other condition
group m by m.GroupName into g
select new
{
groupName = g.Key,
mailings = g
};
It's a Where clause no?
var a = new {m = 1, g = "group1", sid = 3};
var b = new {m = 2, g = "group1", sid = 5};
var c = new {m = 3, g = "group1", sid = 8};
var d = new {m = 4, g = "group2", sid = 0};
var e = new {m = 5, g = "group3", sid = 0};
var l = new List<dynamic>{a,b,c,d,e};
l.Where(it => ( new ArrayList{3, 5}).Contains(it.sid)
|| (new ArrayList{1, 5}).Contains(it.m)).Dump();
http://www.linqpad.net/

Using LINQ to join [n] collections and find matches

Using LINQ I can find matching elements between two collections like this:
var alpha = new List<int>() { 1, 2, 3, 4, 5 };
var beta = new List<int>() { 1, 3, 5 };
return (from a in alpha
join b in beta on a equals b
select a);
I can increased this to three collections, like so:
var alpha = new List<int>() { 1, 2, 3, 4, 5 };
var beta = new List<int>() { 1, 3, 5 };
var gamma = new List<int>() { 3 };
return (from a in alpha
join b in beta on a equals b
join g in gamma on a equals g
select a);
But how can I construct a LINQ query that will return the matches between N number of collections?
I'm thinking if each collection was added to a parent collection, then the parent collection was iterated through using a recursive loop, it may work?
There's no need to recurse - you can just iterate. However, you may find it best to create a set and intersect that each time:
List<List<int>> collections = ...;
HashSet<int> values = new HashSet<int>(collections[0]);
foreach (var collection in collections.Skip(1)) // Already done the first
{
values.IntersectWith(collection);
}
(Like BrokenGlass, I'm assuming you've got distint values, and that you really just want to find the values which are in all the collections.)
If you prefer the immutable and lazy approach, you could use:
List<List<int>> collections = ...;
IEnumerable<int> values = collections[0];
foreach (var collection in collections.Skip(1)) // Already done the first
{
values = values.Intersect(collection);
}
If you have only unique values you can use Intersect:
var result = alpha.Intersect(beta).Intersect(gamma).ToList();
If you need to preserve multiple values that are not unique you can just exclude non-intersecting items from the original collection as an additional step:
alpha = alpha.Where(x => result.Contains(x)).ToList();
To generalize the Intersect approach you can just use a loop to do all intersections one by one:
IEnumerable<List<int>> collections = new [] { alpha, beta, gamma };
IEnumerable<int> result = collections.First();
foreach (var item in collections.Skip(1))
{
result = result.Intersect(item);
}
result = result.ToList();

C# EF Linq bitwise question

Ok for example, I am using bitwise like such: Monday = 1, Tuesday = 2, Wednesday = 4, Thursday = 8 etc...
I am using an Entity Framework class of Business.
I am using a class and passing in a value like 7 (Monday, Tuesday, Wednesday).
I want to return records that match any of those days
public List<Business> GetBusinesses(long daysOfWeek)
{
using (var c = Context())
{
return c.Businesses.Where(???).ToList();
}
}
Any help would be appreciated. Thanks!
EDIT
Ok, so I am attempting the following:
var b = new List<Business>();
var b1 = new Business(){DaysOfWeek = 3};
b.Add(b1);
var b2 = new Business() { DaysOfWeek = 2 };
b.Add(b2);
var decomposedList = new[]{1};
var l = b.Where(o => decomposedList.Any(day => day == o.DaysOfWeek)).ToList();
But l returns 0 results assuming in the decomposedList(1) I am looking for monday.
I created b1 to contain Monday and Tuesday.
Use the bitwise and operator & to combine your desired flags with the actual flags in the database and then check for a non-zero result.
var b1 = new { DaysOfWeek = 3 };
var b2 = new { DaysOfWeek = 2 };
var b = new[] { b1, b2 };
var filter = 1;
var l = b.Where(o => (filter & o.DaysOfWeek) != 0);
foreach (var x in l)
{
Console.WriteLine(x);
}
If you have an array of filter values simply combined then with an OR | first:
var filterArray = new []{1, 4};
var filter = filterArray.Aggregate((x, y) => x | y);
You must decompose the long value(bitflagged enum will be better) to it's parts then pass it to Where
return c.Businesses.Where(o=> DecomposeDays(dayParam).Any(day => day==o)).ToList();
EDIT:
decompose method:
private static IEnumerable<byte> DecomposeDays(byte dayParam)
{
var daysOfWeek = new List<byte> { 1, 2, 4, 6, 8 ,16};
return daysOfWeek.Where(o => (o & dayParam) == dayParam);
}

Resources