September 5, 2017

C# HashSet

Best to demonstrate what some of the methods do with some unit tests. In particular what does 'SymmetricExceptWith' do?

[ TestFixture ]
public class HashSetTests
{
    private HashSet<int> integerSet1;
    private HashSet<int> integerSet1Copy;
    private HashSet<int> integerSet2;

    [ SetUp ]
    public void Setup()
    {
        integerSet1 = new HashSet<int> { 1, 2, 3 };
        integerSet2 = new HashSet<int> { 2, 3, 4 };
        integerSet1Copy = new HashSet<int> { 2, 3, 1 };
    }

    [ Test ]
    // The SetEquals method ignores duplicate entries and 
    // the order of elements in the other parameter.
    public void SetEqualsTest()
    {
        var integerList = new List<int> { 2, 3, 1, 3, 2 };

        Assert.That( !integerSet1.SetEquals( integerSet2 ) );
        Assert.That( integerSet1.SetEquals( integerSet1Copy ) );
        Assert.That( integerSet1.SetEquals( integerList ) );
    }

    [ Test ]
    public void IntersectWithTest() // All elements common to both sets
    {
        integerSet1.IntersectWith( integerSet2 );
        Assert.That( !integerSet1.Contains( 1 ) );
        Assert.That( integerSet1.Contains( 2 ) );
        Assert.That( integerSet1.Contains( 3 ) );
        Assert.That( !integerSet1.Contains( 4 ) );
    }

    [ Test ]
    public void UnionWithTest() // All elements in both sets
    {
        integerSet1.UnionWith( integerSet2 );
        Assert.That( integerSet1.Contains( 1 ) );
        Assert.That( integerSet1.Contains( 2 ) );
        Assert.That( integerSet1.Contains( 3 ) );
        Assert.That( integerSet1.Contains( 4 ) );
    }

    [ Test ]
    public void SymmetricExceptWithTest() // All elements not common to both sets
    {
        integerSet1.SymmetricExceptWith( integerSet2 );
        Assert.That( integerSet1.Contains( 1 ) );
        Assert.That( !integerSet1.Contains( 2 ) );
        Assert.That( !integerSet1.Contains( 3 ) );
        Assert.That( integerSet1.Contains( 4 ) );
    }
}


Here are some extension methods that maintain the original values of the sets (the existing methods modify the calling set)

// Extensions Set operations that maintain the original values of the original sets
// Be aware that these can be expensive operations if the sets are very large 
// I changed the names to make the operations easier to recognise
public static class HashSetExtensions
{
	// A U B => Everything from set A and set B
	public static HashSet<T> Union<T>(this HashSet<T> hashSet1, HashSet<T> hashSet2)
	{
		var res = new HashSet<T>(hashSet1, hashSet1.Comparer);
		res.UnionWith(hashSet2);
		return res;
	}

    // A ∩ B => Everything in both set A and set B
	public static HashSet<T> Intersecting<T>(this HashSet<T> hashSet1, HashSet<T> hashSet2)
	{
		var res = new HashSet<T>(hashSet1, hashSet1.Comparer);
		res.IntersectWith(hashSet2);
		return res;
	}
    
	// A - B => Everything in set A minus anything in set B
	public static HashSet<T> RemoveAnyFrom<T>(this HashSet<T> hashSet1, HashSet<T> hashSet2)
	{
		var res = new HashSet<T>(hashSet1, hashSet1.Comparer);
		res.ExceptWith(hashSet2);
		return res;
	}
    
	// A ∆ B => Equivalent to an XOR, also equivalent to NOT(A ∩ B)
	public static HashSet<T> NotIntersecting<T>(this HashSet<T> hashSet1, HashSet<T> hashSet2)
	{
		var res = new HashSet<T>(hashSet1, hashSet1.Comparer);
		res.SymmetricExceptWith(hashSet2);
		return res;
	}
}

and some tests for them

void Main()
{
	string[] names1 = new string[] {
		"banana","apple","pear","orange","grapes","mango"
	};
	
	string[] names2 = new string[] {
		"advocado","lemon","pear","lime","banana","passion fruit"
	};
	
	HashSet<string> setA = new HashSet<string>(names1, StringComparer.OrdinalIgnoreCase);
	HashSet<string> setB = new HashSet<string>(names2, StringComparer.OrdinalIgnoreCase);
	
	var res1 = setA.Union(setB);
	var res2 = setA.Intersecting(setB);
    var res3 = setA.RemoveAnyFrom(setB);
	var res4 = setA.NotIntersecting(setB);

	Console.WriteLine("A = " + string.Join(", ", setA));
	Console.WriteLine("B = " + string.Join(", ", setB));
	Console.WriteLine("A U B = " + string.Join(", ", res1));
	Console.WriteLine("A ∩ B = " + string.Join(", ", res2));
	Console.WriteLine("A - B = " + string.Join(", ", res3));
	Console.WriteLine("A ∆ B = " + string.Join(", ", res4));
}

which outputs to the console the following:
A = banana, apple, pear, orange, grapes, mango
B = advocado, lemon, pear, lime, banana, passion fruit
A U B = banana, apple, pear, orange, grapes, mango, advocado, lemon, lime, passion fruit
A ∩ B = banana, pear
A - B = apple, orange, grapes, mango
A ∆ B = passion fruit, apple, lime, orange, grapes, mango, advocado, lemon  

No comments: