Linq query to merge distinct items in a list [duplicate] - c#

This question already has an answer here:
Grouping a generic list via LINQ in VB.NET
(1 answer)
Closed 7 years ago.
I have the following model:
public class Result
{
public int Id { get; set; }
public string Company { get; set; }
}
And I have a List with data similar to the following:
Id Company
=================
21 Microsoft
22 Apple
22 IBM
23 Microsoft
How can I use Linq to give me the distinct ID's, concatenating the Company column with a delimiter?
My output should be:
Id Company
=================
21 Microsoft
22 Apple, IBM
23 Microsoft

You can use GroupBy and String.Join:
IEnumerable<Result> query = results.GroupBy(r => r.Id)
.Select(g => new Result
{
Id = g.Key,
Company = String.Join(", ", g.Select(r => r.Company))
});

A slightly different take on Tim's excellent answer if there are duplicate records in your source and you don't want Company names repeated in the same Field:
var data = new List<Result>
{
new Result {Id = 21, Company = "Microsoft"},
new Result {Id = 22, Company = "Apple"},
new Result {Id = 22, Company = "IBM"},
new Result {Id = 23, Company = "Microsoft"},
new Result {Id = 23, Company = "Microsoft"}
};
var x = data.GroupBy(d => d.Id)
.Select(d => new Result { Id = d.Key,
Company =
string.Join(",", d.Select(s => s.Company).Distinct())});

