From 1bb296c04d08d200fe054a7ef5bbf4120a1aadfc Mon Sep 17 00:00:00 2001 From: Flamefire Date: Sun, 14 Dec 2014 14:28:02 +0100 Subject: [PATCH] Add class for calculating combinations and a unitTest --- .../Utils/Combinatorics/CCombinations.cs | 157 ++++++++++++++++++ VocaluxeLib/VocaluxeLib.csproj | 1 + VocaluxeTests/CTestHelpers.cs | 21 +++ VocaluxeTests/CombinatoricsTest.cs | 75 +++++++++ VocaluxeTests/VocaluxeTests.csproj | 2 + VocaluxeTests/XmlSerializerTest.cs | 20 +-- 6 files changed, 259 insertions(+), 17 deletions(-) create mode 100644 VocaluxeLib/Utils/Combinatorics/CCombinations.cs create mode 100644 VocaluxeTests/CTestHelpers.cs create mode 100644 VocaluxeTests/CombinatoricsTest.cs diff --git a/VocaluxeLib/Utils/Combinatorics/CCombinations.cs b/VocaluxeLib/Utils/Combinatorics/CCombinations.cs new file mode 100644 index 00000000..5a147fc2 --- /dev/null +++ b/VocaluxeLib/Utils/Combinatorics/CCombinations.cs @@ -0,0 +1,157 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; + +namespace VocaluxeLib.Utils.Combinatorics +{ + /// + /// Calculates all combinations (without repetition) of a given input sequence
+ /// Example: {A B C}, select 2 --> {A B}, {A C}, {B C} + ///
+ /// Type of the input elements + public class CCombinations : IEnumerable> + { + private readonly List _Values; + private readonly int _NumSelected; + private readonly int _Count; + + /// + /// Construct combinations for the input sequence when selecting numSelected elements
+ /// The values should not contain duplicates as this will result in duplicates in the output + ///
+ /// Input sequence + /// Number of elements to select + public CCombinations(List values, int numSelected) + { + _Values = values; + _NumSelected = numSelected; + // This also acts as a parameter check + _Count = Count(_Values.Count, _NumSelected); + } + + /// + /// Calculates how many combinations are possible when k out of n elements are selected + /// + /// Number of elements + /// Number of selected elements + /// + 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; + } + + /// + /// Gets the number of combinations that are generated + /// + /// + public int Count() + { + return _Count; + } + + /// + /// Gets all possible combinations as a list + /// + /// + public List> GetAll() + { + List> result = new List>(Count()); + result.AddRange(this); + return result; + } + + public IEnumerator> GetEnumerator() + { + return new CEnumerator(this); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + private class CEnumerator : IEnumerator> + { + private readonly int _N; + private readonly int _K; + private readonly CCombinations _Parent; + private int[] _CurSet; + + public CEnumerator(CCombinations 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 Current + { + get + { + List result = new List(_K); + for (int i = 0; i < _K; i++) + result.Add(_Parent._Values[_CurSet[i]]); + return result; + } + } + object IEnumerator.Current + { + get { return Current; } + } + } + } +} \ No newline at end of file diff --git a/VocaluxeLib/VocaluxeLib.csproj b/VocaluxeLib/VocaluxeLib.csproj index b452b5e5..cd947ae2 100644 --- a/VocaluxeLib/VocaluxeLib.csproj +++ b/VocaluxeLib/VocaluxeLib.csproj @@ -108,6 +108,7 @@ + diff --git a/VocaluxeTests/CTestHelpers.cs b/VocaluxeTests/CTestHelpers.cs new file mode 100644 index 00000000..4ad00bf3 --- /dev/null +++ b/VocaluxeTests/CTestHelpers.cs @@ -0,0 +1,21 @@ +using System; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace VocaluxeTests +{ + static class CTestHelpers + { + public static void AssertFail(Action test) where T : Exception + { + try + { + test.Invoke(); + Assert.Fail("Exception " + typeof(T).Name + " not thrown!"); + } + catch (Exception e) + { + Assert.IsInstanceOfType(e, typeof(T)); + } + } + } +} \ No newline at end of file diff --git a/VocaluxeTests/CombinatoricsTest.cs b/VocaluxeTests/CombinatoricsTest.cs new file mode 100644 index 00000000..50d274e1 --- /dev/null +++ b/VocaluxeTests/CombinatoricsTest.cs @@ -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.Count(4, 3)); + Assert.AreEqual(6, CCombinations.Count(4, 2)); + Assert.AreEqual(66, CCombinations.Count(12, 2)); + // Corner cases + for (int i = 1; i < 12; i++) + { + Assert.AreEqual(1, CCombinations.Count(i, 0)); + Assert.AreEqual(1, CCombinations.Count(i, i)); + Assert.AreEqual(0, CCombinations.Count(i, i + 1)); + } + // Check identity (n k) = (n n-k) + for (int i = 0; i <= 12; i++) + Assert.AreEqual(CCombinations.Count(12, i), CCombinations.Count(12, 12 - i)); + CTestHelpers.AssertFail(() => CCombinations.Count(-1, -1)); + CTestHelpers.AssertFail(() => CCombinations.Count(-1, 1)); + CTestHelpers.AssertFail(() => CCombinations.Count(1, -1)); + } + + // ReSharper disable UnusedParameter.Local + private static void _CheckResults(CCombinations combs, List input) + // ReSharper restore UnusedParameter.Local + { + List> results = new List>(); + foreach (List comb in combs) + { + foreach (List 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> 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 collection = new List(n); + int r = rand.Next(1000); + for (int i = 0; i < n; i++) + collection.Add(i + r); + CCombinations combs = new CCombinations(collection, k); + Assert.AreEqual(CCombinations.Count(n, k), combs.Count()); + _CheckResults(combs, collection); + } + } + } + } +} \ No newline at end of file diff --git a/VocaluxeTests/VocaluxeTests.csproj b/VocaluxeTests/VocaluxeTests.csproj index b3a0dade..3b8f65dc 100644 --- a/VocaluxeTests/VocaluxeTests.csproj +++ b/VocaluxeTests/VocaluxeTests.csproj @@ -70,7 +70,9 @@ + + diff --git a/VocaluxeTests/XmlSerializerTest.cs b/VocaluxeTests/XmlSerializerTest.cs index 6b59e5ec..56e45410 100644 --- a/VocaluxeTests/XmlSerializerTest.cs +++ b/VocaluxeTests/XmlSerializerTest.cs @@ -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(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(String xmlString) where T2 : Exception where T : new() { var deserializer = new CXmlDeserializer(); - _AssertFail(() => deserializer.DeserializeString(xmlString)); + CTestHelpers.AssertFail(() => deserializer.DeserializeString(xmlString)); } private static T _AssertSerDeserMatch(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 +@" + Assert.AreEqual(_Head + @" ", newXml); }