I'm mostly with Heslacher's solution but in don't let to any of the collections to be null and i'm preparing to have null values in the collections. To spare some time i don't bother to create arrays from the input sources too.
I also renamed the extensions method to EquivalentWith because SetEqual means a different thing.
Tests
I have used the provided unit test as base to write my ones so here are they:
[TestClass]
public class EquivalentWithTests
{
[TestMethod]
[ExpectedException(typeof(ArgumentNullException))]
public void BothEnumerableNullWillNotPass()
{
IEnumerable<int> source = null;
IEnumerable<int> other = null;
source.EquivalentWith(other);
}
[TestMethod]
[ExpectedException(typeof(ArgumentNullException))]
public void SourceEnumerableNullWillNotPass()
{
IEnumerable<int> source = null;
IEnumerable<int> other = new[] { 1, 2, 3, 4, 5, 6 };
source.EquivalentWith(other);
}
[TestMethod]
[ExpectedException(typeof(ArgumentNullException))]
public void OtherEnumerableNullWillNotPass()
{
IEnumerable<int> source = new[] { 1, 2, 3, 4, 5, 6 };
IEnumerable<int> other = null;
source.EquivalentWith(other);
}
[TestMethod]
[ExpectedException(typeof(ArgumentNullException))]
public void SameEnumerableNullWillPass()
{
IEnumerable<int> source = null;
source.EquivalentWith(source);
}
[TestMethod]
public void SameEnumerableWillPass()
{
IEnumerable<int> source = new[] { 1, 2, 3, 4, 5, 6 };
Assert.IsTrue(source.EquivalentWith(source));
}
[TestMethod]
public void BothEnumerableSameWillPass()
{
IEnumerable<int> source = new[] { 1, 2, 3, 4, 5, 6 };
IEnumerable<int> other = new[] { 1, 2, 3, 4, 5, 6 };
Assert.IsTrue(source.EquivalentWith(other));
}
[TestMethod]
public void EnumerableDifferentSizeWillNotPass()
{
IEnumerable<int> source = Enumerable.Range(0, 5000);
IEnumerable<int> other = Enumerable.Range(0, 500);
Assert.IsFalse(source.EquivalentWith(other));
}
[TestMethod]
public void CollectionsDifferentSizeWillNotPass()
{
var source = new List<int>(Enumerable.Range(0, 50));
var other = new List<int>(Enumerable.Range(0, 40));
Assert.IsFalse(source.EquivalentWith(other));
}
[TestMethod]
public void BothEnumerableDifferentWillNotPass()
{
IEnumerable<int> source = new[] { 1, 2, 3, 4, 5, 6 };
IEnumerable<int> other = new[] { 3, 4, 5, 6, 7, 8 };
Assert.IsFalse(source.EquivalentWith(other));
}
[TestMethod]
public void OtherEnumerableReversedWillPass()
{
IEnumerable<int> source = new[] { 1, 2, 3, 4, 5, 6 };
IEnumerable<int> other = new[] { 6, 5, 4, 3, 2, 1 };
Assert.IsTrue(source.EquivalentWith(other));
}
[TestMethod]
public void OtherEnumerableReversedBothContainingNonUniqueAndUniqueWillPass()
{
IEnumerable<int> source = new[] { 1, 2, 2, 4, 5, 6 };
IEnumerable<int> other = new[] { 6, 5, 4, 2, 2, 1 };
Assert.IsTrue(source.EquivalentWith(other));
}
[TestMethod]
public void OtherEnumerableReversedBothContainingNonUniqueAndUniqueButDifferentWillPass()
{
IEnumerable<int> source = new[] { 1, 2, 2, 4, 5, 6 };
IEnumerable<int> other = new[] { 6, 5, 4, 4, 2, 1 };
Assert.IsFalse(source.EquivalentWith(other));
}
[TestMethod]
public void OtherEnumerableReversedBothContainingUniqueAndNullButDifferentWillPass()
{
var source = new int?[] { 1, 2, 3, 4, 5, 6, null };
var other = new int?[] { 6, 5, 4, 3, 2, 1, null };
Assert.IsTrue(source.EquivalentWith(other));
}
[TestMethod]
public void OtherEnumerableReversedBothContainingUniqueAndNotUniqueAndNullButDifferentWillPass()
{
var source = new int?[] { 1, 2, 3, 2, 5, 6, null };
var other = new int?[] { 6, 5, 2, 3, 2, 1, null };
Assert.IsTrue(source.EquivalentWith(other));
}
}
Implementation
public static class CollectionExtensions
{
public static bool EquivalentWith<T>(this IEnumerable<T> source, IEnumerable<T> other)
{
return EquivalentWith(source, other, EqualityComparer<T>.Default);
}
public static bool EquivalentWith<T>(this IEnumerable<T> source, IEnumerable<T> other, IEqualityComparer<T> comparer)
{
if (source == null) throw new ArgumentNullException("source");
if (other == null) throw new ArgumentNullException("other");
if (comparer == null) throw new ArgumentNullException("comparer");
if (ReferenceEquals(source, other))
{
return true;
}
if (CollectionsButNotSameCount(source, other))
{
return false;
}
int sourceNullCount;
var sourceDict = GetElementCounts(source, comparer, out sourceNullCount);
int otherNullCount;
var otherDict = GetElementCounts(other, comparer, out otherNullCount);
if (otherNullCount != sourceNullCount || sourceDict.Count != otherDict.Count)
{
return false;
}
foreach (var s in sourceDict)
{
int otherCount;
if (!otherDict.TryGetValue(s.Key, out otherCount) || s.Value != otherCount)
{
return false;
}
}
return true;
}
private static bool CollectionsButNotSameCount<T>(IEnumerable<T> source, IEnumerable<T> other)
{
var sourceColl = source as ICollection<T>;
var otherColl = other as ICollection<T>;
return sourceColl != null && otherColl != null && sourceColl.Count != otherColl.Count;
}
private static Dictionary<T, int> GetElementCounts<T>(IEnumerable<T> source, IEqualityComparer<T> comparer, out int nullCount)
{
var asCollection = source as ICollection<T>;
var dictionary = new Dictionary<T, int>(asCollection == null ? 0 : asCollection.Count, comparer);
nullCount = 0;
foreach (var key in source)
{
if (key == null)
{
++nullCount;
}
else
{
int num;
dictionary.TryGetValue(key, out num);
++num;
dictionary[key] = num;
}
}
return dictionary;
}
}
First i have provided az overload to avoid unnecessary passinf if the IEqualityComparer and in the method i'm checking against null values.
Then checking their references because if they are the same there is nothing to do. After this i'm using the as operator to check if the input collections are real ICollections and if they are are their Count equal. The casting is fast no need to iterate throught the IEnumerables to get a fixed size collection to get the item counts.
Next step is to get the item counts it is using a Dictionary (as Hashlacher did it) with another casting to try get the initial size of the dictionary (not good idea when we have a list with 5 million items but every item is the same).
If we have the counts we can make another checks against them: if the item counts or the null counts aren't equal then the collections aren't equals.
If the counts are OK then we try to check every item from the source collection whether it is in the other or not. If not the collections aren't equal.
HashSet<T>
and using HashSet<T>.SetEquals? \$\endgroup\$HashSet<T>
is too expensive. \$\endgroup\$HashSet
solution runs faster for large inputs (timings are printed at the bottom of the page). Can you post more information about the situation you have in which it runs slower? \$\endgroup\$