2

I have a list of parameters that each accept a specific range of inputs. I am working on creating a test that creates every possible valid input. Furthermore, each group is optional (can be skipped entirely), so the combination lengths don't necessarily have to be the same length of the list.

Input

List<string[]> temp = new List<string[]>
{
    // The order of these groups is important
    new string[] { "a", "b", "c" },
    new string[] { "d", "e" },
    new string[] { "f", "g", "h" }
};

Constraints

  1. 0 or 1 item per group (a string[] above)
  2. Order of the List<T> must be preserved

Valid Combinations

  1. a, e, f
  2. a, d, g
  3. c, e, f
  4. b, g
  5. c, f

Invalid Combinations

  1. a, b, f (a and b are from the same group - not allowed)
  2. a, f, d (wrong order - d must come before f)

So far, I have gone back to my library where I have a Combinations LINQ method.

public static class IEnumerableExtensions
{
    // Can be used to get all permutations at a certain level
    // Source: http://stackoverflow.com/questions/127704/algorithm-to-return-all-combinations-of-k-elements-from-n#1898744
    public static IEnumerable<IEnumerable<T>> Combinations<T>(this IEnumerable<T> elements, int k)
    {
        return k == 0 ? new[] { new T[0] } :
          elements.SelectMany((e, i) =>
            elements.Skip(i + 1).Combinations(k - 1).Select(c => (new[] { e }).Concat(c)));
    }
}

In the past I have only used this on single sequence to do things like generate every permutation of URL segments. But I am struggling with its usage on a nested list with the constraints of one per group and in a specific order.

I know I can solve this particular puzzle by doing 3 nested loops and using lists to track which items were already used, but I don't know in advance how many items will be in the List<T>, so that won't work in the general case.

How can I get all of the valid combinations of the above input?

I would prefer LINQ, but will accept any solution that solves this problem.

NightOwl888
  • 55,572
  • 24
  • 139
  • 212
  • this requires a recursive function. You can call recursive method from a linq. Level one of recursion would iterate through "a,b,c",. Level two would iterate through "d,e" level three would iterate through "f,g,h". – jdweng Jul 03 '17 at 21:31
  • 1
    @jdweng - could you provide an example? – NightOwl888 Jul 03 '17 at 21:33

3 Answers3

3

Using some extension functions,

public static IEnumerable<T> Prepend<T>(this IEnumerable<T> rest, params T[] first) => first.Concat(rest);
public static IEnumerable<T> AsSingleton<T>(this T source) {
    yield return source;
}

You can write

public static IEnumerable<IEnumerable<T>> ParametersWithEmpty<T>(this IEnumerable<IEnumerable<T>> ParmLists) {
    if (ParmLists.Count() == 0)
        yield break; // empty
    else {
        var rest = ParametersWithEmpty(ParmLists.Skip(1));
        foreach (var p in ParmLists.First()) {
            yield return p.AsSingleton(); // p.Concat(empty)
            foreach (var r in rest)
                yield return r.Prepend(p); // p.Concat(r)
        }
        foreach (var r in rest)
            yield return r; // empty.Concat(r)
    }
}

You can call it like this:

var ans = temp.ParametersWithEmpty();

To include all the levels in the results, you must skip the empty cases implicit in the above code:

public static IEnumerable<IEnumerable<T>> Parameters<T>(this IEnumerable<IEnumerable<T>> ParmLists) {
    if (ParmLists.Count() == 1)
        foreach (var p in ParmLists.First())
            yield return p.AsSingleton();
    else {
        var rest = Parameters(ParmLists.Skip(1));
        foreach (var p in ParmLists.First()) {
            foreach (var r in rest)
                yield return r.Prepend(p);
        }
    }
}

Finally, here is an alternative version that may make it somewhat clearer as it outputs the sequence as if each parameter list is preceded by empty, but also returns the all empty sequence in the answer.

