How to combine different values of IEnumerable? [duplicate] - c#

This question already has answers here:
C# Linq Grouping
(2 answers)
Closed 1 year ago.
Struggling with the concept of this: I have a method that returns an IEnumerable like so:
public IEnumerable<IFruit> FruitStall(IEnumerable<IFruit> fruits)...
and each IFruit is as follows
public decimal Amount { get; }
public string Code { get; }
and my FruitsStall method returns one IFruit per currency with the sum of all money of the same code.
e.g
{APL10, APL20, APL50} => {APLE80}
or
{APL10, APL20, ORG50} => {APL30, ORG50}
Can anyone point me in the right direction of this? Not too sure how to go about this.
I was looping through the IEnumerable with a
foreach (var item in fruits)
{
fruits.Code
}
but unsure where to go there on

When you said:
{APL10, APL20, APL50} => {APLE80}
or
{APL10, APL20, ORG50} => {APL30, ORG50}
If you meant
I have this: {new Fruit("APL",10), new Fruit("APL", 20), new Fruit("ORG",50)} and I want to generate a list like {new Fruit("APL",30), new Fruit("ORG",50)}
I would say:
You need to have some container that can hold all the different codes and map them to a sum of Amounts. For this we often use a dictionary:
var d = new Dictionary<string,decimal>();
foreach(var f in fruit){
if(!d.ContainsKey(f.Code))
d[f.Code] = f.Amount;
else
d[f.Code] += f.Amount;
}
At the end of this operation your dictionary will contain a unique set of fruit codes and the sum of all the amounts. You can turn it back into a list of fruit by enumerating the dictionary and creating a list in a similar way to how you created the list initially
Once you get your head round that, you can take a look at using LINQ, and do something like:
var summedFruits = fruits
.GroupBy(f => f.Code)
.Select(g => new Fruit(g.Key, g.Sum(f => f.Amount)));
(This assumes your Fruit has a constructor that takes a Code and an Amount). When you GroupBy in LINQ you get an output that is like a List of Lists. The original input list is broken up into some number of lists; where everything in each list has the same value for what you declared was the key (I said to group by code)
So your original representation:
{APL10, APL20, ORG50}
Would end up looking like:
Key = APL, List = {APL10, APL20}
Key = ORG, List = {ORG50}
Your "one list of three things" has become "a list of (a list of two APL ) and (a list of one ORG)"
If you then run a select on them you can create a new Fruit that uses the Key as the code and sums up the amount in each list
Key = APL, List = {APL10, APL20}, Sum = 30
Key = ORG, List = {ORG50}, Sum = 50
In this code:
var summedFruits = fruits
.GroupBy(f => f.Code)
.Select(g => new Fruit(g.Key, g.Sum(f2=> f2.Amount)));
f is a fruit, one of the items in the original list of 3 fruits. We group by the fruit's code. g is the result of the grouping operation, it is a "list of fruit with a common code", the Key is the code (apl or org). g is a list, so you can call sum on it. Every item inside the g list is a fruit, which is why I switch back to f (when I say f2), to help remember that it's an individual fruit- we're summing the amount. For the first list of APL the sum is 30. At the end of the operation a new List results; one with two elements - an APL and an ORG, but the Amounts are the summations of all

var res = fruits.GroupBy(x => x.Code).ToDictionary(g => g.Key, g => g.Sum(x => x.Amount));

Related

Lambda Function to find most popular word in a List C# [duplicate]