var groupesList = result.GroupBy(x => x.Id,
(key, val) => new { Key = key, Value = string.Join(",", val.Select(r => r.Company)} ).ToList();
then you can call Key(unuque) or Value by key for all inform for example all ID

Simply use GroupBy and String.Join methods:-
List<Result> result = Results.GroupBy(x => x.id)
.Select(x => new Result
{
Id = x.Key,
Company = String.Join(",",x.Select(z => z.Company)
}).ToList();

Related

How to group and merge/flatten a list of anonymous objects in LINQ

I have a list of anonymous objects generated by a LINQ query that I do not have access to modify.
The objects have the following properties:
OrderId, RepId, FirstName, LastName, Address
Each "Rep" often places multiple orders, so there are a lot of rows where the only difference is the OrderId. There is a requirement that if the same Rep has placed multiple orders, to batch these together in groups of 6 with a new structure:
OrderId1, OrderId2, ..., OrderId6, RepId, FirstName, LastName, Address
But if the rep has placed say 8 orders, there would be a batch of 6 and a batch of 2. So the new objects don't always have the same number of properties.
I've started by grouping the initial result set by RepId, but I have no clue where to go next.
Is this possible using LINQ?
As your output have anonymous objects with different schema, that make the thing a little more complicate.
Ideally you should design your entity class to use list for orders instead of property like "OrderId1", "OrderId2"... That is not extensible and error prone. But for that specific question, we can combine LINQ and ExpandoObject to achieve this.
orders.GroupBy(order => order.RepId)
.SelectMany(orderGroup => orderGroup.Select((order, i) => new {
Order = order,
ReqId = orderGroup.Key,
SubGroupId = i / 6
}))
.GroupBy(h => new {
ReqId = h.ReqId,
SubGroupId = h.SubGroupId,
FirstName = h.Order.FirstName,
LastName = h.Order.LastName,
Address = h.Order.Address
})
.Select(orderWithRichInfo => {
dynamic dynamicObject = new ExpandoObject();
int i = 1;
foreach(var o in orderWithRichInfo)
{
((IDictionary<string, object>)dynamicObject).Add("OrderId" + i, o.Order.OrderId);
i++;
}
((IDictionary<string, object>)dynamicObject).Add("FirstName", orderWithRichInfo.Key.FirstName);
((IDictionary<string, object>)dynamicObject).Add("LastName", orderWithRichInfo.Key.LastName);
((IDictionary<string, object>)dynamicObject).Add("Address", orderWithRichInfo.Key.Address);
return dynamicObject;
});
Hope it helps.
First option.
If you want to get 6 OrderId-s as a list, you can create
class OrderBundle
{
public int RepId { get; set; }
public List<int> OrderIds { get; set; }
}
Group your items:
var orderBundels = orderList
.GroupBy(m => m.RepId)
.Select(g => new OrderBundle
{
RepId = g.Key,
OrderIds = g.Select(m => m.OrderId).ToList()
});
And then split them into groups:
List<OrderBundle> dividedOrderBundels = new List<OrderBundle>();
foreach (OrderBundle orderBundle in orderBundels)
{
int bundelCount = (int)Math.Ceiling(orderBundle.OrderIds.Count() / 6.0);
for (int i = 0; i < bundelCount; i++)
{
OrderBundle divided = new OrderBundle
{
RepId = orderBundle.RepId,
OrderIds = orderBundle.OrderIds.Skip(i * 6).Take(6).ToList()
};
dividedOrderBundels.Add(divided);
}
}
Second option:
You can achieve the same result without creating model like below:
var result = orderList
.GroupBy(m => m.RepId)
.SelectMany(g => g.Select((m, i) => new
{
RepId = g.Key,
FirstName = m.FirstName,
LastName = m.LastName,
Address = m.Address,
OrderId = m.OrderId,
BunleIndex = i / 6
}))
.GroupBy(m => m.BunleIndex)
.Select(g => new
{
RepId = g.Select(m => m.RepId).First(),
FirstName = g.Select(m => m.FirstName).First(),
LastName = g.Select(m => m.LastName).First(),
Address = g.Select(m => m.Address).First(),
OrderIds = g.Select(m => m.OrderId).ToList()
})
.ToList()

Ordering items by grouped quantity

I want to order some posts by how many times a user has posted a post.
I have the following:
IList<User> orderTopContributors =
this.GetPosts()
.GroupBy(x => x.Author.Id)
.Select(x => new
{
AuthorCount = x.Count()
})
.OrderByDescending( x => x.AuthorCount )
.ToList();
Where am i going wrong? There is an error with the casting:
Severity Code Description Project File Line Error CS0266 Cannot
implicitly convert type 'System.Collections.Generic.List< Items>>' to
'System.Collections.Generic.IList'. An explicit conversion
exists (are you missing a cast?)
tl;dr: Use the SelectMany method
You have few mistakes:
First of all (you fixed this one in an edit), you should use OrderByDescending in order to get the order from the biggest to the smallest.
Next (you fixed this one in an edit), you are expecting to receive IList<User>, either change it to IEnumrable<User> or add .ToList() in the end of your Linq.
Lastly, if you want to flatten your groups to a single list use SelectMany and select your flattened lists:
Example code:
IList<User> orderTopContributors = GetPosts()
.GroupBy(x => x.Id)
.Select(x => new
{
AuthorCount = x.Count(),
Posts = x
})
.OrderByDescending(x => x.AuthorCount)
.SelectMany(x => x.Posts)
.ToList();
When you are using .GroupBy you turn your IEnumerable<User> to IEnumerable<IEnumerable<User>> since there are few groups (many times many), by using the SelectMany method you state which IEnumerable<T> you want to take from each group and aggregate it to the final result:
Example pseudo:
var Users = new List<User>
{
{ UserID = 576, PostId = 7 },
{ UserID = 576, PostId = 4 },
{ UserID = 4, PostId = 2 },
{ UserID = 2, PostId = 5 },
{ UserID = 2, PostId = 1 },
{ UserID = 576, PostId = 9 }
}
var Ordered = Users
.GroupBy(x => x.UserID)
.Select(x => new
{
AuthorCount = x.Count(),
Posts = x
})
.OrderByDescending(x => x.AuthorCount)
.SelectMany(x => x.Posts)
.ToList();
Ordered is now:
List<User>
{
{ UserID = 576, PostId = 7 },
{ UserID = 576, PostId = 4 },
{ UserID = 576, PostId = 9 },
{ UserID = 2, PostId = 5 },
{ UserID = 2, PostId = 1 },
{ UserID = 4, PostId = 2 }
}

returning specified type for a linq query with anonymous type

i have this query:
DbQuery<Logs> logs = context.GetQuery<Logs>();
var MessageLogs =
logs.Where(
s =>
s.DATE == date.Date
.GroupBy(s => new {s.DATE, s.ID})
.Select(
g => new {Date = g.Key.DATE, SID = g.Key.ID, Count = g.Count()})
.GroupBy(x => x.SID, x => new {x.Date, x.Count});
and I have these two classess:
public class Data
{
public Values[] Val { get; set; }
public string Key { get; set; }
}
and this:
public class Values
{
public string type1 { get; set; }
public string type2 { get; set; }
}
all i want to do is using that query to return type of Data.
key in class Data is SID and list of values should be counts and date as type1 and type2.
i know i can do this with anonymous type but i dont know how, i tried many ways but all of them was wrong.
EDIT:
i have this query
logs.Where(
s =>
s.DATE == date.Date
.GroupBy(s => new {s.DATE, s.ID})
.Select(
g => new {Date = g.Key.DATE, SID = g.Key.ID, Count = g.Count()})
this query returns something like this:
key date count
----------------------------
1021 2012 1
1021 2013 5
1022 2001 10
1023 2002 14
what i want is base on each id a list of values
in fact return type should be type of Data which this ids are key fore example
key=1021 and Values[] should be type1=2012, type2=1 and type1=2013, type2=5
Given that your current query returns elements with key/date/count, it sounds like you probably just want:
var result = query.GroupBy(
x => x.Key,
(key, rows) => new Data {
Key = key,
Val = rows.Select(r => new Values { type1 = r.Date, type2 = r.Count })
.ToArray();
});
Basically this overload takes:
A source
A key selector
A transformation from a key and matching rows to a result element (an instance of Data in your case)

join multiple IEnumerable<> using Linq

I have following class which is populated with the data
public class cntrydata
{
public string countryid { get; set; }
public string countryname { get; set; }
public IEnumerable<Data> data { get; set; }
}
public class Data
{
public int year { get; set; }
public float value { get; set; }
}
I have an IEnumerable result which has data like this:
IEnumerable<cntryData> result
USA
United States
2000 12
2001 22
2004 32
CAN
Canada
2001 29
2003 22
2004 24
I want to evaluate "result" object using LINQ to get following result:
2000 USA 12 CAN null
2001 USA 22 CAN 29
2003 USA null CAN 22
2004 USA 32 CAN 24
Also if result has more countries (say China with 1995 value 12) then result should look like this:
1995 USA null CAN null CHN 12
2000 USA 12 CAN null CHN null
2001 USA 22 CAN 29 CHN null
2003 USA null CAN 22 CHN null
2004 USA 32 CAN 24 CHN null
Can this be done using LINQ? Thank you.
I found it surprisingly hard to come up with a clean answer on this one, and I am still not really satisfied, so feedback is welcome:
var countries = result.Select(x => x.countryid).Distinct();
var years = result.SelectMany(x => x.data).Select(x => x.year).Distinct();
var data = result.SelectMany(x => x.data
.Select(y => new { Country = x.countryid,
Data = y }))
.ToDictionary(x => Tuple.Create(x.Country, x.Data.year),
x => x.Data.value);
var pivot = (from c in countries
from y in years
select new { Country = c, Year = y, Value = GetValue(c, y, data) })
.GroupBy(x => x.Year)
.OrderBy(x => x.Key);
public float? GetValue(string country, int year,
IDictionary<Tuple<string, int>, float> data)
{
float result;
if(!data.TryGetValue(Tuple.Create(country, year), out result))
return null;
return result;
}
pivot will contain one item per year. Each of these items will contain one item per country.
If you want to format each line as a string, you can do something like this:
pivot.Select(g => string.Format("{0} {1}", g.Key, string.Join("\t", g.Select(x => string.Format("{0} {1}", x.Country, x.Value)))));
Update
Here is now you use the code below to make a data table:
var newresult = result.SelectMany(cntry => cntry.data.Select(d => new { id = cntry.countryid, name = cntry.countryname, year = d.year, value = d.value }))
.GroupBy(f => f.year)
.Select(g => new { year = g.Key, placeList = g.Select(p => new { p.id, p.value })});
DataTable table = new DataTable();
table.Columns.Add("Year");
foreach(string name in result.Select(x => x.countryid).Distinct())
table.Columns.Add(name);
foreach(var item in newresult)
{
DataRow nr = table.NewRow();
nr["Year"] = item.year;
foreach(var l in item.placeList)
nr[l.id] = l.value;
table.Rows.Add(nr);
}
table.Dump();
And how that looks:
This is what linq can do, you could transform this to a data table easy enough, a list by year of locations and their values.
Flatten the input and then group by. Select what you want. Like this
var newresult = result.SelectMany(cntry => cntry.data.Select(d => new { id = cntry.countryid, name = cntry.countryname, year = d.year, value = d.value }))
.GroupBy(f => f.year)
.Select(g => new { year = g.Key, placeList = g.Select(p => new { p.id, p.value })});
Here is what the dump looks like in LinqPad.
Here is the full test code
void Main()
{
List<cntrydata> result = new List<cntrydata>()
{
new cntrydata() { countryid = "USA", countryname = "United States",
data = new List<Data>() {
new Data() { year = 2000, value = 12 },
new Data() { year = 2001, value = 22 },
new Data() { year = 2004, value = 32 }
}
},
new cntrydata() { countryid = "CAN", countryname = "Canada",
data = new List<Data>() {
new Data() { year = 2001, value = 29 },
new Data() { year = 2003, value = 22 },
new Data() { year = 2004, value = 24 }
}
}
};
var newresult = result.SelectMany(cntry => cntry.data.Select(d => new { id = cntry.countryid, name = cntry.countryname, year = d.year, value = d.value }))
.GroupBy(f => f.year)
.Select(g => new { year = g.Key, placeList = g.Select(p => new { p.id, p.value })});
newresult.Dump();
}
public class cntrydata
{
public string countryid { get; set; }
public string countryname { get; set; }
public IEnumerable<Data> data { get; set; }
}
public class Data
{
public int year { get; set; }
public float value { get; set; }
}
//group things up as required
var mainLookup = result
.SelectMany(
country => country.data,
(country, data) => new {
Name = country.Name,
Year = data.Year,
Val = data.Val
}
)
.ToLookup(row => new {Name= row.Name, Year = row.Year}
List<string> names = mainLookup
.Select(g => g.Key.Name)
.Distinct()
.ToList();
List<string> years = mainLookup
.Select(g => g.Key.Year)
.Distinct()
.ToList();
// generate all possible pairs of names and years
var yearGroups = names
.SelectMany(years, (name, year) => new {
Name = name,
Year = year
})
.GroupBy(x => x.Year)
.OrderBy(g => g.Key);
IEnumerable<string> results =
(
from yearGroup in yearGroups
let year = yearGroup.Key
//establish consistent order of processing
let pairKeys = yearGroup.OrderBy(x => x.Name)
let data = string.Join("\t",
from pairKey in pairKeys
//probe original groups with each possible pair
let val = mainLookup[pairKey].FirstOrDefault()
let valString = val == null ? "null" : val.ToString()
select pairKey.Name + " " + valString
)
select year.ToString() + "\t" + data; //resultItem

Use List.ToLookup()

I have a List like the following:
var products = new List<Product>
{
new Product { Id = 1, Category = "Electronics", Value = 15.0 },
new Product { Id = 2, Category = "Groceries", Value = 40.0 },
new Product { Id = 3, Category = "Garden", Value = 210.3 },
new Product { Id = 4, Category = "Pets", Value = 2.1 },
new Product { Id = 5, Category = "Electronics", Value = 19.95 },
new Product { Id = 6, Category = "Pets", Value = 5.50 },
new Product { Id = 7, Category = "Electronics", Value = 250.0 },
};
I want to group by category and get the sum of 'Values' belonging to that category..
Example: Electronics: 284.95
While I can do this in some other way, I want to learn usage of Look-Up.
Is it possible to get these 2 values (category and Value) in a Look-Up? If yes, How can I do that?
When you retrieve by key from a Lookup, it behaves just like a grouping, so you can do things like this:
var productLookup = products.ToLookup(p => p.Category);
var electronicsTotalValue = productLookup["Electronics"].Sum(p => p.Value);
var petsTotalValue = productLookup["Pets"].Sum(p => p.Value);
//etc
var totalValue = products.Sum(p => p.Value);
// I wouldn't use the Lookup here, the line above makes more sense and would execute faster
var alsoTotalValue = productLookup.Sum(grp => grp.Sum(p => p.Value));
You probably want to use ToDictionary() instead of ToLookup
var dict = products
.GroupBy(p => p.Category)
.ToDictionary(grp => grp.Key, grp => grp.Sum(p => p.Value));
foreach(var item in dict)
{
Console.WriteLine("{0} = {1}", item.Key, item.Value);
}
You don't need a Lookup. You can do this with just a query:
var results =
from p in products
group p by p.Category into g
select new
{
Category = g.Key,
TotalValue = g.Sum(x => x.Value)
};
var rez = products.ToLookup(k => k.Category, v => v.Value).Select(k=>new KeyValuePair<string, double>(k.Key, k.Sum()));

Categories

Resources