LINQ contain each element of all groups? - c#

i have this match conditiond:
var matchConditions = new long[][]
{
new long[] { 109, 145 }, //color Id
new long[] { 202 }, // province Id
new long[] { 303, 309, 317, 318 }, //options Id
};
each of them is id of a different group, for example first nested array is for color.
and i need to know which products meet color id 109 or 145, second one is for something else.
i mean i want to get all products that meet any item of all each group,
{color 109 or 145 - and - province 202 - and - option 303 or 309 or 317 or 318}
i tried:
matchConditions.ToList().ForEach(x => x.Any(j => adAdFields.Select(co => co.listFieldId).Contains(j)))
and
matchConditions.All(x => x.Any(j => adAdFields.Select(co => co.listFieldId).Contains(j)))
but none of them work
EDIT:
before now i had this query:
var adsWithRelevantadFields =
from adField in cwContext.tblAdFields
join ads in cwContext.tblAds on adField.adId equals ads.id
where (prvId == 0 && ads.tabId == tabId) ||
(prvId != 0 & ctsId.Value == 0 && ads.provinceId == prvId & ads.tabId == tabId) ||
(prvId != 0 & ctsId.Value > 0 && ads.provinceId == prvId & ads.cityId == ctsId.Value & ads.tabId == tabId)
where ads.endDate >= theToDay
where ads.conditionId == 1
where ads.payed == true
group adField by adField.adId into adAdFields
where searchIds.All(i => adAdFields.Select(co => co.listFieldId).Contains(i))
this work well, but now i need to search more options that grouped as i showed, so i need to add "&&" to end of query to search these new items, that is my question.
EDIT2
let's suppose i have some lists(color, who produce, options, where, ....) now when i want to add a new product, add proper attribute in these format(productId, attributeId), so for example i have (1,109 - 1,202 - 1, 303 ...) and (2,109 - 2,202 - 2,318...) ...
so as i grouped each product(ads in real), i need just check which products grop:
{color 109 or 145 - and - province 202 - and - option 303 or 309 or 317 or 318}

How about something like this?
adAdFields.Where(x => matchConditions[0].Contains(x.colorId)
&& matchConditions[1].Contains(x.provinceId)
&& matchConditions[2].Contains(x.optionsId))
.ToList();

I think you need to grain you query, how about to build this query - step by step, in that case you can improve readability of your code and check availibility of all conditions.
Look at this:
class Program
{
static void Main(string[] args)
{
var adAdFields = new List<AdAdField>
{
new AdAdField {colorId = 109, optionsId = 303, provinceId = 202},
new AdAdField {colorId = 145, optionsId = 309, provinceId = 2},
new AdAdField {colorId = 3, optionsId = 317, provinceId = 3},
new AdAdField {colorId = 4, optionsId = 318, provinceId = 4}
}.AsQueryable();
var matchConditions = new long[][]
{
new long[] { 109, 145 }, //color Id
new long[] { 202 }, // province Id
new long[] { 303, 309, 317, 318 }, //options Id
};
var result1 = adAdFields.Where(x => matchConditions[0].Contains(x.colorId)
&& matchConditions[1].Contains(x.provinceId)
&& matchConditions[2].Contains(x.optionsId)).ToList();
var query = adAdFields;
if (matchConditions[0].Length > 0)
query = query.Where(x => matchConditions[0].Contains(x.colorId));
if (matchConditions[1].Length > 0)
query = query.Where(x => matchConditions[1].Contains(x.provinceId));
if (matchConditions[2].Length > 0)
query = query.Where(x => matchConditions[2].Contains(x.optionsId));
//below will be other possible conditions....
var result2 = query.ToList();
//result2 and result1 ARE SAME!!!
}
}
public class AdAdField
{
public int colorId { get; set; }
public int provinceId { get; set; }
public int optionsId { get; set; }
}
IQueriable will add conditions to query, eventually in the end of query you will call .ToList(), and it forces orm to generate appropriate sql. Until this time you just building query.

You can use dynamic linq , to construct a linq query similar to the one in #Selman22 answer, with varied number of conditions. Basically, A linq query is composed of a tree with expressions in it.
Dynamic LINQ means you build the tree, well - dynamically, which gives you more power creating the queries.
You can use PredicateBuilder to do so, or use a ready made library such as dynamic-query-library.
See:
http://www.codeproject.com/Articles/231706/Dynamic-query-with-Linq
http://msdn.microsoft.com/en-us/library/bb882637.aspx
http://weblogs.asp.net/scottgu/archive/2008/01/07/dynamic-linq-part-1-using-the-linq-dynamic-query-library.aspx

Related

LINQ complex search query advice

I need some help with some LINQ logic that I am trying to do
Using EF, I have this result set:
Basically what I want to achieve is if the user wants to find an element that has TagID 3 AND TagID 4 it should only return Low, Medium
This should ignore Low as this element doesn't have TagID 4
Also if the user just wants the elements that contain TagID 3, it should return Low, Medium and Low as both contain TagID 3
I have tried this just to get Low, Medium back (the harder logic) but to no prevail.
var result = result.Where(x => x.TagID == 3 && x.TagID == 4).ToList();
A step in the right direction is all that is needed please
This should work if tags are only available once per ID (i.e. no items with the same ID and the same tag ID).
I don't think EF will be available to translate to SQL -> materialize first.
var q = result.ToList();
var tagIDs = new HashSet<int>() { 3, 4 };
IEnumerable<string> itemContents =
q.Where(x => tagIDs.Contains(x.TagID)). // Keep only the tags we're interested in
GroupBy(x => x.Id). // Group the items by ID
Where(g => (g.Count() == tagIDs.Count)). // Select the groups having the right number of items
SelectMany(g => g.Select(x => x.ItemContent)). // Extract ItemContent
Distinct(); // Remove duplicates
I don't know if EF this swallows, here is an example:
var data = new[]
{
new { Id = 12, TagID = 3, ItemContent = "Low" },
new { Id = 13, TagID = 3, ItemContent = "Low, Medium" },
new { Id = 13, TagID = 4, ItemContent = "Low, Medium" },
};
var search = new List<int>(new[] { 3, 4 });
var result = data
// group the items on ItemContent
.GroupBy(item => item.ItemContent, d => d, (k, g) => new { ItemContent = k, g })
// only select groups when all searchitems are found in a list of TagID
.Where(groupedItem => search.All(i => groupedItem.g.Select(y => y.TagID).Contains(i)))
// select the result
.Select(groupedItem => groupedItem);
foreach (var r in result)
Console.WriteLine(r.ItemContent);
Console.ReadLine();

Get count and avg for specific criterias and also the rest

I have my data in the following format..
UserId Property1 Property2 Property3 Testval
1 1 1 10 35
2 1 2 3 45
3 2 5 6 55
and so on..
I have several criterias, a couple of example are as below..
a) Where Property1=1 and Property3=10
b) Where Property1!=1 and Property2=5
What I need is the count of users & testval average who fall within these criterias and also of all the rest who do not.
So, result data structure would be as follows..
User Count
Criteria Users
a 100
b 200
rest 1000
TestVal Average
Criteria avg
a 25
b 45
rest 15
I know how to get the userlist for the specific criterias separately.
data.Where(w=>w.Property1==1).Select(s=>s.UserId).ToList()
But how do I get the usercount and avg val and more importantly the same for the rest of users.
Any help is sincerely appreciated
Thanks
Looks like you are seeking for group by criteria. Something like this:
var result = data.GroupBy(x =>
x.Property1 == 1 && x.Property3 == 10 ? 0 :
x.Property1 != 1 && x.Property2 == 5 ? 1 :
// ...
-1)
.Select(g => new
{
Criteria = g.Key,
Users = g.Count(),
Avg = g.Average(x => x.Testval),
})
.ToList();
To get the count/average for a specific criterion, it's easy
Func<MyUser, boolean> criterion1 = user => user.Property1==1;
var avg = data.Where(criterion1).Average(user => user.Testval);
var count = data.Where(criterion1).Count();
(this will enumerate the data twice, so if that's an issue, you can materialize the data before the calculations)
If you want to evaluate multiple criteria (and don't want to repeat this code as many times as there are criteria), you can put them in a dictionary, and loop over them:
var criteria = new Dictionary<string, Func<MyUser, boolean>>{
{ "criterion1", user => user.Property1==1 },
{ "criterion2", user => user.Property1!=1 && user.Property2=5 },
//...
}
foreach (var criterion in criteria){
var avg = data.Where(criterion.Value).Average(user => user.Testval);
var count = data.Where(criterion).Count();
Console.WriteLine($"{criterion.Key} average: {avg}, count: {count}");
}
You can also put the results in another dictionary, something like
var results = new Dictionary<string, Tuple<string, string>>();
foreach (var criterion in criteria){
var avg = data.Where(criterion.Value).Average(user => user.Testval);
var count = data.Where(criterion).Count();
results.Add(criterion.Key, Tuple.Create(avg, count));
}
and then make a better looking report, or you can even create a specific result class that will be easier to print after.
To get the rest (the count/average of the data that does not fit any predicate) you can loop through all the predicates, negating them;
var query = data;
foreach (var criterion in criteria.Values){
query = query.Where(user => !criterion(user));
}
var restAvg = query.Average(user => user.Testval);
var count = query.Count();
You can do it using select new to return new anonymously typed objects which contains your criteria.
public void Test()
{
var list = new List<User>();
list.Add(new User {UserId = 1, Property1 = 1, Property2 = 1, Property3 = 10, Testval = 35});
list.Add(new User {UserId = 1, Property1 = 2, Property2 = 2, Property3 = 3, Testval = 45});
list.Add(new User {UserId = 1, Property1 = 5, Property2 = 5, Property3 = 6, Testval = 55});
Func<User, bool> crit = u => u.Property1 == 1 & u.Property3==10;
var zz = list.Where(crit)
.GroupBy(t => new {ID = t.UserId})
.Select(w => new
{
average = w.Average(a => a.Testval),
count = w.Count(),
rest = list.Except(list.Where(crit)).Average(a => a.Testval)
}).Single();
}

How to rewrite these LINQ statements into one query

Is it possible to use one LINQ query to do the same?
var ints = new []{1,2,3,4,5};
var odd = from i in ints where i%2==1 select i;
var even = from i in ints where i%2==0 select i;
var q = from s in new[]{""}
select new {oddCount = odd.Count(), evenCount = even.Count()};
Console.Write(q);
Edit: Want to get this
Count() already allows you to specify a predicate. So you can combine the above in one linq like this:
var ints = new[] { 1, 2, 3, 4, 5 };
Console.Write($"Odd={ints.Count(i => i % 2 == 1)}, Even={ints.Count(i => i % 2 == 0)}");
Also note that it will be considerably faster than doing a Where() as counting is easier to perform than actually returning matching elements.
Edit
If all you want is a single linq query, you could do the following clever trick:
var ints = new[] { 1, 2, 3, 4, 5 };
var Odd = ints.Count(i => i % 2 == 1);
Console.Write($"Odd={Odd}, Even={ints.Length - Odd}");
Sounds like a perfect candidate for Aggregate:
var ints = new[] { 1, 2, 3, 4, 5 };
var info = ints.Aggregate(
new { oddCount = 0, evenCount = 0 }, (a, i) =>
new { oddCount = a.oddCount + (i & 1), evenCount = a.evenCount + ((i & 1) ^ 1) });
Console.WriteLine(info);
prints
{ oddCount = 3, evenCount = 2 }
You could do it one query like this:
var q = ints.Select(i => new { Number = i, Type = (i % 2 == 0) ? "Even" : "Odd" }).GroupBy(i => i.Type).Select(g => new { Type = g.Key, Count = g.Count() });
This would return a list though, with 'Type' and 'Count', as shown below.
If you're looking for a simple object as you currently have, you can use something simpler like this:
var q = new { OddCount = ints.Count(i => i % 2 != 0), EvenCount = ints.Count(i => i % 2 == 0) };
This would be a single object with "OddCount" and "EventCount" properties.
Here's another way that does only a single iteration over the original list.
var ints = new []{1,2,3,4,5};
string[] parities = { "even", "odd" };
var result = ints
.GroupBy(i => i % 2)
.Select(g => new { Name = parities[g.Key], Count = g.Count() });
You just move your queries directly into the select
var q = from s in new[] { "" }
select new {
oddCount = (from i in ints where i % 2 == 1 select i).Count(),
evenCount = (from i in ints where i % 2 == 0 select i).Count()};
int odd = 0;
int even = 0;
(from s in ints
let evens = s % 2 == 0 ? even++ : even
let odds = s % 2 != 0 ? odd++ : odd
select true).ToList();
With this you have the values loaded in even and odd.
This approach has the advantage it only iterates the array once

how to get an ordered list with default values using linq

I have an ICollection of records (userID,itemID,rating) and an IEnumerable items
for a specific userID and each itemID from a set of itemIDs, i need to produce a list of the users rating for the items or 0 if no such record exists. the list should be ordered by the items.
example:
records = [(1,1,2),(1,2,3),(2,3,1)]
items = [3,1]
userID = 1
result = [0,2]
my attempt:
dataset.Where((x) => (x.userID == uID) & items.Contains(x.iID)).Select((x) => x.rating);
it does the job but it doesn't return 0 as default value and it isnt ordered...
i'm new to C# and LINQ, a pointer in the correct direction will be very appreciated.
Thank you.
This does the job:
var records = new int[][] { new int[] { 1, 1, 2 }, new int[] { 1, 2, 3 }, new int[] { 2, 3, 1 } };
var items = new int[] { 3, 1 };
var userId = 1;
var result = items.Select(i =>
{
// When there's a match
if (records.Any(r => r[0] == userId && r[1] == i))
{
// Return all numbers
return records.Where(r => r[0] == userId && r[1] == i).Select(r => r[2]);
}
else
{
// Just return 0
return new int[] { 0 };
}
}).SelectMany(r => r); // flatten the int[][] to int[]
// output
result.ToList().ForEach(i => Console.Write("{0} ", i));
Console.ReadKey(true);
How about:
dataset.Where((x) => (x.userID == uID)).Select((x) => items.Contains(x.iID) ? x.rating : 0)
This does the job. But whether it's maintainable/readable solution is topic for another discussion:
// using your example as pseudo-code input
var records = [(1,1,2),(1,2,3),(2,3,1)];
var items = [3,1];
var userID = 1;
var output = items
.OrderByDescending(i => i)
.GroupJoin(records,
i => i,
r => r.ItemId,
(i, r) => new { ItemId = i, Records = r})
.Select(g => g.Records.FirstOrDefault(r => r.UserId == userId))
.Select(r => r == null ? 0 : r.Rating);
How this query works...
ordering is obvious
the ugly GroupJoin - it joins every element from items with all records that share same ItemId into annonymous type {ItemId, Records}
now we select first record for each entry that matches userId - if none is found, null will be returned (thanks to FirstOrDefault)
last thing we do is check whether we have value (we select Rating) or not - 0
How about this. your question sounds bit like an outer join from SQL, and you can do this with a GroupJoin, SelectMany:
var record1 = new Record() { userID = 1, itemID = 1, rating = 2 };
var record2 = new Record() { userID = 1, itemID = 2, rating = 3 };
var record3 = new Record() { userID = 2, itemID = 3, rating = 1 };
var records = new List<Record> { record1, record2, record3 };
int userID = 1;
var items = new List<int> { 3, 1 };
var results = items
.GroupJoin( records.Where(r => r.userID == userID), item => item, record => record.itemID, (item, record) => new { item, ratings = record.Select(r => r.rating) } )
.OrderBy( itemRating => itemRating.item)
.SelectMany( itemRating => itemRating.ratings.DefaultIfEmpty(), (itemRating, rating) => rating);
To explain what is going on
For each item GroupJoin gets the list of rating (or empty list if no rating) for the specified user
OrderBy is obvious
SelectMany flattens the ratings lists, providing a zero if the ratings list is empty (by DefaultIfEmpty)
Hope this makes sense.
Be aware, if there is more than one rating for an item by a user, they will all appear in the final list.

Combining 2 LINQ into one call

I'm using 2 similar LINQ queries to return a result, the only difference is the where clause (&& s.OptIn == "Yes"). Is there a way to execute this with only one query?
Instead of having a result of
A 2
B 3
and another result of
A 1
B 1
I want to have
A 2 1
B 3 1
Here's the LINQ:
var result = from s in pdc.ScanLogs
from e in pdc.Exhibits
from ce in pdc.ClientEvents
where s.ExhibitID == e.ExhibitID
&& e.ClientEventID == ce.ClientEventID
group 1 by new { ce.EventID } into d
select new {
EventID = d.Key.EventID,
Count = d.Count()
};
var result = from s in pdc.ScanLogs
from e in pdc.Exhibits
from ce in pdc.ClientEvents
where s.ExhibitID == e.ExhibitID
&& e.ClientEventID == ce.ClientEventID
&& s.OptIn == "Yes"
group 1 by new { ce.EventID } into d
select new {
EventID = d.Key.EventID,
Count = d.Count()
};
You can supply a predicate in the Count method. An example is below:
List<int> list = new List<int> { 1, 2, 3, 4, 5 };
var counts = new { CountAll = list.Count(), CountEven = list.Count(i => i % 2 == 0) };
Console.WriteLine(counts.CountEven);
A similar query written for Linq-To-Entities also worked and produced working SQL.
I haven't fully reconstructed your sample, but you should be able to rework it to something like this.
var result = from s in pdc.ScanLogs
from e in pdc.Exhibits
from ce in pdc.ClientEvents
where s.ExhibitID == e.ExhibitID
&& e.ClientEventID == ce.ClientEventID
group new { s, e, ce } by new { ce.EventID } into d
select new
{
EventID = d.Key.EventID,
Count = d.Count(),
CountOptIn = d.Count(item => item.s.OptIn == "Yes")
};
IQueryable<ScanLog> scanlogs = pdc.ScanLogs;
if (filter) scanlogs = scanlogs.Where(...);
var result = from s in scanlogs
...

Categories

Resources