Dictionary of List to List of KeyValuePair using LINQ - c#

I'm trying to find the most "elegant" way to deal with the transformation between two collections using LINQ. The source container type is Dictionary<int, List<string>> and I need to convert it to List<KeyValuePair<string, string>>. Basically I need to duplicate the key in the dictionary for every element in its corresponding list into a flattened list. Below shows my 2 attempts to solve the issue.
Dictionary<int, List<string>> source = new() {
{100, new() {"a", "b", "c"}}
};
List<KeyValuePair<string, string>> target = new();
// Solution 1
foreach (var (score, terms) in source)
{
foreach (var term in terms)
{
target.Add(new KeyValuePair<string, string>(score.ToString(), term));
}
}
// Solution 2
target = source.SelectMany(kvp =>
{
var(score, terms) = kvp;
return terms.Select(t => new KeyValuePair<string, string>(score.ToString(), t));
}).ToList();
Runnable sample here on .NET Fiddle.
I'm using .NET Core 5 and C# 9 in a console application.
Both of these work, I think, but I want to see if I can make this cleaner. It's pretty difficult to find out how to use LINQ to solve complex transformations like this. One thing I tried to do was "deconstruct" the KeyValuePair in SelectMany(), like:
source.SelectMany((score, terms) => ...)
But this didn't work. I'm not sure if there's a way to make that kind of deconstruction possible. Little things like this I think could go a long way to making this cleaner.

Maybe
var results = source
.SelectMany(x => x.Value
.Select(y => new KeyValuePair<string, string>(x.Key.ToString(), y)))
.ToList();
Or if you are happy with Value Tuples (which have a few benefits)
var results = source
.SelectMany(x => x.Value
.Select(y => (x.Key.ToString(), y)))
.ToList();

If you want to pattern match it in a single expression ("that kind of deconstruction"), you can abuse (?) the switch expression:
target = source.SelectMany(kvp => kvp switch { var (score, terms) =>
terms.Select(t => new KeyValuePair<string, string>(score.ToString(), t))
}).ToList();