This question already has answers here:
How to Count Duplicates in List with LINQ
(7 answers)
Closed 2 years ago.
I currently have what I believe is a lambda function with C# (fairly new to coding & haven't used a lambda function before so go easy), which adds duplicate strings (From FilteredList) in a list and counts the number of occurrences and stores that value in count. I only want the most used word from the list which I've managed to do by the "groups.OrderBy()... etc) line, however I'm pretty sure that I've made this very complicated for myself and very inefficient. As well as by adding the dictionary and the key value pairs.
var groups =
from s in FilteredList
group s by s into g
// orderby g descending
select new
{
Stuff = g.Key,
Count = g.Count()
};
groups = groups.OrderBy(g => g.Count).Reverse().Take(1);
var dictionary = groups.ToDictionary(g => g.Stuff, g => g.Count);
foreach (KeyValuePair<string, int> kvp in dictionary)
{
Console.WriteLine("Key = {0}, Value = {1}", kvp.Key, kvp.Value);
}
Would someone please either help me through this and explain a little bit of this too me or at least point me in the direction of some learning materials which may help me better understand this.
For extra info: The FilteredList comes from a large piece of external text, read into a List of strings (split by delimiters), minus a list of string stop words.
Also, if this is not a lambda function or I've got any of the info in here incorrect, please kindly correct me so I can fix the question to be more relevant & help me find an answer.
Thanks in advance.
Yes, I think you have overcomplicated it somewhat.. Assuming your list of words is like:
var words = new[] { "what's", "the", "most", "most", "most", "mentioned", "word", "word" };
You can get the most mentioned word with:
words.GroupBy(w => w).OrderByDescending(g => g.Count()).First().Key;
Of course, you'd probably want to assign it to a variable, and presentationally you might want to break it into multiple lines:
var mostFrequentWord = words
.GroupBy(w => w) //make a list of sublists of words, like a dictionary of word:list<word>
.OrderByDescending(g => g.Count()) //order by sublist count descending
.First() //take the first list:sublist
.Key; //take the word
The GroupBy produces a collection of IGroupings, which is like a Dictionary<string, List<string>>. It maps each word (the key of the dictionary) to a list of all the occurrences of that word. In my example data, the IGrouping with the Key of "most" will be mapped to a List<string> of {"most","most","most"} which has the highest count of elements at 3. If we OrderByDescending the grouping based on the Count() of each of the lists then take the First, we'll get the IGrouping with a Key of "most", so all we need to do to retrieve the actual word is pull the Key out
If the word is just one of the properties of a larger object, then you can .GroupBy(o => o.Word). If you want some other property from the IGrouping such as its first or last then you can take that instead of the Key, but bear in mind that the property you end up taking might be different each time unless you enforce ordering of the list inside the grouping
If you want to make this more efficient than you can install MoreLinq and use MaxBy; getting the Max word By the count of the lists means you can avoid a sort operation. You could also avoid LINQ and use a dictionary:
string[] words = new[] { "what", "is", "the", "most", "most", "most", "mentioned", "word", "word" };
var maxK = "";
var maxV = -1;
var d = new Dictionary<string, int>();
foreach(var w in words){
if(!d.ContainsKey(w))
d[w] = 0;
d[w]++;
if(d[w] > maxV){
maxK = w;
maxV = d[w];
}
}
Console.WriteLine(maxK);
This keeps a dictionary that counts words as it goes, and will be more efficient than the LINQ route as it needs only a single pass of the word list, plus the associated dictionary lookups in contrast to "convert wordlist to list of sublists, sort list of sublists by sublist count, take first list item"
This should work:
var mostPopular = groups
.GroupBy(item => new {item.Stuff, item.Count})
.Select(g=> g.OrderByDescending(x=> x.Count).FirstOrDefault())
.ToList();
OrderByDescending along with .First() combines your usage of OrderBy, Reverse() and Take.
First part is a Linq operation to read the groups from the FilteredList.
var groups =
from s in FilteredList
group s by s into g
// orderby g descending
select new
{
Stuff = g.Key,
Count = g.Count()
};
The Lambda usage starts when the => signal is used. Basically means it's going to be computed at run time and an object of that type/format is to be created.
Example on your code:
groups = groups.OrderBy(g => g.Count).Reverse().Take(1);
Reading this, it is going to have an object 'g' that represents the elements on 'groups' with a property 'Count'. Being a list, it allows the 'Reverse' to be applied and the 'Take' to get the first element only.
As for documentation, best to search inside Stack Overflow, please check these links:
C# Lambda expressions: Why should I use them? - StackOverflow
Lambda Expressions in C# - external
Using a Lambda Expression Over a List in C# - external
Second step: if the data is coming from an external source and there are no performance issues, you can leave the code to refactor onwards. A more detail data analysis needs to be made to ensure another algorithm works.

How to access content of the groups shaped by LINQ GroupBy?

I have collection of Customers, potentially group-able by their preferences (oranges, apples)
CustomerID | Preference | Age
1 oranges 35
2 apples 32
... ... ...
100 oranges 48
I need kind of of summary table so can group Customers into new collection like this:
var GroupedCustomers = Customers
.GroupBy (a => new {a.Preference, ...}) //could be grouped by more complex compound key
.Select (a => new { CustomerPreference = a.Key.Prefence, TotalOrders = a.Count () })
How can I access the inner collections of each group with all original properties of their members (e.g. "Age" of each customer)? For example, I need to bind the list of "orange lovers" to a GridView and compute their average age.
The issue in the actual case is a complex compound key and hundreds of groups so I don't want to enumerate them every time from the original collection.
You need to bind the ungrouped collection to the GridView, then you can apply the filters, keeping the View and the ViewModel synchronized
Follow the documentation example under How to: Group, Sort, and Filter Data in the DataGrid Control
Comments
Binding is just subcase. I want to find general approach. Let say I
want to proceed with some math manipulation on the many properties of
the group members (like age). Does you answer mean whatever I apply
GroupBy, all properties that weren't been included to the group key
are lost?
No, they aren't. Add the list of the grouped items to the group, for example
var GroupedCustomers = Customers
.GroupBy (c=> new {a.Preference, ...}, //could be grouped by more complex compound key
c => c,
(key, g) => new {
CustomerPreference = key.Preference,
SubItems = g.ToList(), // <= this is your inner collection
...})
This my own attempt.
We should limit initial statement by GroupBy only to keep collection unchanged:
var GroupedCustomers = Customers
.GroupBy (a => new {a.Preference, ...});
Then, the inner collections would be as simple as:
var GroupByIndex = GroupedCustomers[0]; //retrieve entire group by its index
var GroupdByContent = GroupedCustomers.First(i => i.Key.Preference == "oranges") //retrieve group by key

How to handle duplicate keys while joining two lists?

I'm new to C#.
I have the following struct.
struct Foo
{
string key;
Bar values;
}
I have two lists of Foo, L1 and L2 of equal size both contain same set of keys.
I have to merge the corresponding Foo instances in L1 and L2.
Foo Merge(Foo f1, Foo f2)
{
// merge f1 and f2.
return result.
}
I wrote the following to achieve this.
resultList = L1.Join(L2, f1 => f1.key, f2 => f2.key, (f1, f2) => Merge(f1, f2)
).ToList())
My problem is that my key is not unique. I have n number of elements in L1 with the same key (say "key1") (which are also appearing in L2 somewhere). So, the above join statement selects n matching entries from L2 for each "key1" from L1 and I get n*n elements with key "key1" in the result where I want only n. (So, this is kind of crossproduct for those set of elements).
I want to use Join and still select an element from L1 with "key1" and force the Linq to use the first available 'unused' "key1" element from L2. Is this possible? Is join a bad idea here?
(Also, the I want to preserve the order of the keys as in L1. I tried to handle all elements with such keys before the join and removed those entries from L1 and L2. This disturbed the order of the keys and it looked ugly).
I'm looking for a solution without any explicit for loops.
From your comment to ElectricRouge answer, you could do something like
var z = list1.Join(list2.GroupBy(m => m.Id),
m => m.Id,
g => g.Key,
(l1, l2) => new{l1, l2});
this would give you a list of all keys in l1, and the corresponding grouped keys in l2.
Not sure it's really readable.
I need to find the corresponding entries in two lists and do some operation on them. That is my preliminary requirement.
For this you can do something like this.
var z=S1.Select(i=>i.Key).Tolist(); //make a list of all keys in S1
List<Foo> result=new List<Foo>();
foreach(var item in z) // Compare with S2 using keys in z
{
var x=item.Where(i=>i.Key==item.Key)
result.Add(x);
}
Is this what you are looking for?
I want to use Join and still select an element from L1 with "key1" and force the Linq to use the first available 'unused' "key1" element from L2. Is this possible?
When combining elements from the two lists you want to pick the first element in the second list having the same key as the element in the first list. (Previously, I interpreted you question differently, and a solution to this different problem is available in the edit history of this answer.)
For quick access to the desired values in the second list a dictionary is created providing lookup from keys to the desired value from the second list:
var dictionary2 = list2
.GroupBy(foo => foo.Key)
.ToDictionary(group => group.Key, group => group.First());
The use of First expresses the requirement that you want to pick the first element in the second list having the same key.
The merged list is now created by using projection over the first list:
var mergedList = list1.Select(
foo => Merge(
foo,
dictionary2[foo.Key]
)
);
When you use foreach to iterate mergedList or ToList() the desired result will be computed.
You could use Union to remove the duplicated keys.
Documentation at http://msdn.microsoft.com/en-us/library/bb341731.aspx
List<int> list1 = new List<int> { 1, 12, 12, 5};
List<int> list2 = new List<int> { 12, 5, 7, 9, 1 };
List<int> ulist = list1.Union(list2).ToList();
Example taken from : how to merge 2 List<T> with removing duplicate values in C#
Or you can use Concat to merge a list of different types (Keeping all keys).
See the documentation her : http://msdn.microsoft.com/en-us/library/bb302894(v=vs.110).aspx
var MyCombinedList = Collection1.Concat(Collection2)
.Concat(Collection3)
.ToList();
Example taken from same question : Merge two (or more) lists into one, in C# .NET
Finally I adapted Raphaël's answer as below.
public class FooComparer : IEqualityComparer<Foo>
{
public bool Equals(Foo o1, Foo o2)
{
return o1.key == o2.key;
}
public int GetHashCode(Foo obj)
{
return obj.key.GetHashCode();
}
}
resultList = L1.Join(L2.Select(m => m).Distinct(new FooComparer()).ToList(), f1 => f1.key, f2 => f2.key, (f1, f2) => Merge(f1, f2)
).ToList());
Short explanation:
L2.Select(m => m).Distinct(new FooComparer()).ToList()
creates a new list by removing the duplicate keys from L2. Join L1 with this new list to get the required result.

Sort one list by another

I have 2 list objects, one is just a list of ints, the other is a list of objects but the objects has an ID property.
What i want to do is sort the list of objects by its ID in the same sort order as the list of ints.
Ive been playing around for a while now trying to get it working, so far no joy,
Here is what i have so far...
//**************************
//*** Randomize the list ***
//**************************
if (Session["SearchResultsOrder"] != null)
{
// save the session as a int list
List<int> IDList = new List<int>((List<int>)Session["SearchResultsOrder"]);
// the saved list session exists, make sure the list is orded by this
foreach(var i in IDList)
{
SearchData.ReturnedSearchedMembers.OrderBy(x => x.ID == i);
}
}
else
{
// before any sorts randomize the results - this mixes it up a bit as before it would order the results by member registration date
List<Member> RandomList = new List<Member>(SearchData.ReturnedSearchedMembers);
SearchData.ReturnedSearchedMembers = GloballyAvailableMethods.RandomizeGenericList<Member>(RandomList, RandomList.Count).ToList();
// save the order of these results so they can be restored back during postback
List<int> SearchResultsOrder = new List<int>();
SearchData.ReturnedSearchedMembers.ForEach(x => SearchResultsOrder.Add(x.ID));
Session["SearchResultsOrder"] = SearchResultsOrder;
}
The whole point of this is so when a user searches for members, initially they display in a random order, then if they click page 2, they remain in that order and the next 20 results display.
I have been reading about the ICompare i can use as a parameter in the Linq.OrderBy clause, but i can’t find any simple examples.
I’m hoping for an elegant, very simple LINQ style solution, well I can always hope.
Any help is most appreciated.
Another LINQ-approach:
var orderedByIDList = from i in ids
join o in objectsWithIDs
on i equals o.ID
select o;
One way of doing it:
List<int> order = ....;
List<Item> items = ....;
Dictionary<int,Item> d = items.ToDictionary(x => x.ID);
List<Item> ordered = order.Select(i => d[i]).ToList();
Not an answer to this exact question, but if you have two arrays, there is an overload of Array.Sort that takes the array to sort, and an array to use as the 'key'
https://msdn.microsoft.com/en-us/library/85y6y2d3.aspx
Array.Sort Method (Array, Array)
Sorts a pair of one-dimensional Array objects (one contains the keys
and the other contains the corresponding items) based on the keys in
the first Array using the IComparable implementation of each key.
Join is the best candidate if you want to match on the exact integer (if no match is found you get an empty sequence). If you want to merely get the sort order of the other list (and provided the number of elements in both lists are equal), you can use Zip.
var result = objects.Zip(ints, (o, i) => new { o, i})
.OrderBy(x => x.i)
.Select(x => x.o);
Pretty readable.
Here is an extension method which encapsulates Simon D.'s response for lists of any type.
public static IEnumerable<TResult> SortBy<TResult, TKey>(this IEnumerable<TResult> sortItems,
IEnumerable<TKey> sortKeys,
Func<TResult, TKey> matchFunc)
{
return sortKeys.Join(sortItems,
k => k,
matchFunc,
(k, i) => i);
}
Usage is something like:
var sorted = toSort.SortBy(sortKeys, i => i.Key);
One possible solution:
myList = myList.OrderBy(x => Ids.IndexOf(x.Id)).ToList();
Note: use this if you working with In-Memory lists, doesn't work for IQueryable type, as IQueryable does not contain a definition for IndexOf
docs = docs.OrderBy(d => docsIds.IndexOf(d.Id)).ToList();

LINQ group items. A single item may be in several groups

I have an IEnumerable of items that I would like to group by associated categories. The items are grouped by the categories that are associated with them - which is a List - so a single item can potentially be a part of multiple categories.
var categories = numbers.SelectMany(x => x.Categories).Distinct();
var query =
from cat in categories
select new {Key = cat,
Values = numbers.Where(n => n.Categories.Contains(cat))};
I use the above code, and it does in fact work, but I was wondering if there was a more efficient way of doing this because this operation will likely perform slowly when numbers contains thousands of values.
I am pretty much asking for a refactoring of the code to be more efficient.
You can use LINQ's built-in grouping capabilities, which should be faster than a contains lookup. However, as with any performance-related question, you should really write code to collect performance metrics before deciding how to rewrite code that you know works. It may turn out that there's no performance problem at all for the volumes you will be working with.
So, here's the code. This isn't tested, but something like it should work:
var result = from n in numbers
from c in n.Categories
select new {Key = c, n.Value}
into x group x by x.Key into g
select g;
Each group contains a key and a sequence of values that belong to that key:
foreach( var group in result )
{
Console.WriteLine( group.Key );
foreach( var value in group )
Console.WriteLine( value );
}

Categories