Here I go again. I have been finding a fairly common pattern in business logic code. And that pattern looks like this: int sprocketCount = datastore.GetSprocketOrders(parameters).Distinct().Count();
I decided I wanted to build DistinctCount()
(again from "first principles") as Distinct()
will create a second enumerable off of the first before Count()
is executed. With that, here are four variations of DistinctCount()
:
public static int DistinctCount<TSource>(this IEnumerable<TSource> source) =>
source?.DistinctCount((IEqualityComparer<TSource>)null) ?? throw new ArgumentNullException(nameof(source));
public static int DistinctCount<TSource>(this IEnumerable<TSource> source, IEqualityComparer<TSource> comparer)
{
if (source is null)
{
throw new ArgumentNullException(nameof(source));
}
ISet<TSource> set = new HashSet<TSource>(comparer);
int num = 0;
using (IEnumerator<TSource> enumerator = source.GetEnumerator())
{
while (enumerator.MoveNext())
{
// ReSharper disable once AssignNullToNotNullAttribute
if (set.Add(enumerator.Current))
{
checked
{
++num;
}
}
}
}
return num;
}
public static int DistinctCount<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate)
{
if (source is null)
{
throw new ArgumentNullException(nameof(source));
}
if (predicate is null)
{
throw new ArgumentNullException(nameof(predicate));
}
return source.DistinctCount(predicate, null);
}
public static int DistinctCount<TSource>(
this IEnumerable<TSource> source,
Func<TSource, bool> predicate,
IEqualityComparer<TSource> comparer)
{
if (source is null)
{
throw new ArgumentNullException(nameof(source));
}
if (predicate is null)
{
throw new ArgumentNullException(nameof(predicate));
}
ISet<TSource> set = new HashSet<TSource>(comparer);
int num = 0;
foreach (TSource source1 in source)
{
if (predicate(source1) && set.Add(source1))
{
checked
{
++num;
}
}
}
return num;
}
And here are a battery of unit tests:
[TestMethod]
[ExpectedException(typeof(ArgumentNullException))]
public void TestNull()
{
int[] nullArray = null;
// ReSharper disable once ExpressionIsAlwaysNull
Assert.AreEqual(0, nullArray.DistinctCount());
}
[TestMethod]
[ExpectedException(typeof(ArgumentNullException))]
public void TestNullPredicate()
{
int[] zero = Array.Empty<int>();
Func<int, bool> predicate = null;
// ReSharper disable once ExpressionIsAlwaysNull
Assert.AreEqual(0, zero.DistinctCount(predicate));
}
[TestMethod]
public void TestZero()
{
int[] zero = Array.Empty<int>();
Assert.AreEqual(0, zero.DistinctCount());
}
[TestMethod]
public void TestOne()
{
int[] one = { 1 };
Assert.AreEqual(1, one.DistinctCount());
}
[TestMethod]
public void TestOneWithDuplicate()
{
int[] oneWithDuplicate = { 1, 1, 1, 1, 1 };
Assert.AreEqual(1, oneWithDuplicate.DistinctCount());
}
[TestMethod]
public void TestTwo()
{
int[] two = { 1, 2 };
Assert.AreEqual(2, two.DistinctCount());
}
[TestMethod]
public void TestTwoWithDuplicate()
{
int[] twoWithDuplicate = { 2, 1, 2, 1, 2, 2, 1, 2 };
Assert.AreEqual(2, twoWithDuplicate.DistinctCount());
}
[TestMethod]
public void TestTwoWithDuplicateUsingPredicate()
{
int[] twoWithDuplicate = { 2, 1, 3, 2, 1, 2, 2, 1, 2, 3 };
Assert.AreEqual(2, twoWithDuplicate.DistinctCount(x => x > 1));
}
[TestMethod]
public void TestTwoUsingNullComparer()
{
int[] two = { 1, 2 };
IEqualityComparer<int> comparer = null;
// ReSharper disable once ExpressionIsAlwaysNull
Assert.AreEqual(2, two.DistinctCount(comparer));
}
[TestMethod]
public void TestOneWithDuplicateUsingComparer()
{
string[] one = { "one", "One", "oNe", "ONE" };
Assert.AreEqual(1, one.DistinctCount(StringComparer.InvariantCultureIgnoreCase));
}
[TestMethod]
public void TestTwoWithDuplicateUsingPredicateAndComparer()
{
string[] two = { "one", "two", "One", "Two", "oNe", "TWO", "ONE", "tWo", "three" };
Assert.AreEqual(2, two.DistinctCount(x => x != "three", StringComparer.InvariantCultureIgnoreCase));
}
As always, ooking for overall review - is the code readable, maintainable, performant? Do the tests have the right amount of coverage or are there more particular cases to consider?