You can use the overload of SelectMany that has a parameter resultSelector.
var results = source.SelectMany(sourceItem => sourceItem.Value,
// parameter resultSelector:
(sourceItem, listItem) => ...
In baby steps:
Source is a sequence of KeyValuePair<int, List<string>>. So every sourceItem is one KeyValuePair<int, List<string>>. sourceItem.Value is the List<string> that belongs to the int sourceItem.Key. In the parameter resultSelector you get a combination of the complete sourceItem, with one item from the list:
(sourceItem, listItem) =>
So if your source is:
{ 1, {"A", "B"} }
{ 2, {"C", "D" "E"} }
{ 3, empty list }
You get:
( { 1, {"A", "B"} }, "A")
( { 1, {"A", "B"} }, "B")
( { 2, {"C", "D" "E"} }, "C")
( { 2, {"C", "D" "E"} }, "D")
( { 2, {"C", "D" "E"} }, "E")
No items for 3, the list is empty
Now, from every sourceItem you want sourceItem.Key.ToString(), from every listItem you want the complete listItem. So your SelectMany will be:
var results = source.SelectMany(sourceItem => sourceItem.Value,
(sourceItem, listItem) => new KeyValuePair<string, string>
(sourceItem.Key.ToString, listItem))
.ToList();

Related

Count duplicates and combine them in a list

I have a list like this:
var list = new List<string>() { "a", "b", "c", "a" };
As you can see I have one duplicate inside this list.
The desired output should look like this:
a (2)
b (1)
c (1)
The code which I already have looks like this:
list.GroupBy(x => x).Select(x => x.Key + "(" + x.Count() + ")").ToList().ForEach(x => list2.Add(x));
But as you can see I need a second list to get the desired output. My question now is how can I get the desired output with using only one list.
Why don't we just add the items into existing list2?
var list2 = new List<string>();
...
list2.AddRange(list
.GroupBy(item => item)
.Select(group => $"{group.Key} ({group.Count()})"));
Or create list2 from scratch:
var list2 = list
.GroupBy(item => item)
.Select(group => $"{group.Key} ({group.Count()})")
.ToList();
This does what you need:
var list = new List<string>() { "a", "b", "c", "a" };
foreach (var group in list.GroupBy(b => b).OrderBy(g => g.Key))
{
Console.WriteLine($"{group.Key} ({group.Count()})");
}
Consequentially, if one wishes to dedup this list, one could do:
list = list.GroupBy(b => b).Select(g => g.Key).ToList();
The result would be a deduped list with "a", "b" and "c" in it.
You can do this in one statement (I've split it over multiple lines for readability):
var list = new List<string> { "a", "b", "c", "a" };
Console.WriteLine(
string.Join("\n",
list.GroupBy(_=>_).Select(g => $"{g.Key} ({g.Count()})")));
Output:
a (2)
b (1)
c (1)
You can use the Distinct method to remove duplicates.
This returns a IEnumerable and in order to use the ForEach method you have to apply the ToList method first.
The Foreach iterates over the items {"a", "b", "c"}
and prints the current item and the count of this item in the list with dulicates.
list.Distinct().ToList().ForEach(v1 => Console.WriteLine($"{v1} ({list.Count(v2 => v1 == v2)})"))

Merge List of KeyValue Pairs

I have a List<KeyValuePair<string, List<string>>, so the data looks something like:
user1, [trigger1,trigger2,trigger3]
user2, [trigger1,trigger4]
user3, [trigger2,trigger3,trigger4]
user1, [trigger0,trigger4]
I would like to make this a Dictionary<string,List<string>>, but directly this fails because there would be duplicate keys. Is there a way to merge the contents of the attached Values on the fly (meaning if there is user1 twice as keys, combine the two associated Lists<>) in LINQ?
You need to group KVPs on Key, and use SelectMany to "flatten" the lists. If you do not want duplicates, use Distinct() to remove them:
var res = list
.GroupBy(p => p.Key)
.ToDictionary(
g => g.Key
, g => g.SelectMany(p => p.Value).Distinct().ToList()
); // ^^^^^^^^^^
// Remove Distinct() if you would like to keep all items
A GroupBy will do most of the work, combined with making a list of all the grouped items and you are done. The result of the GroupBy contains an enumerable of lists, so we have to select all string items using SelectMany and construct a list out of that.
Dictionary<string, List<string>> d = l.GroupBy(k => k.Key)
.ToDictionary( k => k.Key
, k => k.SelectMany(s => s.Value).ToList()
);
You can accomplish this through a series of chained LINQ queries in the following order :
GroupBy() - This will create groups of each of your individual users to avoid issues when creating your dictionary (namely duplicate keys).
ToDictionary() - This method will work to bind a given set of data using a key and a value, since you already have your keys (via the earlier call), you just need to get your values from each group.
SelectMany() - This will select all of the individual values from your earlier groups, essentially merging all of the items from each set of users into a single collection.
Distinct() - This will be applied on the list of strings generated in the previous step to remove any duplicates. If you want to allow duplicates, simply remove this step.
Implementing this would look something like the following snippet of code :
// Group the collections by user and then select the keys based on those users
var dictionary = list.GroupBy(l => l.Key)
.ToDictionary(
x => x.Key,
x => x.SelectMany(l => l.Value)
.Distinct()
.ToList()
);
Example
// Example of your existing data
var list = new List<KeyValuePair<string, List<string>>>(){
new KeyValuePair<string,List<string>>("User 1", new List<string>(){ "trigger1" ,"trigger2", "trigger3" }),
new KeyValuePair<string,List<string>>("User 2", new List<string>(){ "trigger1" ,"trigger2" }),
new KeyValuePair<string,List<string>>("User 3", new List<string>(){ "trigger2" ,"trigger3", "trigger4" }),
new KeyValuePair<string,List<string>>("User 1", new List<string>(){ "trigger0" ,"trigger4" }),
};
// Group the collections by user and then select the keys based on those users
var dictionary = list.GroupBy(l => l.Key)
.ToDictionary(
x => x.Key,
x => x.SelectMany(l => l.Value)
.Distinct()
.ToList()
);
// Output each key with it's associated values
foreach(var key in dictionary.Keys)
{
Console.WriteLine("Key: " + key + ", Values: " + String.Join(",",dictionary[key].ToArray()));
}
// Example output
// Key: User 1, Values: trigger1,trigger2,trigger3,trigger0,trigger4
// Key: User 2, Values: trigger1,trigger2
// Key: User 3, Values: trigger2,trigger3,trigger4
You can see an interactive version of this here.
Try this :
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data;
namespace ConsoleApplication1
{
class Program
{
static void Main(string[] args)
{
List<KeyValuePair<string, List<string>>> myList = new List<KeyValuePair<string,List<string>>>() {
new KeyValuePair<string, List<string>>("user1", new List<string> { "trigger1","trigger2","trigger3"}),
new KeyValuePair<string, List<string>>("user2", new List<string> { "trigger1","trigger4"}),
new KeyValuePair<string, List<string>>("user3", new List<string> { "trigger2","trigger3","trigger4"}),
new KeyValuePair<string, List<string>>("user1", new List<string> { "trigger0","trigger4"})
};
Dictionary<string, List<string>> dict = myList
.GroupBy(x => x.Key, y => y)
.ToDictionary(x => x.Key, y => y.Select(z => z.Value).SelectMany(a => a).ToList());
}
}
}

How can I extract a subset of a dictionary into another one in C#?

I want to filter out some dictionary pairs I do not need for further processing. Check this sample code out:
static void Main(string[] args)
{
var source = new Dictionary<string, dynamic>();
source.Add("number", 1);
source.Add("string1", "One");
source.Add("string2", "Two");
source.Add("string3", "Three");
var onlyStrings = source.Where(s => s.Key != "number").ToDictionary(s => s.Key);
}
In this case, onlyStrings is a Dictionary<string, KeyValuePair<string, object>>
but I want onlyStrings to have the following pairs (a subset of the source dictionary):
Key: "string1", Value: "One"
Key: "string2", Value: "Two"
Key: "string3", Value: "Three"
What is the best way to get such result?
There is an overload to the ToDictionary method that also allows for an elementSelector delegate:
var onlyStrings = source.Where(s => s.Key != "number")
.ToDictionary(dict => dict.Key, dict => dict.Value);

Linq query to get a duplicate key once along with multiple values out of a dictionary

I have the following dictionary:
Dictionary<int, string> selectedDrivers = new Dictionary<int, string>()
{
{ 1, "Michael Schumacher" },
{ 2, "Michael Schumacher" },
{ 3, "Jensen Button" }
};
What I try to achieve is a linq query that will find the duplicates, retrieve the name only once, along with the positions this duplicate has. So, for example, the above would result in "Michael Schumacher" 1 2. I would like the result to go into a Dictionary<string, List<int>>.
Getting excited fast, this little nugget I wrote seems to be going in the right direction, except that I have Michael Schumacher twice. And it's not yet in a Dictionary<string, List<int>>
var a = selectedDrivers.GroupBy(a => a.Value).
Where(b => b.Count() > 1);
var a = selectedDrivers.GroupBy(a=>a.Value)
.Where(g=>g.Count() > 1)
.ToDictionary(g => g.Key, g => g.Select(a=>a.Key).ToList());

Merging Dictionary containing a List in C#

This is kind-of related to this question, on how to merge two dictionaries in C#. An elegant Linq solution is presented, which is cool.
However, that question relates to Dictionary<Object1, Object2>, whereas I have a dictionary where the value is a List<Object2>.
I am looking for a solution for merging a Dictionary<Object1, List<Object2>>, with the following requirements:
If Dictionary1 contains the same key as Dictionary2, then their List<Object2> lists should be combined. You would end up with a new key-value-pair with the shared key, and the combined lists from the two dictionaries.
If Dictionary1 contains a key that Dictionary2 doesn't then the List<Object2> list from Dictionary1 should become the value, and vice versa.
This may not be possible in Linq, or it may be worth writing it out longhand with for loops and the like, but it would be nice to have an elegant solution.
I would suggest creating your own extension method. It will be more efficient and easier to modify.
public static void MergeDictionaries<OBJ1, OBJ2>(this IDictionary<OBJ1, List<OBJ2>> dict1, IDictionary<OBJ1, List<OBJ2>> dict2)
{
foreach (var kvp2 in dict2)
{
// If the dictionary already contains the key then merge them
if (dict1.ContainsKey(kvp2.Key))
{
dict1[kvp2.Key].AddRange(kvp2.Value);
continue;
}
dict1.Add(kvp2);
}
}
The difficulty is dealing with the merging of key conflicts.
If we start by flattening all the input dictionaries using SelectMany, we can group together the elements by their key.
var result = dictionaries
.SelectMany(dict => dict)
.GroupBy(kvp => kvp.Key)
The result set contains groups where each group's key is a key from the original dictionaries, and the contents of the group are an IEnumerable<List<T>> of the lists with the same key. From these groups, we can merge all List<T> into a single IEnumerable<T> using a Select transformation with SelectMany.
var result = dictionaries
.SelectMany(dict => dict)
.GroupBy(kvp => kvp.Key)
.Select(grp => new { Key = grp.Key, Items = grp.SelectMany(list => list)})
We can then get a dictionary from this using a ToDictionary transformation, converting the IEnumerable<T> back to a List<T>.
var result = dictionaries
.SelectMany(dict => dict)
.GroupBy(kvp => kvp.Key)
.Select(grp => new { Key = grp.Key, Items = grp.SelectMany(list => list)})
.ToDictionary(kip => kip.Key, kip => new List<T>(kip.Items));
Updated in response to comment
You can populate dictionaries however you like. I have assumed it is a type which implements IEnumerable<IDictionary<TKey, List<T>>> for a TKey and T of your choosing.
The simplest way would be using a List<T> as follows:
List<IDictionary<TKey, List<T>>> dictionaries
= new List<IDictionary<TKey, List<T>>>();
dictionaries.Add(dictionary1); // Your variable
dictionaries.Add(dictionary2); // Your variable
// Add any other dictionaries here.
// Code as above!
You just need to change item merging part in solution to the previous problem.
For object we have this:
.ToDictionary(group => group.Key, group => group.First())
i.e. for duplicated items, just take the first one.
But we could use this:
.ToDictionary(group => group.Key, group => group.SelectMany(list => list).ToList());
to concatenate lists.
So, the final expression would be
var result = dictionaries.SelectMany(dict => dict)
.ToLookup(pair => pair.Key, pair => pair.Value)
.ToDictionary(group => group.Key,
group => group.SelectMany(list => list).ToList());
You could try a different merging expression if you need some extra list merging logic (e.g. only merge distinct items)
I'll be the first to admit that this is not all that pretty but this works for me.
var d1 = new Dictionary<string, List<string>>();
var d2 = new Dictionary<string, List<string>>();
d1["test"] = new List<string>() { "Stockholm", "Motala" };
d1["more"] = new List<string>() { "numerous", "populous", "bigger", "plentiful" };
d2["test"] = new List<string>() { "Washington", "Charlottesville" };
d2["less"] = new List<string>() { "insufficient", "small", "imperceptible" };
var intersect = (from key in d1.Keys.Intersect(d2.Keys) select new { Key = key, Value = new List<string>(d1[key].Concat(d2[key])) }).ToDictionary(d => d.Key, d => d.Value);
var merged = d1.Concat(d2).Where(d => !intersect.Keys.Contains(d.Key)).Concat(intersect).ToDictionary(d => d.Key, d => d.Value);

Categories

Resources