views:

254

answers:

5

I have an IEnumerable<IEnumerable<T>> collection that I want to convert to a single dimension collection. Is it possible to achieve this with a generic extension method? Right now I'm doing this to achieve it.

List<string> filteredCombinations = new List<string>();

//For each collection in the combinated results collection
foreach (var combinatedValues in combinatedResults)
{
    List<string> subCombinations = new List<string>();
    //For each value in the combination collection
    foreach (var value in combinatedValues)
    {

        if (value > 0)
        {
            subCombinations.Add(value.ToString());
        }
    }
    if (subCombinations.Count > 0)
    {
       filteredCombinations.Add(String.Join(",",subCombinations.ToArray()));
    }
}

If it's not possible to get a generic solution, how can I optimize this in an elegant fashioned way.

+17  A: 

You can use the Enumerable.SelectMany extension method for this.

If I read your code correctly, the code for that would be:

var filteredCombinations = combinatedResults.SelectMany(o => o)
    .Where(value => value > 0)
    .Select(v => v.ToString());

Edit: As commented, the above code is not joining each element of the subsets to a string, as the original code does. Using the built-in methods, you can do that using:

var filteredCombinations = combinatedResults
     .Where(resultSet => resultSet.Any(value => value > 0)
     .Select(resultSet => String.Join(",",
         resultSet.Where(value => value > 0)
                  .Select(v => v.ToString()).ToArray()));
driis
beat me to it ...
Brian Rasmussen
What if you want every subset to be one element of the new collection? That's what I do with the String.Join...
John Doe
This isn't right - it ignores the `Join` requirement.
Matt Howells
+1 for answering the title anyway!
Daniel Earwicker
@John, see updated answer :-)
driis
Still not right - you include empty strings in the results.
Daniel Earwicker
@Earwicker, I fail to see how I will get empty strings in the result. The resultSet must start out as an enumerable of a numeric type, since the elements is being compared to zero in the orignal code. (I admit I typed the code without Visual Studio, so feel free to correct me :-)
driis
There are two `if` statements in the original code. You only have one `Where` clause. You've kept the one that filters out the zero values, but you have missed the one that filters out empty sub-lists. A list containing all zeros will become an empty array, which you pass to `Join`. The original code doesn't do that.
Daniel Earwicker
You're right, updated the code again.
driis
Hmm, now you're querying every `resultSet` twice with the same expression. Sorry to be picky... you could just copy my answer to save time! :)
Daniel Earwicker
A: 

use linq SelectMany

Preet Sangha
+3  A: 

I would personally use Enumerable.SelectMany, as suggested by driis.

However, if you wanted to implement this yourself, it would be much cleaner to do:

IEnumerable<T> MakeSingleEnumerable<T>(IEnumerable<IEnumerable<T>> combinatedResults)
{
    foreach (var combinatedValues in combinatedResults) {
         foreach (var value in combinatedValues)
              yield return value;
    }
}
Reed Copsey
yes *SelectMany* is better, but it seems a lot of people don't know about "yield return"--as evidenced by this question.
Dan
+3  A: 

Here you go:

var strings = combinedResults.Select
    (
        c => c.Where(i => i > 0)
        .Select(i => i.ToString())
    ).Where(s => s.Any())
    .Select(s => s.Implode(","));

It relies on an Implode extension method which is just like String.Join but works on IEnumerable. I called it Implode rather than Join because there are already Join extension methods which mean something different, and Implode and Explode are used in other languages to mean String.Join and String.Split respectively.

For completeness' sake, here is my Implode implementation:

public static string Implode(this IEnumerable<string> sequence,
    string separator)
{
    var sb = new StringBuilder();

    foreach (string s in sequence)
    {
        sb.Append(s);
        sb.Append(separator);
    }

    if (separator != null && sb.Length > 0)
        sb.Length -= separator.Length; //remove the last separator

    return sb.ToString();
}

If you would rather not add all this code, you can just use String.Join and ToArray(), but it won't be as elegant.

Matt Howells
But the original code produces a list of strings, not one string...
Daniel Earwicker
D'oh. Correcting.
Matt Howells
This whole question has been bizarre! The wording asked one question, the example asked another. And you've answered a third, different question altogether, and got accepted... I've already posted the correct answer, and have no votes (not that I mind, it's just amusing.)
Daniel Earwicker
I think your `Implode` would be more elegant if it just operated on `IEnumerable<string>`. If you want a conversion on each item of some other type of sequence, use `Select` first.
Daniel Earwicker
@Earwicker: good idea.
Matt Howells
+1  A: 

You asked two different questions. The one you described in the title is already answered by drilis.

But your example code is a different problem. We can refactor it in stages. Step 1, build the subCombinations list using some Linq:

List<string> filteredCombinations = new List<string>();

//For each collection in the combinated results collection
foreach (var combinatedValues in combinatedResults)
{
    var subCombinations = combinatedValues.Where(v => v > 0)
                                          .Select(v => v.ToString())
                                          .ToList();

    if (subCombinations.Count > 0)
       filteredCombinations.Add(string.Join(",",subCombinations.ToArray()));
}

Now the outer loop, leaving us with just this:

var filteredCombinations = combinatedResults
    .Select(values => values.Where(v => v > 0)
                            .Select(v => v.ToString())
                            .ToArray())
    .Where(a => a.Count > 0)
    .Select(a => string.Join(",", a));
Daniel Earwicker