LINQ - Group By child property - linq

I have a list of users, each user has string array property called Tags. I am trying to get a unique list of tags and a total count, any idea what I a missing here? I am using LinqPad to write my test query, please see the example code:
void Main()
{
List<User> users = new List<User>(){
new User {Id = 1, Tags = new string[]{"tag1", "tag2"}},
new User {Id = 2, Tags = new string[]{"tag3", "tag7"}},
new User {Id = 3, Tags = new string[]{"tag7", "tag8"}},
new User {Id = 4, Tags = new string[]{"tag1", "tag4"}},
new User {Id = 5 },
};
var uniqueTags = users.Where(m=>m.Tags != null).GroupBy(m=>m.Tags).Select(m=> new{TagName = m.Key, Count = m.Count()});
uniqueTags.Dump();
// RESULT should BE:
// tag1 - Count(2)
// tag2 - Count(1)
// tag3 - Count(1)
// tag4 - Count(1)
// tag7 - Count(2)
// tag8 - Count(1)
}
public class User{
public int Id {get;set;}
public string[] Tags {get;set;}
}

You can flatten to IEnumerable<string> before grouping:
var uniqueTags = users.SelectMany(u => u.Tags ?? new string[0])
.GroupBy(t => t)
.Select(g => new { TagName = g.Key, Count = g.Count() } );
LINQPad C# Expression version:
new[] {
new { Id = 1, Tags = new[] { "tag1", "tag2" } },
new { Id = 2, Tags = new[] { "tag3", "tag7" } },
new { Id = 3, Tags = new[] { "tag7", "tag8" } },
new { Id = 4, Tags = new[] { "tag1", "tag4" } },
new { Id = 5, Tags = (string[])null }
}
.SelectMany(u => u.Tags ?? Enumerable.Empty<string>())
.GroupBy(t => t)
.Select(g => new { TagName = g.Key, Count = g.Count() } )

Related

Updating Collection from other collection for matching items

I have two collections as mentioned below. I have update two properties of "trade" from the other collection "refData" if those values exists in "refData".
Model:
class Trade
{
public int Id { get; set; }
public string PayIndex { get; set; }
public string RecIndex { get; set; }
public string PayCurrency { get; set; }
public string RecCurrency { get; set; }
}
class RefData
{
public string IndexLabel { get; set; }
public string Symbol { get; set; }
}
Sample Date:
var refData = new List<RefData>
{
new RefData { IndexLabel = "A1", Symbol="ABC1"},
new RefData { IndexLabel = "A2", Symbol="ABC2"},
new RefData { IndexLabel = "B1", Symbol="BCD1"},
new RefData { IndexLabel = "B2", Symbol="BCD2"},
};
var trades = new List<Trade>
{
new Trade { Id = 1, PayIndex = "A1", RecIndex = "B1"},
new Trade { Id = 2, PayIndex = "A2", RecIndex = ""},
new Trade { Id = 3, PayIndex = "", RecIndex = "B2"},
new Trade { Id = 4, PayIndex = "A3", RecIndex = "B3"}
};
I want to update PayCurrency and RecCurrency of "trades" with Symbol property of "refData" if trade's PayIndex and RecCurrency exists in "refData".
Output:
var Output = new List<Trade>
{
new Trade { Id = 1, PayIndex = "A1", RecIndex = "B1", PayCurrency = "ABC1", RecCurrency="BCD1"},
new Trade { Id = 2, PayIndex = "A2", RecIndex = "", PayCurrency = "ABC2", RecCurrency=""},
new Trade { Id = 3, PayIndex = "", RecIndex = "B2", PayCurrency = "", RecCurrency="BCD2"},
new Trade { Id = 4, PayIndex = "A3", RecIndex = "B3", PayCurrency = "", RecCurrency=""}
};
For c#6 and above you can do like below
var result = trades.Select(t => new Trade() {
Id= t.Id,
PayIndex = t.PayIndex,
RecIndex = t.RecIndex,
PayCurrency = refData.SingleOrDefault(r => r.IndexLabel.ToLower().Equals(t.PayIndex.ToLower()))?.Symbol ?? "",
RecCurrency = refData.SingleOrDefault(r => r.IndexLabel.ToLower().Equals(t.RecIndex.ToLower()))?.Symbol ?? ""
}).ToList();
For Older versions
var result = trades.Select(t => new Trade() {
Id= t.Id,
PayIndex = t.PayIndex,
RecIndex = t.RecIndex,
PayCurrency = refData.SingleOrDefault(r => r.IndexLabel.ToLower().Equals(t.PayIndex.ToLower())) != null ? refData.SingleOrDefault(r => r.IndexLabel.ToLower().Equals(t.PayIndex.ToLower())).Symbol : "",
RecCurrency = refData.SingleOrDefault(r => r.IndexLabel.ToLower().Equals(t.RecIndex.ToLower())) != null ? refData.SingleOrDefault(r => r.IndexLabel.ToLower().Equals(t.RecIndex.ToLower())).Symbol : ""
}).ToList();
Here is working example
Update using #SAJ answer
var output = (from r in trades
join p in refData on r.PayIndex equals p.IndexLabel
into g1
from s in g1.DefaultIfEmpty()
join t in refData on r.RecIndex equals t.IndexLabel into g2
from a in g2.DefaultIfEmpty()
select Trade { Id=r.Id,PayIndex=r.PayIndex,RecIndex=r.RecIndex, RecCurrency = a != null ? a.Symbol : "", PayCurrency = s != null ? s.Symbol : ""}).ToList();
You can try this
var output = (from r in trades
join p in refData on r.PayIndex equals p.IndexLabel
into g1
from s in g1.DefaultIfEmpty()
join t in refData on r.RecIndex equals t.IndexLabel into g2
from a in g2.DefaultIfEmpty()
select new { r, RecSymbol = a?.Symbol, PaySymbol = s?.Symbol }).ToList();
output.ForEach(o =>
{
o.r.PayCurrency = o.PaySymbol;
o.r.RecCurrency = o.RecSymbol;
});

