This question is virtually the same as this SO post, only I'm looking for a VB.NET (.NET 4) solution. I've spun my wheels long enough trying to come up with a generic solution to solving this "power set" problem.
Given:
Dim choices As IEnumerable(Of String) = {"Coffee", "Tea", "Milk", "Cookies"}
Dim choiceSets = choices.CombineAll()
I'm looking for choiceSets
to be an IEnumerable(Of IEnumerable(Of T))
so that I can do something like:
For each choiceSet in choiceSets
Console.WriteLine(String.Join(", ", choiceSet))
Next
And get results that look like:
Coffee
Tea
Milk
Cookies
Coffee, Tea
Coffee, Milk
Coffee, Cookies
Tea, Milk
Tea, Cookies
Milk, Cookies
Coffee, Tea, Milk
Coffee, Tea, Cookies
Coffee, Milk, Cookies
Tea, Milk, Cookies
Coffee, Tea, Milk, Cookies
As you can see, this is every non-repeating combination from the source IEnumerable(Of T)
(which could have 1 to many items in it - this example only had 4), it operates based on the order of the items in the source IEnumerable(Of T)
, and each item in the list is >= the previous item in terms of number of items in the inner IEnumerable(Of T)
.
For what it's worth, this is not homework; though it sure does feel like it.
EDIT: Updated the example so it does not look like the result is alphabetically sorted, to stress that the source IEnumerable(Of T)
's existing order is used and added a 4th choice to clarify the sorting requirement within each set.
Here's a pure Linq solution, inspired by Eric Lippert's blog post about computing a cartesian product. I modified the CartesianProduct
method slightly so that it returns combinations:
public static IEnumerable<IEnumerable<T>> Combinations<T>(this IEnumerable<IEnumerable<T>> sequences)
{
IEnumerable<IEnumerable<T>> emptyProduct = new[] { Enumerable.Empty<T>() };
return sequences.Aggregate(
emptyProduct,
(accumulator, sequence) =>
from accseq in accumulator
// Exclude items that were already picked
from item in sequence.Except(accseq)
// Enforce ascending order to avoid same sequence in different order
where !accseq.Any() || Comparer<T>.Default.Compare(item, accseq.Last()) > 0
select accseq.Concat(new[] {item})).ToArray();
}
Based on this extension method, you can produce the desired result as follows:
IEnumerable<string> items = new[] {"Coffee", "Tea", "Milk"};
IEnumerable<IEnumerable<string>> result =
Enumerable.Range(1, items.Count())
.Aggregate(
Enumerable.Empty<IEnumerable<string>>(),
(acc, i) =>
acc.Concat(Enumerable.Repeat(items, i).Combinations()));
(it concatenates all combinations of 1, 2... N items)
Note that it's probably not a very efficient solution, but I think it's an interesting use of Linq...
EDIT: here's a new version of the Combinations
method that maintains the original order:
public static IEnumerable<IEnumerable<T>> Combinations<T>(this IEnumerable<IEnumerable<T>> sequences)
{
var indexedSequences = sequences.Select(seq => seq.Select((item, idx) => new IndexedItem<T>(item, idx)));
IEnumerable<IEnumerable<IndexedItem<T>>> emptyProduct = new[] { Enumerable.Empty<IndexedItem<T>>() };
var indexedResult =
indexedSequences.Aggregate(
emptyProduct,
(accumulator, sequence) =>
from accseq in accumulator
// Exclude items that were already picked
from item in sequence.Except(accseq)
// Enforce ascending order of indexes to avoid same sequence in different order
where !accseq.Any() || item.Index > accseq.Last().Index
select accseq.Concat(new[] {item})).ToArray();
return indexedResult.Select(seq => seq.Select(i => i.Item));
}
class IndexedItem<T>
{
public IndexedItem(T item, int index)
{
this.Item = item;
this.Index = index;
}
public T Item { get; private set; }
public int Index { get; set; }
}
Probably even more inefficient than the previous version, but it gets the job done...
In case it's of use to anyone else, I've converted the original C# extension Thomas Levesque posted to VB.NET:
<System.Runtime.CompilerServices.Extension()> _
Public Function Combinations(Of T)(ByVal sequences As IEnumerable(Of IEnumerable(Of T))) As IEnumerable(Of IEnumerable(Of T))
Dim seed As IEnumerable(Of IEnumerable(Of T)) = { Enumerable.Empty(Of T) }
Dim r = sequences.Aggregate(seed,
Function(ByVal accumulator, ByVal sequence) _
From accseq In accumulator _
From item In sequence.Except(accseq) _
Where (Not accseq.Any()) OrElse Comparer(Of T).Default.Compare(item, accseq.Last()) > 0 _
Select accseq.Concat( {item} ) ).ToArray()
Return r
End Function
It's a bit awkward usage to have to call Repeat n times to generate a repeated Enumerable containing the set of all possible values n times, where n is the number of elements in each resulting unique combination of T, but it gets the job done. Order of the results didn't matter for me, so I didn't convert the 'indexed' version posted later.
Here's my usage of the extension, which operates on an array of Integers instead of Strings, and gets me the 'empty' set with no elements in it and the 'full' (or original) set
Dim allRolesArray = {1,4,5,2,0}
Dim comboCountValues = Enumerable.Range(0, allRolesArray.Count()+1)
Dim allRoleCombos = comboCountValues.Aggregate(
Enumerable.Empty(Of IEnumerable(Of Integer))(),
Function (acc, i) acc.Concat(Enumerable.Repeat(allRolesArray, i).Combinations() ) ).ToList
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With