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();
Related
I have a C#.NET application and want to do Group By on multiple conditions.
I have a list like this:
var testq = new List<TestQuestion>()
{
new TestQuestion
{
Id = 1,
QuestionId = 1,
SelectedAnswerId = null
},
new TestQuestion
{
Id = 2,
QuestionId = 2,
SelectedAnswerId = 1
},
new TestQuestion
{
Id =3,
QuestionId = 1,
SelectedAnswerId = 1
},
new TestQuestion
{
Id = 4,
QuestionId = 3,
SelectedAnswerId = 5
},
new TestQuestion
{
Id = 5,
QuestionId = 1,
SelectedAnswerId = 2
},
new TestQuestion
{
Id = 6,
QuestionId = 3,
SelectedAnswerId = 3
},
new TestQuestion
{
Id =7,
QuestionId = 4,
SelectedAnswerId = null
},
new TestQuestion
{
Id =8,
QuestionId = 5,
SelectedAnswerId = null
},
};
My code is :
var result = testq
.Where(p => p.SelectedAnswerId.HasValue)
.GroupBy(p => p.QuestionId)
.Select(p => p.FirstOrDefault())
.ToList();
now, result ID's is (2 ,3, 4)
but result is not true...
The result should be this :
ID's -> (2 ,3, 4, 7, 8)
I want to group by the result based on the QuestionID field and The first record that does not have a (SelectedAnswerId)field value is empty,
Also, records in which the question ID is only there once, regardless of the value of the field (SelectedAnswerId) in the output. that's mean, last two items in the list
please guide me...
Try this:
var result = testq
.Where(p => p.SelectedAnswerId.HasValue || testq.Count(x => x.QuestionId == p.QuestionId) == 1)
.GroupBy(p => p.QuestionId)
.Select(p => p.FirstOrDefault())
.Distinct()
.ToList();
C# Fiddle
You have to filter the SelectedAnswerId in Select not in the Where clause. Try the below,
var result = testq
.GroupBy(p => p.QuestionId)
.Select(p =>
{
var grouped = p.ToList(); //Get the groupBy list
TestQuestion testQuestion = grouped.FirstOrDefault(x => x.SelectedAnswerId.HasValue); //Get anything with AnswerId
return testQuestion ?? grouped.FirstOrDefault(); //If not not available with Answer then get the First or Default value
})
.ToList();
C# Fiddle with test data.
Although after a GroupBy the order of the elements in each group is fairly defined (see Enumerable.GroupBy, it seems that the element that you want is not the First one in each group.
You want the First element of each group that has a non-null SelectedAnswerId, or if there is no such one, you want the First element of each group that has a null SelectedAnswerId.
How about this:
var result = testQ.GroupBy(question => question.QuestionId);
// every group contains a sequence of questions with same questionId
// select the ones with a null SelectedAnswerId and the ones with a non-null value
.Select(group => new
{
NullSelectedAnswer = group
.Where(group.SelectedAnswerId == null)
.FirstOrDefault(),
NonNullselectedAnswer = group
.Where(group.SelectedAnswerId != null)
.FirstOrDefault(),
})
// if there is any NonNullSelectedAnswer, take that one, otherwise take the null one:
.Select(selectionResult => selectionResult.NonNullSelectedAnswer ??
selectionResult.NullSelectedAnswer);
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();
}
I want to group my data by Name and Amount, each group that is returned must have a unique ID which increments per group in the data.
This is the relevant bit of code:
return JsonConvert.SerializeObject(
data.GroupBy(x=> new {x.Name, x.Amount})
.Where(gp => gp.Count() > 1)
.Select(
gp =>
new Group
{
GroupData = gp.Select(el => new GroupItem
{
Name = el.Name,
Amount = el.Amount,
GroupId = <missing>
})
}));
I have found several questions for adding an incremental ID for each row within a group, which resets per group, however I want the group to be something like this:
Group = {"bob", 145.20, 1},
{"bob", 145.20, 1},
Group = {"steve", 120.00, 2},
{"steve", 120.00, 2}..... etc
The only examples I have found where to do this are in SQL, whereas I need this for LINQ in C#.
You can use Select overload that pass the index too:
return JsonConvert.SerializeObject(
data.GroupBy(x=> new {x.Name, x.Amount})
.Where(gp => gp.Count() > 1)
.Select((gp, idx) =>
new Group
{
GroupData = gp.Select(el => new GroupItem
{
Name = el.Name,
Amount = el.Amount,
GroupId = idx
})
}));
This is my code (copy and paste it into linqpad if you like)
var messageUsers = new [] {
new { MsgId = 2, UserId = 7 },
new { MsgId = 2, UserId = 8 },
new { MsgId = 3, UserId = 7 },
new { MsgId = 3, UserId = 8 },
new { MsgId = 1, UserId = 7 },
new { MsgId = 1, UserId = 8 },
new { MsgId = 1, UserId = 9 }};
messageUsers
.GroupBy (x => x.MsgId, x => x.UserId)
.Select (x => x.Select (y => y))
.Distinct()
.Dump();
The results I get back are {7,8}, {7,8}, {7,8,9}
What I want is {7,8}, {7,8,9}.
Basically I want to remove the duplicate lists.
I haven't tried this but I think I could probably achieve by creating a comparer and passing it into the Distinct method. However I would like to eventually use this in a Linq to Entities query without bringing thousands of rows back to the client so that isn't a good option.
For extra clarification...I need to return a List> where the contents of each inner list is distinct in comparison to any of the other inner list.
The problem is that .Distinct() determines what's distinct based on the GetHashCode() and Equals() implementation of the underlying objects. In this case, the underlying object is something that implements IEnumerable<>, but which uses the default object implementation for those methods--which is based purely on whether the objects occupy the same space in memory. So as far as it can tell, the sequences are not distinct, even though they have the same values in them.
How about this?
messageUsers
.GroupBy (x => x.MsgId, x => x.UserId)
.GroupBy(x => string.Join(",", x))
.Select(x => x.FirstOrDefault())
.Dump();
The idea is to group by a key that represents the combined value of the elements in your list. You could also pass a custom IEqualityComparer<> to the Distinct method in your original code, but that seems like a fair bit of effort for something so trivial.
It's worth noting that this won't work very well if you're using LINQ to Entities or something like that.
Update
To make it a List<List<int>>, you'll need a few .ToList()s thrown in there:
messageUsers
.GroupBy (x => x.MsgId, x => x.UserId)
.GroupBy(x => string.Join(",", x))
.Select(x => x.FirstOrDefault().ToList())
.ToList()
.Dump();
But I'm frankly not sure why that matters to you.
Here is an alternative answer:
messageUsers
.GroupBy (x => x.MsgId, y=>y.UserId)
.Select (x => new HashSet<int>(x))
.Distinct(HashSet<int>.CreateSetComparer())
.Dump();
Consider the following input:
var messageUsers = new [] {
new { MsgId = 2, UserId = 7 },
new { MsgId = 2, UserId = 8 },
new { MsgId = 3, UserId = 8 },
new { MsgId = 3, UserId = 7 },
new { MsgId = 1, UserId = 7 },
new { MsgId = 1, UserId = 8 },
new { MsgId = 1, UserId = 9 }};
What result do you want?
{7,8}, {7,8,9} or {7,8}, {8,7}, {7,8,9}.
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.