This is a variation of an answer I gave to question How to iterate lists with different lengths to find all permutations?, which itself was a variant of on an answer given by Michael Liu's in IEnumerable and Recursion using yield return
It works by creating an Enumerator which returns lists of all the allowed combinations strings in a foreach loop.
Using the Enumerator is as easy as this:
using System;
using System.Collections.Generic;
namespace SO69209314_list_combinations
{
class Program
{
static void Main(string[] args)
{
List<Product> productList = new List<Product>()
{
new Product("Product1", 1,2),
new Product("Product2", 2,3),
new Product("Product3", 1,2),
};
ProductPermuter permuter = new ProductPermuter(productList);
int i = 0;
foreach (IEnumerable<string> list in permuter)
{
Console.WriteLine("{0}:{1}", i, string.Join(",", list));
i++;
}
Console.ReadKey();
}
}
}
This is the Enumerator (including a Product class storing the string and min and max limits). It works by keeping track of the number of Products of each type (at each level), iterating the number of each product. When a product is finished, it resets its iterator and iterates the previous product again, etc. until all levels are finished. So it will eventually return all the allowed combinations.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SO69209314_list_combinations
{
public class Product : IEnumerable<int>
{
public Product(string name, int min, int max)
{
this.name = name ?? throw new ArgumentNullException(nameof(name));
if ((min > max) || (min < 0) || (max < 0)) { throw new InvalidOperationException("Bad min or max"); }
this.min = min;
this.max = max;
}
public string name { get; private set; }
public int min { get; private set; }
public int max { get; private set; }
public IEnumerator<int> GetEnumerator()
{
for (int i = min; i <= max; i++)
{
yield return i;
}
}
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
public class ProductPermuter : IEnumerable<IEnumerable<string>>
{
private int count;
private Product[] products;
public ProductPermuter(IEnumerable<Product> products_)
{
if (object.ReferenceEquals(products_, null))
{
throw new ArgumentNullException(nameof(products_));
}
products = products_.ToArray();
count = products.Length;
for (int i = 0; i < count; i++)
{
if (object.ReferenceEquals(products[i], null))
{
throw new NullReferenceException(string.Format("{0}[{1}] is null.", nameof(products_), i));
}
}
}
// A variant of Michael Liu's answer in StackOverflow
// https://stackoverflow.com/questions/2055927/ienumerable-and-recursion-using-yield-return
public IEnumerator<IEnumerable<string>> GetEnumerator()
{
IEnumerable<string>[] currentList = new IEnumerable<string>[count];
int level = 0;
var enumerators = new Stack<IEnumerator<int>>();
IEnumerator<int> enumerator = products[level].GetEnumerator();
try
{
while (true)
{
if (enumerator.MoveNext())
{
int n = enumerator.Current;
currentList[level] = Enumerable.Repeat(products[level].name, n).ToList();
level++;
if (level >= count)
{
level--;
List<string> list = new List<string>();
for (int i = 0; i < count; i++)
{
list.AddRange(currentList[i]);
}
yield return list;
}
else
{
enumerators.Push(enumerator);
enumerator = products[level].GetEnumerator();
}
}
else
{
if (level == 0)
{
yield break;
}
else
{
enumerator.Dispose();
enumerator = enumerators.Pop();
level--;
}
}
}
}
finally
{
// Clean up in case of an exception.
enumerator?.Dispose();
while (enumerators.Count > 0)
{
enumerator = enumerators.Pop();
enumerator.Dispose();
}
}
}
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
}
This is the output:
0:Product1,Product2,Product2,Product3
1:Product1,Product2,Product2,Product3,Product3
2:Product1,Product2,Product2,Product2,Product3
3:Product1,Product2,Product2,Product2,Product3,Product3
4:Product1,Product1,Product2,Product2,Product3
5:Product1,Product1,Product2,Product2,Product3,Product3
6:Product1,Product1,Product2,Product2,Product2,Product3
7:Product1,Product1,Product2,Product2,Product2,Product3,Product3
If you modify the input to have 0 of Product 3
...
new Product("Product3", 0, 2),
...
then the output is
0:Product1,Product2,Product2
1:Product1,Product2,Product2,Product3
2:Product1,Product2,Product2,Product3,Product3
3:Product1,Product2,Product2,Product2
4:Product1,Product2,Product2,Product2,Product3
5:Product1,Product2,Product2,Product2,Product3,Product3
6:Product1,Product1,Product2,Product2
7:Product1,Product1,Product2,Product2,Product3
8:Product1,Product1,Product2,Product2,Product3,Product3
9:Product1,Product1,Product2,Product2,Product2
10:Product1,Product1,Product2,Product2,Product2,Product3
11:Product1,Product1,Product2,Product2,Product2,Product3,Product3
If zero elements are always allowed, even if min is 1 or 2, then the Enumerator of Product can be changed as follows:
public IEnumerator<int> GetEnumerator()
{
// 0 is always allowed
yield return 0;
int minForLoop = Math.Max(1, min);
for (int i = minForLoop; i <= max; i++)
{
yield return i;
}
}
and the output becomes:
0:
1:Product3
2:Product3,Product3
3:Product2,Product2
4:Product2,Product2,Product3
5:Product2,Product2,Product3,Product3
6:Product2,Product2,Product2
7:Product2,Product2,Product2,Product3
8:Product2,Product2,Product2,Product3,Product3
9:Product1
10:Product1,Product3
11:Product1,Product3,Product3
12:Product1,Product2,Product2
13:Product1,Product2,Product2,Product3
14:Product1,Product2,Product2,Product3,Product3
15:Product1,Product2,Product2,Product2
16:Product1,Product2,Product2,Product2,Product3
17:Product1,Product2,Product2,Product2,Product3,Product3
18:Product1,Product1
19:Product1,Product1,Product3
20:Product1,Product1,Product3,Product3
21:Product1,Product1,Product2,Product2
22:Product1,Product1,Product2,Product2,Product3
23:Product1,Product1,Product2,Product2,Product3,Product3
24:Product1,Product1,Product2,Product2,Product2
25:Product1,Product1,Product2,Product2,Product2,Product3
26:Product1,Product1,Product2,Product2,Product2,Product3,Product3