Add class for calculating combinations and a unitTest

This commit is contained in:
Flamefire committed 2014-12-14 14:28:02 +01:00
1 parent 1ba0d429a1
commit 1bb296c04d
6 files changed
+259 -17

No files matched your search

@@ -0,0 +1,157 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
namespace VocaluxeLib.Utils.Combinatorics
{
/// <summary>
/// Calculates all combinations (without repetition) of a given input sequence <br />
/// Example: {A B C}, select 2 --> {A B}, {A C}, {B C}
/// </summary>
/// <typeparam name="T">Type of the input elements</typeparam>
public class CCombinations<T> : IEnumerable<List<T>>
{
private readonly List<T> _Values;
private readonly int _NumSelected;
private readonly int _Count;
/// <summary>
/// Construct combinations for the input sequence when selecting numSelected elements <br />
/// The values should not contain duplicates as this will result in duplicates in the output
/// </summary>
/// <param name="values">Input sequence</param>
/// <param name="numSelected">Number of elements to select</param>
public CCombinations(List<T> values, int numSelected)
{
_Values = values;
_NumSelected = numSelected;
// This also acts as a parameter check
_Count = Count(_Values.Count, _NumSelected);
}
/// <summary>
/// Calculates how many combinations are possible when k out of n elements are selected
/// </summary>
/// <param name="n">Number of elements</param>
/// <param name="k">Number of selected elements</param>
/// <returns></returns>
public static int Count(int n, int k)
{
if (n < 0 || k < 0)
throw new ArgumentException("Parameters must not be negative");
if (n < k)
return 0;
// Try to optimize because (n k) = (n n-k)
if (n - k < k)
k = n - k;
if (k == 0) // or k == n but both are the same after the optimization above
return 1;
long result = n;
for (long i = 1; i < k; i++)
{
checked
{
result = result * (n - i) / (i + 1);
}
}
return (int)result;
}
/// <summary>
/// Gets the number of combinations that are generated
/// </summary>
/// <returns></returns>
public int Count()
{
return _Count;
}
/// <summary>
/// Gets all possible combinations as a list
/// </summary>
/// <returns></returns>
public List<List<T>> GetAll()
{
List<List<T>> result = new List<List<T>>(Count());
result.AddRange(this);
return result;
}
public IEnumerator<List<T>> GetEnumerator()
{
return new CEnumerator(this);
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
private class CEnumerator : IEnumerator<List<T>>
{
private readonly int _N;
private readonly int _K;
private readonly CCombinations<T> _Parent;
private int[] _CurSet;
public CEnumerator(CCombinations<T> parent)
{
_N = parent._Values.Count;
_K = parent._NumSelected;
_Parent = parent;
Debug.Assert(_N >= 0 && _K >= 0);
}
public void Dispose() {}
public bool MoveNext()
{
if (_N < _K)
return false;
if (_CurSet == null)
{
_CurSet = new int[_K];
for (int i = 0; i < _K; i++)
_CurSet[i] = i;
return true;
}
// Check if we reached the end
if (_K == 0 || _CurSet[0] == _N - _K)
return false;
// Search for element to increment
int idx = _K - 1;
while (idx > 0 && _CurSet[idx] == _N - _K + idx)
idx--;
_CurSet[idx]++;
// Increment all elements "right" of this to 1+their left neighbour
for (int j = idx; j < _K - 1; j++)
_CurSet[j + 1] = _CurSet[j] + 1;
return true;
}
public void Reset()
{
_CurSet = null;
}
public List<T> Current
{
get
{
List<T> result = new List<T>(_K);
for (int i = 0; i < _K; i++)
result.Add(_Parent._Values[_CurSet[i]]);
return result;
}
}
object IEnumerator.Current
{
get { return Current; }
}
}
}
}
+1
View File
@@ -108,6 +108,7 @@
<Compile Include="Draw\CTextureRef.cs" />
<Compile Include="Menu\IThemeable.cs" />
<Compile Include="Menu\SingNotes\CNoteBars.cs" />
<Compile Include="Utils\Combinatorics\CCombinations.cs" />
<Compile Include="XmlStructs.cs" />
<Compile Include="Xml\CTypeExtensions.cs" />
<Compile Include="Xml\CXmlDeserializer.cs" />
+21
View File
@@ -0,0 +1,21 @@
using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace VocaluxeTests
{
static class CTestHelpers
{
public static void AssertFail<T>(Action test) where T : Exception
{
try
{
test.Invoke();
Assert.Fail("Exception " + typeof(T).Name + " not thrown!");
}
catch (Exception e)
{
Assert.IsInstanceOfType(e, typeof(T));
}
}
}
}
+75
View File
@@ -0,0 +1,75 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using VocaluxeLib.Utils.Combinatorics;
namespace VocaluxeTests
{
[TestClass]
public class CCombinatoricsTest
{
[TestMethod]
public void TestCounts()
{
Assert.AreEqual(4, CCombinations<int>.Count(4, 3));
Assert.AreEqual(6, CCombinations<int>.Count(4, 2));
Assert.AreEqual(66, CCombinations<int>.Count(12, 2));
// Corner cases
for (int i = 1; i < 12; i++)
{
Assert.AreEqual(1, CCombinations<int>.Count(i, 0));
Assert.AreEqual(1, CCombinations<int>.Count(i, i));
Assert.AreEqual(0, CCombinations<int>.Count(i, i + 1));
}
// Check identity (n k) = (n n-k)
for (int i = 0; i <= 12; i++)
Assert.AreEqual(CCombinations<int>.Count(12, i), CCombinations<int>.Count(12, 12 - i));
CTestHelpers.AssertFail<ArgumentException>(() => CCombinations<int>.Count(-1, -1));
CTestHelpers.AssertFail<ArgumentException>(() => CCombinations<int>.Count(-1, 1));
CTestHelpers.AssertFail<ArgumentException>(() => CCombinations<int>.Count(1, -1));
}
// ReSharper disable UnusedParameter.Local
private static void _CheckResults<T>(CCombinations<T> combs, List<T> input)
// ReSharper restore UnusedParameter.Local
{
List<List<T>> results = new List<List<T>>();
foreach (List<T> comb in combs)
{
foreach (List<T> other in results)
Assert.IsFalse(other.SequenceEqual(comb), "Result must be unique");
for (int i = 0; i < comb.Count - 1; i++)
{
Assert.IsTrue(comb.IndexOf(comb[i], i + 1) < 0, "Repetition not allowed");
Assert.IsTrue(input.Contains(comb[i]), "Output must be in input sequence");
}
results.Add(comb);
}
Assert.AreEqual(combs.Count(), results.Count);
List<List<T>> results2 = combs.GetAll();
Assert.AreEqual(results.Count, results2.Count);
for (int i = 0; i < results2.Count; i++)
Assert.IsTrue(results[i].SequenceEqual(results2[i]));
}
[TestMethod]
public void TestResults()
{
Random rand = new Random();
for (int n = 0; n < 8; n++)
{
for (int k = 0; k <= n; k++)
{
List<int> collection = new List<int>(n);
int r = rand.Next(1000);
for (int i = 0; i < n; i++)
collection.Add(i + r);
CCombinations<int> combs = new CCombinations<int>(collection, k);
Assert.AreEqual(CCombinations<int>.Count(n, k), combs.Count());
_CheckResults(combs, collection);
}
}
}
}
}
+2
View File
@@ -70,7 +70,9 @@
</Otherwise>
</Choose>
<ItemGroup>
<Compile Include="CTestHelpers.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="CombinatoricsTest.cs" />
<Compile Include="XmlSerializerTest.cs" />
</ItemGroup>
<ItemGroup>
+3 -17
View File
@@ -91,8 +91,7 @@ namespace VocaluxeTests
private struct SDefaultSub
{
[DefaultValue(111)]
public int I;
[DefaultValue(111)] public int I;
}
private struct SDefault
@@ -107,23 +106,10 @@ namespace VocaluxeTests
#pragma warning restore 169
#pragma warning restore 649
private static void _AssertFail<T>(Action test) where T : Exception
{
try
{
test.Invoke();
Assert.Fail("Exception " + typeof(T).Name + " not thrown!");
}
catch (Exception e)
{
Assert.IsInstanceOfType(e, typeof(T));
}
}
private static void _AssertFail<T, T2>(String xmlString) where T2 : Exception where T : new()
{
var deserializer = new CXmlDeserializer();
_AssertFail<T2>(() => deserializer.DeserializeString<T>(xmlString));
CTestHelpers.AssertFail<T2>(() => deserializer.DeserializeString<T>(xmlString));
}
private static T _AssertSerDeserMatch<T>(string xmlString) where T : new()
@@ -454,7 +440,7 @@ namespace VocaluxeTests
Assert.AreEqual(foo.D, 666);
Assert.AreEqual(foo.Sub.I, 111);
string newXml = new CXmlSerializer().Serialize(foo);
Assert.AreEqual(_Head +@"<root>
Assert.AreEqual(_Head + @"<root>
<Sub />
</root>", newXml);
}