public static IEnumerable<IEnumerable<T>> ParametersWithEmpty2<T>(this IEnumerable<IEnumerable<T>> ParmLists) {
    if (ParmLists.Count() == 0)
        yield return Enumerable.Empty<T>();
    else {
        var rest = ParametersWithEmpty2(ParmLists.Skip(1));
        foreach (var r in rest)
            yield return r; // empty.Concat(r)
        foreach (var p in ParmLists.First()) {
            foreach (var r in rest)
                yield return r.Prepend(p); // p.Concat(r)
        }
    }
}
NetMage
  • 26,163
  • 3
  • 34
  • 55
  • Thanks for the correct answer. However, I am here to learn. I was hoping for some more background about how to put something like this together so I can work through this on my own. For example, I have a (separate) special case where all 3 parameters are required. How would I modify this to do that? – NightOwl888 Jul 03 '17 at 23:09
  • Well, this was quite tricky, but conceptually the comment by @jdweng is correct - you want to nest looping through the levels. Think about the base case (with just f,g,h being passed in) and what should be return then the next case up (with d,e and f,g,h) - what should that return? What if it was just (d and f,g)? The original issue is there is no empty choice (imagine each level has an empty before the choices) so I simulate that with the base case being `0` and the extra `yield` of `rest` without the current level. I've added code to not have the empty case included. – NetMage Jul 03 '17 at 23:28
  • I added some comments to try to show the implicit empty in the original function. – NetMage Jul 03 '17 at 23:39
  • I found a bug. It is possible for `ParamLists` to be empty, so `ParamLists.First()` will fail. – NightOwl888 Jul 04 '17 at 04:59
  • I assume you mean in the `Parameters` function? If so, you can just add the first two lines of the `ParametersWithEmpty` to it: `if (ParmLists.Count() == 0) yield break;` at the beginning of the function. – NetMage Jul 05 '17 at 21:43
1

Try following :

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace ConsoleApplication1
{
    class Program
    {
        public static List<string[]> temp = new List<string[]>
        {
            // The order of these groups is important
            new string[] { "a", "b", "c" },
            new string[] { "d", "e" },
            new string[] { "f", "g", "h" }
        };
        static void Main(string[] args)
        {
            Recursive(0, new List<string>());
            Console.ReadLine();
        }
        public static void Recursive(int level, List<string> output)
        {
            if (level < temp.Count)
            {
                foreach (string value in temp[level])
                {
                    List<string> newList = new List<string>();
                    newList.AddRange(output);
                    newList.Add(value);
                    Console.WriteLine(string.Join(",", newList));
                    Recursive(level + 1, newList);
                }
            }
        }
    }
}

To get additional combinations starting with 2nd and 3rd level change main()

        static void Main(string[] args)
        {
            for (int i = 0; i < temp.Count; i++)
            {
                Recursive(i, new List<string>());
            }
            Console.ReadLine();
        }
jdweng
  • 33,250
  • 2
  • 15
  • 20
  • Thanks. This is a good start. But it only includes the 3 letter combinations, it doesn't include the 2 or 1 letter combinations. – NightOwl888 Jul 03 '17 at 22:10
  • I changed code. Just moving a few lines around prints everything. – jdweng Jul 03 '17 at 22:23
  • +1, although NetMage got the answer first. I *do* need to do what you had in the original solution (some parameters are always required), so kudos for that solution as well. – NightOwl888 Jul 03 '17 at 22:28
  • Hmm...I just tried it and it still isn't printing out all of the valid combinations. It is stopping at letter c for the single letter combinations and doesn't go further. The doubles are missing combos too. – NightOwl888 Jul 03 '17 at 22:32
  • It is printing out all combinations taking one from "a,b,c" then one from "d,e" and finally one from "f,g,h". I modified main in above to get additional combos – jdweng Jul 04 '17 at 00:26
1

Here is a Linq expression that enumerates all combinations. You can try it out in online C# repl. The idea is to use accumulator "a" to gather all combinations by going through each item of your temp collection "b" and add elements of "b" (single item combination) as well as add to every accumulated combination so far. Pretty hairy expression though.

var combinations = temp.Aggregate(Enumerable.Empty<IEnumerable<string>>(), (a, b) => a
          .Concat(b.Select(i => Enumerable.Repeat(i, 1)))
          .Concat(a.SelectMany(i => b.Select(j => i.Concat(Enumerable.Repeat(j, 1))))));
  • Nice use of `Aggregate`. I converted it to `Enumerable` instead of string arrays and it still works fine. – NetMage Jul 04 '17 at 00:26
  • oh yea, even better, you're not creating and abandoning collections on each aggregate step, you just chaining up iterators (state machines) – Ievgen Goichuk Jul 04 '17 at 10:21