tags:

views:

1056

answers:

5

I have a dictionary, where the key is a string and the value is a list of strings that correspond to that key. I would like to display all of the keys in the dictionary, with the values associated with that key tabbed in underneath that key. Something like this:

Key 1
    Value 1
    Value 2
    Value 3
Key 2
    Value 1
    Value 2

In C# 2.0, I would do that like this (values is the Dictionary):

StringBuilder sb = new StringBuilder();
foreach(KeyValuePair<string, List<string>> pair in values)
{
    sb.AppendLine(pair.Key);
    foreach(string item in pair.Value)
    {
        sb.AppendLine('\t' + item);
    }
}

How would I do the equivalent using LINQ? It seems like it should be possible, however I can't figure out how to do it.

If I use values.SelectMany(p => p.Values), then only the values will be in the final result, not the keys as well.

Any other solution that I've thought of has a similar limitation.

+1  A: 

I'm not sure how you'd do it in LINQ, but using lambdas you could do something like:

string foo = string.Join(Environment.NewLine, 
    values.Select(k => k.Key + Environment.NewLine + 
        string.Join(Environment.NewLine, 
            k.Value.Select(v => "\t" + v).ToArray())).ToArray());

That's not terribly readable, though.

Adam Robinson
+1  A: 

There isn't anything in LINQ that is going to help you here, because you have a separate set of requirements for the values than you do the keys (you tab between the values).

Even if that wasn't the case, at best, LINQ is going to help you with just getting a single enumeration source to cycle through, which you would have to have some sort of partitioning logic in anyways to indicate when you are processing a set of values versus a key.

What it comes down to is that the Dictionary you have already gives you a natural grouping which LINQ won't be able to help any more with unless you are dealing with an operation other than grouping (sorting, projection, filtering).

casperOne
+1  A: 

If you had a method like this:

public IEnumerable<string> GetStrings
  (KeyValuePair<string, List<string>> kvp)
{
  List<string> result = new List<string>();
  result.Add(kvp.Key);
  result.AddRange(kvp.Value.Select(s => "\t" + s));
  return result;
}

Then you could do this:

List<string> result = theDictionary
  .SelectMany(kvp => GetStrings(kvp)).ToList();

Or generically:

public static IEnumerable<T> GetFlattened<T, U>
  ( this KeyValuePair<T, List<U>> kvp,
    Func<U, T> ValueTransform
  )
{
  List<T> result = new List<T>();
  result.Add(kvp.Key);
  result.AddRange(kvp.Value.Select(v => ValueTransform(v)));
  return result;
}

List<string> result = theDictionary
  .SelectMany(kvp => kvp.GetFlattened(v => "\t" + v))
  .ToList();
David B
This doesn't answer the question, though...sounds like he wants one string
Adam Robinson
I answered the interesting bit, which is how to flatten a dictionary. To tranform the resulting IEnumerable<string> takes little imagination - string.Join works well for that purpose.
David B
Just looks like quite a few lines of code compared to his original, while not actually doing what it did :)
Adam Robinson
Yes, his two foreach loops solve the specific "problem" well. Sometimes, in order to learn how to use a technology or technique, one must write non-optimal code.
David B
+3  A: 

A Solution in C#

Here is a solution using the aggregate extension method:

string result = values.Aggregate("",
                  (keyString, pair) =>
                    keyString + "\n" + pair.Key + ":"
                      + pair.Value.Aggregate("",
                          (str, val) => str + "\n\t" + val)
                  );

There is no LINQ syntax for the aggregate clause in C# (but apparently there is in Visual Basic for some predefined functions).

This solution might look somewhat complex, but the aggregate method is quit useful.

How Aggregate works

It works like this: If you have a List<int> you can combine all values into a single aggregate value.

That is in contrast to the Select method, which doesn't modify the length of the list. Or the Where method, which does shrink the list, but it still remains a list (instead of a single value).

For example, if you have a list {1, 2, 3, 4} you can combine them into a single value like this:

int[] xs = {1, 2, 3, 4};
int sum = xs.Aggregate(0, (sumSoFar, x) => sumSoFar + x);

So you give two values to the Aggregate method; a seed value and a 'combiner' function:

  • You start aggregating with a single seed value (0 in this case).
  • Your combiner function gets called for each value in the list.
    The first argument is the computed result so far (zero the first time, later this will have other values), and it combines it with the value x.

That's in short how the Aggregate method works on List<int>. It works the same on KeyValuePair<string, List<string>>, but just with different types.

Tom Lokhorst
+1 for the clever Aggregate solution even though it's debatable wheter the first example even returns an Aggregate(in the true meaning of the word) such as the 2nd example and if it's actually a better solution then the simple heads on foreach:)
Martijn Laarman
Regarding the `aggregate' name; I don't really like it, maybe because it has a specific meaning in English. In other programming languages this function is sometimes called: reduce, fold or eliminate.
Tom Lokhorst
A: 

I'd do a select on the values to put things in an initial list by item (using string.Join for the values) and then pop that into a string (again using string.Join).

IEnumerable<string> items = values.Select(v => 
    string.Format("{0}{1}{2}", v.Key, Environment.NewLine + "\t", 
    string.Join(Environment.NewLine + "\t", v.Value.ToArray()));
string itemList = string.Join(Environment.NewLine, items.ToArray());
Jacob Proffitt