Getting all elements concatenate in a string with LINQ

I have the following classes:
public class People
{
public int ID { get; set; }
public List<Right> Rights { get; set; }
}
public class Right
{
public int ID { get; set; }
public int Val { get; set; }
}
With the following values:
People: ID: 1
Right:
ID: 1
Val: 5
Right:
ID: 2
Val: 4
I would like to retrieve all Val rights (in a single string) for the people.
So for people with ID 1 : getting "5,4".
List<People> list = ...;
string result = string.Join(",", (people.First(p => p.ID == 1)).Rights.Select(r => r.Val));
Example :
List<People> people = new List<People>()
{
new People()
{
ID = 1, Rights = new List<Right>()
{
new Right() { ID = 1, Val = 5 },
new Right() { ID = 2, Val = 10 },
}
},
new People()
{
ID = 2, Rights = new List<Right>()
{
new Right() { ID = 1, Val = 6 },
new Right() { ID = 2, Val = 11 },
}
}
};
string result = string.Join(",", (people.First(p => p.ID == 1)).Rights.Select(r => r.Val));
Console.WriteLine(result);
Output: 5, 10
You can use SelectMany to flatten a structure, so something like:
var vals = people.Where(p => p.ID == 1)
.SelectMany(p => p.Rights.Select(r => Val));
var str = String.Join(",", vals.Select(v => v.ToString());

Combine properties values from a custom List with LINQ

Lets assume I have a list of Persons, in the List I have the following objects:
Person = { ID= 1, State="CA"}
Person = { ID= 2, State="PA"}
Person = { ID= 1, State="NY"}
Person = { ID= 1, State="OH"}
Person = { ID= 3, State="FL"}
Person = { ID= 2, State="KC"}
How do I get a new List Where I can have new Persons by ID only once and if repeated get the State value and put it in the new Person object separated by comma. For example the new List will be
Person = { ID= 1, State="CA,NY,OH"}
Person = { ID= 2, State="PA,KC"}
Person = { ID= 3, State="FL"}
Is there a way to achieve this?
Use Enumerable.GroupBy and String.Join:
var result = persons.GroupBy(p => p.ID)
.Select(g => new Person{
ID = g.Key,
State = string.Join(",", g.Select(p => p.State))
}).ToList();
here is the extension method version
var persons = Person.GroupBy(p => p.ID).Select(p => new Person() { ID = p.Key, State = String.Join(",", p.Select(p2 => p2.State).ToArray()) }).ToList();

LINQ RowNumber, Aggregate (Sum) and GroupBy

I have an SQL code like;
Select GroupName, sum(LineAmount) as Total, WeekNumber,
ROW_NUMBER() over (partition by WeekNumber order by sum(LineAmount) desc) as RowNum
from
Invoices
where
month(InvoiceDate)=month(getdate())
group by
GroupName,WeekNumber
I would like to convert this to LINQ, but no luck. I am using LINQ to Object. Any help would be appreciated.
TIA
EDIT : Here is some sample data, and the expected result.
public class Invoice
{
public string GroupName { get; set; }
public int LineAmount { get; set; }
public int WeekNum { get; set; }
}
List<Invoice> theData = new List<Invoice>();
theData.Add(new Invoice { GroupName = "A", LineAmount = 1, WeekNum = 1});
theData.Add(new Invoice { GroupName = "A", LineAmount = 2, WeekNum = 1 });
theData.Add(new Invoice { GroupName = "A", LineAmount = 3, WeekNum = 1 });
theData.Add(new Invoice { GroupName = "A", LineAmount = 2, WeekNum = 2 });
theData.Add(new Invoice { GroupName = "A", LineAmount = 3, WeekNum = 2 });
theData.Add(new Invoice { GroupName = "A", LineAmount = 4, WeekNum = 2 });
theData.Add(new Invoice { GroupName = "B", LineAmount = 4, WeekNum = 1 });
theData.Add(new Invoice { GroupName = "B", LineAmount = 3, WeekNum = 1 });
theData.Add(new Invoice { GroupName = "B", LineAmount = 7, WeekNum = 2 });
theData.Add(new Invoice { GroupName = "B", LineAmount = 6, WeekNum = 2 });
theData.Add(new Invoice { GroupName = "B", LineAmount = 5, WeekNum = 2 });
I have removed "where" from my first query as its not a problem at the moment.
theData
.GroupBy(g => new {g.GroupName, g.WeekNum}, (key, gg) => new {key.GroupName, key.WeekNum, Total = gg.Sum(g => g.LineAmount)})
.GroupBy(g => g.WeekNum, (weekNum, gg) => gg.OrderByDescending(g => g.Total).Select((g,i) => new {g.GroupName, g.Total, g.WeekNum, RowNum = i}))
.SelectMany(g => g)
You have not specified the language you need it in. Here is the code in C#
int index = 0;
var filteredInvoices = (from i in invoices
where i.InvoiceDate.Month == DateTime.Now().Month
group i by new { i.GroupName, i.WeekNumber }
into ig
select new {i.GroupName, Total = ig.Sum(i => i.LineAmount), i.WeekNumber, RowNum = ++index}).OrderByDescending(n => n.Total);
filteredInvoices should have the result that you want. Also I am assuming that the i.InvoiceDate is of type DateTime.
Serg Rogovtsev answer gives me expected result. And the below code is what I have done. Don't know which performs better, but results are same.
(theData.GroupBy(f => new { f.GroupName, f.WeekNum})
.Select(r => new {r.Key.WeekNum, r.Key.GroupName, Total = r.Sum(f => f.LineAmount)}))
.GroupBy(r => new {r.WeekNum}).SelectMany(
g =>
g.OrderByDescending(f => f.Total).Select(
(f, index) => new { f.GroupName, f.Total, f.WeekNum, Ix = index + 1 }))

Using the ALL operator in linq to filter child items of EntitySet

I have a two objects as follows:
public class Item
{
public int ItemId {get;set;}
public string ItemName {get;set;}
public List<Tag> ItemTags {get;set;}
public DateTime DateCreated {get;set;}
}
public class Tag
{
public int TagId {get;set;}
public string TagName {get;set;}
}
These are LINQ-to-SQL objects, so the ItemTags will be an EntitySet.
I am trying to perform a search query where a user can provide a comma delimited list of tags as a search filter.
How do I filter my list of items to those which contains all of the tags in the comma delimited list.
EDIT2
e.g.
Item1 has tags of Apple, Banana, Orange
Item2 has tags of Banana, Orange
Item3 has tags of Pineapple, Orange
If the tag filter is "Banana, Orange" I need the results to be Item1 and Item2.
/EDIT2
This is what I have tried thus far:
string tags = "Manchester United,European Cup,2008";
List<string> tagsList = tags.Trim().ToLower()
.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
.Distinct(StringComparer.CurrentCultureIgnoreCase)
.ToList();
List<Item> itemList = ItemRepository.FetchAll();
var query = itemList
.OrderByDescending(p => p.DateCreated)
.ToList();
if (tagsList.Count() > 0)
{
query = query
.Where(p => p.ItemTags
.Select(q => q.TagName.ToLower())
.All(r => tagsList.Contains(r)))
.ToList();
}
However, this doesn't seem to work. Any ideas on what I am doing wrong please?
EDIT1: tags are trimmed and are 'lowercased'.
That because you're puting the tags from the items to lowercase, but not the searched tags.
With this modification it should work:
List<string> tagsList = tags
.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
.Select(s => s.ToLower())
.Distinct()
.ToList();
EDIT: OK, I see what the problem is: you're doing it backwards. You're searching for items that have only the tags that you're looking for.
Try that instead:
query =
(from item in query
let itemTags = p.ItemTags.Select(it => it.TagName.ToLower())
where tags.All(t => itemTags.Contains(t))
select item).ToList();
UPDATE: here's a version with the lambda syntax. It's pretty ugly because of the temporary anonymous type, but that's how the let clause translates to lambda...
query =
query.Select(item => new { item, itemTags = item.ItemTags.Select(it => it.TagName.ToLower()) })
.Where(x => tagsList.All(t => x.itemTags.Contains(t)))
.Select(x => x.item)
.ToList();
I think you need to do something like this:
var query = itemList.OrderByDescending(p => p.DateCreated).ToList();
var results = query.Where(i => i.ItemTags
.All(it => tagsList.Contains(it.TagName.ToLower())));
Then results should then be a list of matching items.
PS. Your code shows you fetching itemList as a List from your repository and then sorting by date created. This means the sorting isn't being done in the database. Once you turn something into a List you give up the benefits of deferred execution as you will bring back the entire collection into memory.
EDIT: Here's the test code to prove it works in Linq to Objects:
public class Item
{
public int ItemId { get; set; }
public string ItemName { get; set; }
public List<Tag> ItemTags { get; set; }
public DateTime DateCreated { get; set; }
}
public class Tag
{
public int TagId { get; set; }
public string TagName { get; set; }
}
class Program
{
static void Main(string[] args)
{
RunTags();
}
private static void RunTags()
{
Item i1 = new Item()
{
ItemId = 1,
ItemName = "Item1",
ItemTags = new List<Tag>() { new Tag { TagId = 1, TagName = "2008" }, new Tag { TagId = 2, TagName = "Donkey" } }
};
Item i2 = new Item()
{
ItemId = 2,
ItemName = "Item2",
ItemTags = new List<Tag>() { new Tag { TagId = 4, TagName = "Cat" }, new Tag { TagId = 2, TagName = "Donkey" }, new Tag { TagId = 3, TagName = "Seattle" } }
};
Item i3 = new Item()
{
ItemId = 3,
ItemName = "Item3",
ItemTags = new List<Tag>() { new Tag { TagId = 523, TagName = "Manchester united" }, new Tag { TagId = 10, TagName = "European Cup" }, new Tag { TagId = 1, TagName = "2008" } }
};
Item i4 = new Item()
{
ItemId = 4,
ItemName = "Item4",
ItemTags = new List<Tag>() { new Tag { TagId = 05, TagName = "Banana" }, new Tag { TagId = 140, TagName = "Foo" }, new Tag { TagId = 4, TagName = "Cat" } }
};
Item i5 = new Item()
{
ItemId = 5,
ItemName = "Item5",
ItemTags = new List<Tag>() { new Tag { TagId = 05, TagName = "Banana" }, new Tag { TagId = 140, TagName = "Foo" } }
};
List<Item> itemList = new List<Item>() { i1, i2, i3, i4, i5 };
string tags = "Manchester United,European Cup,2008";
List<string> tagsList = tags.Trim().ToLower()
.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
.Distinct(StringComparer.CurrentCultureIgnoreCase)
.ToList();
var query = itemList
.OrderByDescending(p => p.DateCreated).ToList();
var results = query.Where(i => i.ItemTags.All(it => tagsList.Contains(it.TagName.ToLower())));
foreach (var item in results)
{
Console.WriteLine(item.ItemName); // Should return "Item3"
}
Console.ReadLine();
}
If you want to match any of the tags in the Item's ItemTag list then just change All to Any i.e.
var results = query.Where(i => i.ItemTags.Any(it => tagsList.Contains(it.TagName.ToLower())));

Resources